<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Xinghe博客</title><description>一个大学在校生</description><link>https://avatars.githubusercontent.com/u/222407735?v=4/</link><templateTheme>Firefly</templateTheme><templateThemeVersion>6.10.7</templateThemeVersion><templateThemeUrl>https://github.com/CuteLeaf/Firefly</templateThemeUrl><lastBuildDate>2026年5月29日 19:58:43</lastBuildDate><item><title>Java Spring Boot 零基础完整学习手册</title><link>https://avatars.githubusercontent.com/posts/2026-5-29/</link><guid isPermaLink="true">https://avatars.githubusercontent.com/posts/2026-5-29/</guid><description>一本真正面向零基础的 Spring Boot 学习手册，从 HelloWorld 到完整后端项目，以校园博客论坛系统为案例，覆盖 Java 语法、Maven、Spring Boot、MyBatis Plus、Spring Security、JWT 认证到项目部署全流程。</description><pubDate>Mon, 27 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;section&gt;&lt;h1&gt;Java Spring Boot 零基础完整学习手册&lt;a href=&quot;#java-spring-boot-零基础完整学习手册&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h1&gt;&lt;section&gt;&lt;h2&gt;——以校园博客论坛系统为案例&lt;a href=&quot;#以校园博客论坛系统为案例&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;hr /&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;╔═══════════════════════════════════════════════════════╗&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;║                                                       ║&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;║           Java Spring Boot 零基础完整学习手册            ║&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;║                                                       ║&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;║           ——从 HelloWorld 到完整后端项目的奇妙之旅        ║&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;║                                                       ║&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;║                                                       ║&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;║            案例项目：校园博客论坛系统 v1.34              ║&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;║            作者：刘畅                                   ║&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;║            发布日期：2026 年 4 月                        ║&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;║                                                       ║&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;╚═══════════════════════════════════════════════════════╝&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;hr /&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;关于本书&lt;a href=&quot;#关于本书&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;这是一本&lt;strong&gt;真正面向零基础&lt;/strong&gt;的 Spring Boot 学习手册。它不会假设你已经懂 Java，也不会跳过任何环节。我们会一起从最基础的 &lt;code&gt;Hello World&lt;/code&gt; 开始，一步一步走到能独立看懂、修改、甚至从零搭建一个完整的后端项目。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;本书的特点：&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;完全零基础友好&lt;/strong&gt;：每个新概念都会用生活类比解释一次&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;真实项目驱动&lt;/strong&gt;：所有讲解都基于真实可运行的”校园博客论坛系统”&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;代码来源真实&lt;/strong&gt;：每一段示例代码都来自项目的真实文件，可以打开 IDE 一一对照&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;覆盖全栈后端&lt;/strong&gt;：从 Java 语法 → Maven → Spring Boot → MyBatis Plus → Spring Security → JWT → 项目部署&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;超过 5 万字&lt;/strong&gt;：足够详细，但不啰嗦&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;学完本书你将获得：&lt;/strong&gt;&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;能独立读懂任何 Spring Boot 项目的源码&lt;/li&gt;
&lt;li&gt;能用 Spring Boot + MyBatis Plus 从零搭建一个 RESTful 后端&lt;/li&gt;
&lt;li&gt;理解用户认证、JWT、密码加密、XSS 防护等安全要点&lt;/li&gt;
&lt;li&gt;会处理常见的并发问题（点赞重复、阅读量竞态等）&lt;/li&gt;
&lt;li&gt;能独立完成项目打包、部署、上线全流程&lt;/li&gt;
&lt;/ol&gt;&lt;hr /&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;适合人群&lt;a href=&quot;#适合人群&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;大学生 / 校招生&lt;/strong&gt;：找后端开发的实习或工作&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;转行学编程&lt;/strong&gt;：从 0 开始想入门后端开发&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;前端转后端&lt;/strong&gt;：会 JavaScript 但想学后端&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;培训班学员&lt;/strong&gt;：想要一份系统化的入门资料&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代码爱好者&lt;/strong&gt;：想看懂别人的 GitHub 项目&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;⚠️ &lt;strong&gt;不适合&lt;/strong&gt;：已经精通 Spring Boot、想看深入源码分析的高级开发者。本书定位是”入门到能上手”，不是”源码精读”。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;学习建议&lt;a href=&quot;#学习建议&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;1. 边读边敲代码&lt;a href=&quot;#1-边读边敲代码&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;这是最重要的建议&lt;/strong&gt;。看再多教程不如自己亲手敲一遍。每一段示例代码都请你打开 IDE，亲自敲进去，亲自跑一次。理解永远来自亲手实践。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;2. 不要跳跃&lt;a href=&quot;#2-不要跳跃&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;虽然你可能急切地想看”项目实战”，但请耐心从”Java 入门”开始。每一章都是后面的基础。跳过基础，后面的代码会让你抓狂。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;3. 善用 IDE 的跳转&lt;a href=&quot;#3-善用-ide-的跳转&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;读到 &lt;code&gt;SysUserController&lt;/code&gt; 时，请打开项目的 &lt;code&gt;SysUserController.java&lt;/code&gt;，按住 &lt;code&gt;Ctrl&lt;/code&gt; 点击各种类名，跳到定义。这种”探索式阅读”比看书快十倍。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;4. 做笔记&lt;a href=&quot;#4-做笔记&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;遇到不理解的概念，不要硬背，先记下来，往后读几章自然就懂了。读完一章再回头看，会有 “原来如此” 的快感。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;5. 不要害怕报错&lt;a href=&quot;#5-不要害怕报错&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;报错是程序员最好的老师。看到红色的 &lt;code&gt;Exception&lt;/code&gt; 不要慌张，而要兴奋——这是你学习的机会。复制错误信息到搜索引擎，99% 的问题都有答案。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;6. 加入社区&lt;a href=&quot;#6-加入社区&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;学习后端开发不要孤军奋战。加入一些 Java / Spring Boot 学习群，和同样在学习的人一起讨论。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;案例项目简介&lt;a href=&quot;#案例项目简介&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;本书使用的案例项目是一个&lt;strong&gt;真实的、可运行的校园博客论坛系统&lt;/strong&gt;：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GitHub&lt;/strong&gt;：&lt;a href=&quot;https://github.com/Xinghe-0203/Campus_Blog&quot; target=&quot;_blank&quot;&gt;https://github.com/Xinghe-0203/Campus_Blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;版本&lt;/strong&gt;：v1.34（最后更新 2026-04-27）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代码量&lt;/strong&gt;：127 个 Java 文件&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据表&lt;/strong&gt;：18 张&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Controller&lt;/strong&gt;：13 个&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;API 接口&lt;/strong&gt;：67 个 HTTP 端点&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;包含的功能模块：&lt;/strong&gt;&lt;/p&gt;












































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;模块&lt;/th&gt;&lt;th&gt;功能&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;👤 用户系统&lt;/td&gt;&lt;td&gt;注册、登录、JWT 认证、修改密码、用户搜索&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;📝 文章系统&lt;/td&gt;&lt;td&gt;发布、编辑、删除、详情、列表、高级搜索、草稿自动保存&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;💬 互动系统&lt;/td&gt;&lt;td&gt;评论（嵌套回复）、点赞、收藏&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;👥 社交系统&lt;/td&gt;&lt;td&gt;关注、粉丝、关注流&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;🔔 通知系统&lt;/td&gt;&lt;td&gt;点赞通知、评论通知、关注通知（事件驱动）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;🌐 校友圈&lt;/td&gt;&lt;td&gt;动态发布、点赞、评论、转发、可见性控制&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;📷 媒体上传&lt;/td&gt;&lt;td&gt;图片/视频上传，文件大小限制 500MB&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;🔥 热门趋势&lt;/td&gt;&lt;td&gt;热门文章、热门标签&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;🚨 举报系统&lt;/td&gt;&lt;td&gt;用户举报、管理员处理&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;&lt;strong&gt;为什么选择这个项目作为案例？&lt;/strong&gt;&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;功能完整&lt;/strong&gt;：覆盖了真实业务系统的方方面面，不是玩具项目&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代码规范&lt;/strong&gt;：遵循阿里 Java 开发规范，分层清晰&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;持续迭代&lt;/strong&gt;：从 v1.0 → v1.34 经过 30 次迭代，包含大量真实的 Bug 修复&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安全加固&lt;/strong&gt;：经过多轮安全审计，体现工业级实践&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;学完即用&lt;/strong&gt;：完全可以二次开发成毕业设计或商业项目&lt;/li&gt;
&lt;/ol&gt;&lt;hr /&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;全书结构&lt;a href=&quot;#全书结构&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;本书分为 &lt;strong&gt;4 大部分共 19 章 + 附录&lt;/strong&gt;，循序渐进：&lt;/p&gt;&lt;section&gt;&lt;h3&gt;🌱 第一部分：入门基础（约 13000 字）&lt;a href=&quot;#-第一部分入门基础约-13000-字&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;blockquote&gt;&lt;p&gt;从 0 开始的 Java 与 Spring Boot 启蒙&lt;/p&gt;&lt;/blockquote&gt;&lt;ul&gt;
&lt;li&gt;写在前面：致零基础读者&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第 1 章&lt;/strong&gt; Java 语言入门&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第 2 章&lt;/strong&gt; 开发环境搭建&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第 3 章&lt;/strong&gt; Maven 与项目结构&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第 4 章&lt;/strong&gt; Spring Boot 第一课&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;🌿 第二部分：框架核心（约 13000 字）&lt;a href=&quot;#-第二部分框架核心约-13000-字&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;blockquote&gt;&lt;p&gt;理解 Spring Boot 项目”内功心法”&lt;/p&gt;&lt;/blockquote&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;第 5 章&lt;/strong&gt; MyBatis Plus 持久层框架&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第 6 章&lt;/strong&gt; 分层架构详解&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第 7 章&lt;/strong&gt; 统一响应与异常处理&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第 8 章&lt;/strong&gt; Spring Security 入门&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第 9 章&lt;/strong&gt; JWT 认证实战&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;🌳 第三部分：项目实战（约 16000 字）&lt;a href=&quot;#-第三部分项目实战约-16000-字&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;blockquote&gt;&lt;p&gt;真实业务场景下的代码与设计&lt;/p&gt;&lt;/blockquote&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;第 10 章&lt;/strong&gt; 用户模块实战&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第 11 章&lt;/strong&gt; 文章模块实战&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第 12 章&lt;/strong&gt; 互动模块：评论 / 点赞 / 收藏&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第 13 章&lt;/strong&gt; 关注与通知&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第 14 章&lt;/strong&gt; 校友圈与媒体&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第 15 章&lt;/strong&gt; 热门趋势 + 举报系统&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;🌲 第四部分：进阶与部署（约 10000 字）&lt;a href=&quot;#-第四部分进阶与部署约-10000-字&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;blockquote&gt;&lt;p&gt;让代码真正”上线为王”&lt;/p&gt;&lt;/blockquote&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;第 16 章&lt;/strong&gt; 安全加固实战&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第 17 章&lt;/strong&gt; 性能优化技巧&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第 18 章&lt;/strong&gt; 测试与调试&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第 19 章&lt;/strong&gt; 部署上线&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;附录 A&lt;/strong&gt; 常见问题 FAQ&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;附录 B&lt;/strong&gt; 项目术语表&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后记&lt;/strong&gt; 你的成长之路&lt;/li&gt;
&lt;/ul&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;如何下载本项目代码&lt;a href=&quot;#如何下载本项目代码&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 1. 克隆项目&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;git&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;clone&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;https://github.com/Xinghe-0203/Campus_Blog.git&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 2. 进入项目目录&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;cd&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Campus_Blog&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 3. 复制环境变量模板（重要！）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;cp&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;.env.example&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;.env&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 4. 编辑 .env 文件，填入你的数据库密码等&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 5. 构建项目&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;mvn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;clean&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;package&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 6. 运行项目&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;mvn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;spring-boot:run&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;详细的环境搭建、IDE 配置、数据库初始化步骤，都在第 2 章中详细讲解。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;致谢&lt;a href=&quot;#致谢&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;感谢你选择这本书。希望读完这本书的你，能从一个完全的零基础小白，蜕变成一个能独立写出 Spring Boot 项目的后端开发者。&lt;/p&gt;&lt;p&gt;如果你在阅读中遇到任何问题，或者发现书中错误，欢迎在 GitHub 项目中提 Issue。&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;学习编程没有捷径，但有最适合你的路径。&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;这本书，就是为零基础的你，定制的那条路径。&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;让我们开始吧！🚀&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h1&gt;《Java Spring Boot 零基础完整学习手册》&lt;a href=&quot;#java-spring-boot-零基础完整学习手册-1&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h1&gt;&lt;section&gt;&lt;h2&gt;第一部分 入门基础&lt;a href=&quot;#第一部分-入门基础&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;blockquote&gt;&lt;p&gt;项目案例：校园博客论坛系统（Campus Blog Forum）
配套代码：&lt;code&gt;D:\MyCode\edu_project&lt;/code&gt;
适合人群：零基础完全没接触过 Java 和后端开发的小白&lt;/p&gt;&lt;/blockquote&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h1&gt;写在前面&lt;a href=&quot;#写在前面&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h1&gt;&lt;p&gt;嗨，朋友 👋&lt;/p&gt;&lt;p&gt;如果你正在打开这本书，说明你大概率是这样的情况：听说过”Java 后端开发”这个词，可能在大学开过 Java 课但是云里雾里，或者完全没碰过编程，只是因为想做一个属于自己的网站项目而走到这里。无论你是哪一种，恭喜你，你来对地方了。&lt;/p&gt;&lt;section&gt;&lt;h2&gt;这本书是给谁看的&lt;a href=&quot;#这本书是给谁看的&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;这本书是写给&lt;strong&gt;完全零基础&lt;/strong&gt;的同学的。我假设你：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;没写过一行 Java 代码&lt;/li&gt;
&lt;li&gt;不知道什么是 IDE、什么是 Maven&lt;/li&gt;
&lt;li&gt;没听说过 Spring Boot&lt;/li&gt;
&lt;li&gt;但是会开电脑、会用浏览器、会复制粘贴 😄&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;如果你已经是有经验的开发者，这本书的前几章对你来说可能会显得啰嗦，可以快速翻过去，直接从后面 Spring Boot 的部分开始读。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;学完后你能掌握什么&lt;a href=&quot;#学完后你能掌握什么&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;读完整本书（包括后续部分），并且&lt;strong&gt;亲手把每一个例子敲一遍&lt;/strong&gt;之后，你将能够：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;独立看懂中等规模的 Spring Boot 项目源码&lt;/li&gt;
&lt;li&gt;自己从零搭建一个 Spring Boot 后端工程&lt;/li&gt;
&lt;li&gt;编写 RESTful API（也就是手机 App、网页前端调用的”接口”）&lt;/li&gt;
&lt;li&gt;操作 MySQL 数据库，写出增删改查功能&lt;/li&gt;
&lt;li&gt;实现用户登录、权限控制、JWT 认证&lt;/li&gt;
&lt;li&gt;看懂我们这个真实的”校园博客论坛系统”项目，并且能在它的基础上继续添加自己的功能&lt;/li&gt;
&lt;/ol&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;学习路径建议&lt;a href=&quot;#学习路径建议&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;⚠️ &lt;strong&gt;请务必遵守这条建议：边读边敲代码！&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;我知道很多人喜欢把书”看完再说”，看完之后觉得自己懂了，结果一上手就抓瞎。学编程&lt;strong&gt;没有捷径&lt;/strong&gt;，你必须把代码亲手敲一遍——不是复制粘贴，是&lt;strong&gt;一个字符一个字符地敲&lt;/strong&gt;。&lt;/p&gt;&lt;p&gt;为什么？因为你的手指在敲键盘的时候，会犯各种错误：拼错单词、忘记分号、括号不匹配……这些错误就是你成长的养料。每一个 bug 都会教你一个知识点。&lt;/p&gt;&lt;p&gt;建议节奏：&lt;/p&gt;&lt;p&gt;1️⃣ 每天学 1 ~ 2 章，不要贪多
2️⃣ 每个例子都敲一遍并跑起来
3️⃣ 出错了先自己查（先看报错信息），实在不会再问 AI 或同学
4️⃣ 学完一章用自己的话复述一遍&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;本项目简介&lt;a href=&quot;#本项目简介&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;我们的案例项目叫 &lt;strong&gt;校园博客论坛系统（Campus Blog Forum）&lt;/strong&gt;。它是一个真实可运行、能部署上线的项目，包含以下功能：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;用户注册、登录、JWT 认证&lt;/li&gt;
&lt;li&gt;发帖、评论、嵌套回复&lt;/li&gt;
&lt;li&gt;点赞、收藏、关注&lt;/li&gt;
&lt;li&gt;通知系统&lt;/li&gt;
&lt;li&gt;校友圈（类似微博）&lt;/li&gt;
&lt;li&gt;媒体上传（图片、视频）&lt;/li&gt;
&lt;li&gt;热度统计、举报管理&lt;/li&gt;
&lt;li&gt;等等……&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;数据库一共 &lt;strong&gt;18 张表&lt;/strong&gt;，技术栈是 &lt;strong&gt;Spring Boot 3.0.12 + MyBatis Plus 3.5.5 + Spring Security + JWT + MySQL&lt;/strong&gt;，JDK 用 &lt;strong&gt;21+&lt;/strong&gt;。是不是听上去很厉害？别怕，等你读完这本书，回过头看会觉得”也就那么回事”。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;学习心态&lt;a href=&quot;#学习心态&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;最后说一句最重要的话：&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;技术不难，难的是耐心。&lt;/strong&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;你会遇到环境装不上、Maven 下载不动、Lombok 不生效、数据库连不上、莫名其妙的红线……这些都是&lt;strong&gt;所有程序员的日常&lt;/strong&gt;。不要怀疑自己笨，没人天生就会，所有”大佬”都是从看到红色报错就慌张开始的。&lt;/p&gt;&lt;p&gt;放轻松，慢慢来。准备好了？我们开始吧 🚀&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h1&gt;第 1 章 Java 语言入门&lt;a href=&quot;#第-1-章-java-语言入门&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h1&gt;&lt;p&gt;在动手搭建项目之前，我们必须先认识 Java 这门语言。这一章我尽量用大白话和生活类比来解释，不会让你看公式和定义。&lt;/p&gt;&lt;section&gt;&lt;h2&gt;1.1 Java 是什么、能做什么&lt;a href=&quot;#11-java-是什么能做什么&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Java 是一门 &lt;strong&gt;编程语言（Programming Language）&lt;/strong&gt;。所谓编程语言，就是人类用来”指挥”电脑做事的一种特殊语言。中文是用来跟人说话的，Java 是用来跟电脑说话的。&lt;/p&gt;&lt;p&gt;🌰 一个生活类比：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;你想让一个外国朋友帮你买杯奶茶，你得用他能听懂的语言（比如英语）告诉他怎么走、要点什么。&lt;/li&gt;
&lt;li&gt;你想让电脑帮你处理数据、显示网页，你也得用电脑能听懂的语言（比如 Java）告诉它步骤。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;Java 由 Sun 公司在 1995 年发布，现在归 Oracle 所有。它最大的特点是 &lt;strong&gt;“一次编写，到处运行（Write Once, Run Anywhere）”&lt;/strong&gt; —— 你写的同一段 Java 代码，可以在 Windows、Mac、Linux、安卓手机上都跑起来。这是怎么做到的？因为 Java 不是直接给电脑硬件看的，它是给一个叫 &lt;strong&gt;JVM（Java Virtual Machine，Java 虚拟机）&lt;/strong&gt; 的”翻译官”看的，每个操作系统上都有自己版本的 JVM。&lt;/p&gt;&lt;p&gt;Java 能做什么？非常多：&lt;/p&gt;




























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;应用场景&lt;/th&gt;&lt;th&gt;举例&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;后端服务器&lt;/td&gt;&lt;td&gt;淘宝、京东、12306 后端大量使用 Java&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;安卓 App&lt;/td&gt;&lt;td&gt;早期安卓应用主要用 Java 写（现在 Kotlin 也很流行）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;大数据&lt;/td&gt;&lt;td&gt;Hadoop、Spark、Flink 都是 Java/Scala&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;桌面应用&lt;/td&gt;&lt;td&gt;IDEA 本身就是 Java 写的&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;我们的项目&lt;/td&gt;&lt;td&gt;校园博客论坛后端 ✅&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;我们这本书重点讲的是 &lt;strong&gt;后端服务器开发&lt;/strong&gt;，也就是给网页和手机 App 提供数据的”幕后英雄”。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;1.2 第一个 Java 程序 HelloWorld&lt;a href=&quot;#12-第一个-java-程序-helloworld&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;按照编程界的传统，学任何新语言第一个程序都叫 “Hello World”（你好，世界）。我们也不能免俗：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;HelloWorld&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;static&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;main&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt;[] &lt;/span&gt;&lt;span&gt;args&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;System&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;out&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;println&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Hello, World!&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;跑起来后屏幕会显示：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;Hello, World!&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;看起来简单，其实里面信息量很大。我们一行一行拆解：&lt;/p&gt;
































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;代码&lt;/th&gt;&lt;th&gt;含义&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;public class HelloWorld&lt;/code&gt;&lt;/td&gt;&lt;td&gt;定义一个名为 HelloWorld 的”类”，public 表示”公开的”&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;{ ... }&lt;/code&gt;&lt;/td&gt;&lt;td&gt;大括号包起来的部分是这个类的内容&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;public static void main(String[] args)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;一个特殊的方法，叫 main，是程序的”入口”&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;System.out.println(...)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;调用系统的”打印”功能，把括号里的内容显示在控制台&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;&quot;Hello, World!&quot;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;一个字符串（一段文字）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;每条语句结束都要加分号，跟英文句号差不多&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;⚠️ &lt;strong&gt;几个零基础最容易忽略的细节：&lt;/strong&gt;&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;Java &lt;strong&gt;严格区分大小写&lt;/strong&gt;：&lt;code&gt;Main&lt;/code&gt; 和 &lt;code&gt;main&lt;/code&gt; 是两个完全不同的东西&lt;/li&gt;
&lt;li&gt;文件名必须叫 &lt;code&gt;HelloWorld.java&lt;/code&gt;，必须和 &lt;code&gt;public class&lt;/code&gt; 后面的名字&lt;strong&gt;完全一致&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;每条语句都必须以 &lt;code&gt;;&lt;/code&gt; 结束，少一个就会编译失败&lt;/li&gt;
&lt;li&gt;大括号 &lt;code&gt;{}&lt;/code&gt; 必须配对，少一个也会失败&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;为什么必须有 &lt;code&gt;main&lt;/code&gt; 方法？因为 JVM 启动时只认它，类似于一栋楼必须有大门口。所有 Java 应用程序都从 &lt;code&gt;main&lt;/code&gt; 开始执行。&lt;code&gt;String[] args&lt;/code&gt; 是命令行参数（一般用不到），先不用管。&lt;/p&gt;&lt;p&gt;&lt;code&gt;System.out.println&lt;/code&gt; 中：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;System&lt;/code&gt; 是 Java 自带的一个系统工具类&lt;/li&gt;
&lt;li&gt;&lt;code&gt;out&lt;/code&gt; 是它里面的”输出”对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;println&lt;/code&gt; 是”打印一行（print line）“的意思，会自动换行&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;如果你只想打印不换行，用 &lt;code&gt;System.out.print&lt;/code&gt; 就行。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;1.3 变量与数据类型&lt;a href=&quot;#13-变量与数据类型&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;变量（Variable）&lt;/strong&gt; 是程序里存数据的”盒子”。盒子上贴个标签（变量名），盒子里放东西（值）。&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; age &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;20&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; name &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;刘畅&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;span&gt; isStudent &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;double&lt;/span&gt;&lt;span&gt; price &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;19.9&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;long&lt;/span&gt;&lt;span&gt; bigNumber &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;99999999999L&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;逐行解释：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;int age = 20;&lt;/code&gt; —— 我准备一个名叫 &lt;code&gt;age&lt;/code&gt; 的盒子，规定只能放整数（int），里面放了 20。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;String name = &quot;刘畅&quot;;&lt;/code&gt; —— 一个名叫 &lt;code&gt;name&lt;/code&gt; 的盒子，放字符串，内容是 “刘畅”。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;boolean isStudent = true;&lt;/code&gt; —— 布尔盒子，只能装 &lt;code&gt;true&lt;/code&gt; 或 &lt;code&gt;false&lt;/code&gt;（是或否）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;double price = 19.9;&lt;/code&gt; —— 装小数的盒子。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;long bigNumber = 99999999999L;&lt;/code&gt; —— 装超大整数的盒子，注意末尾的 &lt;code&gt;L&lt;/code&gt;，告诉编译器这是 long 类型。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;Java 的常用基本数据类型一览：&lt;/p&gt;


























































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;类型&lt;/th&gt;&lt;th&gt;中文名&lt;/th&gt;&lt;th&gt;占用字节&lt;/th&gt;&lt;th&gt;范围/示例&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;byte&lt;/code&gt;&lt;/td&gt;&lt;td&gt;字节&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;-128 ~ 127&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;short&lt;/code&gt;&lt;/td&gt;&lt;td&gt;短整数&lt;/td&gt;&lt;td&gt;2&lt;/td&gt;&lt;td&gt;-32768 ~ 32767&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;int&lt;/code&gt;&lt;/td&gt;&lt;td&gt;整数&lt;/td&gt;&lt;td&gt;4&lt;/td&gt;&lt;td&gt;约 ±21 亿&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;long&lt;/code&gt;&lt;/td&gt;&lt;td&gt;长整数&lt;/td&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;非常大，需加 L&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;float&lt;/code&gt;&lt;/td&gt;&lt;td&gt;浮点数&lt;/td&gt;&lt;td&gt;4&lt;/td&gt;&lt;td&gt;小数，需加 F&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;double&lt;/code&gt;&lt;/td&gt;&lt;td&gt;双精度浮点&lt;/td&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;默认的小数类型&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;boolean&lt;/code&gt;&lt;/td&gt;&lt;td&gt;布尔&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;true/false&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;char&lt;/code&gt;&lt;/td&gt;&lt;td&gt;单个字符&lt;/td&gt;&lt;td&gt;2&lt;/td&gt;&lt;td&gt;’A’、‘中’&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;⚠️ 注意 &lt;code&gt;String&lt;/code&gt; 不是基本类型，它是&lt;strong&gt;类（Class）&lt;/strong&gt;，所以首字母大写。我们项目里几乎所有的”文字”都用 String。&lt;/p&gt;&lt;p&gt;类比一下：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;int&lt;/code&gt; 像一个小号文件柜（只能放整数文件夹）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;String&lt;/code&gt; 像一个大号档案盒（能放各种长度的文字资料）&lt;/li&gt;
&lt;li&gt;给变量赋值 = 把东西塞进盒子&lt;/li&gt;
&lt;li&gt;读取变量 = 看盒子里有什么&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;1.4 控制流：if/else、for、while、switch&lt;a href=&quot;#14-控制流ifelseforwhileswitch&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;程序如果只是从上往下执行，能做的事情就太少了。&lt;strong&gt;控制流（Control Flow）&lt;/strong&gt; 让程序可以根据条件判断、循环执行。&lt;/p&gt;&lt;section&gt;&lt;h3&gt;1.4.1 if / else（如果……否则……）&lt;a href=&quot;#141-if--else如果否则&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; score &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;85&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (score &lt;/span&gt;&lt;span&gt;&amp;gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;90&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;System&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;out&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;println&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;优秀&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;else&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (score &lt;/span&gt;&lt;span&gt;&amp;gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;60&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;System&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;out&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;println&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;及格&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;else&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;System&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;out&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;println&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;不及格&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;读起来跟中文几乎一样：如果分数 ≥ 90，打印优秀；否则如果 ≥ 60，打印及格；否则打印不及格。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;1.4.2 for 循环（重复 N 次）&lt;a href=&quot;#142-for-循环重复-n-次&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; i &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;&lt;span&gt;;&lt;/span&gt;&lt;span&gt; i &lt;/span&gt;&lt;span&gt;&amp;lt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt;&lt;span&gt;;&lt;/span&gt;&lt;span&gt; i&lt;/span&gt;&lt;span&gt;++&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;System&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;out&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;println&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;第 &quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; i &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot; 次打印&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;for&lt;/code&gt; 三段式：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;&lt;code&gt;int i = 1&lt;/code&gt; —— 初始化，从 1 开始&lt;/li&gt;
&lt;li&gt;&lt;code&gt;i &amp;lt;= 5&lt;/code&gt; —— 条件，只要满足就继续循环&lt;/li&gt;
&lt;li&gt;&lt;code&gt;i++&lt;/code&gt; —— 每轮结束加 1（等同于 &lt;code&gt;i = i + 1&lt;/code&gt;）&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;输出：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;第 1 次打印&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;第 2 次打印&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;第 3 次打印&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;第 4 次打印&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;第 5 次打印&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;1.4.3 while 循环（条件满足就一直循环）&lt;a href=&quot;#143-while-循环条件满足就一直循环&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; count &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;while&lt;/span&gt;&lt;span&gt;&lt;span&gt; (count &lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;System&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;out&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;println&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;当前 count = &quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; count);&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;count&lt;/span&gt;&lt;span&gt;++;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;适合 &lt;strong&gt;不知道要循环多少次&lt;/strong&gt; 的场景，比如”读到文件结尾就停”。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;1.4.4 switch（多分支选择）&lt;a href=&quot;#144-switch多分支选择&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; day &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;switch&lt;/span&gt;&lt;span&gt; (day) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;case&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;System&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;out&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;println&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;周一&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;);&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;break&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;case&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;System&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;out&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;println&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;周二&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;);&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;break&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;case&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;System&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;out&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;println&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;周三&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;);&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;break&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;default:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;System&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;out&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;println&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;其他&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;switch&lt;/code&gt; 适合分支很多的情况。⚠️ 别忘了 &lt;code&gt;break;&lt;/code&gt;，不然会”穿透”到下一个 case。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;1.5 类和对象（重点！）&lt;a href=&quot;#15-类和对象重点&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;这是 Java 的核心概念，也是零基础最难的一关。请反复读这一节。&lt;/p&gt;&lt;section&gt;&lt;h3&gt;类是模子，对象是铸造出来的零件 🏭&lt;a href=&quot;#类是模子对象是铸造出来的零件-&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;想象一个工厂：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;类（Class）&lt;/strong&gt; 就像一个”铸造模子”。它定义了：要做什么形状、有几个洞、用什么材料。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对象（Object）&lt;/strong&gt; 就像用模子铸造出来的”实物零件”。一个模子可以铸造无数个零件。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;举个例子，我们项目里有个 &lt;code&gt;SysUser&lt;/code&gt;（系统用户）类，它定义了”用户”长什么样。简化版：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SysUser&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 字段（属性）：每个用户都有这些信息&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; id&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; username&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; password&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Integer&lt;/span&gt;&lt;span&gt; age&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 方法（行为）：用户能做的事&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;sayHello&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;System&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;out&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;println&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;大家好，我是 &quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; username);&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// Getter / Setter（读写字段）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;getUsername&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; username; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;setUsername&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;username&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{ &lt;/span&gt;&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;username&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; username; }&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;代码解读表：&lt;/p&gt;




























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;行号片段&lt;/th&gt;&lt;th&gt;作用&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;public class SysUser&lt;/code&gt;&lt;/td&gt;&lt;td&gt;定义一个名为 SysUser 的类（模子）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;private Long id;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;模子里有一个 id 属性，private 表示外部不能直接访问&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;private String username;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;用户名属性&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;public void sayHello()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;一个行为：打招呼&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;getUsername()&lt;/code&gt; / &lt;code&gt;setUsername()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;暴露给外部读写 username 的入口&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;接着我们用这个模子”铸造”两个对象：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Test&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;static&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;main&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt;[] &lt;/span&gt;&lt;span&gt;args&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// new 关键字 = 用模子铸造一个新对象&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;SysUser&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;user1&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SysUser&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;user1&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setUsername&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;张三&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;SysUser&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;user2&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SysUser&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;user2&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setUsername&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;李四&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;user1&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;sayHello&lt;/span&gt;&lt;span&gt;();  &lt;/span&gt;&lt;span&gt;// 输出：大家好，我是 张三&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;user2&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;sayHello&lt;/span&gt;&lt;span&gt;();  &lt;/span&gt;&lt;span&gt;// 输出：大家好，我是 李四&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;逐行解释：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;new SysUser()&lt;/code&gt; —— 用 SysUser 这个模子铸造一个新对象&lt;/li&gt;
&lt;li&gt;&lt;code&gt;user1&lt;/code&gt; 和 &lt;code&gt;user2&lt;/code&gt; 是两个不同的对象，互相独立&lt;/li&gt;
&lt;li&gt;改 user1 的名字，不会影响 user2&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;🎯 &lt;strong&gt;核心要点：&lt;/strong&gt;&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;类是”设计图”，对象是”按设计图盖出来的房子”。设计图只有一份，房子可以盖很多座。&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;我们项目里 &lt;code&gt;SysUser&lt;/code&gt;、&lt;code&gt;BlogPost&lt;/code&gt;、&lt;code&gt;BlogComment&lt;/code&gt; 等所有 entity（实体类）都是这种模式，分别对应数据库的一张表。一行数据 = 一个对象。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;private、public 是什么？&lt;a href=&quot;#privatepublic-是什么&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;它们叫 &lt;strong&gt;访问修饰符（Access Modifier）&lt;/strong&gt;：&lt;/p&gt;
























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;修饰符&lt;/th&gt;&lt;th&gt;含义&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;public&lt;/code&gt;&lt;/td&gt;&lt;td&gt;公开，谁都能用&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;private&lt;/code&gt;&lt;/td&gt;&lt;td&gt;私有，只有本类内部能用&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;protected&lt;/code&gt;&lt;/td&gt;&lt;td&gt;保护，本类和子类能用&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;不写&lt;/td&gt;&lt;td&gt;默认，同一个包内可用&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;通常字段写 &lt;code&gt;private&lt;/code&gt;，方法写 &lt;code&gt;public&lt;/code&gt;，这是”封装”的最佳实践。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;1.6 方法：参数、返回值、重载&lt;a href=&quot;#16-方法参数返回值重载&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;方法（Method）&lt;/strong&gt; 就是一段可以重复使用的代码块，类似数学里的函数。&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt;&lt;span&gt; a&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; b) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt;&lt;span&gt; a &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; b&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;拆解：&lt;/p&gt;




























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;部分&lt;/th&gt;&lt;th&gt;含义&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;public&lt;/code&gt;&lt;/td&gt;&lt;td&gt;谁都可以调用&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;int&lt;/code&gt;&lt;/td&gt;&lt;td&gt;返回值类型，这里返回一个整数&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;add&lt;/code&gt;&lt;/td&gt;&lt;td&gt;方法名&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;(int a, int b)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;两个参数：a 和 b&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;return a + b;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;返回 a + b 的结果&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;调用：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; sum &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// sum = 8&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;section&gt;&lt;h3&gt;没有返回值用 void&lt;a href=&quot;#没有返回值用-void&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;printHello&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;System&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;out&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;println&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Hello&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;void&lt;/code&gt; 表示”没有东西要返回”，类似”我帮你做这件事，但不给你东西”。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;方法重载（Overload）&lt;a href=&quot;#方法重载overload&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;同名方法、参数不同 = 重载。&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt;&lt;span&gt; a&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; b) { &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt;&lt;span&gt; a &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; b&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;double&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;double&lt;/span&gt;&lt;span&gt;&lt;span&gt; a&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;double&lt;/span&gt;&lt;span&gt; b) { &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt;&lt;span&gt; a &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; b&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt;&lt;span&gt; a&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt;&lt;span&gt; b&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; c) { &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt;&lt;span&gt; a &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; b &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; c&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;编译器会根据你传的参数自动选择最匹配的版本。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;1.7 集合：List 和 Map&lt;a href=&quot;#17-集合list-和-map&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;集合（Collection）&lt;/strong&gt; 就是装多个数据的容器。最常用的两种：&lt;/p&gt;&lt;section&gt;&lt;h3&gt;List —— 有序列表（像火车车厢，一节挨一节）&lt;a href=&quot;#list--有序列表像火车车厢一节挨一节&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;java.util.ArrayList&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;java.util.List&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; users &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ArrayList&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;users&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;张三&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;users&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;李四&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;users&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;王五&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;System&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;out&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;println&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;users&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;&lt;span&gt;));&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// 张三&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;System&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;out&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;println&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;users&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;size&lt;/span&gt;&lt;span&gt;&lt;span&gt;());&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// 3&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;List&amp;lt;String&amp;gt;&lt;/code&gt; 表示”装 String 的列表”。&lt;code&gt;&amp;lt;String&amp;gt;&lt;/code&gt; 这种语法叫 &lt;strong&gt;泛型（Generics）&lt;/strong&gt;，限定列表里只能放 String。&lt;/p&gt;&lt;p&gt;我们项目里查询博客文章列表，就会得到一个 &lt;code&gt;List&amp;lt;BlogPost&amp;gt;&lt;/code&gt;。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;Map —— 键值对（像字典，查”苹果”得到”apple”）&lt;a href=&quot;#map--键值对像字典查苹果得到apple&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;java.util.HashMap&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;java.util.Map&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;Map&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Object&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; userInfo &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;HashMap&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;userInfo&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;put&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;username&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;张三&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;userInfo&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;put&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;age&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;20&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;userInfo&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;put&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;isStudent&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;System&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;out&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;println&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;userInfo&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;username&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;));&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// 张三&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;Map&amp;lt;String, Object&amp;gt;&lt;/code&gt; 表示”键是 String，值是任意类型”的字典。&lt;/p&gt;&lt;p&gt;后面写接口返回数据时，经常会用 Map 临时组装一些字段。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;1.8 异常处理 try-catch&lt;a href=&quot;#18-异常处理-try-catch&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;程序运行时可能会出错，比如：除以 0、读取一个不存在的文件、网络断开。这些情况叫 &lt;strong&gt;异常（Exception）&lt;/strong&gt;。&lt;/p&gt;&lt;p&gt;🌰 类比：你正在烧开水，突然停电了。如果不处理，整个程序会”崩溃退出”。处理它的方式就是 &lt;code&gt;try-catch&lt;/code&gt;：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;try&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; result &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;&lt;span&gt;;&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// 这行会抛异常&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;System&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;out&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;println&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;永远不会执行到这里&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;ArithmeticException&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;System&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;out&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;println&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;出错了：&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getMessage&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;finally&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;System&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;out&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;println&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;不管出不出错，这里都会执行&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;




















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;块&lt;/th&gt;&lt;th&gt;作用&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;try&lt;/code&gt;&lt;/td&gt;&lt;td&gt;把可能出错的代码放在这里&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;catch&lt;/code&gt;&lt;/td&gt;&lt;td&gt;抓住异常，做处理&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;finally&lt;/code&gt;&lt;/td&gt;&lt;td&gt;无论是否异常都会执行，常用于关闭资源&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;我们项目里有个 &lt;strong&gt;全局异常处理器&lt;/strong&gt; &lt;code&gt;GlobalExceptionHandler&lt;/code&gt;，所有 Controller 抛出的异常都会被它统一捕获并返回友好的错误信息。这样我们 Controller 里只管写正常逻辑，出错的事交给它就行。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;1.9 注解（Annotation）是什么&lt;a href=&quot;#19-注解annotation是什么&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;注解（Annotation）&lt;/strong&gt; 是一种”标签”，放在类、方法、字段头上，告诉编译器或框架”我有特殊用途”。&lt;/p&gt;&lt;p&gt;最常见的内置注解 &lt;code&gt;@Override&lt;/code&gt;：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Override&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;toString&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;我是一个用户对象&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;@Override&lt;/code&gt; 表示”我重写了父类的方法”。如果你拼错了方法名，编译器会报错提醒你。&lt;/p&gt;&lt;p&gt;Spring Boot 大量使用注解，看一眼我们项目的启动类（来自 &lt;code&gt;EduProjectApplication.java&lt;/code&gt;）：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;SpringBootApplication&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;exclude&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;SecurityAutoConfiguration&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;UserDetailsServiceAutoConfiguration&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;})&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Import&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;SecurityConfig&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;EnableScheduling&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;EduProjectApplication&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;这里就有 &lt;code&gt;@SpringBootApplication&lt;/code&gt;、&lt;code&gt;@Import&lt;/code&gt;、&lt;code&gt;@EnableScheduling&lt;/code&gt; 三个注解，每个都对应一种”功能开关”。第 4 章会详细讲。&lt;/p&gt;&lt;p&gt;🎯 现在你只要记住一句话：&lt;strong&gt;注解 = 框架用的标签，告诉框架”我是个特殊角色”&lt;/strong&gt;。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;1.10 包和导入 import&lt;a href=&quot;#110-包和导入-import&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;当代码量多了，几百个 Java 文件挤在一起会乱套，所以 Java 用 &lt;strong&gt;包（Package）&lt;/strong&gt; 来分组，类似文件夹。&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;package&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;com.example.edu_project.entity&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;这一行说”我这个文件属于 &lt;code&gt;com.example.edu_project.entity&lt;/code&gt; 包”。对应到磁盘上的目录就是：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;src/main/java/com/example/edu_project/entity/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;要使用别的包里的类，得先 &lt;code&gt;import&lt;/code&gt;：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;java.util.List&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;java.util.ArrayList&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;com.example.edu_project.entity.SysUser&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;⚠️ 包名习惯用&lt;strong&gt;全小写&lt;/strong&gt;，并且采用”反向域名”风格（com.公司.项目.模块），保证全球唯一。我们项目所有代码都在 &lt;code&gt;com.example.edu_project&lt;/code&gt; 这个根包下。&lt;/p&gt;&lt;hr /&gt;&lt;p&gt;学完第 1 章，你已经掌握了 Java 语言的基本骨架。接下来我们要把”工具”装到电脑上，让代码能跑起来。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h1&gt;第 2 章 开发环境搭建&lt;a href=&quot;#第-2-章-开发环境搭建&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h1&gt;&lt;p&gt;工欲善其事，必先利其器。这一章我们一起把开发环境装好。请按顺序操作，不要跳步。&lt;/p&gt;&lt;section&gt;&lt;h2&gt;2.1 安装 JDK 21&lt;a href=&quot;#21-安装-jdk-21&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;JDK（Java Development Kit）&lt;/strong&gt; 是 Java 开发工具包，包含编译器、JVM 等。我们项目要求 JDK 21（在 &lt;code&gt;pom.xml&lt;/code&gt; 里看到 &lt;code&gt;&amp;lt;java.version&amp;gt;21&amp;lt;/java.version&amp;gt;&lt;/code&gt;）。&lt;/p&gt;&lt;section&gt;&lt;h3&gt;1️⃣ 下载&lt;a href=&quot;#1️⃣-下载&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;推荐去 Oracle 官网或者 Adoptium 下载：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;Oracle: &lt;a href=&quot;https://www.oracle.com/java/technologies/downloads/&quot; target=&quot;_blank&quot;&gt;https://www.oracle.com/java/technologies/downloads/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Adoptium（推荐，免费开源）: &lt;a href=&quot;https://adoptium.net/&quot; target=&quot;_blank&quot;&gt;https://adoptium.net/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;选择 &lt;strong&gt;JDK 21 LTS&lt;/strong&gt; 版本，对应你的操作系统（Windows x64 选 .msi 安装包）。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;2️⃣ 安装&lt;a href=&quot;#2️⃣-安装&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Windows 下双击 &lt;code&gt;.msi&lt;/code&gt;，一路下一步即可。默认会装到 &lt;code&gt;C:\Program Files\Java\jdk-21\&lt;/code&gt;。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;3️⃣ 配置 JAVA_HOME 环境变量&lt;a href=&quot;#3️⃣-配置-java_home-环境变量&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;这一步很关键。Maven、IDEA 都靠它找 JDK。&lt;/p&gt;&lt;p&gt;Windows 11 操作步骤：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;右键”此电脑” → 属性 → 高级系统设置 → 环境变量&lt;/li&gt;
&lt;li&gt;在”系统变量”里新建：
&lt;ul&gt;
&lt;li&gt;变量名：&lt;code&gt;JAVA_HOME&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;变量值：&lt;code&gt;C:\Program Files\Java\jdk-21&lt;/code&gt;（你的实际安装路径）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;找到 &lt;code&gt;Path&lt;/code&gt; 变量，编辑，新增一行：&lt;code&gt;%JAVA_HOME%\bin&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;一路确定保存&lt;/li&gt;
&lt;/ol&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;4️⃣ 验证&lt;a href=&quot;#4️⃣-验证&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;打开 &lt;strong&gt;新的&lt;/strong&gt; cmd 或 PowerShell（必须新打开，旧窗口的环境变量不会刷新），输入：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;java&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-version&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;javac&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-version&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;应该看到类似：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;java version &quot;21.0.2&quot; 2024-01-16 LTS&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;javac 21.0.2&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;⚠️ 如果出现”不是内部或外部命令”，说明 Path 没配对，回去检查。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;2.2 安装 IntelliJ IDEA&lt;a href=&quot;#22-安装-intellij-idea&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;IDE（Integrated Development Environment，集成开发环境）&lt;/strong&gt; 是写代码的”超级记事本”，自带智能提示、自动补全、调试器等。Java 后端开发首选 IntelliJ IDEA。&lt;/p&gt;&lt;p&gt;下载地址：&lt;a href=&quot;https://www.jetbrains.com/idea/download/&quot; target=&quot;_blank&quot;&gt;https://www.jetbrains.com/idea/download/&lt;/a&gt;&lt;/p&gt;&lt;section&gt;&lt;h3&gt;社区版 vs 专业版&lt;a href=&quot;#社区版-vs-专业版&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;






















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;版本&lt;/th&gt;&lt;th&gt;价格&lt;/th&gt;&lt;th&gt;是否支持 Spring&lt;/th&gt;&lt;th&gt;推荐&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Community（社区版）&lt;/td&gt;&lt;td&gt;免费&lt;/td&gt;&lt;td&gt;部分支持，不带 Spring 插件&lt;/td&gt;&lt;td&gt;学习初期可用&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Ultimate（专业版）&lt;/td&gt;&lt;td&gt;收费（学生免费）&lt;/td&gt;&lt;td&gt;完整支持 Spring、数据库面板等&lt;/td&gt;&lt;td&gt;✅ 强烈推荐&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;学生可以用 .edu 邮箱去 &lt;a href=&quot;https://www.jetbrains.com/community/education/&quot; target=&quot;_blank&quot;&gt;JetBrains 学生计划&lt;/a&gt; 申请免费的专业版。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;初次设置&lt;a href=&quot;#初次设置&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;1️⃣ 启动 IDEA，跳过欢迎页面
2️⃣ 选择主题（Darcula 黑色护眼）
3️⃣ 在 Settings → Build → Build Tools → Maven 里检查 Maven 配置（后面再调）
4️⃣ 在 File → Project Structure → Project SDK 里选 JDK 21&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;2.3 安装 Lombok 插件&lt;a href=&quot;#23-安装-lombok-插件&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Lombok&lt;/strong&gt; 是一个神奇的工具，能自动帮你生成 getter / setter / 构造函数等”模板代码”。我们项目大量使用它（&lt;code&gt;pom.xml&lt;/code&gt; 里能看到 &lt;code&gt;lombok&lt;/code&gt; 1.18.40 依赖）。&lt;/p&gt;&lt;p&gt;IDEA 在新版本里已经内置 Lombok 插件，无需手动安装。但你需要确保它启用了：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;File → Settings → Plugins&lt;/li&gt;
&lt;li&gt;搜索 “Lombok”，确认状态是 Enabled&lt;/li&gt;
&lt;/ol&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;2.4 启用注解处理器（关键！）&lt;a href=&quot;#24-启用注解处理器关键&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;⚠️ &lt;strong&gt;这一步如果忘了，整个项目会爆红，所有 &lt;code&gt;@Data&lt;/code&gt;、&lt;code&gt;@Getter&lt;/code&gt; 都不生效！&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;操作步骤：&lt;/p&gt;&lt;p&gt;1️⃣ File → Settings
2️⃣ 搜索 “Annotation Processors”
3️⃣ 找到 “Build, Execution, Deployment → Compiler → Annotation Processors”
4️⃣ 勾选 ✅ &lt;strong&gt;Enable annotation processing&lt;/strong&gt;
5️⃣ 点 OK 保存&lt;/p&gt;&lt;p&gt;这一步告诉 IDEA：“请在编译时运行 Lombok 的注解处理器，自动帮我生成代码。“&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;2.5 安装并配置 Maven&lt;a href=&quot;#25-安装并配置-maven&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;IDEA 自带了 Maven，可以先用着。如果你想自己装：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;下载 Maven：&lt;a href=&quot;https://maven.apache.org/download.cgi&quot; target=&quot;_blank&quot;&gt;https://maven.apache.org/download.cgi&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;解压到 &lt;code&gt;D:\apache-maven-3.9.x&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;配置环境变量 &lt;code&gt;MAVEN_HOME&lt;/code&gt; = &lt;code&gt;D:\apache-maven-3.9.x&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;在 Path 里添加 &lt;code&gt;%MAVEN_HOME%\bin&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;验证：&lt;code&gt;mvn -version&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;&lt;section&gt;&lt;h3&gt;配置国内镜像（极重要）&lt;a href=&quot;#配置国内镜像极重要&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Maven 默认从国外服务器下载依赖，速度极慢。我们必须配置阿里云镜像。&lt;/p&gt;&lt;p&gt;打开 &lt;code&gt;C:\Users\你的用户名\.m2\settings.xml&lt;/code&gt;（没有就自己建一个），加入：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;settings&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;mirrors&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;mirror&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;&amp;gt;aliyunmaven&amp;lt;/&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;mirrorOf&lt;/span&gt;&lt;span&gt;&amp;gt;*&amp;lt;/&lt;/span&gt;&lt;span&gt;mirrorOf&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;&amp;gt;阿里云公共仓库&amp;lt;/&lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;url&lt;/span&gt;&lt;span&gt;&amp;gt;https://maven.aliyun.com/repository/public&amp;lt;/&lt;/span&gt;&lt;span&gt;url&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;mirror&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;mirrors&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;settings&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;然后在 IDEA 的 Settings → Maven 里把 “User settings file” 指向这个 &lt;code&gt;settings.xml&lt;/code&gt;，下载速度会从 KB/s 提升到 MB/s。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;2.6 安装 MySQL 8 + 创建数据库&lt;a href=&quot;#26-安装-mysql-8--创建数据库&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;我们项目用的是 &lt;strong&gt;MySQL 8.x&lt;/strong&gt;。&lt;/p&gt;&lt;section&gt;&lt;h3&gt;1️⃣ 下载安装&lt;a href=&quot;#1️⃣-下载安装&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;去 &lt;a href=&quot;https://dev.mysql.com/downloads/installer/&quot; target=&quot;_blank&quot;&gt;https://dev.mysql.com/downloads/installer/&lt;/a&gt; 下 MySQL Installer for Windows，选 Community 版，按引导一步步装。&lt;/p&gt;&lt;p&gt;安装时会让你设 root 密码，&lt;strong&gt;务必记住这个密码&lt;/strong&gt;。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;2️⃣ 验证&lt;a href=&quot;#2️⃣-验证&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;打开命令行：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;mysql&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-u&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;root&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-p&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;输入密码后能进入 &lt;code&gt;mysql&amp;gt;&lt;/code&gt; 提示符就说明成功了。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;3️⃣ 创建项目数据库&lt;a href=&quot;#3️⃣-创建项目数据库&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;从我们的 &lt;code&gt;.env.example&lt;/code&gt; 文件看到，项目数据库名应该叫 &lt;code&gt;campus_blog&lt;/code&gt;：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;CREATE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;DATABASE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;campus_blog&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;DEFAULT&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CHARACTER&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SET&lt;/span&gt;&lt;span&gt; utf8mb4 &lt;/span&gt;&lt;span&gt;COLLATE&lt;/span&gt;&lt;span&gt; utf8mb4_unicode_ci;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;CREATE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;USER&lt;/span&gt;&lt;span&gt; &apos;&lt;/span&gt;&lt;span&gt;campus_blog&lt;/span&gt;&lt;span&gt;&apos;@&lt;/span&gt;&lt;span&gt;&apos;%&apos;&lt;/span&gt;&lt;span&gt; IDENTIFIED &lt;/span&gt;&lt;span&gt;BY&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;your_password_here&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;GRANT&lt;/span&gt;&lt;span&gt; ALL PRIVILEGES &lt;/span&gt;&lt;span&gt;ON&lt;/span&gt;&lt;span&gt; campus_blog.* &lt;/span&gt;&lt;span&gt;TO&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;campus_blog&apos;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;&apos;%&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;FLUSH PRIVILEGES;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;逐行解释：&lt;/p&gt;
























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;语句&lt;/th&gt;&lt;th&gt;作用&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;CREATE DATABASE campus_blog ...&lt;/code&gt;&lt;/td&gt;&lt;td&gt;创建一个名叫 campus_blog 的数据库，字符集 utf8mb4（支持 emoji）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;CREATE USER &apos;campus_blog&apos; ...&lt;/code&gt;&lt;/td&gt;&lt;td&gt;创建一个专用账号，避免直接用 root&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;GRANT ALL PRIVILEGES ...&lt;/code&gt;&lt;/td&gt;&lt;td&gt;把这个数据库的所有权限给这个账号&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;FLUSH PRIVILEGES&lt;/code&gt;&lt;/td&gt;&lt;td&gt;刷新权限使生效&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;2.7 安装数据库可视化工具&lt;a href=&quot;#27-安装数据库可视化工具&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;光用命令行操作 MySQL 太累，我们要装个可视化工具：&lt;/p&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;工具&lt;/th&gt;&lt;th&gt;价格&lt;/th&gt;&lt;th&gt;推荐&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Navicat&lt;/td&gt;&lt;td&gt;收费&lt;/td&gt;&lt;td&gt;⭐⭐⭐⭐⭐&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;DataGrip（JetBrains）&lt;/td&gt;&lt;td&gt;收费（学生免费）&lt;/td&gt;&lt;td&gt;⭐⭐⭐⭐⭐&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;DBeaver&lt;/td&gt;&lt;td&gt;免费开源&lt;/td&gt;&lt;td&gt;⭐⭐⭐⭐&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;IDEA 自带 Database 面板（专业版）&lt;/td&gt;&lt;td&gt;内置&lt;/td&gt;&lt;td&gt;⭐⭐⭐⭐⭐ 推荐！&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;如果你用 IDEA 专业版，直接用右侧的 “Database” 面板：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;点 + → Data Source → MySQL&lt;/li&gt;
&lt;li&gt;填 Host、Port (3306)、User、Password、Database (campus_blog)&lt;/li&gt;
&lt;li&gt;点 Test Connection&lt;/li&gt;
&lt;li&gt;第一次会让你下载 driver，点 Download&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;成功后你能在面板里看到所有表、字段，能直接写 SQL 查询。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;2.8 验证整套环境&lt;a href=&quot;#28-验证整套环境&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;我们用一个最小项目把流程跑通。打开 IDEA：&lt;/p&gt;&lt;p&gt;1️⃣ New Project → 选 Maven，JDK 选 21
2️⃣ GroupId: &lt;code&gt;com.test&lt;/code&gt;, ArtifactId: &lt;code&gt;hello-test&lt;/code&gt;
3️⃣ 创建后等 Maven 下载完依赖
4️⃣ 在 &lt;code&gt;src/main/java&lt;/code&gt; 下新建 &lt;code&gt;com.test.HelloApp&lt;/code&gt; 类：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;package&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;com.test&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;HelloApp&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;static&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;main&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt;[] &lt;/span&gt;&lt;span&gt;args&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;System&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;out&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;println&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;环境装好啦！🎉&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;5️⃣ 右键 → Run ‘HelloApp.main()’
6️⃣ 看到底部控制台输出 “环境装好啦！🎉” 就成功了&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;2.9 常见环境问题排查&lt;a href=&quot;#29-常见环境问题排查&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;零基础最容易踩的坑：&lt;/p&gt;












































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;问题&lt;/th&gt;&lt;th&gt;原因&lt;/th&gt;&lt;th&gt;解决方法&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;java -version&lt;/code&gt; 找不到命令&lt;/td&gt;&lt;td&gt;JAVA_HOME 或 Path 没配对&lt;/td&gt;&lt;td&gt;重新检查环境变量，&lt;strong&gt;重新打开&lt;/strong&gt; cmd&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;IDEA 显示 “Cannot resolve symbol”&lt;/td&gt;&lt;td&gt;Maven 没下载完依赖&lt;/td&gt;&lt;td&gt;右键 pom.xml → Maven → Reload Project&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;@Data&lt;/code&gt; 等 Lombok 注解爆红&lt;/td&gt;&lt;td&gt;注解处理器没启用&lt;/td&gt;&lt;td&gt;见 2.4 节&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Maven 下载特别慢&lt;/td&gt;&lt;td&gt;没配阿里云镜像&lt;/td&gt;&lt;td&gt;见 2.5 节&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;连不上 MySQL&lt;/td&gt;&lt;td&gt;密码错 / 端口被占 / 防火墙&lt;/td&gt;&lt;td&gt;检查 .env 里的密码、端口；用可视化工具先测连通&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;项目启动报 “端口已占用”&lt;/td&gt;&lt;td&gt;8825 端口被别的程序占了&lt;/td&gt;&lt;td&gt;在 .env 里改 SERVER_PORT 或杀掉占端口的进程&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;启动后说 “找不到 DB_HOST”&lt;/td&gt;&lt;td&gt;没建 .env 文件&lt;/td&gt;&lt;td&gt;&lt;code&gt;cp .env.example .env&lt;/code&gt;，再修改值&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;hr /&gt;&lt;p&gt;环境装好了，恭喜你迈过最痛苦的一关 💪 接下来我们认识 Maven 和项目结构。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h1&gt;第 3 章 Maven 与项目结构&lt;a href=&quot;#第-3-章-maven-与项目结构&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h1&gt;&lt;section&gt;&lt;h2&gt;3.1 Maven 是什么&lt;a href=&quot;#31-maven-是什么&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Maven&lt;/strong&gt; 是一个 &lt;strong&gt;项目构建和依赖管理工具&lt;/strong&gt;。这句话翻译成人话：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;依赖管理&lt;/strong&gt;：你想用别人写好的库（比如 Spring Boot、MySQL 驱动），不用自己去网上找 jar 包，只要在 pom.xml 里写一行配置，Maven 自动帮你下载。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;项目构建&lt;/strong&gt;：编译、打包、运行测试，一行命令搞定。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;🌰 类比：&lt;/p&gt;
























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;你认识的&lt;/th&gt;&lt;th&gt;类似的工具&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;安卓开发&lt;/td&gt;&lt;td&gt;Gradle&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;前端开发&lt;/td&gt;&lt;td&gt;npm / yarn / pnpm&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Python 开发&lt;/td&gt;&lt;td&gt;pip / poetry&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Java 开发&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Maven&lt;/strong&gt; / Gradle&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;它们都在做同一件事：管理项目用到的”积木块”。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;3.2 pom.xml 文件结构详解&lt;a href=&quot;#32-pomxml-文件结构详解&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;pom.xml&lt;/code&gt; 是 Maven 项目的”身份证”，POM = Project Object Model（项目对象模型）。我们看一下我们项目的实际 pom.xml（节选）：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;project&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;xmlns&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;http://maven.apache.org/POM/4.0.0&quot;&lt;/span&gt;&lt;span&gt; ...&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;modelVersion&lt;/span&gt;&lt;span&gt;&amp;gt;4.0.0&amp;lt;/&lt;/span&gt;&lt;span&gt;modelVersion&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;parent&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&amp;gt;org.springframework.boot&amp;lt;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&amp;gt;spring-boot-starter-parent&amp;lt;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&amp;gt;3.0.12&amp;lt;/&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;relativePath&lt;/span&gt;&lt;span&gt;/&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;parent&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&amp;gt;com.example&amp;lt;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&amp;gt;edu_project&amp;lt;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&amp;gt;0.0.1-SNAPSHOT&amp;lt;/&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;&amp;gt;edu_project&amp;lt;/&lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;description&lt;/span&gt;&lt;span&gt;&amp;gt;校园博客论坛系统 - 后端&amp;lt;/&lt;/span&gt;&lt;span&gt;description&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;properties&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;java.version&lt;/span&gt;&lt;span&gt;&amp;gt;21&amp;lt;/&lt;/span&gt;&lt;span&gt;java.version&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;mybatis-plus.version&lt;/span&gt;&lt;span&gt;&amp;gt;3.5.5&amp;lt;/&lt;/span&gt;&lt;span&gt;mybatis-plus.version&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;properties&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;23&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;dependencies&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;24&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;25&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;dependencies&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;26&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;project&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;骨架解读：&lt;/p&gt;








































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;标签&lt;/th&gt;&lt;th&gt;含义&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;&amp;lt;modelVersion&amp;gt;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;POM 的版本，固定 4.0.0&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;&amp;lt;parent&amp;gt;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;父工程，继承其依赖管理&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;&amp;lt;groupId&amp;gt;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;组织 ID，类似公司域名反写&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;&amp;lt;artifactId&amp;gt;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;项目 ID（artifact = 工件）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;&amp;lt;version&amp;gt;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;项目版本号&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;&amp;lt;properties&amp;gt;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;属性配置，可被下面引用&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;&amp;lt;dependencies&amp;gt;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;项目依赖列表&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;&amp;lt;build&amp;gt;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;构建相关配置（插件）&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;3.3 依赖管理：groupId / artifactId / version&lt;a href=&quot;#33-依赖管理groupid--artifactid--version&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;每个 Java 库都用三个坐标唯一标识，叫 &lt;strong&gt;GAV（GroupId-ArtifactId-Version）&lt;/strong&gt;：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&amp;gt;com.baomidou&amp;lt;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&amp;gt;mybatis-plus-boot-starter&amp;lt;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&amp;gt;3.5.5&amp;lt;/&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;这三行说：“我要 baomidou 公司开发的、名为 mybatis-plus-boot-starter 的、3.5.5 版本的库。”&lt;/p&gt;&lt;p&gt;Maven 拿到这个坐标后，会去仓库（阿里云镜像）找对应的 jar 包，下载到你电脑的 &lt;code&gt;~/.m2/repository/&lt;/code&gt; 目录里。&lt;/p&gt;&lt;p&gt;🌰 类比：GAV 就像快递地址：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;groupId = 城市&lt;/li&gt;
&lt;li&gt;artifactId = 街道+门牌号&lt;/li&gt;
&lt;li&gt;version = 具体楼层&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;3.4 父工程 spring-boot-starter-parent&lt;a href=&quot;#34-父工程-spring-boot-starter-parent&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;parent&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&amp;gt;org.springframework.boot&amp;lt;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&amp;gt;spring-boot-starter-parent&amp;lt;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&amp;gt;3.0.12&amp;lt;/&lt;/span&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;parent&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;这一段表示我们继承了 Spring Boot 的父工程。继承的好处是：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;版本统一&lt;/strong&gt;：父工程已经规定好了几百个常用库的版本，我们写依赖时可以&lt;strong&gt;省略 version&lt;/strong&gt;，直接用父工程的版本。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置统一&lt;/strong&gt;：编码、JDK 版本、插件版本都已经默认好了。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;类比一下：父工程像”一份大礼包套餐”，里面所有东西都已经搭配好。你只要从套餐里”挑你想要的”，不用自己一个个调味。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;3.5 starter 启动器&lt;a href=&quot;#35-starter-启动器&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Spring Boot 有一个超棒的发明叫 &lt;strong&gt;starter（启动器）&lt;/strong&gt;。&lt;/p&gt;&lt;p&gt;看我们项目里的：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&amp;gt;org.springframework.boot&amp;lt;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&amp;gt;spring-boot-starter-web&amp;lt;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;这个 &lt;code&gt;spring-boot-starter-web&lt;/code&gt; 实际上是个 &lt;strong&gt;“全家桶”&lt;/strong&gt;：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;Spring MVC&lt;/li&gt;
&lt;li&gt;Tomcat（内置 Web 服务器）&lt;/li&gt;
&lt;li&gt;Jackson（JSON 处理）&lt;/li&gt;
&lt;li&gt;几十个常用 Web 依赖&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;只引一个 starter，就把做 Web 开发的所有东西都备齐了，太爽了。&lt;/p&gt;&lt;p&gt;我们项目用到的主要 starter：&lt;/p&gt;
































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;starter&lt;/th&gt;&lt;th&gt;作用&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;spring-boot-starter-web&lt;/td&gt;&lt;td&gt;Web 开发（MVC、Tomcat）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;spring-boot-starter-security&lt;/td&gt;&lt;td&gt;安全认证&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;spring-boot-starter-validation&lt;/td&gt;&lt;td&gt;参数校验&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;spring-boot-starter-test&lt;/td&gt;&lt;td&gt;单元测试&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;mybatis-plus-boot-starter&lt;/td&gt;&lt;td&gt;MyBatis Plus 数据库 ORM&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;knife4j-openapi3-jakarta-spring-boot-starter&lt;/td&gt;&lt;td&gt;API 文档 UI&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;3.6 标准的 Maven 项目目录结构&lt;a href=&quot;#36-标准的-maven-项目目录结构&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;edu_project/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;├── pom.xml                  ← Maven 配置&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;├── src/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;│   ├── main/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;│   │   ├── java/            ← Java 源代码&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;│   │   │   └── com/example/edu_project/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;│   │   └── resources/       ← 配置文件、静态资源&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;│   │       ├── application.yml&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;│   │       └── mapper/      ← MyBatis XML&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;│   └── test/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;│       └── java/            ← 测试代码&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;└── target/                  ← 编译输出（自动生成，不要手动改）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;牢记 3 大区域：&lt;/p&gt;




















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;路径&lt;/th&gt;&lt;th&gt;用途&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;src/main/java&lt;/code&gt;&lt;/td&gt;&lt;td&gt;业务代码&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;src/main/resources&lt;/code&gt;&lt;/td&gt;&lt;td&gt;配置文件（yml、xml、静态资源）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;src/test/java&lt;/code&gt;&lt;/td&gt;&lt;td&gt;单元测试代码&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;3.7 解读本项目的目录结构&lt;a href=&quot;#37-解读本项目的目录结构&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;打开 &lt;code&gt;D:\MyCode\edu_project\src\main\java\com\example\edu_project\&lt;/code&gt; 你会看到：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;edu_project/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;├── controller/   ← 控制器层（接收 HTTP 请求）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;├── service/      ← 业务逻辑层&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;│   └── impl/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;├── mapper/       ← 数据库访问层&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;├── entity/       ← 实体类（对应数据库表）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;├── dto/          ← Data Transfer Object（接收前端传来的数据）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;├── vo/           ← View Object（返回给前端的数据）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;├── config/       ← 配置类&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;├── common/       ← 公共类（Result、异常等）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;├── filter/       ← 过滤器&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;├── utils/        ← 工具类&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;└── EduProjectApplication.java  ← 启动入口&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;这是经典的 &lt;strong&gt;分层架构（Layered Architecture）&lt;/strong&gt;。请求流向：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;浏览器 → Controller → Service → Mapper → MySQL&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;各层职责一句话总结：&lt;/p&gt;







































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;层&lt;/th&gt;&lt;th&gt;职责&lt;/th&gt;&lt;th&gt;例子&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Controller&lt;/td&gt;&lt;td&gt;接 HTTP 请求、返回响应&lt;/td&gt;&lt;td&gt;用户访问 /api/posts&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Service&lt;/td&gt;&lt;td&gt;写业务逻辑&lt;/td&gt;&lt;td&gt;”发帖时积分 +5”&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Mapper&lt;/td&gt;&lt;td&gt;读写数据库&lt;/td&gt;&lt;td&gt;SELECT * FROM blog_post&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Entity&lt;/td&gt;&lt;td&gt;表结构对应的 Java 类&lt;/td&gt;&lt;td&gt;BlogPost.java&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;DTO&lt;/td&gt;&lt;td&gt;前端请求参数&lt;/td&gt;&lt;td&gt;LoginDTO（用户名+密码）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;VO&lt;/td&gt;&lt;td&gt;给前端的数据&lt;/td&gt;&lt;td&gt;UserVO（不含密码）&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;3.8 常用 Maven 命令&lt;a href=&quot;#38-常用-maven-命令&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;在项目根目录打开终端，可以用这些命令：&lt;/p&gt;




































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;命令&lt;/th&gt;&lt;th&gt;作用&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;mvn clean&lt;/code&gt;&lt;/td&gt;&lt;td&gt;清理 target 目录&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;mvn compile&lt;/code&gt;&lt;/td&gt;&lt;td&gt;编译代码&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;mvn test&lt;/code&gt;&lt;/td&gt;&lt;td&gt;运行单元测试&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;mvn package&lt;/code&gt;&lt;/td&gt;&lt;td&gt;打包成 jar&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;mvn clean package&lt;/code&gt;&lt;/td&gt;&lt;td&gt;先清理再打包（最常用）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;mvn install&lt;/code&gt;&lt;/td&gt;&lt;td&gt;打包并放进本地仓库&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;mvn spring-boot:run&lt;/code&gt;&lt;/td&gt;&lt;td&gt;直接启动 Spring Boot 应用&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;我们项目实际上线时通常是：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;mvn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;clean&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;package&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-DskipTests&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;java&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-jar&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;target/edu_project-0.0.1-SNAPSHOT.jar&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;第一行打成 jar（跳过测试节省时间），第二行用 java 启动。&lt;/p&gt;&lt;hr /&gt;&lt;p&gt;讲完了 Maven，你应该对”项目”有了整体感。最后一章我们正式见识 Spring Boot。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h1&gt;第 4 章 Spring Boot 第一课&lt;a href=&quot;#第-4-章-spring-boot-第一课&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h1&gt;&lt;section&gt;&lt;h2&gt;4.1 Spring 框架家族介绍&lt;a href=&quot;#41-spring-框架家族介绍&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;我们经常听到的几个名字，关系是这样的：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;Spring Framework （地基）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;└─ Spring Boot （脚手架，让搭楼变快）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;└─ Spring Cloud （微服务集群方案）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;




















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;名字&lt;/th&gt;&lt;th&gt;一句话介绍&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Spring Framework&lt;/td&gt;&lt;td&gt;老牌 IoC + AOP 框架，1.0 发布于 2004 年&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Spring Boot&lt;/td&gt;&lt;td&gt;简化 Spring 配置的”懒人套餐”，2014 年发布，现在最常用&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Spring Cloud&lt;/td&gt;&lt;td&gt;在 Spring Boot 基础上，提供分布式系统解决方案&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;我们这本书学的是 &lt;strong&gt;Spring Boot&lt;/strong&gt;，因为它已经够用并且最受欢迎。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;4.2 Spring Boot 的”约定大于配置”理念&lt;a href=&quot;#42-spring-boot-的约定大于配置理念&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;老 Spring 时代，要做一个 Web 项目你得写几百行 XML 配置：哪个文件夹放代码、用什么数据库连接池、JSON 怎么序列化……非常痛苦。&lt;/p&gt;&lt;p&gt;Spring Boot 提出了 &lt;strong&gt;“约定大于配置（Convention over Configuration）”&lt;/strong&gt;：&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;我们已经帮你想好了 99% 场景的最佳实践，你&lt;strong&gt;啥都不写就能用&lt;/strong&gt;；只有特殊需求时再覆盖默认值。&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;举个例子：你引了 &lt;code&gt;spring-boot-starter-web&lt;/code&gt;，Spring Boot 自动：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;启动一个 Tomcat 服务器（端口 8080）&lt;/li&gt;
&lt;li&gt;配置 JSON 序列化&lt;/li&gt;
&lt;li&gt;配置静态资源目录&lt;/li&gt;
&lt;li&gt;配置错误页面&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;你只要写 Controller 就行了，相比老 Spring 节省了 90% 的配置工作。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;4.3 启动类 EduProjectApplication 详解&lt;a href=&quot;#43-启动类-eduprojectapplication-详解&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;打开 &lt;code&gt;D:\MyCode\edu_project\src\main\java\com\example\edu_project\EduProjectApplication.java&lt;/code&gt;：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;package&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;com.example.edu_project&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;com.example.edu_project.config.DotenvConfig&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;org.slf4j.Logger&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;org.slf4j.LoggerFactory&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;org.springframework.boot.SpringApplication&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;org.springframework.boot.autoconfigure.SpringBootApplication&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;org.springframework.context.annotation.Import&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;org.springframework.scheduling.annotation.EnableScheduling&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;com.example.edu_project.config.SecurityConfig&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;SpringBootApplication&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;exclude&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;SecurityAutoConfiguration&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;UserDetailsServiceAutoConfiguration&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;})&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Import&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;SecurityConfig&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;EnableScheduling&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;EduProjectApplication&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;static&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;final&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Logger&lt;/span&gt;&lt;span&gt; log &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;LoggerFactory&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getLogger&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;EduProjectApplication&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;23&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;24&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;static&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;main&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt;[] &lt;/span&gt;&lt;span&gt;args&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;25&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;DotenvConfig&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;load&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;26&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;info&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;========================================&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;27&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;SpringApplication&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;EduProjectApplication&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;, args);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;28&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;info&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;   校园博客论坛系统启动成功！&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;29&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;info&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;   API文档地址：http://localhost:8825/api/doc.html&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;30&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;info&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;========================================&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;31&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;32&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;逐段解读：&lt;/p&gt;&lt;section&gt;&lt;h3&gt;&lt;code&gt;@SpringBootApplication&lt;/code&gt;&lt;a href=&quot;#springbootapplication&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;这是 Spring Boot 的&lt;strong&gt;核心注解&lt;/strong&gt;，它实际上是 3 个注解的组合：&lt;/p&gt;




















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;子注解&lt;/th&gt;&lt;th&gt;作用&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;@SpringBootConfiguration&lt;/code&gt;&lt;/td&gt;&lt;td&gt;标记这是一个配置类&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;@EnableAutoConfiguration&lt;/code&gt;&lt;/td&gt;&lt;td&gt;启用自动配置（最神奇的部分！）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;@ComponentScan&lt;/code&gt;&lt;/td&gt;&lt;td&gt;自动扫描本包及子包下所有标有 @Controller、@Service 等注解的类&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;&lt;strong&gt;自动配置&lt;/strong&gt; 的意思是：Spring Boot 会查看你引了哪些 starter，自动帮你配好相关功能。比如发现你引了 mysql-connector-j，就自动准备好数据源配置。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;&lt;code&gt;exclude = {...}&lt;/code&gt;&lt;a href=&quot;#exclude--&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;排除两个默认的 Spring Security 自动配置类，因为我们项目自定义了 &lt;code&gt;SecurityConfig&lt;/code&gt;，不想让默认配置干扰。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;&lt;code&gt;@Import(SecurityConfig.class)&lt;/code&gt;&lt;a href=&quot;#importsecurityconfigclass&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;手动导入我们自己的安全配置类。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;&lt;code&gt;@EnableScheduling&lt;/code&gt;&lt;a href=&quot;#enablescheduling&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;启用定时任务。我们项目里有 &lt;code&gt;JwtSchedulerConfig&lt;/code&gt;，定期清理 JWT 黑名单。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;&lt;code&gt;main&lt;/code&gt; 方法&lt;a href=&quot;#main-方法&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;DotenvConfig&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;load&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;SpringApplication&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;EduProjectApplication&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;, args);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;第一行：先加载 &lt;code&gt;.env&lt;/code&gt; 文件里的环境变量（数据库密码、JWT 密钥等），这是项目自己写的工具。&lt;/p&gt;&lt;p&gt;第二行：启动 Spring Boot，Spring 会扫描所有组件、连接数据库、启动 Tomcat。&lt;/p&gt;&lt;p&gt;启动成功后日志会打印 API 文档地址。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;4.4 application.yml 配置文件解读&lt;a href=&quot;#44-applicationyml-配置文件解读&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;src/main/resources/application.yml&lt;/code&gt; 是 Spring Boot 主配置文件。我们的实际配置（节选）：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;spring&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;application&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;campus-blog-forum&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;datasource&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;driver-class-name&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;com.mysql.cj.jdbc.Driver&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;url&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_NAME}?useUnicode=true&amp;amp;characterEncoding=utf-8&amp;amp;serverTimezone=Asia/Shanghai&amp;amp;useSSL=false&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;username&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;${DB_USERNAME}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;password&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;${DB_PASSWORD}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;hikari&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;minimum-idle&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;maximum-pool-size&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;20&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;pool-name&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;CampusBlogHikariCP&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;mybatis-plus&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;mapper-locations&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;classpath*:/mapper/**/*.xml&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;global-config&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;db-config&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;id-type&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;logic-delete-field&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;isDeleted&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;logic-delete-value&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;logic-not-delete-value&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;23&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;24&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;server&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;25&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;port&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;${SERVER_PORT:8825}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;26&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;servlet&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;27&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;context-path&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;/api&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;28&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;29&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;jwt&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;30&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;secret&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;${JWT_SECRET}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;31&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;expiration&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;${JWT_EXPIRATION}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;32&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;refresh-expiration&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;${JWT_REFRESH_EXPIRATION}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;关键点表格：&lt;/p&gt;




































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;配置&lt;/th&gt;&lt;th&gt;含义&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;spring.application.name&lt;/code&gt;&lt;/td&gt;&lt;td&gt;应用名称&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;spring.datasource.url&lt;/code&gt;&lt;/td&gt;&lt;td&gt;数据库连接地址&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;${DB_HOST}&lt;/code&gt;&lt;/td&gt;&lt;td&gt;引用环境变量（来自 .env 文件）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;${SERVER_PORT:8825}&lt;/code&gt;&lt;/td&gt;&lt;td&gt;默认端口 8825，可被环境变量覆盖&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;server.servlet.context-path: /api&lt;/code&gt;&lt;/td&gt;&lt;td&gt;所有接口前缀加 /api&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;mybatis-plus.global-config.db-config.logic-delete-field&lt;/code&gt;&lt;/td&gt;&lt;td&gt;逻辑删除字段名&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;jwt.secret&lt;/code&gt;&lt;/td&gt;&lt;td&gt;JWT 签名密钥&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;⚠️ 注意 YAML 格式：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;用 &lt;strong&gt;空格&lt;/strong&gt; 缩进，绝对不能用 Tab！&lt;/li&gt;
&lt;li&gt;冒号后面要有 &lt;strong&gt;一个空格&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;同一层级的字段缩进要一致&lt;/li&gt;
&lt;/ul&gt;&lt;section&gt;&lt;h3&gt;为什么用环境变量？&lt;a href=&quot;#为什么用环境变量&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;直接把数据库密码写在 yml 里推到 GitHub？那等于把家门钥匙挂在大门上！我们用 &lt;code&gt;${DB_PASSWORD}&lt;/code&gt; 占位符，真实值放在本地 &lt;code&gt;.env&lt;/code&gt; 文件里（已加进 &lt;code&gt;.gitignore&lt;/code&gt;）。&lt;/p&gt;&lt;p&gt;&lt;code&gt;.env.example&lt;/code&gt; 就是模板，让别人 &lt;code&gt;cp .env.example .env&lt;/code&gt; 后填自己的值：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;DB_HOST&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;IP&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;DB_PORT&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;3306&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;DB_NAME&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;campus_blog&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;DB_USERNAME&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;campus_blog&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;DB_PASSWORD&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;your_database_password_here&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;JWT_SECRET&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;your_jwt_secret_key_here_minimum_32_characters&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;JWT_EXPIRATION&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;86400000&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;JWT_REFRESH_EXPIRATION&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;604800000&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;SERVER_PORT&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;8825&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;⚠️ JWT_SECRET 必须 &lt;strong&gt;至少 32 位&lt;/strong&gt;，否则 JJWT 会拒绝签名。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;4.5 第一个 RestController 示例&lt;a href=&quot;#45-第一个-restcontroller-示例&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;让我们写一个简单接口感受 Spring Boot 的魅力：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;package&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;com.example.edu_project.controller&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;org.springframework.web.bind.annotation.GetMapping&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;org.springframework.web.bind.annotation.RequestParam&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;org.springframework.web.bind.annotation.RestController&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;RestController&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;HelloController&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;GetMapping&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/hello&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;hello&lt;/span&gt;&lt;span&gt;(@&lt;/span&gt;&lt;span&gt;RequestParam&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;defaultValue&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;World&quot;&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Hello, &quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; name &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;! 欢迎来到校园博客论坛 🎉&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;代码解析：&lt;/p&gt;
























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;注解/语法&lt;/th&gt;&lt;th&gt;作用&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;@RestController&lt;/code&gt;&lt;/td&gt;&lt;td&gt;标记这是个 REST 控制器，所有方法返回的内容会直接写到 HTTP 响应体（JSON/字符串）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;@GetMapping(&quot;/hello&quot;)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;把 HTTP GET 请求 &lt;code&gt;/hello&lt;/code&gt; 映射到这个方法&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;@RequestParam&lt;/code&gt;&lt;/td&gt;&lt;td&gt;从 URL 中取参数，比如 &lt;code&gt;?name=张三&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;defaultValue = &quot;World&quot;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;没传 name 时默认用 “World”&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;启动项目后，浏览器访问：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;http://localhost:8825/api/hello&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;http://localhost:8825/api/hello?name=刘畅&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;注意路径是 &lt;code&gt;/api/hello&lt;/code&gt; 而不是 &lt;code&gt;/hello&lt;/code&gt;，因为我们在 yml 里设了 &lt;code&gt;context-path: /api&lt;/code&gt;。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;4.6 项目运行：mvn spring-boot vs java -jar&lt;a href=&quot;#46-项目运行mvn-spring-boot-vs-java--jar&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;两种运行方式对比：&lt;/p&gt;



















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;方式&lt;/th&gt;&lt;th&gt;命令&lt;/th&gt;&lt;th&gt;适合场景&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;开发期运行&lt;/td&gt;&lt;td&gt;&lt;code&gt;mvn spring-boot:run&lt;/code&gt; 或 IDEA 直接 Run&lt;/td&gt;&lt;td&gt;边改代码边跑&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;打包后运行&lt;/td&gt;&lt;td&gt;&lt;code&gt;mvn clean package&lt;/code&gt; → &lt;code&gt;java -jar target/edu_project-0.0.1-SNAPSHOT.jar&lt;/code&gt;&lt;/td&gt;&lt;td&gt;部署上线&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;打包后的 jar 文件叫 &lt;strong&gt;fat jar（胖 jar）&lt;/strong&gt;，里面已经把 Tomcat 都打进去了，部署到任何装了 Java 21 的机器上都能跑：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;java&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-jar&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;edu_project-0.0.1-SNAPSHOT.jar&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;✨ 这就是 Spring Boot 最爽的特性：&lt;strong&gt;不需要单独装 Tomcat&lt;/strong&gt;！&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;4.7 访问地址 &amp;amp; Knife4j 文档&lt;a href=&quot;#47-访问地址--knife4j-文档&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;启动成功后，你能访问以下地址：&lt;/p&gt;




















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;地址&lt;/th&gt;&lt;th&gt;用途&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;a href=&quot;http://localhost:8825/api&quot; target=&quot;_blank&quot;&gt;http://localhost:8825/api&lt;/a&gt;&lt;/td&gt;&lt;td&gt;接口根地址&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;a href=&quot;http://localhost:8825/api/doc.html&quot; target=&quot;_blank&quot;&gt;http://localhost:8825/api/doc.html&lt;/a&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Knife4j 接口文档（重点！）&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;a href=&quot;http://localhost:8825/api/v3/api-docs&quot; target=&quot;_blank&quot;&gt;http://localhost:8825/api/v3/api-docs&lt;/a&gt;&lt;/td&gt;&lt;td&gt;OpenAPI JSON 原始数据&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;section&gt;&lt;h3&gt;Knife4j 是什么&lt;a href=&quot;#knife4j-是什么&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Knife4j 是基于 Swagger 的增强版 API 文档工具。打开 doc.html 你会看到一个超漂亮的网页：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;左侧是按模块分组的接口列表（用户、文章、评论…）&lt;/li&gt;
&lt;li&gt;中间是接口详情：URL、参数、响应&lt;/li&gt;
&lt;li&gt;右侧可以&lt;strong&gt;直接在线测试接口&lt;/strong&gt;，不需要 Postman&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;⚠️ 默认管理员账号是 &lt;code&gt;admin&lt;/code&gt; / &lt;code&gt;admin123&lt;/code&gt;，可以登录后试用所有需要鉴权的接口。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;项目结构再回顾&lt;a href=&quot;#项目结构再回顾&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;至此你应该对项目有完整的画面感了：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;浏览器访问 http://localhost:8825/api/posts&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;↓&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;Tomcat 接收请求&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;↓&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;Spring 路由到 PostController&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;↓&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;PostController 调 PostService&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;↓&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;PostService 调 PostMapper&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;↓&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;MyBatis Plus 生成 SQL，访问 MySQL&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;↓&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;查到数据 → 一路返回 → 序列化成 JSON&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;↓&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;前端拿到结果显示&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;这就是一个典型的 Spring Boot RESTful 应用的请求流程。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;第一部分小结&lt;a href=&quot;#第一部分小结&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;🎉 恭喜你读完了第一部分！让我们回顾一下：&lt;/p&gt;&lt;p&gt;1️⃣ &lt;strong&gt;第 1 章&lt;/strong&gt; 学了 Java 语言基础：变量、控制流、类与对象、集合、异常、注解、包
2️⃣ &lt;strong&gt;第 2 章&lt;/strong&gt; 装好了开发环境：JDK 21、IDEA、Lombok、Maven、MySQL
3️⃣ &lt;strong&gt;第 3 章&lt;/strong&gt; 认识了 Maven 和我们项目的目录结构
4️⃣ &lt;strong&gt;第 4 章&lt;/strong&gt; 第一次接触 Spring Boot：启动类、yml 配置、写了一个 hello 接口&lt;/p&gt;&lt;p&gt;到这里你应该能：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;看懂 Java 基本语法&lt;/li&gt;
&lt;li&gt;把项目跑起来&lt;/li&gt;
&lt;li&gt;写出最简单的 GET 接口&lt;/li&gt;
&lt;li&gt;看懂项目的整体架构&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;接下来在第二部分，我们会深入 Controller、Service、Mapper 的写法，开始真正动手开发”用户注册登录”这样的实际功能。&lt;/p&gt;&lt;p&gt;请休息一下，喝杯水，做几个深呼吸，然后我们继续 💪&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;记住开发荣耻第一条：&lt;strong&gt;以瞎清接口为耻，以认真查询为荣。&lt;/strong&gt;
写代码不是在炫技，是在认真解决问题。慢就是快。&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;下一部分见！&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h1&gt;《Java Spring Boot 零基础完整学习手册》&lt;a href=&quot;#java-spring-boot-零基础完整学习手册-2&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h1&gt;&lt;section&gt;&lt;h2&gt;第二部分 框架核心&lt;a href=&quot;#第二部分-框架核心&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;blockquote&gt;&lt;p&gt;案例项目：校园博客论坛系统（Campus Blog）
适合人群：完全没有 Java Web 经验的初学者
写作原则：每个概念都有比喻、每段代码都来自真实项目&lt;/p&gt;&lt;/blockquote&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h1&gt;第 5 章 MyBatis Plus 持久层框架&lt;a href=&quot;#第-5-章-mybatis-plus-持久层框架&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h1&gt;&lt;section&gt;&lt;h2&gt;5.1 ORM 思想：类↔表，对象↔行&lt;a href=&quot;#51-orm-思想类表对象行&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;刚开始接触 Java Web 的朋友，最容易卡住的地方就是”数据库 SQL”和”Java 代码”之间的鸿沟。数据库里有一张表，比如 &lt;code&gt;sys_user&lt;/code&gt;，它有 &lt;code&gt;id&lt;/code&gt;、&lt;code&gt;username&lt;/code&gt;、&lt;code&gt;password&lt;/code&gt; 这些列。Java 程序里有一个类，比如 &lt;code&gt;SysUser&lt;/code&gt;，它有 &lt;code&gt;id&lt;/code&gt;、&lt;code&gt;username&lt;/code&gt;、&lt;code&gt;password&lt;/code&gt; 这些字段。它们看起来是一回事，但底层完全不同：一个是关系型数据库的二维表，一个是 JVM 内存里的对象。&lt;/p&gt;&lt;p&gt;ORM（Object-Relational Mapping，对象关系映射）就是把这两个世界连起来的”翻译官”。它的核心思想可以用一个比喻来理解：&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;把数据库表想象成一个 Excel 表格。
表的”表头”（列名）= Java 类里的”字段名”。
表的”每一行”= Java 里的”一个对象”。
你在 Java 里 &lt;code&gt;new SysUser()&lt;/code&gt;，就好比在 Excel 里新增一行；你给对象的字段赋值，就好比往单元格里填数据；最后你保存对象到数据库，就好比把这一行写到 Excel 里。&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;有了 ORM，你就不用一直手写 &lt;code&gt;INSERT INTO sys_user (username, password) VALUES (?, ?)&lt;/code&gt; 这种 SQL 了。框架会替你拼 SQL、执行、把结果集映射回 Java 对象。&lt;/p&gt;&lt;p&gt;ORM 的三组对应关系一定要牢记：&lt;/p&gt;




















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;数据库世界&lt;/th&gt;&lt;th&gt;Java 世界&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;表（table）&lt;/td&gt;&lt;td&gt;类（class）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;行（row）&lt;/td&gt;&lt;td&gt;对象（object）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;列（column）&lt;/td&gt;&lt;td&gt;字段（field）&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;5.2 MyBatis 与 MyBatis Plus 的区别&lt;a href=&quot;#52-mybatis-与-mybatis-plus-的区别&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;MyBatis 是国内最流行的 ORM 框架之一，特点是”半自动”：你写 SQL，它帮你映射结果。但即便是简单的 CRUD（增删改查），也得写一堆 XML 或注解 SQL，重复且枯燥。&lt;/p&gt;&lt;p&gt;MyBatis Plus（简称 MP）是在 MyBatis 之上做的”增强工具”，特点是”全自动 + 可扩展”：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;简单的 CRUD（按 ID 查、新增、更新、删除、分页）：完全不用写 SQL，框架自动生成。&lt;/li&gt;
&lt;li&gt;复杂的业务 SQL：你照样可以用 MyBatis 的 &lt;code&gt;@Select&lt;/code&gt;、XML 写自定义 SQL，两种方式无缝共存。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;形象比喻：MyBatis 像一台手动挡汽车，所有挡位都得自己换；MyBatis Plus 则像自动挡，普通路况自动操作，遇到山路你可以切手动模式。&lt;/p&gt;&lt;p&gt;本项目的 &lt;code&gt;pom.xml&lt;/code&gt; 里引入的就是 MyBatis Plus 3.5.5，整个项目几乎所有简单 CRUD 都没有写一行 SQL，全靠框架生成。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;5.3 实体类的关键注解（基于真实代码）&lt;a href=&quot;#53-实体类的关键注解基于真实代码&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;实体类（Entity）是 ORM 里”表”的 Java 化身。我们以本项目的 &lt;code&gt;SysUser.java&lt;/code&gt; 为例（路径：&lt;code&gt;src/main/java/com/example/edu_project/entity/SysUser.java&lt;/code&gt;）：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Data&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;TableName&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;sys_user&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SysUser&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;implements&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Serializable&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;TableId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;IdType&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;AUTO&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; id&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; username&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;JsonIgnore&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; password&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; nickname&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; avatar&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; email&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; role&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Integer&lt;/span&gt;&lt;span&gt; status&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;TableField&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;fill&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;FieldFill&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;INSERT&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Integer&lt;/span&gt;&lt;span&gt; loginFailCount&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LocalDateTime&lt;/span&gt;&lt;span&gt; lockUntil&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;23&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;24&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;TableField&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;fill&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;FieldFill&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;INSERT&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;25&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LocalDateTime&lt;/span&gt;&lt;span&gt; createTime&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;26&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;27&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;TableField&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;fill&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;FieldFill&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;INSERT_UPDATE&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;28&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LocalDateTime&lt;/span&gt;&lt;span&gt; updateTime&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;29&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;30&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;TableLogic&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;31&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Integer&lt;/span&gt;&lt;span&gt; isDeleted&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;32&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;这里出现的注解，是你今后每写一个实体类都会用到的”四大金刚”：&lt;/p&gt;&lt;section&gt;&lt;h3&gt;&lt;code&gt;@TableName(&quot;sys_user&quot;)&lt;/code&gt;&lt;a href=&quot;#tablenamesys_user&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;告诉 MyBatis Plus：“这个 Java 类对应数据库里的 &lt;code&gt;sys_user&lt;/code&gt; 表”。如果你把表名写成蛇形（小写下划线），类名写成帕斯卡（大写驼峰），MP 也能默认推断（&lt;code&gt;SysUser&lt;/code&gt; → &lt;code&gt;sys_user&lt;/code&gt;），但显式写出来更清晰、更安全。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;&lt;code&gt;@TableId(type = IdType.AUTO)&lt;/code&gt;&lt;a href=&quot;#tableidtype--idtypeauto&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;声明这个字段是数据库的主键，并指定主键策略：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;IdType.AUTO&lt;/code&gt;：交给数据库自增（MySQL 的 &lt;code&gt;AUTO_INCREMENT&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;IdType.ASSIGN_ID&lt;/code&gt;：MP 用雪花算法生成全局唯一 ID（分布式场景）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;IdType.INPUT&lt;/code&gt;：用户自己传入。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;本项目 &lt;code&gt;SysUser&lt;/code&gt; 用的是 &lt;code&gt;AUTO&lt;/code&gt;，简单直接，依赖 MySQL 的自增能力。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;&lt;code&gt;@TableField&lt;/code&gt;&lt;a href=&quot;#tablefield&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;控制字段的细节行为。最常用的两个用法：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;指定列名&lt;/strong&gt;：当 Java 字段名和数据库列名不一致时（比如 &lt;code&gt;userName&lt;/code&gt; 对应 &lt;code&gt;user_name&lt;/code&gt;），可以写 &lt;code&gt;@TableField(&quot;user_name&quot;)&lt;/code&gt;。MP 默认会把驼峰转下划线，所以大部分时候不用写。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自动填充&lt;/strong&gt;：&lt;code&gt;@TableField(fill = FieldFill.INSERT)&lt;/code&gt; 表示插入时框架自动填值；&lt;code&gt;FieldFill.INSERT_UPDATE&lt;/code&gt; 表示插入和更新时都自动填值。这跟 5.5 节的 &lt;code&gt;MyMetaObjectHandler&lt;/code&gt; 配合工作。&lt;/li&gt;
&lt;/ol&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;&lt;code&gt;@TableLogic&lt;/code&gt;&lt;a href=&quot;#tablelogic&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;逻辑删除标记。本项目里 &lt;code&gt;isDeleted&lt;/code&gt; 字段加了这个注解后，调用 &lt;code&gt;mapper.deleteById(1L)&lt;/code&gt; 时，MP 不会真的执行 &lt;code&gt;DELETE&lt;/code&gt;，而是会执行 &lt;code&gt;UPDATE sys_user SET is_deleted = 1 WHERE id = 1&lt;/code&gt;。从此以后所有查询会自动加上 &lt;code&gt;WHERE is_deleted = 0&lt;/code&gt;，被”删除”的数据对业务层面隐形，但实际仍然存在于数据库。&lt;/p&gt;&lt;p&gt;⚠️ 这是企业级开发的标配。物理删除的数据找不回来，逻辑删除随时可以恢复。本项目最近一次安全升级（v1.28）就是把所有物理删除改成了逻辑删除。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;&lt;code&gt;@JsonIgnore&lt;/code&gt;&lt;a href=&quot;#jsonignore&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;虽然这是 Jackson 的注解（不是 MP 的），但很关键。&lt;code&gt;SysUser&lt;/code&gt; 的 &lt;code&gt;password&lt;/code&gt; 字段加了 &lt;code&gt;@JsonIgnore&lt;/code&gt;，意思是”序列化为 JSON 时跳过这个字段”。这样即使你不小心把 &lt;code&gt;SysUser&lt;/code&gt; 直接返回给前端，密文密码也不会泄露。安全第一。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;&lt;code&gt;@Data&lt;/code&gt;（Lombok）&lt;a href=&quot;#datalombok&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Lombok 的 &lt;code&gt;@Data&lt;/code&gt; 自动生成 Getter、Setter、&lt;code&gt;toString&lt;/code&gt;、&lt;code&gt;equals&lt;/code&gt;、&lt;code&gt;hashCode&lt;/code&gt;。如果没有它，一个有 14 个字段的 &lt;code&gt;SysUser&lt;/code&gt; 类要写几百行样板代码。&lt;/p&gt;&lt;p&gt;我们再快速看一下 &lt;code&gt;BlogPost.java&lt;/code&gt;，结构非常类似：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Data&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;TableName&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;blog_post&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BlogPost&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;implements&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Serializable&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;TableId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;IdType&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;AUTO&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; id&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; userId&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; title&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; content&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Integer&lt;/span&gt;&lt;span&gt; viewCount&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Integer&lt;/span&gt;&lt;span&gt; likeCount&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;TableField&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;fill&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;FieldFill&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;INSERT&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LocalDateTime&lt;/span&gt;&lt;span&gt; createTime&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;TableField&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;fill&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;FieldFill&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;INSERT_UPDATE&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LocalDateTime&lt;/span&gt;&lt;span&gt; updateTime&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;TableLogic&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Integer&lt;/span&gt;&lt;span&gt; isDeleted&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;注意到 &lt;code&gt;viewCount&lt;/code&gt;、&lt;code&gt;likeCount&lt;/code&gt; 等都是普通字段，对应数据库的 &lt;code&gt;view_count&lt;/code&gt;、&lt;code&gt;like_count&lt;/code&gt;。MP 默认会把驼峰转下划线匹配。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;5.4 BaseMapper 内置方法&lt;a href=&quot;#54-basemapper-内置方法&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;写好实体类后，下一步就是 Mapper 接口。看本项目的 &lt;code&gt;SysUserMapper.java&lt;/code&gt;：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Mapper&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;interface&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SysUserMapper&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;extends&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BaseMapper&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;SysUser&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 自定义 SQL 方法...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;@Mapper&lt;/code&gt; 告诉 MyBatis 这是一个 Mapper 接口，需要被扫描；&lt;code&gt;extends BaseMapper&amp;lt;SysUser&amp;gt;&lt;/code&gt; 一句话就让你”白嫖”了所有 CRUD 方法。BaseMapper 里到底有哪些方法？常用的有：&lt;/p&gt;








































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;方法&lt;/th&gt;&lt;th&gt;作用&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;insert(T entity)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;新增一条记录&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;deleteById(Serializable id)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;按主键删除（实际是逻辑删除）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;updateById(T entity)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;按主键更新&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;selectById(Serializable id)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;按主键查询&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;selectList(Wrapper&amp;lt;T&amp;gt;)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;按条件查询多条&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;selectOne(Wrapper&amp;lt;T&amp;gt;)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;按条件查询一条&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;selectCount(Wrapper&amp;lt;T&amp;gt;)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;按条件统计行数&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;selectPage(IPage&amp;lt;T&amp;gt;, Wrapper&amp;lt;T&amp;gt;)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;分页查询&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;⚠️ 看清楚：你只是写了一个继承 &lt;code&gt;BaseMapper&lt;/code&gt; 的&lt;strong&gt;空接口&lt;/strong&gt;，就拥有了上面所有方法！这就是 MP 让人爱不释手的地方。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;5.5 自动填充机制&lt;a href=&quot;#55-自动填充机制&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;新增和更新数据时，每次都要手动 &lt;code&gt;setCreateTime(LocalDateTime.now())&lt;/code&gt;、&lt;code&gt;setUpdateTime(LocalDateTime.now())&lt;/code&gt; 吗？太累。MP 提供了”自动填充”机制，本项目里通过 &lt;code&gt;MyMetaObjectHandler.java&lt;/code&gt; 实现：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Slf4j&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Component&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MyMetaObjectHandler&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;implements&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MetaObjectHandler&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Override&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;insertFill&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;MetaObject&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;metaObject&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;info&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;开始插入填充...&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;strictInsertFill&lt;/span&gt;&lt;span&gt;(metaObject, &lt;/span&gt;&lt;span&gt;&quot;createTime&quot;&lt;/span&gt;&lt;span&gt;, LocalDateTime&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;now, &lt;/span&gt;&lt;span&gt;LocalDateTime&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;strictInsertFill&lt;/span&gt;&lt;span&gt;(metaObject, &lt;/span&gt;&lt;span&gt;&quot;updateTime&quot;&lt;/span&gt;&lt;span&gt;, LocalDateTime&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;now, &lt;/span&gt;&lt;span&gt;LocalDateTime&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Override&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;updateFill&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;MetaObject&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;metaObject&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;info&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;开始更新填充...&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;strictUpdateFill&lt;/span&gt;&lt;span&gt;(metaObject, &lt;/span&gt;&lt;span&gt;&quot;updateTime&quot;&lt;/span&gt;&lt;span&gt;, LocalDateTime&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;now, &lt;/span&gt;&lt;span&gt;LocalDateTime&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;它的工作原理：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;任何实体类的字段加上 &lt;code&gt;@TableField(fill = FieldFill.INSERT)&lt;/code&gt;，新增时框架会触发 &lt;code&gt;insertFill&lt;/code&gt; 方法。&lt;/li&gt;
&lt;li&gt;任何字段加上 &lt;code&gt;@TableField(fill = FieldFill.INSERT_UPDATE)&lt;/code&gt;，新增和更新时都会触发 &lt;code&gt;updateFill&lt;/code&gt; 方法。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;strictInsertFill&lt;/code&gt; 是”严格模式”，只在字段为 &lt;code&gt;null&lt;/code&gt; 时才填，已经有值则不覆盖。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LocalDateTime::now&lt;/code&gt; 是 Java 的方法引用，等价于 &lt;code&gt;() -&amp;gt; LocalDateTime.now()&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;效果就是：你 service 层写 &lt;code&gt;userService.save(user)&lt;/code&gt; 时根本不用管时间，MP 会自动给 &lt;code&gt;createTime&lt;/code&gt; 和 &lt;code&gt;updateTime&lt;/code&gt; 塞值。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;5.6 逻辑删除：&lt;code&gt;@TableLogic&lt;/code&gt; + &lt;code&gt;isDeleted&lt;/code&gt;&lt;a href=&quot;#56-逻辑删除tablelogic--isdeleted&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;前面已经提过，这里再深入一点。本项目所有业务表都有 &lt;code&gt;isDeleted&lt;/code&gt; 字段（&lt;code&gt;0=正常，1=已删除&lt;/code&gt;），加了 &lt;code&gt;@TableLogic&lt;/code&gt;。它的好处：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;误删可恢复：把 &lt;code&gt;is_deleted&lt;/code&gt; 改回 &lt;code&gt;0&lt;/code&gt; 即可。&lt;/li&gt;
&lt;li&gt;历史可追溯：删除的数据仍在表里，可用于审计。&lt;/li&gt;
&lt;li&gt;软删 + 关联表保护：避免外键级联删除带来的不可控影响。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;&lt;code&gt;application.yml&lt;/code&gt; 里通常会有全局配置：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;mybatis-plus&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;global-config&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;db-config&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;logic-delete-field&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;isDeleted&lt;/span&gt;&lt;span&gt;   &lt;/span&gt;&lt;span&gt;# 全局逻辑删除字段&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;logic-delete-value&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;           &lt;/span&gt;&lt;span&gt;# 已删除值&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;logic-not-delete-value&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;       &lt;/span&gt;&lt;span&gt;# 未删除值&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;这样后续就算不在每个实体类上写 &lt;code&gt;@TableLogic&lt;/code&gt;，只要字段名叫 &lt;code&gt;isDeleted&lt;/code&gt;，也会自动生效。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;5.7 分页插件（基于 &lt;code&gt;MybatisPlusConfig.java&lt;/code&gt;）&lt;a href=&quot;#57-分页插件基于-mybatisplusconfigjava&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;MP 默认没开启分页，需要手动注册一个分页拦截器。本项目的配置类：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Configuration&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;MapperScan&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;com.example.edu_project.mapper&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MybatisPlusConfig&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Bean&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MybatisPlusInterceptor&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;mybatisPlusInterceptor&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;MybatisPlusInterceptor&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;interceptor&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MybatisPlusInterceptor&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;interceptor&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;addInnerInterceptor&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;PaginationInnerInterceptor&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;DbType&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;MYSQL&lt;/span&gt;&lt;span&gt;));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; interceptor;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;两个关键点：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;&lt;code&gt;@MapperScan(&quot;com.example.edu_project.mapper&quot;)&lt;/code&gt;：告诉 Spring 扫描这个包下所有 Mapper 接口，自动生成代理实现。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PaginationInnerInterceptor(DbType.MYSQL)&lt;/code&gt;：为 MySQL 注入分页 SQL 改写能力。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;启用后你就能这样写：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;Page&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;SysUser&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; page &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Page&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// 第 1 页，每页 10 条&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;IPage&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;SysUser&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; result &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;userMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;selectPage&lt;/span&gt;&lt;span&gt;(page, wrapper);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;result&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getRecords&lt;/span&gt;&lt;span&gt;&lt;span&gt;();&lt;/span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// 当前页数据&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;result&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getTotal&lt;/span&gt;&lt;span&gt;&lt;span&gt;();&lt;/span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// 总记录数&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;result&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getPages&lt;/span&gt;&lt;span&gt;&lt;span&gt;();&lt;/span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// 总页数&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;底层 SQL 框架会自动给你拼成：&lt;code&gt;SELECT ... FROM sys_user ... LIMIT 0, 10&lt;/code&gt;，并额外执行 &lt;code&gt;SELECT count(*) ...&lt;/code&gt; 求总数。&lt;/p&gt;&lt;p&gt;本项目 &lt;code&gt;SysUserServiceImpl.searchUsers&lt;/code&gt; 方法就是这样用的：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;Page&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;SysUser&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; page &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Page&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getPage&lt;/span&gt;&lt;span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getPageSize&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;LambdaQueryWrapper&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;SysUser&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; wrapper &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LambdaQueryWrapper&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getKeyword&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getKeyword&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;trim&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;isEmpty&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; keyword &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getKeyword&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;trim&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;wrapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;and&lt;/span&gt;&lt;span&gt;(w &lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;w&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;like&lt;/span&gt;&lt;span&gt;(SysUser&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getUsername, keyword)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;or&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;like&lt;/span&gt;&lt;span&gt;(SysUser&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getNickname, keyword));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;wrapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;orderByDesc&lt;/span&gt;&lt;span&gt;(SysUser&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getCreateTime);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;IPage&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;SysUser&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; userPage &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt;(page, wrapper);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;5.8 LambdaQueryWrapper 条件构造器&lt;a href=&quot;#58-lambdaquerywrapper-条件构造器&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;Wrapper&lt;/code&gt; 是 MP 用来构造 WHERE 条件的工具，相当于”用 Java 写 SQL 的 WHERE 部分”。最推荐的版本是 &lt;code&gt;LambdaQueryWrapper&lt;/code&gt;，因为它用方法引用 &lt;code&gt;SysUser::getUsername&lt;/code&gt; 代替字符串 &lt;code&gt;&quot;username&quot;&lt;/code&gt;，编译期能检查字段名拼写错误。&lt;/p&gt;&lt;p&gt;常见 API：&lt;/p&gt;








































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;方法&lt;/th&gt;&lt;th&gt;等价 SQL&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;eq(SysUser::getUsername, &quot;刘畅&quot;)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;username = &apos;刘畅&apos;&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;ne(...)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;&amp;lt;&amp;gt;&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;gt / ge / lt / le&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;&amp;gt; / &amp;gt;= / &amp;lt; / &amp;lt;=&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;like(...)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;LIKE &apos;%xxx%&apos;&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;in(...)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;IN (...)&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;between(...)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;BETWEEN a AND b&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;orderByDesc(...)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;ORDER BY xxx DESC&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;and(w -&amp;gt; ...)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;AND (...)&lt;/code&gt; 嵌套条件&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;在本项目登录接口里就有典型用法：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;LambdaQueryWrapper&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;SysUser&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; wrapper &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LambdaQueryWrapper&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;wrapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;eq&lt;/span&gt;&lt;span&gt;(SysUser&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getUsername, &lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getUsername&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;SysUser&lt;/span&gt;&lt;span&gt; user &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getOne&lt;/span&gt;&lt;span&gt;(wrapper);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;5.9 自定义 SQL：&lt;code&gt;@Update&lt;/code&gt; 注解或 XML&lt;a href=&quot;#59-自定义-sqlupdate-注解或-xml&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;简单 CRUD 用 BaseMapper 就够了，但有些场景需要”原子操作”，比如：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;计数器自增：&lt;code&gt;UPDATE sys_user SET follower_count = follower_count + 1 WHERE id = ?&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;复杂条件更新&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;本项目的 &lt;code&gt;SysUserMapper&lt;/code&gt; 就有大量这样的注解 SQL：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Update&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;UPDATE sys_user SET login_fail_count = login_fail_count + 1, &quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;lock_until = CASE WHEN login_fail_count + 1 &amp;gt;= #{maxFailCount} &quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;THEN DATE_ADD(NOW(), INTERVAL #{lockMinutes} MINUTE) ELSE lock_until END &quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;&quot;WHERE id = #{userId} AND (lock_until IS NULL OR lock_until &amp;lt;= NOW())&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;incrementLoginFailCount&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;/span&gt;&lt;span&gt;Param&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;userId&quot;&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt;&lt;span&gt; userId&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                             &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Param&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;maxFailCount&quot;&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt;&lt;span&gt; maxFailCount&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                             &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Param&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;lockMinutes&quot;&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt;&lt;span&gt; lockMinutes)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Update&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;UPDATE sys_user SET follower_count = follower_count + 1 WHERE id = #{userId}&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;incrementFollowerCount&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;/span&gt;&lt;span&gt;Param&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;userId&quot;&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt;&lt;span&gt; userId)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;#{xxx}&lt;/code&gt; 是 MyBatis 的占位符，会自动 PreparedStatement 化，防止 SQL 注入。&lt;code&gt;@Param&lt;/code&gt; 给参数命名以便 SQL 里引用。&lt;/p&gt;&lt;p&gt;⚠️ 这些自增 SQL 在数据库层就保证了原子性，不会出现”100 个人同时点赞，结果计数器只 +1”的并发 Bug。这是&lt;strong&gt;数据库层的并发安全&lt;/strong&gt;，比 Java 层的锁更可靠。&lt;/p&gt;&lt;section&gt;&lt;h3&gt;一句话总结&lt;a href=&quot;#一句话总结&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;blockquote&gt;&lt;p&gt;MyBatis Plus 把”对象”和”表”自动绑定，简单 CRUD 一行 SQL 不用写，复杂 SQL 又能随时手写，是 Java Web 开发的标配。&lt;/p&gt;&lt;/blockquote&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h1&gt;第 6 章 分层架构详解&lt;a href=&quot;#第-6-章-分层架构详解&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h1&gt;&lt;section&gt;&lt;h2&gt;6.1 为什么要分层（饭店比喻）&lt;a href=&quot;#61-为什么要分层饭店比喻&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;想象你去饭店吃饭：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;服务员（Controller）&lt;/strong&gt;：接你的菜单订单，告诉厨房要做什么，最后把菜端给你。他不会做菜，只负责沟通。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;厨师（Service）&lt;/strong&gt;：真正炒菜的人。他会根据菜单决定先切菜还是先开火，把”做菜”的复杂逻辑封装起来。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;采购员（Mapper）&lt;/strong&gt;：去仓库取原料、把原料带回厨房。他不关心怎么烹饪，只管”从数据库拿数据”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;食材（Entity）&lt;/strong&gt;：摆在仓库里的牛肉、青菜，对应数据库表里的一行。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;如果让一个人同时干这三件事，会乱套：服务员一边记单一边切菜一边搬蔬菜。所以企业级项目都强制分层，每一层只做自己的事，便于维护、便于测试、便于团队协作。&lt;/p&gt;&lt;p&gt;Spring Boot 项目的标准分层：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;浏览器 / 前端&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;↓ HTTP&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;Controller（控制器）  ← 接请求、返响应&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;↓ Java 调用&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;Service（业务层）     ← 业务规则、事务&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;↓ Java 调用&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;Mapper（持久层）      ← SQL、与数据库交互&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;↓ JDBC&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;MySQL&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;6.2 Controller 层（基于真实 &lt;code&gt;SysUserController.java&lt;/code&gt;）&lt;a href=&quot;#62-controller-层基于真实-sysusercontrollerjava&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Controller 是”接待大厅”。看看本项目的注册接口长什么样：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Tag&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;用户管理&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;description&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;用户相关接口&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;RestController&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;RequestMapping&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/user&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SysUserController&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Autowired&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SysUserService&lt;/span&gt;&lt;span&gt; sysUserService&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Operation&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;summary&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;用户注册&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;PostMapping&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/register&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;UserRegisterResponse&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;register&lt;/span&gt;&lt;span&gt;(@&lt;/span&gt;&lt;span&gt;Valid&lt;/span&gt;&lt;span&gt; @&lt;/span&gt;&lt;span&gt;RequestBody&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;UserRegisterRequest&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;UserRegisterResponse&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;response&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;sysUserService&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;register&lt;/span&gt;&lt;span&gt;(request);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;success&lt;/span&gt;&lt;span&gt;(response);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;逐行解读：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@RestController&lt;/code&gt;：等于 &lt;code&gt;@Controller + @ResponseBody&lt;/code&gt;，意思是这个类的所有方法直接返回 JSON，不走视图模板。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@RequestMapping(&quot;/user&quot;)&lt;/code&gt;：这个类下的所有接口都以 &lt;code&gt;/user&lt;/code&gt; 开头。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@PostMapping(&quot;/register&quot;)&lt;/code&gt;：完整路径就是 &lt;code&gt;POST /user/register&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@RequestBody&lt;/code&gt;：把请求体的 JSON 反序列化为 &lt;code&gt;UserRegisterRequest&lt;/code&gt; 对象。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Valid&lt;/code&gt;：触发参数校验（详见 7.5 节）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Autowired&lt;/code&gt;：把 &lt;code&gt;SysUserService&lt;/code&gt; 这个 Bean 自动注入进来（详见 6.6 节）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Result.success(response)&lt;/code&gt;：把业务结果包成统一响应格式（详见第 7 章）。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;⚠️ 控制器里不要写业务逻辑，只做三件事：&lt;strong&gt;接参 → 调 service → 返结果&lt;/strong&gt;。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;6.3 Service 层（接口 + 实现类）&lt;a href=&quot;#63-service-层接口--实现类&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Service 层是”厨房”，是业务规则的核心。本项目用 &lt;strong&gt;接口 + 实现类&lt;/strong&gt; 的方式分离：&lt;/p&gt;&lt;p&gt;接口 &lt;code&gt;SysUserService.java&lt;/code&gt;：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;interface&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SysUserService&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;extends&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IService&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;SysUser&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;UserRegisterResponse&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;register&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;UserRegisterRequest&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;UserLoginResponse&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;login&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;UserLoginRequest&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;SysUser&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;getUserById&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;changePassword&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;userId&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;oldPassword&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;newPassword&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;IPage&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;UserVO&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;searchUsers&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;UserSearchRequest&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;实现类 &lt;code&gt;SysUserServiceImpl.java&lt;/code&gt;：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Slf4j&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Service&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SysUserServiceImpl&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;extends&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ServiceImpl&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;SysUserMapper&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SysUser&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;implements&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SysUserService&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Override&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Transactional&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;rollbackFor&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Exception&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;UserRegisterResponse&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;register&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;UserRegisterRequest&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// ... 一堆业务逻辑&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;为什么要”接口 + 实现”？&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;解耦&lt;/strong&gt;：Controller 依赖接口而不是具体实现，将来想换实现（比如对接其他数据源）只需新写一个实现类。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AOP 友好&lt;/strong&gt;：Spring 的事务、日志、缓存代理需要接口才能用 JDK 动态代理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;易测试&lt;/strong&gt;：写单元测试时可以方便地 Mock 接口。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;注意几个关键点：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;extends ServiceImpl&amp;lt;SysUserMapper, SysUser&amp;gt;&lt;/code&gt;：MP 提供的通用 Service 实现，让你在 service 里直接用 &lt;code&gt;this.save()&lt;/code&gt;、&lt;code&gt;this.getById()&lt;/code&gt; 等方法，免去写 &lt;code&gt;userMapper.insert()&lt;/code&gt; 的麻烦。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Service&lt;/code&gt;：告诉 Spring “我是一个 service Bean，请管理我”。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Transactional(rollbackFor = Exception.class)&lt;/code&gt;：所有写操作都加事务，遇到异常自动回滚，保证数据一致性。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Transactional(readOnly = true)&lt;/code&gt;：只读事务可以让数据库做优化（比如只读副本路由），本项目 &lt;code&gt;getUserById&lt;/code&gt;、&lt;code&gt;searchUsers&lt;/code&gt; 都加了。&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;6.4 Mapper 层&lt;a href=&quot;#64-mapper-层&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Mapper 层是”采购员”，只做一件事：&lt;strong&gt;和数据库说话&lt;/strong&gt;。看本项目的 &lt;code&gt;SysUserMapper&lt;/code&gt;：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Mapper&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;interface&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SysUserMapper&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;extends&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BaseMapper&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;SysUser&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Update&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;UPDATE sys_user SET login_fail_count = login_fail_count + 1, ... &quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;incrementLoginFailCount&lt;/span&gt;&lt;span&gt;(@&lt;/span&gt;&lt;span&gt;Param&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;userId&quot;&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;userId&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                                 &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Param&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;maxFailCount&quot;&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;maxFailCount&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                                 &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Param&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;lockMinutes&quot;&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;lockMinutes&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;它就是一个接口，没有方法体。简单 CRUD 由 BaseMapper 提供，复杂 SQL 用注解或 XML。框架启动时会用动态代理生成实现类。&lt;/p&gt;&lt;p&gt;⚠️ Service 层可以调 Mapper，但 Controller 层禁止直接调 Mapper！否则就破坏了分层架构。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;6.5 Entity / DTO / VO 区别&lt;a href=&quot;#65-entity--dto--vo-区别&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;这三个东西看起来都是”装数据的盒子”，但用途完全不同。本项目里非常清晰：&lt;/p&gt;




























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;类型&lt;/th&gt;&lt;th&gt;全称&lt;/th&gt;&lt;th&gt;用途&lt;/th&gt;&lt;th&gt;例子&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Entity&lt;/td&gt;&lt;td&gt;实体类&lt;/td&gt;&lt;td&gt;对应数据库表，给 Mapper 用&lt;/td&gt;&lt;td&gt;&lt;code&gt;SysUser&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;DTO&lt;/td&gt;&lt;td&gt;Data Transfer Object&lt;/td&gt;&lt;td&gt;接收前端请求参数&lt;/td&gt;&lt;td&gt;&lt;code&gt;UserRegisterRequest&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;VO&lt;/td&gt;&lt;td&gt;View Object&lt;/td&gt;&lt;td&gt;给前端返回的展示对象&lt;/td&gt;&lt;td&gt;&lt;code&gt;UserVO&lt;/code&gt;、&lt;code&gt;UserLoginResponse&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;为什么要分？看一个例子。&lt;/p&gt;&lt;p&gt;数据库表 &lt;code&gt;sys_user&lt;/code&gt; 有 &lt;code&gt;password&lt;/code&gt;、&lt;code&gt;isDeleted&lt;/code&gt;、&lt;code&gt;lockUntil&lt;/code&gt; 这些敏感/无关字段。&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;注册时，前端只该传 &lt;code&gt;username&lt;/code&gt;、&lt;code&gt;password&lt;/code&gt;、&lt;code&gt;nickname&lt;/code&gt;、&lt;code&gt;email&lt;/code&gt;，所以本项目定义了 &lt;code&gt;UserRegisterRequest&lt;/code&gt;：&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Data&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;UserRegisterRequest&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;NotBlank&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;message&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;用户名不能为空&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Size&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;min&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;max&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;20&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; username&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;NotBlank&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;/span&gt;&lt;span&gt;Size&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;min&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;8&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;max&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; password&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Size&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;max&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;30&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; nickname&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Email&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; email&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;这就是 &lt;strong&gt;DTO&lt;/strong&gt;，只有前端会传的字段。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查询用户时，给前端返回的对象不能包含 &lt;code&gt;password&lt;/code&gt;、&lt;code&gt;isDeleted&lt;/code&gt;，所以另定义了 &lt;code&gt;UserVO&lt;/code&gt;，只放可公开的字段（id、username、nickname、avatar 等）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;如果直接把 &lt;code&gt;SysUser&lt;/code&gt; 实体类返回给前端：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;密码会泄露（虽然 &lt;code&gt;@JsonIgnore&lt;/code&gt; 能挡一下）。&lt;/li&gt;
&lt;li&gt;数据库字段一变，前端契约就跟着崩。&lt;/li&gt;
&lt;li&gt;没法精细控制权限（比如本项目里”只有本人或管理员能看到 email、role”）。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;⚠️ 一条铁律：&lt;strong&gt;Controller 入参用 DTO，出参用 VO，Entity 只在 service / mapper 内部流转&lt;/strong&gt;。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;6.6 IoC / DI 速通&lt;a href=&quot;#66-ioc--di-速通&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Spring 最核心的概念是 &lt;strong&gt;IoC（控制反转）&lt;/strong&gt; 和 &lt;strong&gt;DI（依赖注入）&lt;/strong&gt;。听起来玄乎，其实一个比喻就懂：&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;老式做法：你想喝水，自己走到饮水机接水（自己 new 对象）。
Spring 做法：你举手喊”我要水”，服务员自动送过来（容器自动注入）。&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;控制反转 = 把”创建对象”的权力从你手里反转给 Spring 容器。
依赖注入 = 容器把你需要的对象塞给你。&lt;/p&gt;&lt;p&gt;本项目里到处可见：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Service&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SysUserServiceImpl&lt;/span&gt;&lt;span&gt; ... &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Autowired&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;JwtUtils&lt;/span&gt;&lt;span&gt; jwtUtils&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Autowired&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BCryptPasswordEncoder&lt;/span&gt;&lt;span&gt; passwordEncoder&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;@Autowired&lt;/code&gt; 告诉 Spring：“请把已经创建好的 &lt;code&gt;JwtUtils&lt;/code&gt; Bean 塞给我”。Spring 启动时会扫描所有 &lt;code&gt;@Component / @Service / @Controller / @Repository / @Configuration&lt;/code&gt;，把它们都注册进容器，然后按需注入。&lt;/p&gt;&lt;p&gt;常见 Bean 注解：&lt;/p&gt;
































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;注解&lt;/th&gt;&lt;th&gt;用途&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;@Component&lt;/code&gt;&lt;/td&gt;&lt;td&gt;通用 Bean&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;@Service&lt;/code&gt;&lt;/td&gt;&lt;td&gt;业务层 Bean&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;@Repository&lt;/code&gt;&lt;/td&gt;&lt;td&gt;持久层 Bean（MP 用 &lt;code&gt;@Mapper&lt;/code&gt;）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;@Controller&lt;/code&gt; / &lt;code&gt;@RestController&lt;/code&gt;&lt;/td&gt;&lt;td&gt;控制器 Bean&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;@Configuration&lt;/code&gt;&lt;/td&gt;&lt;td&gt;配置类&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;@Bean&lt;/code&gt;&lt;/td&gt;&lt;td&gt;在配置类的方法上声明一个 Bean&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;⚠️ 推荐用&lt;strong&gt;构造器注入&lt;/strong&gt;而不是字段注入（更利于测试和不可变性），但小型项目用 &lt;code&gt;@Autowired&lt;/code&gt; 字段注入也很常见。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;6.7 项目实际调用链：注册接口&lt;a href=&quot;#67-项目实际调用链注册接口&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;最后我们走一遍”注册接口”的完整调用链，把所有层串起来：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;前端&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;↓ POST /api/user/register  body: {username, password, nickname, email}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;SysUserController.register(...)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;↓ @Valid 触发参数校验&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;↓ 调用&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;SysUserService.register(request)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;↓ 实现是&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;SysUserServiceImpl.register(...)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;1. 校验密码复杂度（至少 8 位、含 3 类字符）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;2. LambdaQueryWrapper 检查 username 是否已存在&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;3. LambdaQueryWrapper 检查 email 是否已存在&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;4. BCryptPasswordEncoder.encode(password) 加密密码&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;5. this.save(user) → 调用 BaseMapper.insert&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;       &lt;/span&gt;&lt;/span&gt;&lt;span&gt;↓&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;SysUserMapper（BaseMapper）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;INSERT INTO sys_user (...) VALUES (...)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;       &lt;/span&gt;&lt;/span&gt;&lt;span&gt;↓&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;MySQL&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;       &lt;/span&gt;&lt;/span&gt;&lt;span&gt;↓ 返回新增 id&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;回到 service：log.info(&quot;用户注册成功&quot;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;回到 controller：Result.success(new UserRegisterResponse(id, username))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;       &lt;/span&gt;&lt;/span&gt;&lt;span&gt;↓ 序列化为 JSON&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;23&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;前端收到：{&quot;code&quot;:200,&quot;message&quot;:&quot;操作成功&quot;,&quot;data&quot;:{&quot;id&quot;:1,&quot;username&quot;:&quot;...&quot;}}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;整条链路非常清晰，每一层职责单一，这就是分层的力量。&lt;/p&gt;&lt;section&gt;&lt;h3&gt;一句话总结&lt;a href=&quot;#一句话总结-1&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;blockquote&gt;&lt;p&gt;Controller 接请求、Service 写业务、Mapper 跑 SQL，Entity / DTO / VO 各司其职，靠 IoC 容器把它们粘起来。&lt;/p&gt;&lt;/blockquote&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h1&gt;第 7 章 统一响应与异常处理&lt;a href=&quot;#第-7-章-统一响应与异常处理&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h1&gt;&lt;section&gt;&lt;h2&gt;7.1 为什么要统一响应&lt;a href=&quot;#71-为什么要统一响应&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;设想一下：登录接口返回 &lt;code&gt;{&quot;token&quot;: &quot;xxx&quot;}&lt;/code&gt;，注册接口返回 &lt;code&gt;{&quot;id&quot;: 1, &quot;ok&quot;: true}&lt;/code&gt;，查询接口返回 &lt;code&gt;[{...}, {...}]&lt;/code&gt;。前端每接一个接口都得猜成功失败如何判断、错误信息在哪。混乱无比。&lt;/p&gt;&lt;p&gt;所以企业级项目都规定：&lt;strong&gt;所有接口都返回同一个外层结构&lt;/strong&gt;，里面有：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;code&lt;/code&gt;：业务状态码（200 成功、500 失败、401 未登录、403 无权限…）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;message&lt;/code&gt;：可读的提示&lt;/li&gt;
&lt;li&gt;&lt;code&gt;data&lt;/code&gt;：业务数据（可能为 null）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;timestamp&lt;/code&gt;：时间戳（便于排查）&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;这样前端只需要写一套全局拦截器：&lt;code&gt;if (res.code === 200) ...else 弹错误&lt;/code&gt;。本项目里就这么干。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;7.2 Result 类详解（基于真实 &lt;code&gt;Result.java&lt;/code&gt;）&lt;a href=&quot;#72-result-类详解基于真实-resultjava&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Data&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;implements&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Serializable&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Integer&lt;/span&gt;&lt;span&gt; code&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; message&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt; data&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;long&lt;/span&gt;&lt;span&gt; timestamp&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;timestamp&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;System&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;currentTimeMillis&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;static&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;success&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{ ... }&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;static&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;success&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;data&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt; &lt;/span&gt;&lt;span&gt;result&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;&amp;lt;&amp;gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;result&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setCode&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;200&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;result&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setMessage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;操作成功&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;result&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setData&lt;/span&gt;&lt;span&gt;(data);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; result;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;static&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;success&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;message&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;data&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{ ... }&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;static&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{ ... }&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;static&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;message&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{ ... }&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;23&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;static&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;T&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Integer&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;code&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;message&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{ ... }&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;24&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;几个值得讲的设计点：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;泛型 &lt;code&gt;&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/strong&gt;：&lt;code&gt;Result&amp;lt;UserVO&amp;gt;&lt;/code&gt;、&lt;code&gt;Result&amp;lt;IPage&amp;lt;BlogPost&amp;gt;&amp;gt;&lt;/code&gt;、&lt;code&gt;Result&amp;lt;Void&amp;gt;&lt;/code&gt;，让任何业务数据都能装进 &lt;code&gt;data&lt;/code&gt;，且类型安全。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;私有构造方法 + 静态工厂&lt;/strong&gt;：避免外部直接 &lt;code&gt;new Result()&lt;/code&gt;，强制走 &lt;code&gt;Result.success()&lt;/code&gt; / &lt;code&gt;Result.error()&lt;/code&gt;，命名更语义化。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;timestamp 自动赋值&lt;/strong&gt;：构造方法里 &lt;code&gt;this.timestamp = System.currentTimeMillis()&lt;/code&gt;，不用每次手动设置。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多个重载&lt;/strong&gt;：你可以 &lt;code&gt;success()&lt;/code&gt;、&lt;code&gt;success(data)&lt;/code&gt;、&lt;code&gt;success(msg, data)&lt;/code&gt;、&lt;code&gt;error()&lt;/code&gt;、&lt;code&gt;error(msg)&lt;/code&gt;、&lt;code&gt;error(code, msg)&lt;/code&gt;，按需选用。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;序列化为 JSON 的样子：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;code&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;200&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;message&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;操作成功&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;data&quot;&lt;/span&gt;&lt;span&gt;: { &lt;/span&gt;&lt;span&gt;&quot;id&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;username&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;liuchang&quot;&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;&quot;timestamp&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;1719999999999&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;7.3 BusinessException（基于真实代码）&lt;a href=&quot;#73-businessexception基于真实代码&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;业务异常专门用来表达”业务规则不允许”，比如”用户名已存在”、“密码错误”、“无权操作”。本项目的定义：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Getter&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;extends&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;RuntimeException&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;final&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Integer&lt;/span&gt;&lt;span&gt; code&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;message&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;super&lt;/span&gt;&lt;span&gt;(message);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;code&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;500&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Integer&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;code&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;message&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;super&lt;/span&gt;&lt;span&gt;(message);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;code&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; code;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;继承 &lt;code&gt;RuntimeException&lt;/code&gt;（运行时异常）的好处是：方法签名上不用 &lt;code&gt;throws&lt;/code&gt;，调用方不用强制 try-catch，清爽。&lt;/p&gt;&lt;p&gt;&lt;code&gt;@Getter&lt;/code&gt; 是 Lombok 注解，自动生成 &lt;code&gt;getCode()&lt;/code&gt;。&lt;code&gt;message&lt;/code&gt; 由父类管理，通过 &lt;code&gt;getMessage()&lt;/code&gt; 取。&lt;/p&gt;&lt;p&gt;使用方式遍布全项目：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (user &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;401&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;用户名或密码错误&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getStatus&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;403&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;账号已被禁用&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (categories &lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;400&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;密码必须包含大小写字母、数字或特殊字符中的至少3种&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;⚠️ &lt;strong&gt;永远不要在 service / controller 里写 &lt;code&gt;try { ... } catch { return Result.error(...) }&lt;/code&gt; 来处理业务错误&lt;/strong&gt;。统一抛 &lt;code&gt;BusinessException&lt;/code&gt;，让全局异常处理器去转换。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;7.4 GlobalExceptionHandler（&lt;code&gt;@RestControllerAdvice&lt;/code&gt;）&lt;a href=&quot;#74-globalexceptionhandlerrestcontrolleradvice&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;异常抛出后被谁接住？答：&lt;code&gt;GlobalExceptionHandler&lt;/code&gt;。这是一个全局拦截器，专门捕获所有控制器抛出的异常，转成统一的 &lt;code&gt;Result&lt;/code&gt; 返回。&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Slf4j&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;RestControllerAdvice&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;GlobalExceptionHandler&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;ExceptionHandler&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;Void&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;handleBusinessException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;warn&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;业务异常：[{}]&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getClass&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;getSimpleName&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getCode&lt;/span&gt;&lt;span&gt;(), &lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getMessage&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;ExceptionHandler&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;MethodArgumentNotValidException&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;Void&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;handleValidationException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;MethodArgumentNotValidException&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;message&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getBindingResult&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;getFieldError&lt;/span&gt;&lt;span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;                &lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getBindingResult&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;getFieldError&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;getDefaultMessage&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;                &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;参数校验失败&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;warn&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;参数校验异常：{}&quot;&lt;/span&gt;&lt;span&gt;, message);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;400&lt;/span&gt;&lt;span&gt;, message);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;ExceptionHandler&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;DuplicateKeyException&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;Void&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;handleDuplicateKeyException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;DuplicateKeyException&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;409&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;数据已存在，操作冲突&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;23&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;24&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;25&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;ExceptionHandler&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;AccessDeniedException&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;26&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;Void&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;handleAccessDeniedException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;AccessDeniedException&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;27&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;403&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;权限不足，拒绝访问&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;28&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;29&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;30&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;ExceptionHandler&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;AuthenticationException&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;31&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;Void&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;handleAuthenticationException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;AuthenticationException&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;32&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;401&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;认证失败，请先登录&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;33&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;34&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;35&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;ExceptionHandler&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Throwable&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;36&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;Void&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;handleThrowable&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Throwable&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;37&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;系统异常: {}&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getMessage&lt;/span&gt;&lt;span&gt;(), e);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;38&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;500&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;系统内部错误，请稍后重试&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;39&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;40&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;关键注解：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@RestControllerAdvice&lt;/code&gt;：等于 &lt;code&gt;@ControllerAdvice + @ResponseBody&lt;/code&gt;，对所有 &lt;code&gt;@RestController&lt;/code&gt; 生效，且返回 JSON。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@ExceptionHandler(XxxException.class)&lt;/code&gt;：声明这个方法处理哪种异常。Spring 会按”最具体优先”的原则匹配。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;本项目处理的异常类型很全：&lt;/p&gt;





































































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;异常&lt;/th&gt;&lt;th&gt;含义&lt;/th&gt;&lt;th&gt;返回 code&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;BusinessException&lt;/code&gt;&lt;/td&gt;&lt;td&gt;业务异常&lt;/td&gt;&lt;td&gt;500 / 自定义&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;MethodArgumentNotValidException&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;@Valid&lt;/code&gt; 参数校验失败&lt;/td&gt;&lt;td&gt;400&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;BindException&lt;/code&gt;&lt;/td&gt;&lt;td&gt;表单绑定校验失败&lt;/td&gt;&lt;td&gt;400&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;ConstraintViolationException&lt;/code&gt;&lt;/td&gt;&lt;td&gt;JPA 风格的校验失败&lt;/td&gt;&lt;td&gt;400&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;MissingServletRequestParameterException&lt;/code&gt;&lt;/td&gt;&lt;td&gt;缺少必填参数&lt;/td&gt;&lt;td&gt;400&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;HttpMessageNotReadableException&lt;/code&gt;&lt;/td&gt;&lt;td&gt;请求体格式错误&lt;/td&gt;&lt;td&gt;400&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;NoHandlerFoundException&lt;/code&gt;&lt;/td&gt;&lt;td&gt;404 资源不存在&lt;/td&gt;&lt;td&gt;404&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;DuplicateKeyException&lt;/code&gt;&lt;/td&gt;&lt;td&gt;数据库唯一约束冲突&lt;/td&gt;&lt;td&gt;409&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;AccessDeniedException&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Spring Security 鉴权失败&lt;/td&gt;&lt;td&gt;403&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;AuthenticationException&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Spring Security 认证失败&lt;/td&gt;&lt;td&gt;401&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;MaxUploadSizeExceededException&lt;/code&gt;&lt;/td&gt;&lt;td&gt;文件上传超限&lt;/td&gt;&lt;td&gt;400&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;Throwable&lt;/code&gt;（兜底）&lt;/td&gt;&lt;td&gt;任何未捕获的异常&lt;/td&gt;&lt;td&gt;500&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;⚠️ 注意 &lt;code&gt;Throwable&lt;/code&gt; 兜底处理时只 &lt;code&gt;log.error&lt;/code&gt; 而&lt;strong&gt;不把堆栈泄露给前端&lt;/strong&gt;，避免暴露内部结构（这是 v1.34 安全增强的细节之一）。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;7.5 参数校验注解&lt;a href=&quot;#75-参数校验注解&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;参数校验是 Web 应用的”第一道防线”。本项目用 Bean Validation（JSR-303）规范：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;UserRegisterRequest&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;NotBlank&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;message&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;用户名不能为空&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Size&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;min&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;max&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;20&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;message&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;用户名长度必须在3-20个字符之间&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; username&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;NotBlank&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;message&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;密码不能为空&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Size&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;min&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;8&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;max&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;message&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;密码长度必须在8-50个字符之间&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; password&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Size&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;max&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;30&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;message&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;昵称长度不能超过30个字符&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; nickname&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Email&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;message&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;邮箱格式不正确&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; email&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;常用注解：&lt;/p&gt;




































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;注解&lt;/th&gt;&lt;th&gt;含义&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;@NotNull&lt;/code&gt;&lt;/td&gt;&lt;td&gt;不能为 null&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;@NotEmpty&lt;/code&gt;&lt;/td&gt;&lt;td&gt;不能为 null 且 size &amp;gt; 0（用于 String / Collection）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;@NotBlank&lt;/code&gt;&lt;/td&gt;&lt;td&gt;字符串不能为 null / 空串 / 全空格&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;@Size(min=, max=)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;长度限制&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;@Min&lt;/code&gt; / &lt;code&gt;@Max&lt;/code&gt;&lt;/td&gt;&lt;td&gt;数值最小 / 最大&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;@Email&lt;/code&gt;&lt;/td&gt;&lt;td&gt;邮箱格式&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;@Pattern(regexp=...)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;正则匹配&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;要让校验生效，&lt;strong&gt;必须&lt;/strong&gt;在 controller 方法的参数前加 &lt;code&gt;@Valid&lt;/code&gt;：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;PostMapping&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/register&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;UserRegisterResponse&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;register&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;/span&gt;&lt;span&gt;Valid&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;/span&gt;&lt;span&gt;RequestBody&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;UserRegisterRequest&lt;/span&gt;&lt;span&gt; request) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;success&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;sysUserService&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;register&lt;/span&gt;&lt;span&gt;(request));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;校验失败会抛 &lt;code&gt;MethodArgumentNotValidException&lt;/code&gt;，被 &lt;code&gt;GlobalExceptionHandler&lt;/code&gt; 转成：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;{ &lt;/span&gt;&lt;span&gt;&quot;code&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;400&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;message&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;用户名长度必须在3-20个字符之间&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;data&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;GET 请求的查询参数（如 &lt;code&gt;UserSearchRequest&lt;/code&gt;）也要加 &lt;code&gt;@Valid&lt;/code&gt;：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;GetMapping&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/search&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;IPage&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;UserVO&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;searchUsers&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;/span&gt;&lt;span&gt;Valid&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;UserSearchRequest&lt;/span&gt;&lt;span&gt; request) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;success&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;sysUserService&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;searchUsers&lt;/span&gt;&lt;span&gt;(request));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;7.6 完整请求处理流程图&lt;a href=&quot;#76-完整请求处理流程图&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;我们把 7.1 ~ 7.5 串起来：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;前端发起 POST /user/register&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;↓&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;DispatcherServlet（Spring MVC 入口）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;↓ 路由到 SysUserController.register&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;↓ @RequestBody 解析 JSON → UserRegisterRequest&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;↓ @Valid 触发 Bean Validation&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│     ├── 校验失败 → 抛 MethodArgumentNotValidException&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│     │              → GlobalExceptionHandler 接住&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│     │              → Result.error(400, &quot;...&quot;) → 返回 JSON&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│     └── 校验通过 → 进入 service&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;↓&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;SysUserService.register&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│     ├── 业务规则不通过 → throw new BusinessException(400, &quot;...&quot;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│     │              → GlobalExceptionHandler 接住&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│     │              → Result.error(400, &quot;...&quot;) → 返回 JSON&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;│     └── 通过 → return UserRegisterResponse&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;↓&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;Controller: return Result.success(response)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;↓&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;Spring 序列化为 JSON 返回前端&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;无论成功失败，前端总是收到 &lt;code&gt;{ code, message, data, timestamp }&lt;/code&gt; 这种结构。&lt;/p&gt;&lt;section&gt;&lt;h3&gt;一句话总结&lt;a href=&quot;#一句话总结-2&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;blockquote&gt;&lt;p&gt;&lt;code&gt;Result&lt;/code&gt; 统一返回格式，&lt;code&gt;BusinessException&lt;/code&gt; 优雅抛业务错误，&lt;code&gt;GlobalExceptionHandler&lt;/code&gt; 全局兜底，三件套让接口又干净又一致。&lt;/p&gt;&lt;/blockquote&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h1&gt;第 8 章 Spring Security 入门&lt;a href=&quot;#第-8-章-spring-security-入门&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h1&gt;&lt;section&gt;&lt;h2&gt;8.1 认证 vs 授权&lt;a href=&quot;#81-认证-vs-授权&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;学 Spring Security 之前，先分清两个词：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;认证（Authentication）&lt;/strong&gt;：你是谁？比如登录就是认证：你提交用户名密码，系统识别”哦，你是用户 ID 1 的刘畅”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;授权（Authorization）&lt;/strong&gt;：你能做什么？比如普通用户不能访问 &lt;code&gt;/admin/**&lt;/code&gt;，管理员可以；这就是授权。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;二者经常一起出现，但概念上必须分开。Spring Security 是 Java 世界处理这两件事的事实标准。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;8.2 SecurityConfig 配置类详解（基于真实 &lt;code&gt;SecurityConfig.java&lt;/code&gt;）&lt;a href=&quot;#82-securityconfig-配置类详解基于真实-securityconfigjava&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;本项目的核心配置在 &lt;code&gt;SecurityConfig.java&lt;/code&gt;：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Configuration&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;EnableWebSecurity&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SecurityConfig&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Autowired&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;JwtAuthenticationFilter&lt;/span&gt;&lt;span&gt; jwtAuthenticationFilter&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Value&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;${cors.allowed-origins:http://localhost:8080,http://127.0.0.1:8080}&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; allowedOrigins&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Bean&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BCryptPasswordEncoder&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;passwordEncoder&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BCryptPasswordEncoder&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;12&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Bean&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CorsConfigurationSource&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;corsConfigurationSource&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{ ... }&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Bean&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SecurityFilterChain&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;securityFilterChain&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;HttpSecurity&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;http&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;throws&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Exception&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;http&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;csrf&lt;/span&gt;&lt;span&gt;(csrf &lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;csrf&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;disable&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;23&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;cors&lt;/span&gt;&lt;span&gt;(cors &lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cors&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;configurationSource&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;corsConfigurationSource&lt;/span&gt;&lt;span&gt;()))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;24&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;sessionManagement&lt;/span&gt;&lt;span&gt;(session &lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;session&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;sessionCreationPolicy&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;SessionCreationPolicy&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;STATELESS&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;25&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;authorizeHttpRequests&lt;/span&gt;&lt;span&gt;(authorize &lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt; authorize&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;26&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;requestMatchers&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;HttpMethod&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;OPTIONS&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;/**&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;permitAll&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;27&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;requestMatchers&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/user/register&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;/user/login&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;/user/refresh&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;permitAll&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;28&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;requestMatchers&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/doc.html&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;/swagger-ui/**&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;/v3/api-docs/**&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;/swagger-resources/**&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;/webjars/**&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;permitAll&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;29&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;requestMatchers&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/admin/**&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;hasRole&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;admin&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;30&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;requestMatchers&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;HttpMethod&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;GET&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;/post/**&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;permitAll&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;31&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;requestMatchers&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;HttpMethod&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;GET&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;/comment/post/**&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;permitAll&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;32&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;requestMatchers&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;HttpMethod&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;GET&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;/like/check/**&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;/collect/check/**&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;permitAll&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;33&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;requestMatchers&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;HttpMethod&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;GET&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;/tag/**&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;permitAll&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;34&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;requestMatchers&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;HttpMethod&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;GET&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;/follow/check/**&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;permitAll&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;35&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;requestMatchers&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/follow/**&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;authenticated&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;36&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;requestMatchers&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;HttpMethod&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;POST&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;/post/**&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;authenticated&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;37&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;requestMatchers&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;HttpMethod&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;PUT&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;/post/**&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;authenticated&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;38&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;requestMatchers&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;HttpMethod&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;DELETE&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;/post/**&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;authenticated&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;39&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;anyRequest&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;authenticated&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;40&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;41&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;addFilterBefore&lt;/span&gt;&lt;span&gt;(jwtAuthenticationFilter, &lt;/span&gt;&lt;span&gt;UsernamePasswordAuthenticationFilter&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;42&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;http&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;build&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;43&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;44&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;逐项解读：&lt;/p&gt;&lt;section&gt;&lt;h3&gt;&lt;code&gt;@EnableWebSecurity&lt;/code&gt;&lt;a href=&quot;#enablewebsecurity&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;启用 Spring Security 的 Web 安全支持。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;&lt;code&gt;csrf(csrf -&amp;gt; csrf.disable())&lt;/code&gt;&lt;a href=&quot;#csrfcsrf---csrfdisable&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;关闭 CSRF（跨站请求伪造）防护。&lt;strong&gt;为什么关？&lt;/strong&gt; 因为我们用的是 JWT 无状态认证，不依赖 Cookie/Session，CSRF 攻击的载体（自动发送的 Cookie）不存在，所以可以关闭。但如果你用 Session 认证，&lt;strong&gt;绝不能关 CSRF&lt;/strong&gt;。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;&lt;code&gt;cors(...)&lt;/code&gt;&lt;a href=&quot;#cors&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;开启跨域支持。本项目通过 &lt;code&gt;@Value(&quot;${cors.allowed-origins:...}&quot;)&lt;/code&gt; 从配置读取允许的来源，例如 &lt;code&gt;http://localhost:8080&lt;/code&gt;。前后端分离开发时（前端跑在 8080、后端跑在 8825），必须正确配置 CORS，否则浏览器会拦截请求。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;&lt;code&gt;sessionCreationPolicy(STATELESS)&lt;/code&gt;&lt;a href=&quot;#sessioncreationpolicystateless&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;不创建 HTTP Session。无状态意味着每个请求都得自己带身份凭证（也就是 JWT）。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;&lt;code&gt;authorizeHttpRequests(...)&lt;/code&gt;&lt;a href=&quot;#authorizehttprequests&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;最重要的部分：&lt;strong&gt;URL 鉴权规则&lt;/strong&gt;。规则按从上到下顺序匹配，先匹配的先生效：&lt;/p&gt;




































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;规则&lt;/th&gt;&lt;th&gt;含义&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;OPTIONS /**&lt;/code&gt; permitAll&lt;/td&gt;&lt;td&gt;跨域预检请求一律放行&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;/user/register, /user/login, /user/refresh&lt;/code&gt; permitAll&lt;/td&gt;&lt;td&gt;注册、登录、刷新 token 不需要登录&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Swagger 相关路径 permitAll&lt;/td&gt;&lt;td&gt;API 文档对公网开放&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;/admin/**&lt;/code&gt; hasRole(“admin”)&lt;/td&gt;&lt;td&gt;管理员路径必须 admin 角色&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;GET /post/**&lt;/code&gt; permitAll&lt;/td&gt;&lt;td&gt;文章列表/详情匿名可看&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;POST/PUT/DELETE /post/**&lt;/code&gt; authenticated&lt;/td&gt;&lt;td&gt;发布/修改/删除文章必须登录&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;anyRequest().authenticated()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;其他所有请求都要登录&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;&lt;code&gt;addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)&lt;/code&gt;&lt;a href=&quot;#addfilterbeforejwtauthenticationfilter-usernamepasswordauthenticationfilterclass&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;把自定义的 JWT 过滤器插到 Spring Security 的过滤链中（详见第 9 章）。位置很关键：放在 &lt;code&gt;UsernamePasswordAuthenticationFilter&lt;/code&gt; 之前，先用 JWT 完成认证。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;8.3 BCrypt 密码加密（强度 12）&lt;a href=&quot;#83-bcrypt-密码加密强度-12&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Bean&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BCryptPasswordEncoder&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;passwordEncoder&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BCryptPasswordEncoder&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;12&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;BCrypt&lt;/strong&gt; 是当前业界推荐的密码哈希算法。它有两个特征：
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;加盐（salt）&lt;/strong&gt;：每次哈希都加随机盐值，相同密码每次哈希结果不同，防止彩虹表攻击。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;慢哈希&lt;/strong&gt;：故意设计得慢，使暴力破解代价极高。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;强度（cost factor）= 12&lt;/strong&gt; 表示进行 2^12 = 4096 轮哈希。值越大越安全但越耗时。本项目选 12，既安全又能在普通服务器上几十毫秒内完成验证。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;使用方式：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// 注册时加密&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setPassword&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;passwordEncoder&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;encode&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getPassword&lt;/span&gt;&lt;span&gt;()));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// 登录时校验&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;/span&gt;&lt;span&gt;passwordEncoder&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;matches&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getPassword&lt;/span&gt;&lt;span&gt;(), &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getPassword&lt;/span&gt;&lt;span&gt;&lt;span&gt;())&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;401&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;用户名或密码错误&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;⚠️ &lt;strong&gt;绝对不能&lt;/strong&gt;用 MD5、SHA1、SHA256 直接存密码！它们速度太快，已经被破解到不安全的程度。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;8.4 CSRF / CORS 简介&lt;a href=&quot;#84-csrf--cors-简介&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;CSRF（Cross-Site Request Forgery，跨站请求伪造）&lt;a href=&quot;#csrfcross-site-request-forgery跨站请求伪造&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;攻击者诱导你访问一个伪装的网站，该网站偷偷向你已登录的银行系统发请求（利用浏览器自动带 Cookie）。防御靠 CSRF Token。
本项目用 JWT 无状态、不靠 Cookie，所以可以放心 &lt;code&gt;csrf().disable()&lt;/code&gt;。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;CORS（Cross-Origin Resource Sharing，跨源资源共享）&lt;a href=&quot;#corscross-origin-resource-sharing跨源资源共享&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;浏览器的同源策略：脚本不能访问与当前页面”协议+域名+端口”不同的资源。前后端分离时，前端 &lt;code&gt;http://localhost:8080&lt;/code&gt; 想访问后端 &lt;code&gt;http://localhost:8825&lt;/code&gt;，端口不同就跨源了。CORS 就是后端通过响应头告诉浏览器：“我允许这个来源”。&lt;/p&gt;&lt;p&gt;本项目的 CORS 配置（节选）：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;configuration&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;addAllowedOriginPattern&lt;/span&gt;&lt;span&gt;&lt;span&gt;(trimmed);&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// 允许的来源&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;configuration&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;addAllowedHeader&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;*&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;configuration&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;addAllowedMethod&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;*&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;configuration&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setAllowCredentials&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;&lt;span&gt;);&lt;/span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// 允许带凭证（Cookie / Authorization）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;configuration&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setMaxAge&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;3600L&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;⚠️ 安全提示：&lt;strong&gt;绝不要 &lt;code&gt;addAllowedOrigin(&quot;*&quot;)&lt;/code&gt; + &lt;code&gt;setAllowCredentials(true)&lt;/code&gt; 同时存在&lt;/strong&gt;，那是非常严重的安全漏洞。本项目从环境变量读取明确的白名单。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;8.5 一次登录请求完整流程&lt;a href=&quot;#85-一次登录请求完整流程&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;走一遍 &lt;code&gt;POST /user/login&lt;/code&gt;：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;1. 前端提交 {&quot;username&quot;:&quot;liuchang&quot;,&quot;password&quot;:&quot;Abc12345&quot;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;2. Spring Security 过滤链开始：&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- JwtAuthenticationFilter 检查 Authorization header（没有，跳过）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- 路径 &quot;/user/login&quot; 匹配 permitAll，不需要认证，放行&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;3. 进入 SysUserController.login(...)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;4. SysUserServiceImpl.login(...)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- 用 LambdaQueryWrapper 按 username 查 user&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- 检查锁定状态、账号状态&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- passwordEncoder.matches(明文, 数据库密文) 对比&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- 失败 → handleLoginFailAtomic(userId) 原子+1，可能锁定&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;       &lt;/span&gt;&lt;/span&gt;&lt;span&gt;throw new BusinessException(401, &quot;用户名或密码错误&quot;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- 成功 → 重置失败计数，生成 token&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;5. jwtUtils.generateToken(userId, username, role)  → 普通 token&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;jwtUtils.generateRefreshToken(...)              → 刷新 token&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;6. 返回 UserLoginResponse(id, username, nickname, avatar, token, refreshToken)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;7. Result.success(response) → JSON 给前端&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;8. 前端把 token 存 localStorage，后续请求带在 header：&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;section&gt;&lt;h3&gt;一句话总结&lt;a href=&quot;#一句话总结-3&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;blockquote&gt;&lt;p&gt;Spring Security 把”哪些路径需要登录、哪些角色才能访问、密码怎么加密、跨域怎么配置”全打包好，本项目用它 + JWT 实现了无状态的安全防护。&lt;/p&gt;&lt;/blockquote&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h1&gt;第 9 章 JWT 认证实战&lt;a href=&quot;#第-9-章-jwt-认证实战&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h1&gt;&lt;section&gt;&lt;h2&gt;9.1 JWT 是什么&lt;a href=&quot;#91-jwt-是什么&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;JWT（JSON Web Token）&lt;/strong&gt; 是一种”自包含的、可验证的”令牌格式。它解决了一个问题：&lt;strong&gt;如何在无状态的 HTTP 上识别用户身份&lt;/strong&gt;。&lt;/p&gt;&lt;p&gt;传统 Session 方案：服务器存 sessionId → user 的映射，前端的 Cookie 带 sessionId 来。
JWT 方案：服务器签发一个加密字符串（token），里面已经包含了用户 id、角色等信息，前端每次请求带在 &lt;code&gt;Authorization&lt;/code&gt; 头里。&lt;/p&gt;&lt;p&gt;JWT 的特点：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;无状态&lt;/strong&gt;：服务器不存 session，多实例部署时不用同步。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自包含&lt;/strong&gt;：token 解码后能直接读到 userId、role。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可验证&lt;/strong&gt;：服务器用密钥签名，任何篡改会导致签名验证失败。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;⚠️ JWT 本身&lt;strong&gt;不加密&lt;/strong&gt;，只签名！payload 是 Base64 编码的，谁拿到都能解码，所以&lt;strong&gt;绝不要在 payload 里放密码、银行卡号等敏感信息&lt;/strong&gt;。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;9.2 JWT 三段结构&lt;a href=&quot;#92-jwt-三段结构&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;一个典型的 JWT 字符串长这样：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEsInVzZXJuYW1lIjoibGl1Y2hhbmciLCJyb2xlIjoidXNlciIsInN1YiI6ImxpdWNoYW5nIn0.5xj6q0...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;被两个”.”分成三段：&lt;/p&gt;
























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;段&lt;/th&gt;&lt;th&gt;名字&lt;/th&gt;&lt;th&gt;内容&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;第 1 段&lt;/td&gt;&lt;td&gt;Header（头部）&lt;/td&gt;&lt;td&gt;算法 + 类型，如 &lt;code&gt;{&quot;alg&quot;:&quot;HS256&quot;,&quot;typ&quot;:&quot;JWT&quot;}&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;第 2 段&lt;/td&gt;&lt;td&gt;Payload（载荷）&lt;/td&gt;&lt;td&gt;业务数据，如 &lt;code&gt;{&quot;userId&quot;:1,&quot;username&quot;:&quot;liuchang&quot;,&quot;role&quot;:&quot;user&quot;,&quot;exp&quot;:1719999999}&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;第 3 段&lt;/td&gt;&lt;td&gt;Signature（签名）&lt;/td&gt;&lt;td&gt;用服务器密钥对前两段做 HMAC-SHA256 签名&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;签名公式：&lt;code&gt;HMACSHA256(base64(header) + &quot;.&quot; + base64(payload), secret)&lt;/code&gt;。&lt;/p&gt;&lt;p&gt;任何人想篡改 payload（比如把 &lt;code&gt;role&lt;/code&gt; 改成 &lt;code&gt;admin&lt;/code&gt;），都没办法重新算出正确的签名（因为没有 secret），服务器一验签就失败。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;9.3 JwtUtils 工具类详解（基于真实代码）&lt;a href=&quot;#93-jwtutils-工具类详解基于真实代码&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;本项目的 &lt;code&gt;JwtUtils.java&lt;/code&gt; 封装了所有 JWT 操作。挑核心方法讲：&lt;/p&gt;&lt;section&gt;&lt;h3&gt;配置注入&lt;a href=&quot;#配置注入&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Value&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;${jwt.secret}&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; secret&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Value&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;${jwt.expiration}&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; expiration&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Value&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;${jwt.refresh-expiration}&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; refreshExpiration&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;从配置文件（最终来自 &lt;code&gt;.env&lt;/code&gt;）读取密钥和过期时间。&lt;strong&gt;密钥必须至少 32 位&lt;/strong&gt;，否则 HmacSHA256 会拒绝。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;生成 Token&lt;a href=&quot;#生成-token&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;generateToken&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt;&lt;span&gt; userId&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt; username&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; role) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;Map&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Object&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; claims &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;HashMap&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;claims&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;put&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;userId&quot;&lt;/span&gt;&lt;span&gt;, userId);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;claims&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;put&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;username&quot;&lt;/span&gt;&lt;span&gt;, username);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;claims&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;put&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;role&quot;&lt;/span&gt;&lt;span&gt;, role);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;createToken&lt;/span&gt;&lt;span&gt;&lt;span&gt;(claims&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; username&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; expiration)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;createToken&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Map&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; Object&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; claims&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt; subject&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; expirationMs) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;SecretKey&lt;/span&gt;&lt;span&gt; key &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Keys&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;hmacShaKeyFor&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;secret&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getBytes&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;StandardCharsets&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;UTF_8&lt;/span&gt;&lt;span&gt;));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Jwts&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;builder&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;claims&lt;/span&gt;&lt;span&gt;(claims)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;subject&lt;/span&gt;&lt;span&gt;(subject)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;issuedAt&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Date&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;expiration&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Date&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;System&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;currentTimeMillis&lt;/span&gt;&lt;span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; expirationMs))&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;signWith&lt;/span&gt;&lt;span&gt;(key)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;compact&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;claims&lt;/code&gt; 就是 payload 里的自定义字段；&lt;code&gt;subject&lt;/code&gt; 是标准字段（一般填用户名）；&lt;code&gt;issuedAt&lt;/code&gt; 签发时间；&lt;code&gt;expiration&lt;/code&gt; 过期时间。&lt;code&gt;signWith(key)&lt;/code&gt; 用 HMAC-SHA256 签名。最后 &lt;code&gt;.compact()&lt;/code&gt; 拼成字符串。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;解析与校验&lt;a href=&quot;#解析与校验&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Claims&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;parseToken&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; token) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;SecretKey&lt;/span&gt;&lt;span&gt; key &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Keys&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;hmacShaKeyFor&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;secret&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getBytes&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;StandardCharsets&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;UTF_8&lt;/span&gt;&lt;span&gt;));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Jwts&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;parser&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;verifyWith&lt;/span&gt;&lt;span&gt;(key)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;build&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;parseSignedClaims&lt;/span&gt;&lt;span&gt;(token)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getPayload&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;isTokenExpired&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; token) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;try&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;Claims&lt;/span&gt;&lt;span&gt; claims &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;parseToken&lt;/span&gt;&lt;span&gt;&lt;span&gt;(token)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;claims&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getExpiration&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;before&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Date&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;io.jsonwebtoken.ExpiredJwtException&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt;&lt;span&gt; (...) { &lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;getUserIdFromToken&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; token) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;Claims&lt;/span&gt;&lt;span&gt; claims &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;parseToken&lt;/span&gt;&lt;span&gt;&lt;span&gt;(token)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;claims&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;userId&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;parseSignedClaims&lt;/code&gt; 内部会&lt;strong&gt;自动验签&lt;/strong&gt;，签名错就抛异常。所以 &lt;code&gt;parseToken&lt;/code&gt; 只要不抛异常，就说明 token 是真的、未被篡改。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;从请求中提取 Token&lt;a href=&quot;#从请求中提取-token&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;extractTokenFromRequest&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;HttpServletRequest&lt;/span&gt;&lt;span&gt; request) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; bearerToken &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getHeader&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Authorization&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;StringUtils&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;hasText&lt;/span&gt;&lt;span&gt;&lt;span&gt;(bearerToken)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;bearerToken&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;startsWith&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Bearer &quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;bearerToken&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;substring&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Bearer &quot;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;length&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;约定：前端把 token 放在 &lt;code&gt;Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code&gt; 头里。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;9.4 JwtAuthenticationFilter 拦截器（OncePerRequestFilter）&lt;a href=&quot;#94-jwtauthenticationfilter-拦截器onceperrequestfilter&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;每个请求都需要识别”你是谁”，这个工作由 &lt;code&gt;JwtAuthenticationFilter&lt;/code&gt; 完成：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Component&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;JwtAuthenticationFilter&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;extends&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;OncePerRequestFilter&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Autowired&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;JwtUtils&lt;/span&gt;&lt;span&gt; jwtUtils&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Override&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;protected&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;doFilterInternal&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;HttpServletRequest&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;                                    &lt;/span&gt;&lt;span&gt;HttpServletResponse&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;response&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;                                    &lt;/span&gt;&lt;span&gt;FilterChain&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;filterChain&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;throws&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ServletException&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;IOException&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;try&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;token&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;jwtUtils&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;extractTokenFromRequest&lt;/span&gt;&lt;span&gt;(request);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;StringUtils&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;hasText&lt;/span&gt;&lt;span&gt;(token)) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;                &lt;/span&gt;&lt;span&gt;try&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;                    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;/span&gt;&lt;span&gt;jwtUtils&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;isTokenExpired&lt;/span&gt;&lt;span&gt;&lt;span&gt;(token) &lt;/span&gt;&lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;/span&gt;&lt;span&gt;jwtUtils&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;isTokenRevoked&lt;/span&gt;&lt;span&gt;(token)) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;                        &lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;userId&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;jwtUtils&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getUserIdFromToken&lt;/span&gt;&lt;span&gt;(token);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;                        &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;username&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;jwtUtils&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getUsernameFromToken&lt;/span&gt;&lt;span&gt;(token);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;                        &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;role&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;jwtUtils&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getRoleFromToken&lt;/span&gt;&lt;span&gt;(token);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;                        &lt;/span&gt;&lt;span&gt;UserContext&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;userContext&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;UserContext&lt;/span&gt;&lt;span&gt;(userId, role);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;23&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;                        &lt;/span&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;GrantedAuthority&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt; &lt;/span&gt;&lt;span&gt;authorities&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Collections&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;singletonList&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;24&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;                                &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SimpleGrantedAuthority&lt;/span&gt;&lt;span&gt;&lt;span&gt;(role &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;ROLE_&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; role &lt;/span&gt;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;ROLE_USER&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;25&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;26&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;                        &lt;/span&gt;&lt;span&gt;UsernamePasswordAuthenticationToken&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;authentication&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;27&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;                                &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;UsernamePasswordAuthenticationToken&lt;/span&gt;&lt;span&gt;(userContext, &lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;, authorities);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;28&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;                        &lt;/span&gt;&lt;span&gt;authentication&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setDetails&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;WebAuthenticationDetailsSource&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;buildDetails&lt;/span&gt;&lt;span&gt;(request));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;29&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;30&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;                        &lt;/span&gt;&lt;span&gt;SecurityContextHolder&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getContext&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;setAuthentication&lt;/span&gt;&lt;span&gt;(authentication);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;31&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;32&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;Exception&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;33&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;                    &lt;/span&gt;&lt;span&gt;logger&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;warn&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;JWT Authentication failed: &quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getMessage&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;34&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;35&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;36&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;Exception&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;37&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;logger&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;warn&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;JWT Authentication failed&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;38&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;39&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;filterChain&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;doFilter&lt;/span&gt;&lt;span&gt;(request, response);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;40&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;41&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;要点：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;继承 &lt;code&gt;OncePerRequestFilter&lt;/code&gt;：保证同一个请求只走一次（避免被多次过滤）。&lt;/li&gt;
&lt;li&gt;从请求头解析 token → 校验过期 → 校验黑名单 → 解析 userId/role。&lt;/li&gt;
&lt;li&gt;把用户身份塞进 &lt;code&gt;SecurityContextHolder&lt;/code&gt;：之后业务代码任何地方都可以通过 &lt;code&gt;SecurityUtils.getCurrentUserId()&lt;/code&gt; 拿到当前用户。&lt;/li&gt;
&lt;li&gt;token 无效不抛异常，只是不设置认证信息，让后续 Spring Security 决定是否拒绝（401）。&lt;/li&gt;
&lt;li&gt;必须 &lt;code&gt;filterChain.doFilter(request, response)&lt;/code&gt; 让请求继续往下走。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;⚠️ 一定要先验签再查黑名单，否则攻击者可以用伪造 token 撑爆黑名单。&lt;/p&gt;&lt;p&gt;配套 &lt;code&gt;SecurityUtils.java&lt;/code&gt; 提供了便捷方法：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;static&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;getCurrentUserId&lt;/span&gt;&lt;span&gt;&lt;span&gt;() { &lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt; }       &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// 强制要求登录&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;static&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;getCurrentUserIdOrNull&lt;/span&gt;&lt;span&gt;&lt;span&gt;() { &lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt; } &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// 可空，匿名访问也能用&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;static&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;isCurrentUserAdmin&lt;/span&gt;&lt;span&gt;&lt;span&gt;() { &lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;业务代码（如 &lt;code&gt;SysUserController.getById&lt;/code&gt;）就这样用：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; currentUserId &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;SecurityUtils&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getCurrentUserIdOrNull&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;span&gt; isOwner &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; currentUserId &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;currentUserId&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;equals&lt;/span&gt;&lt;span&gt;(id);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;span&gt; isAdmin &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;SecurityUtils&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;isCurrentUserAdmin&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (isOwner &lt;/span&gt;&lt;span&gt;||&lt;/span&gt;&lt;span&gt; isAdmin) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;userVO&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setEmail&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getEmail&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;userVO&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setRole&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getRole&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;9.5 Token 刷新机制（access + refresh）&lt;a href=&quot;#95-token-刷新机制access--refresh&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;JWT 有个两难：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;过期时间长 → 一旦泄露危害大。&lt;/li&gt;
&lt;li&gt;过期时间短 → 用户频繁要重登录，体验差。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;业界通用方案：&lt;strong&gt;双 token&lt;/strong&gt;。&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;AccessToken&lt;/strong&gt;（短期，比如 2 小时）：每次业务请求带它。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RefreshToken&lt;/strong&gt;（长期，比如 7 天）：只用于换新的 AccessToken。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;本项目登录后同时返回两个 token：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; token &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;jwtUtils&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;generateToken&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getId&lt;/span&gt;&lt;span&gt;(), &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getUsername&lt;/span&gt;&lt;span&gt;(), &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getRole&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; refreshToken &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;jwtUtils&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;generateRefreshToken&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getId&lt;/span&gt;&lt;span&gt;(), &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getUsername&lt;/span&gt;&lt;span&gt;(), &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getRole&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;刷新接口 &lt;code&gt;POST /user/refresh&lt;/code&gt; 的逻辑（节选自 &lt;code&gt;SysUserController.refreshToken&lt;/code&gt;）：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; refreshToken &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;authHeader&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;substring&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;7&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;/span&gt;&lt;span&gt;jwtUtils&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;isRefreshToken&lt;/span&gt;&lt;span&gt;&lt;span&gt;(refreshToken)&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;401&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;无效的刷新Token&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;jwtUtils&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;isTokenExpired&lt;/span&gt;&lt;span&gt;&lt;span&gt;(refreshToken)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;||&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;jwtUtils&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;isTokenRevoked&lt;/span&gt;&lt;span&gt;&lt;span&gt;(refreshToken)&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;401&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;刷新Token已过期或已撤销&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; userId &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;jwtUtils&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getUserIdFromToken&lt;/span&gt;&lt;span&gt;(refreshToken);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// 校验用户当前状态&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;SysUser&lt;/span&gt;&lt;span&gt; currentUser &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;sysUserService&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getUserById&lt;/span&gt;&lt;span&gt;(userId);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (currentUser &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;||&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;currentUser&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getStatus&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;||&lt;/span&gt;&lt;span&gt; locked) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;403&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;...&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// 撤销旧的 refresh token（refresh token rotation）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;jwtUtils&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;revokeToken&lt;/span&gt;&lt;span&gt;(refreshToken);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; newToken &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;jwtUtils&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;generateToken&lt;/span&gt;&lt;span&gt;(userId, username, role);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; newRefreshToken &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;jwtUtils&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;generateRefreshToken&lt;/span&gt;&lt;span&gt;(userId, username, role);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;⚠️ &lt;strong&gt;Refresh Token Rotation（刷新令牌轮换）&lt;/strong&gt; 是非常重要的安全实践：每次刷新都把旧 refresh token 加入黑名单，新签发一个。这样即便老 refresh token 被偷走，攻击者也只能用一次，且会被合法用户触发的轮换”踢掉”。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;9.6 Token 黑名单&lt;a href=&quot;#96-token-黑名单&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;JWT 一旦签发就无法主动让它失效（除非过期）。但用户主动登出、修改密码、被踢下线时，必须能撤销 token。本项目用”黑名单”实现：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;final&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Set&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; tokenBlacklist &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;ConcurrentHashMap&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;newKeySet&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;revokeToken&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; token) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (token &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;try&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;parseToken&lt;/span&gt;&lt;span&gt;&lt;span&gt;(token)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// 先验签&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;/span&gt;&lt;span&gt;isTokenExpired&lt;/span&gt;&lt;span&gt;(token)) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;tokenBlacklist&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(token);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;Exception&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// 无效 token 不加入&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;isTokenRevoked&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; token) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;tokenBlacklist&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;contains&lt;/span&gt;&lt;span&gt;(token);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cleanExpiredTokens&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;tokenBlacklist&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;removeIf&lt;/span&gt;&lt;span&gt;(token &lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;try&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;isTokenExpired&lt;/span&gt;&lt;span&gt;(token); }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;Exception&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;) { &lt;/span&gt;&lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;23&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;24&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;注意：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;&lt;code&gt;ConcurrentHashMap.newKeySet()&lt;/code&gt; 提供线程安全的 Set。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;revokeToken&lt;/code&gt; &lt;strong&gt;必须先验签&lt;/strong&gt;：避免攻击者用伪造 token 撑爆内存。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cleanExpiredTokens()&lt;/code&gt; 由 &lt;code&gt;JwtSchedulerConfig&lt;/code&gt; 定时调度，避免黑名单无限膨胀。&lt;/li&gt;
&lt;li&gt;⚠️ 当前是内存方案，只支持单实例。生产环境多实例部署时，应换成 &lt;strong&gt;Redis SET + TTL&lt;/strong&gt;，本项目源码注释里也明确写了这点 TODO。&lt;/li&gt;
&lt;/ol&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;9.7 安全注意事项（务必记住）&lt;a href=&quot;#97-安全注意事项务必记住&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;最后总结一下 JWT 实战必须遵守的安全准则：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;密钥（secret）至少 32 位且必须保密&lt;/strong&gt;。本项目通过 &lt;code&gt;.env&lt;/code&gt; 文件管理，&lt;code&gt;.gitignore&lt;/code&gt; 忽略，&lt;strong&gt;永远不要提交到 Git&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;必须校验签名&lt;/strong&gt;：自动由 &lt;code&gt;parseSignedClaims&lt;/code&gt; 完成，不要自己写”只 base64 解码不验签”的代码。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;payload 不能存敏感信息&lt;/strong&gt;：密码、银行卡、身份证等绝对不放。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;设置合理的过期时间&lt;/strong&gt;：access 12 小时、refresh 730 天。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;必须有撤销机制&lt;/strong&gt;：黑名单 / Redis / 数据库 token 表。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用 HTTPS 传输&lt;/strong&gt;：HTTP 下 token 在网络上裸奔，会被中间人截获。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;拒绝弱算法&lt;/strong&gt;：不要用 &lt;code&gt;alg: none&lt;/code&gt;、不要用 RS256 时把公钥当对称密钥（历史上著名漏洞）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;登录失败要锁账号&lt;/strong&gt;：本项目实现了 5 次失败锁 15 分钟，且用数据库原子 SQL 防并发绕过。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;修改密码、登出后撤销旧 token&lt;/strong&gt;：避免泄露的 token 继续可用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生产环境用 Redis 存黑名单&lt;/strong&gt;：内存方案多实例不一致。&lt;/li&gt;
&lt;/ol&gt;&lt;section&gt;&lt;h3&gt;一句话总结&lt;a href=&quot;#一句话总结-4&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;blockquote&gt;&lt;p&gt;JWT 是”带签名的身份证”，本项目用 access + refresh 双 token、轮换、黑名单、登录锁、HTTPS、CORS 白名单等组合拳，把无状态认证做得既好用又安全。&lt;/p&gt;&lt;/blockquote&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h1&gt;第二部分小结&lt;a href=&quot;#第二部分小结&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h1&gt;&lt;p&gt;到这里，框架核心的四大支柱你都已经掌握：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;MyBatis Plus&lt;/strong&gt;：用 &lt;code&gt;@TableName / @TableId / @TableLogic / @TableField&lt;/code&gt; 把类映射到表，BaseMapper 白嫖 CRUD，分页、自动填充、逻辑删除一键开启。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分层架构&lt;/strong&gt;：Controller / Service / Mapper / Entity 各司其职，DTO 接前端、VO 给前端，IoC 容器把它们串起来。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;统一响应与异常&lt;/strong&gt;：&lt;code&gt;Result&lt;/code&gt; 包返回值、&lt;code&gt;BusinessException&lt;/code&gt; 抛业务错、&lt;code&gt;GlobalExceptionHandler&lt;/code&gt; 全局兜底，前后端契约清晰。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Spring Security + JWT&lt;/strong&gt;：BCrypt 加密密码、过滤器解析 token、双 token 刷新、黑名单撤销、CORS 白名单、登录锁定，构成一套企业级的安全防线。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;接下来的第三部分将进入”业务实战”：从一个真实的”发布文章 / 点赞 / 评论 / 关注 / 通知 / 圈子”功能，把前面学的所有知识揉在一起，做出可运行的成品。&lt;/p&gt;&lt;hr /&gt;&lt;blockquote&gt;&lt;p&gt;全章节配套真实代码均位于本项目仓库：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;GitHub：&lt;a href=&quot;https://github.com/Xinghe-0203/Campus_Blog&quot; target=&quot;_blank&quot;&gt;https://github.com/Xinghe-0203/Campus_Blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;关键文件：&lt;code&gt;Result.java&lt;/code&gt;、&lt;code&gt;BusinessException.java&lt;/code&gt;、&lt;code&gt;GlobalExceptionHandler.java&lt;/code&gt;、&lt;code&gt;MybatisPlusConfig.java&lt;/code&gt;、&lt;code&gt;MyMetaObjectHandler.java&lt;/code&gt;、&lt;code&gt;SecurityConfig.java&lt;/code&gt;、&lt;code&gt;JwtAuthenticationFilter.java&lt;/code&gt;、&lt;code&gt;JwtUtils.java&lt;/code&gt;、&lt;code&gt;SecurityUtils.java&lt;/code&gt;、&lt;code&gt;SysUser.java&lt;/code&gt;、&lt;code&gt;BlogPost.java&lt;/code&gt;、&lt;code&gt;UserRegisterRequest.java&lt;/code&gt;、&lt;code&gt;SysUserMapper.java&lt;/code&gt;、&lt;code&gt;SysUserService.java&lt;/code&gt;、&lt;code&gt;SysUserServiceImpl.java&lt;/code&gt;、&lt;code&gt;SysUserController.java&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;&lt;/blockquote&gt;&lt;/section&gt;
&lt;section&gt;&lt;h1&gt;《Java Spring Boot 零基础完整学习手册》&lt;a href=&quot;#java-spring-boot-零基础完整学习手册-3&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h1&gt;&lt;section&gt;&lt;h2&gt;第三部分 项目实战：一行行读懂校园博客论坛&lt;a href=&quot;#第三部分-项目实战一行行读懂校园博客论坛&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;blockquote&gt;&lt;p&gt;本部分基于真实项目 &lt;code&gt;edu_project&lt;/code&gt;（v1.34）的源码，按照”需求 → 表 → DTO → Service → Controller”的顺序，把六大业务模块拆给零基础读者看。
阅读本部分前，请确保已经掌握第二部分中关于 Spring Boot、MyBatis Plus、Spring Security 的入门知识。&lt;/p&gt;&lt;/blockquote&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h1&gt;第 10 章 用户模块实战&lt;a href=&quot;#第-10-章-用户模块实战&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h1&gt;&lt;p&gt;用户模块是整个论坛的入口。所有”我”才能做的事——发文章、点赞、关注、收藏——都依赖一件事：&lt;strong&gt;系统知道当前请求是谁发的&lt;/strong&gt;。本章我们就一行行读懂它。&lt;/p&gt;&lt;section&gt;&lt;h2&gt;10.1 用户表 sys_user 字段解读&lt;a href=&quot;#101-用户表-sys_user-字段解读&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;校园博客的用户信息全部保存在 &lt;code&gt;sys_user&lt;/code&gt; 表中。让我们先看看这张表都有哪些字段：&lt;/p&gt;






























































































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;字段名&lt;/th&gt;&lt;th&gt;类型&lt;/th&gt;&lt;th&gt;含义&lt;/th&gt;&lt;th&gt;设计要点&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;id&lt;/code&gt;&lt;/td&gt;&lt;td&gt;BIGINT&lt;/td&gt;&lt;td&gt;主键，自增&lt;/td&gt;&lt;td&gt;全局唯一身份标识&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;username&lt;/code&gt;&lt;/td&gt;&lt;td&gt;VARCHAR(50)&lt;/td&gt;&lt;td&gt;登录用户名&lt;/td&gt;&lt;td&gt;&lt;strong&gt;唯一索引&lt;/strong&gt;，禁止重复&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;password&lt;/code&gt;&lt;/td&gt;&lt;td&gt;VARCHAR(100)&lt;/td&gt;&lt;td&gt;加密后的密码&lt;/td&gt;&lt;td&gt;BCrypt 摘要，永不明文存储&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;nickname&lt;/code&gt;&lt;/td&gt;&lt;td&gt;VARCHAR(50)&lt;/td&gt;&lt;td&gt;昵称（公开展示）&lt;/td&gt;&lt;td&gt;没填就用 username 兜底&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;email&lt;/code&gt;&lt;/td&gt;&lt;td&gt;VARCHAR(100)&lt;/td&gt;&lt;td&gt;邮箱&lt;/td&gt;&lt;td&gt;&lt;strong&gt;唯一索引&lt;/strong&gt;，可空&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;avatar&lt;/code&gt;&lt;/td&gt;&lt;td&gt;VARCHAR(255)&lt;/td&gt;&lt;td&gt;头像 URL&lt;/td&gt;&lt;td&gt;默认空，前端可显示占位图&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;role&lt;/code&gt;&lt;/td&gt;&lt;td&gt;VARCHAR(20)&lt;/td&gt;&lt;td&gt;角色：&lt;code&gt;user&lt;/code&gt; / &lt;code&gt;admin&lt;/code&gt;&lt;/td&gt;&lt;td&gt;用于 Spring Security 权限判断&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;status&lt;/code&gt;&lt;/td&gt;&lt;td&gt;TINYINT&lt;/td&gt;&lt;td&gt;账号状态：1 正常，0 禁用&lt;/td&gt;&lt;td&gt;管理员可一键封禁&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;login_fail_count&lt;/code&gt;&lt;/td&gt;&lt;td&gt;INT&lt;/td&gt;&lt;td&gt;连续登录失败次数&lt;/td&gt;&lt;td&gt;防爆破核心&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;lock_until&lt;/code&gt;&lt;/td&gt;&lt;td&gt;DATETIME&lt;/td&gt;&lt;td&gt;锁定截止时间&lt;/td&gt;&lt;td&gt;失败超 5 次锁 15 分钟&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;follower_count&lt;/code&gt;&lt;/td&gt;&lt;td&gt;INT&lt;/td&gt;&lt;td&gt;粉丝数&lt;/td&gt;&lt;td&gt;冗余字段，避免每次 COUNT&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;following_count&lt;/code&gt;&lt;/td&gt;&lt;td&gt;INT&lt;/td&gt;&lt;td&gt;关注数&lt;/td&gt;&lt;td&gt;同上&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;create_time&lt;/code&gt; / &lt;code&gt;update_time&lt;/code&gt;&lt;/td&gt;&lt;td&gt;DATETIME&lt;/td&gt;&lt;td&gt;创建/更新时间&lt;/td&gt;&lt;td&gt;MyBatis Plus 自动填充&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;is_deleted&lt;/code&gt;&lt;/td&gt;&lt;td&gt;TINYINT&lt;/td&gt;&lt;td&gt;逻辑删除标记&lt;/td&gt;&lt;td&gt;1 删除，0 正常&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;💡 &lt;strong&gt;设计决策&lt;/strong&gt;：把”粉丝数”这种统计值落地存储而不是每次 &lt;code&gt;SELECT COUNT(*)&lt;/code&gt;。这叫”冗余字段”。代价是每次关注/取关需要做一次 &lt;code&gt;UPDATE&lt;/code&gt;，但相对每次刷新主页都执行 COUNT 来说，性价比极高。&lt;/p&gt;&lt;p&gt;⚠️ &lt;strong&gt;易错点&lt;/strong&gt;：&lt;code&gt;is_deleted&lt;/code&gt; 字段不能直接 &lt;code&gt;WHERE is_deleted = 0&lt;/code&gt;，MyBatis Plus 的 &lt;code&gt;@TableLogic&lt;/code&gt; 注解会自动加这个条件，手写反而容易写双层。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;10.2 SysUser 实体类（@TableLogic、@JsonIgnore 防密码泄露）&lt;a href=&quot;#102-sysuser-实体类tablelogicjsonignore-防密码泄露&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;实体类是 Java 世界对数据库行的镜像：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Data&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;TableName&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;sys_user&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SysUser&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;TableId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;value&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;id&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;IdType&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;AUTO&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; id&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; username&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;JsonIgnore&lt;/span&gt;&lt;span&gt;   &lt;/span&gt;&lt;span&gt;// ⚠️ 关键：序列化为 JSON 时忽略密码字段&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; password&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; nickname&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; email&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; avatar&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; role&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Integer&lt;/span&gt;&lt;span&gt; status&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Integer&lt;/span&gt;&lt;span&gt; loginFailCount&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LocalDateTime&lt;/span&gt;&lt;span&gt; lockUntil&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Integer&lt;/span&gt;&lt;span&gt; followerCount&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;23&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Integer&lt;/span&gt;&lt;span&gt; followingCount&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;24&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;25&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;TableField&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;fill&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;FieldFill&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;INSERT&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;26&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LocalDateTime&lt;/span&gt;&lt;span&gt; createTime&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;27&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;28&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;TableField&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;fill&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;FieldFill&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;INSERT_UPDATE&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;29&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LocalDateTime&lt;/span&gt;&lt;span&gt; updateTime&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;30&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;31&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;TableLogic&lt;/span&gt;&lt;span&gt;   &lt;/span&gt;&lt;span&gt;// 逻辑删除标记&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;32&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Integer&lt;/span&gt;&lt;span&gt; isDeleted&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;33&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;逐行解读：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@TableName(&quot;sys_user&quot;)&lt;/code&gt;：告诉 MyBatis Plus，这个 Java 类对应数据库的 &lt;code&gt;sys_user&lt;/code&gt; 表。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@TableId(type = IdType.AUTO)&lt;/code&gt;：主键由数据库自增生成，新增时不要自己写 id。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@JsonIgnore&lt;/code&gt;：Spring MVC 把对象序列化成 JSON 返回前端时，跳过该字段——&lt;strong&gt;这是防止密码泄露给前端最简单也最有效的一道闸门&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@TableField(fill = ...)&lt;/code&gt;：配合 &lt;code&gt;MyMetaObjectHandler&lt;/code&gt;，自动填充时间。INSERT 时填 &lt;code&gt;createTime&lt;/code&gt;，UPDATE 时同时填 &lt;code&gt;updateTime&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@TableLogic&lt;/code&gt;：表示这是逻辑删除字段。调用 &lt;code&gt;removeById&lt;/code&gt; 时不会真的执行 DELETE，而是 UPDATE 把 &lt;code&gt;is_deleted&lt;/code&gt; 改为 1。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;⚠️ &lt;strong&gt;新手最爱犯的错&lt;/strong&gt;：直接把 &lt;code&gt;SysUser&lt;/code&gt; 当响应返回。即便有 &lt;code&gt;@JsonIgnore&lt;/code&gt;，未来某次需求改动忘了打这个注解，密码就裸奔到前端了。&lt;strong&gt;所以项目里专门有一个 &lt;code&gt;UserVO&lt;/code&gt;（视图对象）类用来返回&lt;/strong&gt;，从源头杜绝信息外泄。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;10.3 注册接口：DTO 校验 → 唯一性检查 → BCrypt → 用户枚举防护&lt;a href=&quot;#103-注册接口dto-校验--唯一性检查--bcrypt--用户枚举防护&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;我们从 &lt;code&gt;SysUserController#register&lt;/code&gt; 看起：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;PostMapping&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/register&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;UserRegisterResponse&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;register&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Valid&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;/span&gt;&lt;span&gt;RequestBody&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;UserRegisterRequest&lt;/span&gt;&lt;span&gt; request) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;UserRegisterResponse&lt;/span&gt;&lt;span&gt; response &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;sysUserService&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;register&lt;/span&gt;&lt;span&gt;(request);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;success&lt;/span&gt;&lt;span&gt;(response);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@RequestBody&lt;/code&gt;：把请求体里的 JSON 反序列化成 Java 对象。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Valid&lt;/code&gt;：触发 Bean Validation。校验不通过会被 &lt;code&gt;GlobalExceptionHandler&lt;/code&gt; 捕获并返回 400。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;DTO（请求对象）类似：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Data&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;UserRegisterRequest&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;NotBlank&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;message&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;用户名不能为空&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Size&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;min&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;max&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;20&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; username&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;NotBlank&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;message&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;密码不能为空&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Size&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;min&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;8&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;max&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; password&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Size&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;max&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; nickname&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Email&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; email&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;进入 Service 层就开始做”硬规则”：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Override&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Transactional&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;rollbackFor&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Exception&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;UserRegisterResponse&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;register&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;UserRegisterRequest&lt;/span&gt;&lt;span&gt; request) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 1. 密码强度校验：至少 8 位 + 包含大小写/数字/特殊字符任意 3 类&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; password &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getPassword&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; categories &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;password&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;matches&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;.*[A-Z].*&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;) categories&lt;/span&gt;&lt;span&gt;++;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;password&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;matches&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;.*[a-z].*&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;) categories&lt;/span&gt;&lt;span&gt;++;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;password&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;matches&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;.*&lt;/span&gt;&lt;span&gt;\\&lt;/span&gt;&lt;span&gt;d.*&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;) categories&lt;/span&gt;&lt;span&gt;++;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;password&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;matches&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;.*[!@#$%^&amp;amp;*()_+...].*&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;) categories&lt;/span&gt;&lt;span&gt;++;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (categories &lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;400&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;密码必须包含大小写字母、数字或特殊字符中的至少3种&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 2. 用户名唯一性 ——— 注意错误信息！&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;count&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LambdaQueryWrapper&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;SysUser&lt;/span&gt;&lt;span&gt;&amp;gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;eq&lt;/span&gt;&lt;span&gt;(SysUser&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getUsername, &lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getUsername&lt;/span&gt;&lt;span&gt;&lt;span&gt;()))&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;400&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;注册失败，请稍后重试&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 3. 邮箱唯一性&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;23&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getEmail&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getEmail&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;isEmpty&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;24&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;count&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LambdaQueryWrapper&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;SysUser&lt;/span&gt;&lt;span&gt;&amp;gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;25&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;eq&lt;/span&gt;&lt;span&gt;(SysUser&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getEmail, &lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getEmail&lt;/span&gt;&lt;span&gt;&lt;span&gt;()))&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;26&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;400&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;注册失败，请稍后重试&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;27&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;28&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;29&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;30&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 4. 创建用户对象&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;31&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;SysUser&lt;/span&gt;&lt;span&gt; user &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SysUser&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;32&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setUsername&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getUsername&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;33&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setPassword&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;passwordEncoder&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;encode&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getPassword&lt;/span&gt;&lt;span&gt;&lt;span&gt;()));&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// BCrypt&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;34&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setNickname&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getNickname&lt;/span&gt;&lt;span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;35&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getNickname&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getUsername&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;36&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setEmail&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getEmail&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;37&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setRole&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;user&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;38&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setStatus&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;39&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setLoginFailCount&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;40&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;41&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 5. 入库（捕获并发场景下的唯一约束冲突）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;42&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;try&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;43&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;save&lt;/span&gt;&lt;span&gt;(user);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;44&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;DuplicateKeyException&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;45&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;400&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;注册失败，请稍后重试&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;46&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;47&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;48&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;UserRegisterResponse&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getId&lt;/span&gt;&lt;span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getUsername&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;49&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;关键点逐一展开：&lt;/p&gt;&lt;p&gt;💡 &lt;strong&gt;为什么错误信息如此模糊？&lt;/strong&gt;
注意第 2/3 步的错误都是”注册失败，请稍后重试”，&lt;strong&gt;不是&lt;/strong&gt;”用户名已存在&quot;&quot;邮箱已注册”。这叫&lt;strong&gt;用户枚举防护（User Enumeration Protection）&lt;/strong&gt;：如果系统老老实实告诉攻击者”邮箱已注册”，攻击者就能批量猜出哪些邮箱在你站点上有账号，再去对应的邮件账号尝试爆破。&lt;/p&gt;&lt;p&gt;💡 &lt;strong&gt;为什么先 count 检查，后又要 try DuplicateKeyException？&lt;/strong&gt;
两道防线缺一不可：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;单线程时 count 检查就够了，但&lt;strong&gt;两个请求同时注册同一个用户名时&lt;/strong&gt;，它们都能通过 count 检查，然后两个都往数据库插。&lt;/li&gt;
&lt;li&gt;数据库的唯一索引是终极防线，必然抛 &lt;code&gt;DuplicateKeyException&lt;/code&gt;。我们捕获并返回友好提示。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;💡 &lt;strong&gt;BCrypt 的强度 12 是什么？&lt;/strong&gt;
&lt;code&gt;BCryptPasswordEncoder&lt;/code&gt; 默认 strength 是 10，本项目设为 12。每加 1 计算时间翻倍——即便数据库被拖库，攻击者每尝试一个密码也要 200 多毫秒，1 亿次组合需要约 240 天，足以让用户在察觉后改密码。&lt;/p&gt;&lt;p&gt;⚠️ &lt;strong&gt;&lt;code&gt;@Transactional(rollbackFor = Exception.class)&lt;/code&gt;&lt;/strong&gt; 必不可少。Spring 默认只对 &lt;code&gt;RuntimeException&lt;/code&gt; 回滚，但保险起见把所有 &lt;code&gt;Exception&lt;/code&gt; 都纳入回滚条件，避免某些 Checked Exception 让”半成品账号”留在表里。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;10.4 登录接口：账户锁定检查 → 密码验证 → JWT + refreshToken 返回&lt;a href=&quot;#104-登录接口账户锁定检查--密码验证--jwt--refreshtoken-返回&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;登录是一段非常微妙的代码：你要把”用户体验好”和”防爆破”两个需求拧在一起。&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Override&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Transactional&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;rollbackFor&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Exception&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;UserLoginResponse&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;login&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;UserLoginRequest&lt;/span&gt;&lt;span&gt; request) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 1. 取出用户&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;SysUser&lt;/span&gt;&lt;span&gt; user &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getOne&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LambdaQueryWrapper&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;SysUser&lt;/span&gt;&lt;span&gt;&amp;gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;eq&lt;/span&gt;&lt;span&gt;(SysUser&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getUsername, &lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getUsername&lt;/span&gt;&lt;span&gt;()));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (user &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;401&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;用户名或密码错误&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 2. 锁定检查（先于密码校验！）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getLockUntil&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getLockUntil&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;isAfter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;LocalDateTime&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;now&lt;/span&gt;&lt;span&gt;&lt;span&gt;())&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;403&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;登录失败次数过多，请稍后再试&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 3. 状态检查&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getStatus&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;403&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;账号已被禁用&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 4. 密码校验&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;23&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;/span&gt;&lt;span&gt;passwordEncoder&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;matches&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getPassword&lt;/span&gt;&lt;span&gt;(), &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getPassword&lt;/span&gt;&lt;span&gt;&lt;span&gt;())&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;24&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// 4.1 失败时原子地累加失败次数&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;25&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;handleLoginFailAtomic&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getId&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;26&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;27&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// 4.2 重新查询拿到最新锁定状态&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;28&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;SysUser&lt;/span&gt;&lt;span&gt; updated &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getById&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getId&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;29&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;updated&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getLockUntil&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;30&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;updated&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getLockUntil&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;isAfter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;LocalDateTime&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;now&lt;/span&gt;&lt;span&gt;&lt;span&gt;())&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;31&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;403&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;登录失败次数过多，请稍后再试&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;32&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;33&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;401&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;用户名或密码错误&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;34&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;35&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;36&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 5. 登录成功，重置计数&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;37&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getLoginFailCount&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;||&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getLoginFailCount&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;38&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setLoginFailCount&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;39&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setLockUntil&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;40&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;updateById&lt;/span&gt;&lt;span&gt;(user);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;41&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;42&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;43&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 6. 生成双 Token&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;44&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; token &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;jwtUtils&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;generateToken&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;45&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getId&lt;/span&gt;&lt;span&gt;(), &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getUsername&lt;/span&gt;&lt;span&gt;(), &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getRole&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;46&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; refreshToken &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;jwtUtils&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;generateRefreshToken&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;47&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getId&lt;/span&gt;&lt;span&gt;(), &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getUsername&lt;/span&gt;&lt;span&gt;(), &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getRole&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;48&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;49&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;UserLoginResponse&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getId&lt;/span&gt;&lt;span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getUsername&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;50&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getNickname&lt;/span&gt;&lt;span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getAvatar&lt;/span&gt;&lt;span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;span&gt; token&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; refreshToken)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;51&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;逐项拆解：&lt;/p&gt;&lt;p&gt;💡 &lt;strong&gt;顺序很关键&lt;/strong&gt;：先看锁，再看密码。
如果先校验密码、再判断是否锁定，攻击者每次仍然会消耗一次 &lt;code&gt;passwordEncoder.matches&lt;/code&gt;（约 250ms），但更糟的是会让”已锁定但仍可继续提交密码”——锁定就形同虚设。本项目顺序保证：账号一旦锁定，后续请求会被快速拦截（无 BCrypt 计算），保护数据库压力。&lt;/p&gt;&lt;p&gt;💡 &lt;strong&gt;错误信息”用户名或密码错误”&lt;/strong&gt;——同样是用户枚举防护：不告诉攻击者究竟是用户名错还是密码错。&lt;/p&gt;&lt;p&gt;💡 &lt;strong&gt;refreshToken 是什么？&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;token&lt;/code&gt;（accessToken）：1 小时有效期，每次请求带上。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;refreshToken&lt;/code&gt;：7 天有效期，&lt;strong&gt;只&lt;/strong&gt;给 &lt;code&gt;/user/refresh&lt;/code&gt; 端点用。当 accessToken 过期时，前端拿 refreshToken 去换一对新的 token。这样既能控制 accessToken 短生命周期（被盗也很快失效），又不需要让用户每小时重新登录。&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;10.5 登录失败锁定（5 次失败锁 15 分钟）&lt;a href=&quot;#105-登录失败锁定5-次失败锁-15-分钟&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;handleLoginFailAtomic&lt;/code&gt; 调用了 mapper 中的一段自定义 SQL：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;update&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;incrementLoginFailCount&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;UPDATE sys_user&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;SET login_fail_count = login_fail_count + 1,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;lock_until = CASE&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;WHEN login_fail_count + 1 &amp;gt;= #{maxFailCount}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;THEN DATE_ADD(NOW(), INTERVAL #{lockMinutes} MINUTE)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;ELSE lock_until&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;END&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;WHERE id = #{userId}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;update&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;为什么用一条 SQL 完成所有事？&lt;/p&gt;&lt;p&gt;⚠️ &lt;strong&gt;错误示范&lt;/strong&gt;：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;SysUser&lt;/span&gt;&lt;span&gt; u &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;userMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;selectById&lt;/span&gt;&lt;span&gt;(id);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;u&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setLoginFailCount&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;u&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getLoginFailCount&lt;/span&gt;&lt;span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;u&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getLoginFailCount&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;u&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setLockUntil&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;LocalDateTime&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;now&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;plusMinutes&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;15&lt;/span&gt;&lt;span&gt;));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;userMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;updateById&lt;/span&gt;&lt;span&gt;(u);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;这段代码看似没问题，但在并发下：两个失败请求同时读到 &lt;code&gt;loginFailCount = 4&lt;/code&gt;，都加 1 写回 5，最终值仍是 5——计数偏少了 1。我们用一条 SQL 让数据库自己 + 1 + 判断，一原子搞定，杜绝竞态。&lt;/p&gt;&lt;p&gt;💡 &lt;strong&gt;细节&lt;/strong&gt;：被锁定后，&lt;code&gt;lockUntil &amp;gt; now()&lt;/code&gt; 就直接拦截，连密码校验都不做——这就是前面强调”锁定检查先于密码校验”的原因。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;10.6 修改密码 + 用户搜索&lt;a href=&quot;#106-修改密码--用户搜索&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;修改密码和注册流程几乎对称，多了一步”验证旧密码”：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;changePassword&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt;&lt;span&gt; userId&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt; oldPassword&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; newPassword) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;SysUser&lt;/span&gt;&lt;span&gt; user &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getById&lt;/span&gt;&lt;span&gt;(userId);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (user &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;404&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;用户不存在&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;/span&gt;&lt;span&gt;passwordEncoder&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;matches&lt;/span&gt;&lt;span&gt;(oldPassword, &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getPassword&lt;/span&gt;&lt;span&gt;&lt;span&gt;())&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;400&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;旧密码不正确&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 复杂度校验同注册（略）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setPassword&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;passwordEncoder&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;encode&lt;/span&gt;&lt;span&gt;(newPassword));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;updateById&lt;/span&gt;&lt;span&gt;(user);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;⚠️ 修改密码后理论上应该把已签发的 token 加入黑名单，强制其它设备重新登录。本项目的 JWT 黑名单机制可以做到这点（在第二部分讲过），生产中务必启用。&lt;/p&gt;&lt;p&gt;用户搜索就是一个简单的分页 + 模糊查询：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IPage&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;UserVO&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;searchUsers&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;UserSearchRequest&lt;/span&gt;&lt;span&gt; request) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;Page&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;SysUser&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; page &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Page&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getPage&lt;/span&gt;&lt;span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getPageSize&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;LambdaQueryWrapper&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;SysUser&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; wrapper &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LambdaQueryWrapper&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;StringUtils&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;hasText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getKeyword&lt;/span&gt;&lt;span&gt;&lt;span&gt;())&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; kw &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getKeyword&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;trim&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;wrapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;and&lt;/span&gt;&lt;span&gt;(w &lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;w&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;like&lt;/span&gt;&lt;span&gt;(SysUser&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getUsername, kw)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;or&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;like&lt;/span&gt;&lt;span&gt;(SysUser&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getNickname, kw));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;wrapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;orderByDesc&lt;/span&gt;&lt;span&gt;(SysUser&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getCreateTime);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt;(page, wrapper).&lt;/span&gt;&lt;span&gt;convert&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;convertToUserVO);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;💡 这里 &lt;code&gt;convertToUserVO&lt;/code&gt; 会清掉密码字段，只留下展示用的属性，再次贯彻”永远不返回 SysUser”的纪律。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h1&gt;第 11 章 文章模块实战&lt;a href=&quot;#第-11-章-文章模块实战&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h1&gt;&lt;section&gt;&lt;h2&gt;11.1 blog_post 表结构&lt;a href=&quot;#111-blog_post-表结构&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;










































































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;字段&lt;/th&gt;&lt;th&gt;类型&lt;/th&gt;&lt;th&gt;说明&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;id&lt;/td&gt;&lt;td&gt;BIGINT&lt;/td&gt;&lt;td&gt;主键&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;user_id&lt;/td&gt;&lt;td&gt;BIGINT&lt;/td&gt;&lt;td&gt;作者ID（FK）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;title&lt;/td&gt;&lt;td&gt;VARCHAR(200)&lt;/td&gt;&lt;td&gt;标题&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;summary&lt;/td&gt;&lt;td&gt;VARCHAR(500)&lt;/td&gt;&lt;td&gt;摘要（手动或自动截取）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;content&lt;/td&gt;&lt;td&gt;TEXT&lt;/td&gt;&lt;td&gt;正文（最长 50000 字符）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;category&lt;/td&gt;&lt;td&gt;VARCHAR(50)&lt;/td&gt;&lt;td&gt;分类（默认”默认分类”）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;view_count&lt;/td&gt;&lt;td&gt;INT&lt;/td&gt;&lt;td&gt;阅读数&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;like_count&lt;/td&gt;&lt;td&gt;INT&lt;/td&gt;&lt;td&gt;点赞数&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;comment_count&lt;/td&gt;&lt;td&gt;INT&lt;/td&gt;&lt;td&gt;评论数&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;collect_count&lt;/td&gt;&lt;td&gt;INT&lt;/td&gt;&lt;td&gt;收藏数&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;status&lt;/td&gt;&lt;td&gt;TINYINT&lt;/td&gt;&lt;td&gt;1 已发布 / 0 草稿 / 2 下架&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;create_time / update_time&lt;/td&gt;&lt;td&gt;DATETIME&lt;/td&gt;&lt;td&gt;时间戳&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;is_deleted&lt;/td&gt;&lt;td&gt;TINYINT&lt;/td&gt;&lt;td&gt;逻辑删除&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;💡 四个 &lt;code&gt;count&lt;/code&gt; 字段都是冗余字段。原则同 &lt;code&gt;sys_user&lt;/code&gt; 的粉丝数：写入次数远小于读取次数，把统计落地能换来巨大性能优势。&lt;/p&gt;&lt;p&gt;⚠️ 这些 count 不能用 Java 层代码 &lt;code&gt;++&lt;/code&gt;、&lt;code&gt;--&lt;/code&gt;，&lt;strong&gt;必须&lt;/strong&gt;通过 &lt;code&gt;UPDATE blog_post SET like_count = like_count + 1 WHERE id = ?&lt;/code&gt; 由数据库做加法，避免并发丢失更新。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;11.2 发布文章（DTO + 标签关联 + XSS 过滤）&lt;a href=&quot;#112-发布文章dto--标签关联--xss-过滤&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;BlogPostController#createPost&lt;/code&gt;：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;PostMapping&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;createPost&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;/span&gt;&lt;span&gt;Valid&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;/span&gt;&lt;span&gt;RequestBody&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;PostCreateRequest&lt;/span&gt;&lt;span&gt; request) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; userId &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;SecurityUtils&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getCurrentUserIdOrNull&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (userId &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;401&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;请先登录&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; postId &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;blogPostService&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;createPost&lt;/span&gt;&lt;span&gt;(request, userId);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;success&lt;/span&gt;&lt;span&gt;(postId);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;DTO &lt;code&gt;PostCreateRequest&lt;/code&gt; 包含：title、summary、content、category、tagIds（List）。&lt;/p&gt;&lt;p&gt;Service 内部最关键的两步：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;XSS 过滤&lt;/strong&gt;：&lt;/li&gt;
&lt;/ol&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; sanitizedTitle   &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;htmlSanitizer&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;sanitizeRichText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getTitle&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; sanitizedSummary &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;htmlSanitizer&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;sanitizeRichText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getSummary&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; sanitizedContent &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;htmlSanitizer&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;sanitizeRichText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getContent&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;HtmlSanitizer&lt;/code&gt; 内部用 Jsoup 的白名单：富文本允许 &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt;/&lt;code&gt;&amp;lt;strong&amp;gt;&lt;/code&gt;/&lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; 等安全标签，剥离 &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;/&lt;code&gt;onerror=&lt;/code&gt; 等危险属性。category 用更严格的 &lt;code&gt;sanitizePlainText&lt;/code&gt; 完全剥离 HTML。&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;标签关联&lt;/strong&gt;：&lt;/li&gt;
&lt;/ol&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;save&lt;/span&gt;&lt;span&gt;&lt;span&gt;(post);&lt;/span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// 先保存文章拿到 id&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getTagIds&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getTagIds&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;isEmpty&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;savePostTags&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getId&lt;/span&gt;&lt;span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getTagIds&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;blog_post_tag&lt;/code&gt; 是中间表，主键 (postId, tagId)。&lt;code&gt;savePostTags&lt;/code&gt; 内部循环 insert。&lt;/p&gt;&lt;p&gt;⚠️ &lt;strong&gt;必须先保存文章拿到自增 id&lt;/strong&gt;，再写中间表。新手常犯的错：在 save 之前就拿 id 用，结果 id 还是 null。&lt;/p&gt;&lt;p&gt;💡 &lt;strong&gt;校验标签 ID 有效性&lt;/strong&gt;：项目调用 &lt;code&gt;validateTagIds&lt;/code&gt; 检查传入的 tagId 都真实存在，避免脏数据。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;11.3 文章列表（分页 + 多条件 LambdaQueryWrapper）&lt;a href=&quot;#113-文章列表分页--多条件-lambdaquerywrapper&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IPage&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;PostListResponse&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;getPostList&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;PostQueryRequest&lt;/span&gt;&lt;span&gt; request) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;Page&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;BlogPost&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; page &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Page&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getPage&lt;/span&gt;&lt;span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getPageSize&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;LambdaQueryWrapper&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;BlogPost&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; wrapper &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LambdaQueryWrapper&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;wrapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;eq&lt;/span&gt;&lt;span&gt;(BlogPost&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getStatus, &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;         &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// 已发布&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;           &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ne&lt;/span&gt;&lt;span&gt;(BlogPost&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getIsDeleted, &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;&lt;span&gt;);&lt;/span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// 排除已删除&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;StringUtils&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;hasText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getKeyword&lt;/span&gt;&lt;span&gt;&lt;span&gt;())&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;wrapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;and&lt;/span&gt;&lt;span&gt;(w &lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;w&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;like&lt;/span&gt;&lt;span&gt;(BlogPost&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getTitle, &lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getKeyword&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;or&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;like&lt;/span&gt;&lt;span&gt;(BlogPost&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getContent, &lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getKeyword&lt;/span&gt;&lt;span&gt;()));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;StringUtils&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;hasText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getCategory&lt;/span&gt;&lt;span&gt;&lt;span&gt;())&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;wrapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;eq&lt;/span&gt;&lt;span&gt;(BlogPost&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getCategory, &lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getCategory&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getUserId&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;wrapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;eq&lt;/span&gt;&lt;span&gt;(BlogPost&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getUserId, &lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getUserId&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getTagId&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// 通过中间表反查&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; ids &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;blogPostTagMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;selectList&lt;/span&gt;&lt;span&gt;(...).&lt;/span&gt;&lt;span&gt;stream&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;(BlogPostTag&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getPostId).&lt;/span&gt;&lt;span&gt;toList&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;ids&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;isEmpty&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Page&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;23&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;wrapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt;(BlogPost&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getId, ids);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;24&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;25&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;wrapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;orderByDesc&lt;/span&gt;&lt;span&gt;(BlogPost&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getCreateTime);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;26&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;27&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt;(page, wrapper).&lt;/span&gt;&lt;span&gt;convert&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;toListVO);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;28&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;💡 &lt;strong&gt;&lt;code&gt;LambdaQueryWrapper&lt;/code&gt; 的 &lt;code&gt;.and(w -&amp;gt; ...)&lt;/code&gt;&lt;/strong&gt;：括号里的 &lt;code&gt;or()&lt;/code&gt; 不会逃出去影响外层条件——这一点新手很容易踩坑。比如不写 &lt;code&gt;.and(...)&lt;/code&gt; 包裹 OR 子句，就会变成 &lt;code&gt;WHERE status=1 AND title LIKE &apos;x&apos; OR content LIKE &apos;x&apos;&lt;/code&gt;，关键字段全部被 OR “短路”过滤。&lt;/p&gt;&lt;p&gt;⚠️ 标签筛选这里有 &lt;strong&gt;N+1 隐患&lt;/strong&gt;：先查中间表，再 &lt;code&gt;IN&lt;/code&gt; 主表。如果某标签下文章过多，可能产生上千个 ID 的 IN 子句，未来需要换成 JOIN SQL 优化。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;11.4 文章详情 + 阅读量防刷&lt;a href=&quot;#114-文章详情--阅读量防刷&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;详情接口很普通——根据 ID 查表，拼装作者信息和标签。&lt;strong&gt;重头戏是阅读量防刷&lt;/strong&gt;。&lt;/p&gt;&lt;p&gt;&lt;code&gt;incrementViewCount&lt;/code&gt; 在 Controller 层先做了一道过滤：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;PutMapping&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/{id}/view&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;Void&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;incrementViewCount&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;/span&gt;&lt;span&gt;PathVariable&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt;&lt;span&gt; id&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;HttpServletRequest&lt;/span&gt;&lt;span&gt; req) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; userKey &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;getUserIdentifier&lt;/span&gt;&lt;span&gt;&lt;span&gt;(req) &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;-&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; id&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;long&lt;/span&gt;&lt;span&gt; now &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;System&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;currentTimeMillis&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;AtomicLong&lt;/span&gt;&lt;span&gt; lastViewTime &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;viewCountCache&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;computeIfAbsent&lt;/span&gt;&lt;span&gt;(userKey, k &lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;AtomicLong&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;while&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;long&lt;/span&gt;&lt;span&gt; lastTime &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;lastViewTime&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (now &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; lastTime &lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt; VIEW_COUNT_INTERVAL_MS) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;success&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;&lt;span&gt;);&lt;/span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// 1 分钟内不重复计数&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;lastViewTime&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;compareAndSet&lt;/span&gt;&lt;span&gt;&lt;span&gt;(lastTime, now)&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;/span&gt;&lt;span&gt;break&lt;/span&gt;&lt;span&gt;&lt;span&gt;;&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// CAS 成功&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// 失败，重读 lastTime 再次循环&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;blogPostService&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;incrementViewCount&lt;/span&gt;&lt;span&gt;(id);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;success&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;逐步解释：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;用户标识 &lt;code&gt;getUserIdentifier&lt;/code&gt;&lt;/strong&gt;：登录用户用 &lt;code&gt;user-{id}&lt;/code&gt;；未登录用 &lt;code&gt;guest-{IP}-{UA hash}&lt;/code&gt;，这就是常说的”指纹”。比单纯 IP 更难伪造，但比强账号体系成本更低。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缓存使用 LRU&lt;/strong&gt;：&lt;code&gt;new LinkedHashMap&amp;lt;&amp;gt;(16, 0.75f, true)&lt;/code&gt; + &lt;code&gt;removeEldestEntry&lt;/code&gt; 实现简单 LRU，最多 10000 条，避免内存泄漏。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CAS（Compare-And-Set）&lt;/strong&gt;：&lt;code&gt;compareAndSet(lastTime, now)&lt;/code&gt; 在并发场景下保证只有一个线程能成功更新时间戳，其它线程读到新值后会自动判断”在 1 分钟内”而 return。这就解决了所谓的 &lt;strong&gt;TOCTOU&lt;/strong&gt;（Time-Of-Check-To-Time-Of-Use）竞态。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;💡 真正的 incrementViewCount 落表也是一条原子 SQL：&lt;code&gt;UPDATE blog_post SET view_count = view_count + 1 WHERE id = ?&lt;/code&gt;。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;11.5 编辑/删除（仅作者或管理员）&lt;a href=&quot;#115-编辑删除仅作者或管理员&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getUserId&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;equals&lt;/span&gt;&lt;span&gt;&lt;span&gt;(userId)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;/span&gt;&lt;span&gt;SecurityUtils&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;isCurrentUserAdmin&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;403&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;无权修改此文章&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;💡 这一行权限检查，是项目里至少出现过 20 次的固定模式：&lt;strong&gt;资源所有者&lt;/strong&gt; 或 &lt;strong&gt;管理员&lt;/strong&gt; 可操作，其他人 403。&lt;/p&gt;&lt;p&gt;删除采用&lt;strong&gt;逻辑删除&lt;/strong&gt;——&lt;code&gt;removeById&lt;/code&gt; 内部由 &lt;code&gt;@TableLogic&lt;/code&gt; 转化为 UPDATE。同时手动清理 &lt;code&gt;blog_post_tag&lt;/code&gt; 中间表关联（标签是共享资源，关系不应保留）。&lt;/p&gt;&lt;p&gt;⚠️ 评论/点赞/收藏数据&lt;strong&gt;不级联删&lt;/strong&gt;，保留可用于审计，展示时通过 &lt;code&gt;is_deleted&lt;/code&gt; / &lt;code&gt;status&lt;/code&gt; 过滤即可。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;11.6 草稿自动保存&lt;a href=&quot;#116-草稿自动保存&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;blog_draft&lt;/code&gt; 表与 &lt;code&gt;blog_post&lt;/code&gt; 字段类似，多了 &lt;code&gt;user_id&lt;/code&gt;，少了统计字段。&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;saveDraft&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt;&lt;span&gt; userId&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;SaveDraftRequest&lt;/span&gt;&lt;span&gt; req) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;BlogDraft&lt;/span&gt;&lt;span&gt; draft &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BlogDraft&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;draft&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setUserId&lt;/span&gt;&lt;span&gt;(userId);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;draft&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setTitle&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;htmlSanitizer&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;sanitizeRichText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getTitle&lt;/span&gt;&lt;span&gt;()));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;draft&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setContent&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;htmlSanitizer&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;sanitizeRichText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getContent&lt;/span&gt;&lt;span&gt;()));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// ...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;blogDraftMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;insert&lt;/span&gt;&lt;span&gt;(draft);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;draft&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getId&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SaveDraftRequest&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;getLatestDraft&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; userId) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;blogDraftMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;selectOne&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LambdaQueryWrapper&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;BlogDraft&lt;/span&gt;&lt;span&gt;&amp;gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;eq&lt;/span&gt;&lt;span&gt;(BlogDraft&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getUserId, userId)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;orderByDesc&lt;/span&gt;&lt;span&gt;(BlogDraft&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getUpdateTime)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;last&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;LIMIT 1&quot;&lt;/span&gt;&lt;span&gt;));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;💡 前端可以做&lt;strong&gt;节流（debounce）30 秒触发一次&lt;/strong&gt;自动保存，加上手动保存按钮，体验和”知乎写文章”接近。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;11.7 高级搜索 + 搜索建议&lt;a href=&quot;#117-高级搜索--搜索建议&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;advancedSearch&lt;/code&gt; 比基础列表多了：分类组合、时间区间、按 view/like/createTime 排序。&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getStartDate&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;wrapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ge&lt;/span&gt;&lt;span&gt;(BlogPost&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getCreateTime, &lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getStartDate&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getEndDate&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;wrapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;le&lt;/span&gt;&lt;span&gt;(BlogPost&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getCreateTime, &lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getEndDate&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;switch&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getSortBy&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;case&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;viewCount&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;wrapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;orderByDesc&lt;/span&gt;&lt;span&gt;(BlogPost&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getViewCount);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;case&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;likeCount&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;wrapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;orderByDesc&lt;/span&gt;&lt;span&gt;(BlogPost&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getLikeCount);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;default&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;wrapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;orderByDesc&lt;/span&gt;&lt;span&gt;(BlogPost&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getCreateTime);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;getSearchSuggestions&lt;/code&gt; 给搜索框做自动补全：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;getSearchSuggestions&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; keyword) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;/span&gt;&lt;span&gt;StringUtils&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;hasText&lt;/span&gt;&lt;span&gt;&lt;span&gt;(keyword)&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;of&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;blogPostMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;selectList&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LambdaQueryWrapper&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;BlogPost&lt;/span&gt;&lt;span&gt;&amp;gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;eq&lt;/span&gt;&lt;span&gt;(BlogPost&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getStatus, &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;likeRight&lt;/span&gt;&lt;span&gt;(BlogPost&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getTitle, keyword)  &lt;/span&gt;&lt;span&gt;// 前缀匹配&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;orderByDesc&lt;/span&gt;&lt;span&gt;(BlogPost&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getViewCount)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;last&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;LIMIT 10&quot;&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stream&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;(BlogPost&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getTitle).&lt;/span&gt;&lt;span&gt;distinct&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;toList&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;💡 &lt;code&gt;likeRight&lt;/code&gt; 等价于 &lt;code&gt;LIKE &apos;keyword%&apos;&lt;/code&gt;，能命中索引；如果用 &lt;code&gt;like&lt;/code&gt;，前缀通配符会让 MySQL 全表扫描。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h1&gt;第 12 章 互动模块（评论 / 点赞 / 收藏）&lt;a href=&quot;#第-12-章-互动模块评论--点赞--收藏&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h1&gt;&lt;p&gt;互动模块是论坛的”血液”。三个动作各有难点：评论考验&lt;strong&gt;树形结构&lt;/strong&gt;，点赞考验&lt;strong&gt;并发写&lt;/strong&gt;，收藏与点赞高度相似。&lt;/p&gt;&lt;section&gt;&lt;h2&gt;12.1 评论：嵌套回复、树形组装、级联删除&lt;a href=&quot;#121-评论嵌套回复树形组装级联删除&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;blog_comment&lt;/code&gt; 字段：id、postId、userId、parentId、content、createTime、isDeleted。&lt;/p&gt;&lt;p&gt;新建评论时校验”父评论必须属于同一篇文章”是关键：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getParentId&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;BlogComment&lt;/span&gt;&lt;span&gt; parent &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getById&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getParentId&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (parent &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;404&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;父评论不存在&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;/span&gt;&lt;span&gt;request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getPostId&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;equals&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;parent&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getPostId&lt;/span&gt;&lt;span&gt;&lt;span&gt;())&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;400&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;父评论不属于该文章&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;💡 &lt;strong&gt;为什么校验？&lt;/strong&gt; 否则攻击者可以通过 &lt;code&gt;parentId&lt;/code&gt; 把回复”移植”到别的文章下，制造混乱。&lt;/p&gt;&lt;p&gt;⚠️ &lt;strong&gt;评论用 &lt;code&gt;sanitizePlainText&lt;/code&gt;&lt;/strong&gt;——比文章正文更严格，&lt;strong&gt;剥离所有 HTML&lt;/strong&gt;。因为评论场景用户没必要发富文本。这是 XSS 的额外加固。&lt;/p&gt;&lt;p&gt;随后 &lt;code&gt;incrementCommentCount(postId)&lt;/code&gt; 用 SQL 原子加 1。&lt;/p&gt;&lt;p&gt;事件机制同步抛出 &lt;code&gt;CommentCreatedEvent&lt;/code&gt;，留给通知模块去消费（见 13 章）。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;树形组装&lt;/strong&gt;：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;BlogComment&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; comments &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;list&lt;/span&gt;&lt;span&gt;&lt;span&gt;(...);&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// 拍平的评论列表&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;Map&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;CommentVO&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; voMap &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;comments&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stream&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;toVO)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;collect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Collectors&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toMap&lt;/span&gt;&lt;span&gt;(CommentVO&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getId, c &lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt; c));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;CommentVO&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; roots &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ArrayList&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;CommentVO&lt;/span&gt;&lt;span&gt; vo &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;voMap&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;values&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;vo&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getParentId&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;roots&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(vo);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;else&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;CommentVO&lt;/span&gt;&lt;span&gt; parent &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;voMap&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;vo&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getParentId&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (parent &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;parent&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getReplies&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(vo);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt;&lt;span&gt; roots&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;💡 这是一遍循环就构建树的经典写法——先把所有节点 put 进 Map，再二次循环挂到 parent 的 children 上。&lt;strong&gt;O(n) 不重复查询&lt;/strong&gt;，远快于”递归+SQL 多次访问”。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;级联删除&lt;/strong&gt;：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;deleteComment&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt;&lt;span&gt; commentId&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; userId) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;BlogComment&lt;/span&gt;&lt;span&gt; c &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getById&lt;/span&gt;&lt;span&gt;(commentId);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 权限检查同前&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; idsToDelete &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ArrayList&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;idsToDelete&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(commentId);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;collectChildCommentIds&lt;/span&gt;&lt;span&gt;&lt;span&gt;(commentId&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; idsToDelete&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// 递归收集&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;removeByIds&lt;/span&gt;&lt;span&gt;&lt;span&gt;(idsToDelete);&lt;/span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// 批量逻辑删除&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;⚠️ &lt;code&gt;collectChildCommentIds&lt;/code&gt; 第三个参数是&lt;strong&gt;递归深度&lt;/strong&gt;，限制嵌套层级（如 5 层）防止恶意构造的”深度炸弹”撑爆 JVM。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;12.2 点赞：唯一约束 + 逻辑删除 + 细粒度锁 + DuplicateKeyException&lt;a href=&quot;#122-点赞唯一约束--逻辑删除--细粒度锁--duplicatekeyexception&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;blog_like&lt;/code&gt; 表的复合唯一索引 &lt;code&gt;(user_id, post_id)&lt;/code&gt;：保证一人对一文章只能点一次赞。&lt;/p&gt;&lt;p&gt;但加了 &lt;code&gt;is_deleted&lt;/code&gt; 字段后，会出现一个矛盾：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;A 点赞 → 插入记录 (1, 100, isDeleted=0)&lt;/li&gt;
&lt;li&gt;A 取消 → 逻辑删除变成 (1, 100, isDeleted=1)&lt;/li&gt;
&lt;li&gt;A 又点赞 → 想再插 (1, 100, isDeleted=0) &lt;strong&gt;却被唯一索引挡住&lt;/strong&gt;！&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;项目的解法：&lt;strong&gt;唯一索引带上 isDeleted 列&lt;/strong&gt;：&lt;code&gt;UNIQUE (user_id, post_id, is_deleted)&lt;/code&gt;，这样删除后的记录与新记录的 (is_deleted) 不同，就不会冲突。或者另一种实现：&lt;strong&gt;不再插新行，把已删除行 isDeleted 改回 0&lt;/strong&gt;。本项目走前者方案。&lt;/p&gt;&lt;p&gt;核心实现 &lt;code&gt;toggleLike&lt;/code&gt;：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; lockKey &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; userId &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;-&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; postId&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;synchronized&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;getLock&lt;/span&gt;&lt;span&gt;(lockKey)) {                &lt;/span&gt;&lt;span&gt;// 细粒度锁&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;BlogLike&lt;/span&gt;&lt;span&gt; existing &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getOne&lt;/span&gt;&lt;span&gt;(...);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (existing &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// 取消点赞（逻辑删除）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;((BlogLikeMapper) baseMapper)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;logicalDeleteById&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;existing&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getId&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;blogPostService&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;decrementLikeCount&lt;/span&gt;&lt;span&gt;(postId);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;result&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setAction&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;unlike&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;else&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;try&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;save&lt;/span&gt;&lt;span&gt;(newLike);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;blogPostService&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;incrementLikeCount&lt;/span&gt;&lt;span&gt;(postId);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;eventPublisher&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;publishEvent&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LikeCreatedEvent&lt;/span&gt;&lt;span&gt;(...));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;result&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setAction&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;like&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;DuplicateKeyException&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;// 并发：另一线程已经插入&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;BlogLike&lt;/span&gt;&lt;span&gt; concurrent &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getOne&lt;/span&gt;&lt;span&gt;(...);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (concurrent &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;                &lt;/span&gt;&lt;span&gt;logicalDelete&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;concurrent&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getId&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;                &lt;/span&gt;&lt;span&gt;decrementLikeCount&lt;/span&gt;&lt;span&gt;&lt;span&gt;(postId)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;                &lt;/span&gt;&lt;span&gt;result&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setAction&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;unlike&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;23&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;24&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;25&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;💡 &lt;strong&gt;细粒度锁&lt;/strong&gt;：用 &lt;code&gt;userId-postId&lt;/code&gt; 作为 key，从 &lt;code&gt;ConcurrentHashMap&lt;/code&gt; 取一把 Object 锁。同一用户对同一文章的并发请求会被串行化，但不影响 100 万其它用户。比 &lt;code&gt;synchronized(this)&lt;/code&gt; 那种粗粒度锁高效百倍。&lt;/p&gt;&lt;p&gt;⚠️ &lt;strong&gt;细粒度锁的内存泄漏问题&lt;/strong&gt;：每次点赞都 put 一个新对象进 map，永远不删——百万用户后 OOM。本项目通过 &lt;code&gt;LockEntry.createTime&lt;/code&gt; + &lt;code&gt;MAX_LOCKS_SIZE&lt;/code&gt; 做 LRU + 过期清理。&lt;/p&gt;&lt;p&gt;💡 &lt;strong&gt;DuplicateKeyException 是最后防线&lt;/strong&gt;：即使前面 &lt;code&gt;getOne&lt;/code&gt; 没查到，仍可能有另一线程刚插完。数据库唯一索引会兜住，并由代码处理为”取消点赞”或”重试”。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;12.3 收藏：类似点赞 + “我的收藏”列表&lt;a href=&quot;#123-收藏类似点赞--我的收藏列表&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;blog_collect&lt;/code&gt; 表结构与 &lt;code&gt;blog_like&lt;/code&gt; 几乎一模一样，逻辑也复用同样的”细粒度锁 + 唯一索引 + DuplicateKey 兜底”模式。差异在多了一个”我的收藏”接口：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IPage&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;PostListResponse&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;myCollects&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt;&lt;span&gt; userId&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; size) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;Page&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;BlogCollect&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; p &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Page&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span&gt;(page&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; size)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;IPage&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;BlogCollect&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; collects &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;blogCollectMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;selectPage&lt;/span&gt;&lt;span&gt;(p,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LambdaQueryWrapper&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;BlogCollect&lt;/span&gt;&lt;span&gt;&amp;gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;eq&lt;/span&gt;&lt;span&gt;(BlogCollect&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getUserId, userId)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;orderByDesc&lt;/span&gt;&lt;span&gt;(BlogCollect&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getCreateTime));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; postIds &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;collects&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getRecords&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;stream&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;(BlogCollect&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getPostId).&lt;/span&gt;&lt;span&gt;toList&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;postIds&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;isEmpty&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Page&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span&gt;(page&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; size&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;BlogPost&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; posts &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;blogPostMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;selectBatchIds&lt;/span&gt;&lt;span&gt;(postIds);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 转 VO 略&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;💡 &lt;strong&gt;批量查询 selectBatchIds&lt;/strong&gt;——避免 N+1。一定要忍住”在循环里 selectById”的冲动。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h1&gt;第 13 章 关注与通知&lt;a href=&quot;#第-13-章-关注与通知&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h1&gt;&lt;section&gt;&lt;h2&gt;13.1 关注关系表 blog_follow&lt;a href=&quot;#131-关注关系表-blog_follow&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;




























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;字段&lt;/th&gt;&lt;th&gt;说明&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;id&lt;/td&gt;&lt;td&gt;主键&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;follower_id&lt;/td&gt;&lt;td&gt;谁关注（粉丝）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;following_id&lt;/td&gt;&lt;td&gt;被谁关注（博主）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;create_time&lt;/td&gt;&lt;td&gt;关注时间&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;is_deleted&lt;/td&gt;&lt;td&gt;逻辑删除&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;唯一索引 &lt;code&gt;(follower_id, following_id, is_deleted)&lt;/code&gt;，含义同点赞。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;13.2 关注/取关接口（不能关自己 + 双向计数）&lt;a href=&quot;#132-关注取关接口不能关自己--双向计数&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;FollowServiceImpl#follow&lt;/code&gt; 与 &lt;code&gt;toggleLike&lt;/code&gt; 高度相似：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;targetUserId&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;equals&lt;/span&gt;&lt;span&gt;&lt;span&gt;(currentUserId)&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;400&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;不能关注自己&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;SysUser&lt;/span&gt;&lt;span&gt; target &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;sysUserMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;selectById&lt;/span&gt;&lt;span&gt;(targetUserId);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (target &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;404&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;用户不存在&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;synchronized&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;getLock&lt;/span&gt;&lt;span&gt;&lt;span&gt;(currentUserId &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;-&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; targetUserId)) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;BlogFollow&lt;/span&gt;&lt;span&gt; exists &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getOne&lt;/span&gt;&lt;span&gt;(...);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (exists &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;result&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setAction&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;already_following&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;else&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;try&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;save&lt;/span&gt;&lt;span&gt;(newFollow);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;sysUserMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;incrementFollowerCount&lt;/span&gt;&lt;span&gt;&lt;span&gt;(targetUserId);&lt;/span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// 博主粉丝+1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;sysUserMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;incrementFollowingCount&lt;/span&gt;&lt;span&gt;&lt;span&gt;(currentUserId);&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// 我关注+1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;eventPublisher&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;publishEvent&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;FollowCreatedEvent&lt;/span&gt;&lt;span&gt;(currentUserId, targetUserId));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;DuplicateKeyException&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;// 并发兜底&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;💡 &lt;strong&gt;双向计数&lt;/strong&gt;：粉丝数和关注数各自维护在 &lt;code&gt;sys_user&lt;/code&gt; 表，原子 SQL 加减。&lt;/p&gt;&lt;p&gt;⚠️ &lt;strong&gt;必须先校验”目标用户存在”再插表&lt;/strong&gt;——否则插了脏数据，统计永远偏。&lt;/p&gt;&lt;p&gt;取关流程几乎对称：检查记录在 → 逻辑删除 → 双向减 1。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;13.3 粉丝/关注列表（分页）&lt;a href=&quot;#133-粉丝关注列表分页&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IPage&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;UserVO&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;getFollowersPage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt;&lt;span&gt; userId&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; size) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;Page&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;BlogFollow&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; p &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Page&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span&gt;(page&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; size)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;IPage&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;BlogFollow&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; followPage &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt;(p,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LambdaQueryWrapper&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;BlogFollow&lt;/span&gt;&lt;span&gt;&amp;gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;eq&lt;/span&gt;&lt;span&gt;(BlogFollow&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getFollowingId, userId)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;orderByDesc&lt;/span&gt;&lt;span&gt;(BlogFollow&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getCreateTime));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; followerIds &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;followPage&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getRecords&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;stream&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;(BlogFollow&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getFollowerId).&lt;/span&gt;&lt;span&gt;toList&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;followerIds&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;isEmpty&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Page&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span&gt;(page&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; size&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;SysUser&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; users &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;sysUserMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;selectBatchIds&lt;/span&gt;&lt;span&gt;(followerIds);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 拼 VO 返回&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;💡 同样是 &lt;strong&gt;selectBatchIds&lt;/strong&gt; 避免 N+1。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;13.4 通知系统：blog_notification&lt;a href=&quot;#134-通知系统blog_notification&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;
















































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;字段&lt;/th&gt;&lt;th&gt;说明&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;id&lt;/td&gt;&lt;td&gt;主键&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;type&lt;/td&gt;&lt;td&gt;通知类型（comment/like/follow/system）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;title&lt;/td&gt;&lt;td&gt;标题&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;content&lt;/td&gt;&lt;td&gt;内容&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;from_user_id&lt;/td&gt;&lt;td&gt;触发者（可能为 null：系统通知）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;to_user_id&lt;/td&gt;&lt;td&gt;接收者&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;target_type&lt;/td&gt;&lt;td&gt;目标类型（post/comment）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;target_id&lt;/td&gt;&lt;td&gt;目标 ID&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;is_read&lt;/td&gt;&lt;td&gt;0 未读 / 1 已读&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;create_time&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;&lt;code&gt;NotificationServiceImpl#sendNotification&lt;/code&gt; 写得很谨慎：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;sendNotification&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt; type&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt; title&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt; content&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt;&lt;span&gt; fromUserId&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt;&lt;span&gt; toUserId&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt; targetType&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; targetId) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 1. 不通知自己（自己评论自己的文章不会产生通知）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (fromUserId &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;fromUserId&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;equals&lt;/span&gt;&lt;span&gt;&lt;span&gt;(toUserId)&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 2. 检查接收者是否还存在&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (toUserId &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;SysUser&lt;/span&gt;&lt;span&gt; target &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;sysUserMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;selectById&lt;/span&gt;&lt;span&gt;(toUserId);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (target &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;BlogNotification&lt;/span&gt;&lt;span&gt; n &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BlogNotification&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setType&lt;/span&gt;&lt;span&gt;(type);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setTitle&lt;/span&gt;&lt;span&gt;(title);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setContent&lt;/span&gt;&lt;span&gt;(content);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setFromUserId&lt;/span&gt;&lt;span&gt;(fromUserId);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setToUserId&lt;/span&gt;&lt;span&gt;(toUserId);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setTargetType&lt;/span&gt;&lt;span&gt;(targetType);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setTargetId&lt;/span&gt;&lt;span&gt;(targetId);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setIsRead&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;save&lt;/span&gt;&lt;span&gt;(n);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;💡 “自己不通知自己”看似小事，实际是用户体验细节：你给自己文章点了赞、自己回复自己评论时，通知中心不会刷出来一条无意义记录。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;13.5 事件机制：CommentCreatedEvent / LikeCreatedEvent / FollowCreatedEvent&lt;a href=&quot;#135-事件机制commentcreatedevent--likecreatedevent--followcreatedevent&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;事件类只是简单的数据载体：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Getter&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LikeCreatedEvent&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;final&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; fromUserId&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;final&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; toUserId&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;final&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; postId&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;final&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; postTitle&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LikeCreatedEvent&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;fromUserId&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;toUserId&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;postId&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;title&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fromUserId&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; fromUserId;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toUserId&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; toUserId;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;postId&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; postId;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;postTitle&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; title;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;我们在三个地方 &lt;code&gt;eventPublisher.publishEvent(...)&lt;/code&gt;：评论创建、点赞、关注。&lt;/p&gt;&lt;p&gt;💡 &lt;strong&gt;为什么要事件机制？&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;解耦&lt;/strong&gt;：业务逻辑（点赞）不需要知道有”通知中心”存在。未来加”短信提醒”也只需多一个监听器。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可异步&lt;/strong&gt;：监听器可注解为异步执行，避免拖慢主流程。&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;13.6 @TransactionalEventListener 异步发送通知（事务提交后）&lt;a href=&quot;#136-transactionaleventlistener-异步发送通知事务提交后&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Component&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;NotificationEventListener&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Autowired&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;NotificationService&lt;/span&gt;&lt;span&gt; notificationService&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Async&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;TransactionalEventListener&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;phase&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;TransactionPhase&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;AFTER_COMMIT&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;onCommentCreated&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;CommentCreatedEvent&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;notificationService&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;sendNotification&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;comment&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;您的文章收到了新评论&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getContent&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getFromUserId&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getToUserId&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;post&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getPostId&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Async&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;TransactionalEventListener&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;phase&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;TransactionPhase&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;AFTER_COMMIT&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;onLikeCreated&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;LikeCreatedEvent&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;notificationService&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;sendNotification&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;23&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;like&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;您的文章收到了新的点赞&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;24&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;点赞了你的文章《&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getPostTitle&lt;/span&gt;&lt;span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;》&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;25&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getFromUserId&lt;/span&gt;&lt;span&gt;(), &lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getToUserId&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;26&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;&quot;post&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getPostId&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;27&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;28&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;逐项解释：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@TransactionalEventListener(phase = AFTER_COMMIT)&lt;/code&gt;：等当前事务&lt;strong&gt;真的提交&lt;/strong&gt;之后才触发，避免”事务回滚但通知已发”的脏数据。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Async&lt;/code&gt;：把发通知逻辑放到独立线程池执行，不阻塞主请求。&lt;/li&gt;
&lt;li&gt;需要在配置类加 &lt;code&gt;@EnableAsync&lt;/code&gt; 启用。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;⚠️ &lt;strong&gt;为何不用 Phase.BEFORE_COMMIT？&lt;/strong&gt; 因为如果事务最后回滚，通知就成了幽灵：用户永远找不到那条点赞，但被通知”被点赞了”。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h1&gt;第 14 章 校友圈与媒体&lt;a href=&quot;#第-14-章-校友圈与媒体&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h1&gt;&lt;p&gt;校友圈（Circle）类似微博/Twitter 的短动态系统，与博客（长文章）平行。&lt;/p&gt;&lt;section&gt;&lt;h2&gt;14.1 blog_circle_post 字段&lt;a href=&quot;#141-blog_circle_post-字段&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;




































































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;字段&lt;/th&gt;&lt;th&gt;说明&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;id&lt;/td&gt;&lt;td&gt;主键&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;user_id&lt;/td&gt;&lt;td&gt;作者&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;content&lt;/td&gt;&lt;td&gt;文字内容（最长 2000 字）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;content_type&lt;/td&gt;&lt;td&gt;1 纯文本 / 2 图文 / 3 转发&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;image_urls&lt;/td&gt;&lt;td&gt;JSON 数组，存图片 URL&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;location&lt;/td&gt;&lt;td&gt;位置信息&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;repost_id&lt;/td&gt;&lt;td&gt;原动态 id（仅转发时）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;view_count / like_count / comment_count / repost_count&lt;/td&gt;&lt;td&gt;计数&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;visibility&lt;/td&gt;&lt;td&gt;0 公开 / 1 仅关注者 / 2 仅自己&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;allow_comment&lt;/td&gt;&lt;td&gt;是否允许评论（0/1）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;allow_repost&lt;/td&gt;&lt;td&gt;是否允许转发（0/1）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;is_top&lt;/td&gt;&lt;td&gt;是否置顶&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;status&lt;/td&gt;&lt;td&gt;1 正常 / 2 已删除（这里用 status 代替 is_deleted）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;tags&lt;/td&gt;&lt;td&gt;JSON 数组&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;create_time / update_time&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;💡 &lt;strong&gt;为何 status 而不用 @TableLogic&lt;/strong&gt;？因为校友圈的”删除”不是简单标志，可能有”暂存&quot;&quot;违规下架”等状态，故用 status 数值。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;14.2 推荐流 vs 关注流&lt;a href=&quot;#142-推荐流-vs-关注流&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;getRecommendFeed&lt;/code&gt;：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;LambdaQueryWrapper&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;CirclePost&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; wrapper &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LambdaQueryWrapper&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;wrapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;eq&lt;/span&gt;&lt;span&gt;(CirclePost&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getStatus, &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;       &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;and&lt;/span&gt;&lt;span&gt;(w &lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;w&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;eq&lt;/span&gt;&lt;span&gt;(CirclePost&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getVisibility, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)        &lt;/span&gt;&lt;span&gt;// 公开&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;or&lt;/span&gt;&lt;span&gt;&lt;span&gt;(currentUserId &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;, w2 &lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;                      &lt;/span&gt;&lt;span&gt;w2&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;eq&lt;/span&gt;&lt;span&gt;(CirclePost&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getUserId, currentUserId)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ne&lt;/span&gt;&lt;span&gt;(CirclePost&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getVisibility, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)))&lt;/span&gt;&lt;span&gt;// 自己的非公开&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;       &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;orderByDesc&lt;/span&gt;&lt;span&gt;(CirclePost&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;&lt;span&gt;getIsTop)&lt;/span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// 置顶优先&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;       &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;orderByDesc&lt;/span&gt;&lt;span&gt;(CirclePost&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getCreateTime);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;getFollowingFeed&lt;/code&gt;：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;UserVO&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; followings &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;followService&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getFollowing&lt;/span&gt;&lt;span&gt;(userId);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; ids &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;followings&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stream&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;(UserVO&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getId).&lt;/span&gt;&lt;span&gt;toList&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;wrapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;eq&lt;/span&gt;&lt;span&gt;(CirclePost&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getStatus, &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;       &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt;(CirclePost&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getUserId, ids)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;       &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;and&lt;/span&gt;&lt;span&gt;(w &lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;w&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;eq&lt;/span&gt;&lt;span&gt;(CirclePost&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getVisibility, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;or&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;eq&lt;/span&gt;&lt;span&gt;(CirclePost&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getVisibility, &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;or&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;eq&lt;/span&gt;&lt;span&gt;(CirclePost&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getVisibility, &lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                       &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;eq&lt;/span&gt;&lt;span&gt;(CirclePost&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getUserId, userId));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;💡 &lt;strong&gt;关注流&lt;/strong&gt;只展示&lt;strong&gt;我关注的人&lt;/strong&gt;的动态：先取关注列表，再用 &lt;code&gt;in&lt;/code&gt; 拼出 SQL。空关注列表直接返回空集合（避免 IN () 报错）。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;14.3 canViewPost 权限校验&lt;a href=&quot;#143-canviewpost-权限校验&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;canViewPost&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;CirclePost&lt;/span&gt;&lt;span&gt;&lt;span&gt; post&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; currentUserId) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 1. 作者本人总是可以看&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (currentUserId &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getUserId&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;equals&lt;/span&gt;&lt;span&gt;&lt;span&gt;(currentUserId)&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 2. 公开动态人人可看&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getVisibility&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;||&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getVisibility&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 3. 仅关注者可见——必须是登录用户且关注了博主&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getVisibility&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (currentUserId &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;followService&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;isFollowing&lt;/span&gt;&lt;span&gt;(currentUserId, &lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getUserId&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 4. 仅自己可见，但不是作者本人，拒绝&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;⚠️ 进入 &lt;code&gt;getPostDetail&lt;/code&gt; 等方法时务必先调 &lt;code&gt;canViewPost&lt;/code&gt;，否则会泄露隐私动态。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;14.4 转发动态（原动态隐藏处理）&lt;a href=&quot;#144-转发动态原动态隐藏处理&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (repostId &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;CirclePost&lt;/span&gt;&lt;span&gt; original &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getById&lt;/span&gt;&lt;span&gt;(repostId);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (original &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;||&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;original&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getStatus&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;404&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;原动态不存在&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;original&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getAllowRepost&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;403&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;该动态禁止转发&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;/span&gt;&lt;span&gt;canViewPost&lt;/span&gt;&lt;span&gt;&lt;span&gt;(original&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; userId)) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;403&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;无权转发此动态&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;original&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getVisibility&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;) {  &lt;/span&gt;&lt;span&gt;// 原动态仅自己可见&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;visibility &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;&lt;span&gt;;&lt;/span&gt;&lt;span&gt;                    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// 自动降级到仅关注者&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;contentType &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;💡 一系列校验保证转发关系不被滥用：原动态必须存在 / 允许转发 / 我能看到。最后那条”原动态仅自己可见时，转发自动设为仅关注者”是个隐私升级——不可能让”自我可见”通过转发暴露给陌生人。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;14.5 校友圈点赞/评论/搜索&lt;a href=&quot;#145-校友圈点赞评论搜索&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;技术细节与博客点赞、博客评论一模一样：细粒度锁、DuplicateKeyException 兜底、原子 +/-、事件机制。&lt;strong&gt;复用既有模式，避免重复造轮子&lt;/strong&gt;——这是项目代码自始至终的纪律。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;14.6 媒体上传 blog_media（500MB + 类型白名单 + 重命名）&lt;a href=&quot;#146-媒体上传-blog_media500mb--类型白名单--重命名&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;upload&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;MultipartFile&lt;/span&gt;&lt;span&gt;&lt;span&gt; file&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; userId) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;file&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getSize&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;500L&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;1024&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;1024&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;400&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;文件不能超过 500MB&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; original &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;file&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getOriginalFilename&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; ext &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;getExtension&lt;/span&gt;&lt;span&gt;&lt;span&gt;(original)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;span&gt;toLowerCase&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;/span&gt;&lt;span&gt;ALLOWED_EXT&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;contains&lt;/span&gt;&lt;span&gt;&lt;span&gt;(ext)&lt;/span&gt;&lt;span&gt;) {  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// jpg/png/mp4/...&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;400&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;不支持的文件类型&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; newName &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;UUID&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;randomUUID&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;.&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; ext&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;Path&lt;/span&gt;&lt;span&gt; target &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Paths&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(uploadDir, newName);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;Files&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;copy&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;file&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getInputStream&lt;/span&gt;&lt;span&gt;(), target);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;BlogMedia&lt;/span&gt;&lt;span&gt; m &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BlogMedia&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;m&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setUserId&lt;/span&gt;&lt;span&gt;(userId);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;m&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setOriginalName&lt;/span&gt;&lt;span&gt;(original);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;m&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setStoredName&lt;/span&gt;&lt;span&gt;(newName);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;m&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setUrl&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/media/&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; newName);&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;m&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setSize&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;file&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getSize&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;m&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setMimeType&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;file&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getContentType&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;blogMediaMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;insert&lt;/span&gt;&lt;span&gt;(m);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;m&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getId&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;23&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;💡 &lt;strong&gt;三道防线&lt;/strong&gt;：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;大小限制&lt;/strong&gt;：避免 DOS（用户疯狂上传超大文件）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后缀白名单&lt;/strong&gt;：拒绝 .exe / .sh / .php，避免在服务器上落地恶意可执行文件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;UUID 重命名&lt;/strong&gt;：防止用户上传 &lt;code&gt;../../../etc/passwd&lt;/code&gt; 这样的”路径穿越”攻击。&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;⚠️ 仅靠后缀不够——攻击者可以把 .php 文件改名为 .jpg。生产环境还需校验 magic number（文件头）。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;14.7 文章绑定媒体 blog_post_media&lt;a href=&quot;#147-文章绑定媒体-blog_post_media&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;中间表 &lt;code&gt;(post_id, media_id, sort_order)&lt;/code&gt;：一篇文章可挂多张图，按 sort_order 排序。删除文章时只需移除关联，不删媒体本体——媒体可被多个动态/文章共享。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h1&gt;第 15 章 趋势 + 举报系统&lt;a href=&quot;#第-15-章-趋势--举报系统&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h1&gt;&lt;section&gt;&lt;h2&gt;15.1 blog_trending 热度统计&lt;a href=&quot;#151-blog_trending-热度统计&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;




































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;字段&lt;/th&gt;&lt;th&gt;说明&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;id&lt;/td&gt;&lt;td&gt;主键&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;target_type&lt;/td&gt;&lt;td&gt;post / tag&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;target_id&lt;/td&gt;&lt;td&gt;目标 ID&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;score&lt;/td&gt;&lt;td&gt;热度分（计算结果）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;period&lt;/td&gt;&lt;td&gt;时间周期：daily / weekly&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;stat_time&lt;/td&gt;&lt;td&gt;统计基准时间&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;create_time&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;15.2 热度公式&lt;a href=&quot;#152-热度公式&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;真实项目的热度计算采用&lt;strong&gt;固定权重加权累加&lt;/strong&gt;，公式如下：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;score = view_count × 1 + like_count × 5 + comment_count × 10&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;三个常量的定义在 &lt;code&gt;TrendingServiceImpl.java&lt;/code&gt; 中（第 43-45 行）：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;static&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;final&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; VIEW_WEIGHT &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;static&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;final&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; LIKE_WEIGHT &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;static&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;final&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; COMMENT_WEIGHT &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;没有&lt;/strong&gt; &lt;code&gt;collect_count&lt;/code&gt; 权重，也没有时间衰减函数 &lt;code&gt;decay()&lt;/code&gt;。每天凌晨 0 点定时任务（&lt;code&gt;@Scheduled(cron = &quot;0 0 0 * * ?&quot;)&lt;/code&gt;）全量重新计算所有已发布文章的热度分，结果写入 &lt;code&gt;blog_trending&lt;/code&gt; 表。查询接口只读这张表，零计算压力。&lt;/p&gt;&lt;p&gt;⚠️ 项目未使用收藏数权重和时间衰减。如需调整权重，需修改源码中这三个常量并重新部署。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;15.3 热门文章/热门标签 API&lt;a href=&quot;#153-热门文章热门标签-api&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;GetMapping&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/trending/posts&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;PostListResponse&lt;/span&gt;&lt;span&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;hotPosts&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;RequestParam&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;defaultValue&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;daily&quot;&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt; period&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;RequestParam&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;defaultValue&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;10&quot;&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt; limit) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;success&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;trendingService&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getHotPosts&lt;/span&gt;&lt;span&gt;(period, limit));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;Service 内部按 score 降序，再去 &lt;code&gt;blog_post&lt;/code&gt; 表查详情。&lt;/p&gt;&lt;p&gt;💡 &lt;strong&gt;每天 0 点跑一次定时任务&lt;/strong&gt;重新计算所有文章热度，结果写入 &lt;code&gt;blog_trending&lt;/code&gt;。读接口只查这张表，零计算。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;15.4 举报模块：用户举报 + 重复检查 + 管理员封禁/下架&lt;a href=&quot;#154-举报模块用户举报--重复检查--管理员封禁下架&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;blog_report&lt;/code&gt; 字段：id、reporterId、targetType（post/comment/circle/user）、targetId、reason、status（0 待处理/1 已处理/2 已驳回）、handleResult、handleTime、handlerId、create_time。&lt;/p&gt;&lt;p&gt;举报核心代码：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;report&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;ReportCreateRequest&lt;/span&gt;&lt;span&gt;&lt;span&gt; req&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; userId) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 1. 24 小时内不能重复举报同一目标&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;LambdaQueryWrapper&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;BlogReport&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; w &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LambdaQueryWrapper&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;w&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;eq&lt;/span&gt;&lt;span&gt;(BlogReport&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getReporterId, userId)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;eq&lt;/span&gt;&lt;span&gt;(BlogReport&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getTargetType, &lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getTargetType&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;eq&lt;/span&gt;&lt;span&gt;(BlogReport&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getTargetId, &lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getTargetId&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ge&lt;/span&gt;&lt;span&gt;(BlogReport&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getCreateTime, &lt;/span&gt;&lt;span&gt;LocalDateTime&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;now&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;minusHours&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;24&lt;/span&gt;&lt;span&gt;));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;count&lt;/span&gt;&lt;span&gt;&lt;span&gt;(w)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;400&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;您已举报过此内容&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 2. 入库&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;BlogReport&lt;/span&gt;&lt;span&gt; r &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BlogReport&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;BeanUtils&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;copyProperties&lt;/span&gt;&lt;span&gt;(req, r);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setReporterId&lt;/span&gt;&lt;span&gt;(userId);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setStatus&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;save&lt;/span&gt;&lt;span&gt;(r);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getId&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;管理员处理时：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;handleReport&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt;&lt;span&gt; reportId&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Integer&lt;/span&gt;&lt;span&gt;&lt;span&gt; action&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt; result&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; adminId) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;BlogReport&lt;/span&gt;&lt;span&gt; r &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getById&lt;/span&gt;&lt;span&gt;(reportId);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (r &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;404&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;举报不存在&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setStatus&lt;/span&gt;&lt;span&gt;&lt;span&gt;(action &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setHandleResult&lt;/span&gt;&lt;span&gt;(result);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setHandlerId&lt;/span&gt;&lt;span&gt;(adminId);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setHandleTime&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;LocalDateTime&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;now&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;this&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;updateById&lt;/span&gt;&lt;span&gt;(r);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (action &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// 处置：根据 targetType 下架内容 / 封禁用户&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;switch&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getTargetType&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;case&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;post&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;blogPostMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;updateStatus&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getTargetId&lt;/span&gt;&lt;span&gt;(), &lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;case&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;comment&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;blogCommentMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;deleteById&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getTargetId&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;case&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;user&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;sysUserMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;disableUser&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getTargetId&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;💡 &lt;strong&gt;重复举报防护&lt;/strong&gt;：限制 24 小时内只能举报同一目标一次，避免一个用户疯狂刷量；与”通知不通知自己”同属于体验细节。&lt;/p&gt;&lt;p&gt;⚠️ 真正生产环境还需要：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;举报内容 XSS 过滤（reason 字段同样要 sanitizePlainText）。&lt;/li&gt;
&lt;li&gt;管理员操作日志（哪个 admin 在何时封了谁，便于事后追溯）。&lt;/li&gt;
&lt;li&gt;严重举报阈值机制：同一内容被 N 个用户举报后自动隐藏（人工复核前先撤下）。&lt;/li&gt;
&lt;/ul&gt;&lt;hr /&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;本章小结&lt;a href=&quot;#本章小结&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;走完六大模块，我们能回答下面这些问题了：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;如何设计一张实体表？&lt;/strong&gt; → 业务字段 + 冗余统计 + 时间戳 + 逻辑删除标记。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;如何防止用户枚举？&lt;/strong&gt; → 注册和登录的失败提示统一模糊化。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;如何防爆破？&lt;/strong&gt; → 失败次数原子累加 + 锁定时间窗口。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;如何防 XSS？&lt;/strong&gt; → Jsoup 白名单过滤，富文本宽松、纯文本严格。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;如何防并发写错乱？&lt;/strong&gt; → 细粒度锁 + 数据库唯一索引 + DuplicateKeyException 兜底。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;如何避免 N+1 查询？&lt;/strong&gt; → &lt;code&gt;selectBatchIds&lt;/code&gt; 一次取齐 + Map O(1) 查找。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;如何让通知不污染主流程？&lt;/strong&gt; → ApplicationEventPublisher + &lt;code&gt;@TransactionalEventListener(AFTER_COMMIT) + @Async&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;如何统计热度？&lt;/strong&gt; → 固定权重加权（view×1 + like×5 + comment×10）+ 每天凌晨定时任务全量预计算。&lt;/li&gt;
&lt;/ol&gt;&lt;hr /&gt;&lt;p&gt;下一部分将进入 &lt;strong&gt;前端实战&lt;/strong&gt;，把这些 API 接到一个用 HTML5 + JavaScript 写的论坛主页里——届时你就能从浏览器的”点击点赞”按钮，一路追到本章里的 &lt;code&gt;toggleLike&lt;/code&gt; 方法，把整个全栈链路打通。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h1&gt;《Java Spring Boot 零基础完整学习手册》&lt;a href=&quot;#java-spring-boot-零基础完整学习手册-4&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h1&gt;&lt;section&gt;&lt;h2&gt;第四部分 进阶与部署&lt;a href=&quot;#第四部分-进阶与部署&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;blockquote&gt;&lt;p&gt;基于真实项目”校园博客论坛系统 v1.34”的实战教材
项目地址：&lt;a href=&quot;https://github.com/Xinghe-0203/Campus_Blog&quot; target=&quot;_blank&quot;&gt;https://github.com/Xinghe-0203/Campus_Blog&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr /&gt;&lt;p&gt;读完前三部分，你应该已经能写出基本的 Spring Boot CRUD 接口，能登录、能发文章、能评论。但这远远不够。一个能上线、能扛住真实用户的系统，必须经过&lt;strong&gt;安全加固、性能优化、充分测试、规范部署&lt;/strong&gt;这四关。&lt;/p&gt;&lt;p&gt;第四部分就是带你走完这四关。所有讲解都会引用本项目从 v1.10 到 v1.34 的真实修复案例 —— 这些”坑”是项目作者真踩过的，每一个都对应一次代码提交。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h1&gt;第 16 章 安全加固实战&lt;a href=&quot;#第-16-章-安全加固实战&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h1&gt;&lt;section&gt;&lt;h2&gt;16.1 OWASP Top 10 安全风险概览&lt;a href=&quot;#161-owasp-top-10-安全风险概览&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;OWASP（Open Web Application Security Project）每隔几年会发布一次”Web 应用十大安全风险榜单”。学习安全的第一步，是知道有哪些坑。下面是 2021 版的精简清单：&lt;/p&gt;






































































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;风险编号&lt;/th&gt;&lt;th&gt;名称&lt;/th&gt;&lt;th&gt;通俗解释&lt;/th&gt;&lt;th&gt;本项目对应防护&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;A01&lt;/td&gt;&lt;td&gt;失效的访问控制&lt;/td&gt;&lt;td&gt;普通用户能改别人文章&lt;/td&gt;&lt;td&gt;越权校验（16.8 节）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;A02&lt;/td&gt;&lt;td&gt;加密机制失效&lt;/td&gt;&lt;td&gt;密码明文存数据库&lt;/td&gt;&lt;td&gt;BCrypt 加盐（16.4 节）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;A03&lt;/td&gt;&lt;td&gt;注入&lt;/td&gt;&lt;td&gt;SQL 注入、命令注入&lt;/td&gt;&lt;td&gt;MyBatis #{} 参数化（16.2 节）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;A04&lt;/td&gt;&lt;td&gt;不安全设计&lt;/td&gt;&lt;td&gt;没有限流、没有锁&lt;/td&gt;&lt;td&gt;登录限流（16.7 节）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;A05&lt;/td&gt;&lt;td&gt;安全配置错误&lt;/td&gt;&lt;td&gt;默认密码、调试模式开&lt;/td&gt;&lt;td&gt;.env 隔离（16.5 节）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;A06&lt;/td&gt;&lt;td&gt;自带漏洞和过时组件&lt;/td&gt;&lt;td&gt;老版本依赖有 CVE&lt;/td&gt;&lt;td&gt;pom.xml 用最新 LTS&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;A07&lt;/td&gt;&lt;td&gt;身份认证失效&lt;/td&gt;&lt;td&gt;Token 不能撤销&lt;/td&gt;&lt;td&gt;JWT 黑名单（16.5 节）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;A08&lt;/td&gt;&lt;td&gt;软件和数据完整性失效&lt;/td&gt;&lt;td&gt;反序列化漏洞&lt;/td&gt;&lt;td&gt;不接收不可信对象&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;A09&lt;/td&gt;&lt;td&gt;安全日志和监控缺失&lt;/td&gt;&lt;td&gt;攻击了都不知道&lt;/td&gt;&lt;td&gt;log.warn 关键操作&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;A10&lt;/td&gt;&lt;td&gt;服务端请求伪造（SSRF）&lt;/td&gt;&lt;td&gt;让服务器去访问内网&lt;/td&gt;&lt;td&gt;URL 白名单&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;⚠️ &lt;strong&gt;重要观念&lt;/strong&gt;：安全不是某个”模块”，而是渗透到每一行代码里的”思维方式”。本项目从 v1.10 开始就一直在做安全加固，到 v1.34 已经迭代了 20 多次。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;16.2 SQL 注入防护&lt;a href=&quot;#162-sql-注入防护&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;16.2.1 什么是 SQL 注入&lt;a href=&quot;#1621-什么是-sql-注入&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;假设有一段拼接 SQL 的代码：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// 危险写法（不要这样写！）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; sql &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;SELECT * FROM sys_user WHERE username = &apos;&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; username &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;&apos;&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;如果攻击者把 &lt;code&gt;username&lt;/code&gt; 传成 &lt;code&gt;&apos; OR &apos;1&apos;=&apos;1&lt;/code&gt;，最终拼出来的 SQL 是：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; * &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; sys_user &lt;/span&gt;&lt;span&gt;WHERE&lt;/span&gt;&lt;span&gt;&lt;span&gt; username &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&apos;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;OR&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;1&apos;&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&apos;1&apos;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;&apos;1&apos;=&apos;1&apos;&lt;/code&gt; 永远为真，这条 SQL 会把整张用户表查出来。更狠的攻击者甚至能传 &lt;code&gt;&apos;; DROP TABLE sys_user; --&lt;/code&gt;，直接把表删了。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;16.2.2 MyBatis 的 #{} 与 ${}&lt;a href=&quot;#1622-mybatis-的--与-&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;本项目使用 MyBatis Plus，所有 SQL 参数都通过 &lt;code&gt;#{}&lt;/code&gt; 占位符传递。&lt;strong&gt;这是最重要的注入防护&lt;/strong&gt;。&lt;/p&gt;



















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;写法&lt;/th&gt;&lt;th&gt;效果&lt;/th&gt;&lt;th&gt;安全性&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;#{username}&lt;/code&gt;&lt;/td&gt;&lt;td&gt;编译为预编译语句的 &lt;code&gt;?&lt;/code&gt; 占位符，参数值由 JDBC 驱动转义&lt;/td&gt;&lt;td&gt;安全&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;${username}&lt;/code&gt;&lt;/td&gt;&lt;td&gt;直接做字符串替换&lt;/td&gt;&lt;td&gt;危险&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;举个例子，下面这两条乍看一样：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&amp;lt;!-- 安全：参数化查询 --&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;select&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;findByUsername&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;SELECT * FROM sys_user WHERE username = #{username}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;select&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&amp;lt;!-- 危险：字符串拼接 --&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;select&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;findByUsername&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;SELECT * FROM sys_user WHERE username = &apos;${username}&apos;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;select&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;第一条，无论 &lt;code&gt;username&lt;/code&gt; 是什么字符（哪怕带单引号），都只会被当成”一个值”处理；第二条，攻击者完全能改变 SQL 结构。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;16.2.3 项目中的实践&lt;a href=&quot;#1623-项目中的实践&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;本项目的所有 Mapper 都继承自 &lt;code&gt;BaseMapper&amp;lt;T&amp;gt;&lt;/code&gt;，标准 CRUD 方法（insert、updateById、selectById、selectList）都是 MyBatis Plus 内置的，全部使用预编译 SQL，零拼接风险。&lt;/p&gt;&lt;p&gt;⚠️ &lt;strong&gt;铁律&lt;/strong&gt;：除了 &lt;code&gt;ORDER BY 字段名&lt;/code&gt;、&lt;code&gt;表名&lt;/code&gt; 这种结构性内容外，&lt;strong&gt;任何用户输入都不能用 &lt;code&gt;${}&lt;/code&gt;&lt;/strong&gt;。如果一定要做动态排序，务必先用白名单校验字段名。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;16.3 XSS 跨站脚本防护（基于真实 HtmlSanitizer.java）&lt;a href=&quot;#163-xss-跨站脚本防护基于真实-htmlsanitizerjava&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;16.3.1 什么是 XSS&lt;a href=&quot;#1631-什么是-xss&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;XSS（Cross-Site Scripting）的本质是&lt;strong&gt;让浏览器去执行攻击者注入的脚本&lt;/strong&gt;。比如：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;用户 A 发了一篇文章，标题里写了 &lt;code&gt;&amp;lt;script&amp;gt;alert(&apos;被攻击了&apos;)&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;这段标题被原样存进数据库&lt;/li&gt;
&lt;li&gt;用户 B 打开这篇文章列表，浏览器看到 &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; 就会执行&lt;/li&gt;
&lt;li&gt;攻击者可以用脚本偷 B 的 Cookie、改 B 的密码、转账……&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;XSS 在博客、评论这种富文本场景里最常见。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;16.3.2 项目的解决方案：Jsoup 白名单过滤&lt;a href=&quot;#1632-项目的解决方案jsoup-白名单过滤&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;本项目在 v1.10 引入 Jsoup 做 XSS 防护，核心代码就在 &lt;code&gt;HtmlSanitizer.java&lt;/code&gt; 里：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Component&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;HtmlSanitizer&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;/**&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* 宽松白名单：允许部分 HTML 标签（用于文章内容等富文本）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;*/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;static&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;final&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Safelist&lt;/span&gt;&lt;span&gt; RELAXED_WHITELIST &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Safelist&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;relaxed&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;addTags&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;span&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;div&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;hr&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;table&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;thead&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;tbody&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;tr&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;th&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;td&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;addAttributes&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;a&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;href&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;title&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;target&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;addAttributes&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;img&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;src&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;alt&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;title&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;width&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;height&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;addProtocols&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;img&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;src&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;http&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;https&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;addProtocols&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;a&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;href&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;http&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;https&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;mailto&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;preserveRelativeLinks&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;/**&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* 严格白名单：完全清空所有 HTML 标签（用于评论）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;*/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;static&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;final&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Safelist&lt;/span&gt;&lt;span&gt; STRICT_WHITELIST &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Safelist&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;none&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;sanitizeRichText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;html&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (html &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;||&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;html&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;isEmpty&lt;/span&gt;&lt;span&gt;()) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; html;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;23&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;24&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Jsoup&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;clean&lt;/span&gt;&lt;span&gt;(html, RELAXED_WHITELIST);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;25&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;26&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;27&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;sanitizePlainText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;text&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;28&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (text &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;||&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;text&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;isEmpty&lt;/span&gt;&lt;span&gt;()) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;29&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; text;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;30&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;31&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Jsoup&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;clean&lt;/span&gt;&lt;span&gt;(text, STRICT_WHITELIST);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;32&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;33&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;两套白名单的设计理念&lt;/strong&gt;：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;RELAXED_WHITELIST&lt;/code&gt;（宽松）&lt;/strong&gt;：允许文章正文出现 &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt;、&lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;、&lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; 这些常见富文本标签，但禁止 &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;、&lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt;、&lt;code&gt;onclick=...&lt;/code&gt; 等危险元素。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;STRICT_WHITELIST&lt;/code&gt;（严格）&lt;/strong&gt;：用于评论、分类名等不需要富文本的场景，&lt;strong&gt;所有&lt;/strong&gt; HTML 标签全部清掉。&lt;/li&gt;
&lt;/ol&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;16.3.3 关键细节：限制图片协议&lt;a href=&quot;#1633-关键细节限制图片协议&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;注意这一行：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;addProtocols&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;img&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;src&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;http&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;https&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;这是 v1.10 一次安全审查后专门加的。&lt;strong&gt;Jsoup 默认允许 &lt;code&gt;data:&lt;/code&gt; 协议&lt;/strong&gt;，攻击者可能写：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;img&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;src&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;data:image/svg+xml,&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;svg onload=alert(1)&amp;gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;/svg&amp;gt;&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;只允许 &lt;code&gt;http&lt;/code&gt;、&lt;code&gt;https&lt;/code&gt; 协议后，这种 bypass 就被堵死了。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;16.3.4 实际使用&lt;a href=&quot;#1634-实际使用&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;在文章 Service 里：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Autowired&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;HtmlSanitizer&lt;/span&gt;&lt;span&gt; htmlSanitizer&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;createPost&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;BlogPost&lt;/span&gt;&lt;span&gt; post) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 文章内容是富文本，使用宽松白名单&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setTitle&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;htmlSanitizer&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;sanitizePlainText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getTitle&lt;/span&gt;&lt;span&gt;()));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setContent&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;htmlSanitizer&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;sanitizeRichText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getContent&lt;/span&gt;&lt;span&gt;()));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setSummary&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;htmlSanitizer&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;sanitizePlainText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getSummary&lt;/span&gt;&lt;span&gt;()));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;blogPostMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;insert&lt;/span&gt;&lt;span&gt;(post);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;⚠️ &lt;strong&gt;过滤的位置&lt;/strong&gt;：必须在&lt;strong&gt;入库前&lt;/strong&gt;过滤。如果只在前端展示时过滤，绕过非常容易（比如直接用 Postman 调接口）。&lt;strong&gt;永远不要相信前端&lt;/strong&gt;。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;16.4 密码安全&lt;a href=&quot;#164-密码安全&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;16.4.1 为什么不能明文存密码&lt;a href=&quot;#1641-为什么不能明文存密码&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;数据库被脱库的事经常发生。如果你存的是明文，攻击者拿到数据库就拿到了所有用户的密码 —— 而且很多用户在多个网站用同一个密码，你还连累了别的网站。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;16.4.2 BCrypt 加盐 + 12 轮&lt;a href=&quot;#1642-bcrypt-加盐--12-轮&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;本项目在 &lt;code&gt;SecurityConfig&lt;/code&gt; 中配置：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Bean&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;PasswordEncoder&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;passwordEncoder&lt;/span&gt;&lt;span&gt;() {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 强度 12，更安全&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BCryptPasswordEncoder&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;12&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;BCrypt 三大特性&lt;/strong&gt;：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;加盐&lt;/strong&gt;：每次加密都自动生成不同的随机盐，所以同一个密码”123456”两次加密结果不同。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;慢函数&lt;/strong&gt;：12 轮 BCrypt 大约需要 200~300 毫秒。攻击者就算拿到哈希，暴力破解也极慢。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不可逆&lt;/strong&gt;：只能”验证”密码，不能”还原”密码。所以”忘记密码”功能是发邮件重置，不是把密码寄给你。&lt;/li&gt;
&lt;/ol&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;16.4.3 @JsonIgnore 防序列化泄露&lt;a href=&quot;#1643-jsonignore-防序列化泄露&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;光加密还不够，还得防止密码哈希被序列化到 JSON 返回给前端。看本项目 &lt;code&gt;SysUser&lt;/code&gt; 实体：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;JsonIgnore&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;TableField&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;password&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; password&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;@JsonIgnore&lt;/code&gt; 是 Jackson 提供的注解，告诉 JSON 序列化器：“这个字段永远不要输出”。即使你不小心 &lt;code&gt;return user&lt;/code&gt;，密码也不会泄露。&lt;/p&gt;&lt;p&gt;⚠️ &lt;strong&gt;更稳妥做法&lt;/strong&gt;：永远不要直接返回 &lt;code&gt;SysUser&lt;/code&gt;，而是返回 &lt;code&gt;UserVO&lt;/code&gt;（View Object，专门给前端看的）。本项目所有返回用户信息的接口都遵守这一规则。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;16.5 JWT 安全&lt;a href=&quot;#165-jwt-安全&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;16.5.1 项目的环境变量隔离&lt;a href=&quot;#1651-项目的环境变量隔离&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;JWT 密钥不能写死在代码里。本项目通过 &lt;code&gt;.env&lt;/code&gt; 文件 + &lt;code&gt;DotenvConfig&lt;/code&gt; 加载器实现密钥隔离：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;.env.example&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;JWT_SECRET=your_jwt_secret_key_here_minimum_32_characters&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;JWT_EXPIRATION=86400000&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;JWT_REFRESH_EXPIRATION=604800000&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;启动时 &lt;code&gt;EnvValidationConfig&lt;/code&gt; 会先校验：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;isBlank&lt;/span&gt;&lt;span&gt;(jwtSecret)) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;   ✗ JWT_SECRET 未配置&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;hasError &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// ……&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (hasError) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;IllegalStateException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;环境变量配置不完整，启动失败&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;只要 &lt;code&gt;JWT_SECRET&lt;/code&gt; 没配，应用直接启动失败。这种”快速失败”（fail-fast）思想能避免线上环境裸奔。&lt;/p&gt;&lt;p&gt;⚠️ &lt;strong&gt;密钥长度&lt;/strong&gt;：JWT HS256 算法要求密钥至少 256 位，也就是 &lt;strong&gt;32 个 ASCII 字符&lt;/strong&gt;。短了 jjwt 库会直接抛异常。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;16.5.2 JWT 黑名单机制（撤销 Token）&lt;a href=&quot;#1652-jwt-黑名单机制撤销-token&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;JWT 的痛点是”无状态” —— Token 一旦签发，到期前就一直有效，&lt;strong&gt;没法主动撤销&lt;/strong&gt;。但用户登出、修改密码、被封禁时，明明应该让旧 Token 立刻失效。&lt;/p&gt;&lt;p&gt;本项目用”黑名单”解决这个问题。&lt;code&gt;JwtUtils&lt;/code&gt; 维护一个 ConcurrentMap，存放被撤销的 Token：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// 用户登出&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;logout&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; token) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;jwtUtils&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;revokeToken&lt;/span&gt;&lt;span&gt;&lt;span&gt;(token);&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// 加入黑名单&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// 验证 Token 时&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;validateToken&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; token) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;jwtUtils&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;isBlacklisted&lt;/span&gt;&lt;span&gt;&lt;span&gt;(token)&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// ……&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;但黑名单会越来越大，所以本项目用 &lt;code&gt;JwtSchedulerConfig&lt;/code&gt; 定时清理：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Component&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;EnableScheduling&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;JwtSchedulerConfig&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Autowired&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;JwtUtils&lt;/span&gt;&lt;span&gt; jwtUtils&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;/**&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;* 每小时清理一次过期 Token 黑名单&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;*/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Scheduled&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;fixedRate&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;3600000&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;// 1 小时&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;cleanExpiredTokens&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;jwtUtils&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;cleanExpiredTokens&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;⚠️ &lt;strong&gt;生产级建议&lt;/strong&gt;：内存黑名单在多机部署时不一致，建议升级到 Redis（自带 TTL 过期）。本项目已规划但尚未集成。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;16.5.3 Refresh Token 轮换（v1.29 修复）&lt;a href=&quot;#1653-refresh-token-轮换v129-修复&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;刷新 Token（refresh token）有效期更长（7 天），是个高价值目标，一旦泄露危害更大。所以本项目实现了&lt;strong&gt;轮换（rotation）&lt;/strong&gt;：&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;用一次就废弃，每次刷新颁发&lt;strong&gt;新的 access + 新的 refresh&lt;/strong&gt;，旧的 refresh 立即加入黑名单。&lt;/p&gt;&lt;/blockquote&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;TokenPair&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;refreshToken&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; oldRefreshToken) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 1. 校验旧 refresh token&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; userId &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;jwtUtils&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;parseUserId&lt;/span&gt;&lt;span&gt;(oldRefreshToken);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 2. 立刻把旧 refresh 加入黑名单（防止重放）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;jwtUtils&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;revokeToken&lt;/span&gt;&lt;span&gt;(oldRefreshToken);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 3. 颁发新的 access + refresh&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; newAccessToken &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;jwtUtils&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;generateAccessToken&lt;/span&gt;&lt;span&gt;(userId);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; newRefreshToken &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;jwtUtils&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;generateRefreshToken&lt;/span&gt;&lt;span&gt;(userId);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;TokenPair&lt;/span&gt;&lt;span&gt;&lt;span&gt;(newAccessToken&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; newRefreshToken)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;16.5.4 先验签再查黑名单（v1.34 修复）&lt;a href=&quot;#1654-先验签再查黑名单v134-修复&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;v1.34 修复了一个潜在 bypass：原本黑名单查询用的是 Token 字符串本身，攻击者可能伪造个看起来像 Token 的字符串去”查黑名单”，绕过逻辑。&lt;/p&gt;&lt;p&gt;修复方法是先做签名验证，验签通过的才查黑名单：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;validateToken&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; token) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;try&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// 1. 先验证签名（伪造的 Token 在这里就被拒了）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;Claims&lt;/span&gt;&lt;span&gt; claims &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Jwts&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;parser&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;verifyWith&lt;/span&gt;&lt;span&gt;(secretKey)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;build&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;parseSignedClaims&lt;/span&gt;&lt;span&gt;(token)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getPayload&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// 2. 验签通过后再查黑名单&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;isBlacklisted&lt;/span&gt;&lt;span&gt;(token)) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;JwtException&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;16.6 防止信息泄露&lt;a href=&quot;#166-防止信息泄露&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;16.6.1 登录响应不返回敏感信息（v1.29 修复）&lt;a href=&quot;#1661-登录响应不返回敏感信息v129-修复&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;v1.29 之前，登录成功后返回的 JSON 包含了 email、role、phone 等字段。这些信息一旦在前端 JS 里，浏览器调试工具就能看到，普通用户能看到自己的没问题，但有些字段（如 &lt;code&gt;loginFailCount&lt;/code&gt;）属于内部实现，不该暴露。&lt;/p&gt;&lt;p&gt;修复后只返回必要信息：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LoginResponseVO&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;login&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;LoginRequest&lt;/span&gt;&lt;span&gt; request) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// ……验证逻辑&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;LoginResponseVO&lt;/span&gt;&lt;span&gt; vo &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LoginResponseVO&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;vo&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setUserId&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getId&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;vo&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setUsername&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getUsername&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;vo&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setNickname&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getNickname&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;vo&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setAvatar&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getAvatar&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;vo&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setAccessToken&lt;/span&gt;&lt;span&gt;(accessToken);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;vo&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setRefreshToken&lt;/span&gt;&lt;span&gt;(refreshToken);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 不再返回 email、role、phone 等&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt;&lt;span&gt; vo&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;16.6.2 用户枚举防护&lt;a href=&quot;#1662-用户枚举防护&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;注册接口的常见漏洞：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;用户名已存在 → 返回 “用户名已被注册”&lt;/li&gt;
&lt;li&gt;用户名不存在 → 返回 “注册成功”&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;攻击者用脚本枚举一万个常见用户名，就能知道哪些已经存在 —— 这叫&lt;strong&gt;用户枚举（user enumeration）&lt;/strong&gt;。&lt;/p&gt;&lt;p&gt;本项目的修复方案：注册失败一律返回通用错误：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (existingUser &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 不暴露&quot;用户名已存在&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;400&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;注册失败，请稍后重试&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;登录接口同理：用户名不存在和密码错误，&lt;strong&gt;返回完全相同的错误信息&lt;/strong&gt;：“用户名或密码错误”。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;16.6.3 全局异常兜底不暴露异常类名&lt;a href=&quot;#1663-全局异常兜底不暴露异常类名&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;GlobalExceptionHandler.java&lt;/code&gt; 处理所有未捕获异常：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;ExceptionHandler&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Exception&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;handleException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Exception&lt;/span&gt;&lt;span&gt; e) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;系统异常&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;, e);&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// 服务器日志记录详细堆栈&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;服务器繁忙，请稍后重试&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;);&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// 给用户的友好提示&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;⚠️ &lt;strong&gt;绝对不要&lt;/strong&gt;这样写：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Result&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;系统错误：&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getMessage&lt;/span&gt;&lt;span&gt;&lt;span&gt;());&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// 危险！&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;e.getMessage()&lt;/code&gt; 可能包含 SQL 语句、文件路径、数据库表名，攻击者从中可推断架构细节。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;16.7 登录限流与账户锁定&lt;a href=&quot;#167-登录限流与账户锁定&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;16.7.1 暴力破解的危害&lt;a href=&quot;#1671-暴力破解的危害&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;如果接口对同一账号无限次尝试登录，攻击者可以用脚本一秒尝试一千次，常用密码字典只需几分钟就能跑完。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;16.7.2 项目的方案：5 次失败锁 15 分钟&lt;a href=&quot;#1672-项目的方案5-次失败锁-15-分钟&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;SysUser&lt;/code&gt; 实体里有这两个字段：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Integer&lt;/span&gt;&lt;span&gt; loginFailCount&lt;/span&gt;&lt;span&gt;&lt;span&gt;;&lt;/span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// 失败次数&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LocalDateTime&lt;/span&gt;&lt;span&gt; lockedUntil&lt;/span&gt;&lt;span&gt;&lt;span&gt;;&lt;/span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// 锁定到期时间&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;登录逻辑（简化版）：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;login&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt; username&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; password) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;SysUser&lt;/span&gt;&lt;span&gt; user &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;sysUserMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;findByUsername&lt;/span&gt;&lt;span&gt;(username);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 1. 是否在锁定期内？&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getLockedUntil&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getLockedUntil&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;isAfter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;LocalDateTime&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;now&lt;/span&gt;&lt;span&gt;&lt;span&gt;())&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;423&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;账号已锁定，请 15 分钟后再试&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 2. 校验密码&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;/span&gt;&lt;span&gt;passwordEncoder&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;matches&lt;/span&gt;&lt;span&gt;(password, &lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getPassword&lt;/span&gt;&lt;span&gt;&lt;span&gt;())&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// 失败次数 +1（原子更新，下一节讲）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;sysUserMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;incrementLoginFailCount&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getId&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getLoginFailCount&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;// 锁定 15 分钟&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;sysUserMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;lockUser&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getId&lt;/span&gt;&lt;span&gt;(), &lt;/span&gt;&lt;span&gt;LocalDateTime&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;now&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;plusMinutes&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;15&lt;/span&gt;&lt;span&gt;));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;401&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;用户名或密码错误&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 3. 登录成功，清零失败次数&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;sysUserMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;resetLoginFailCount&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getId&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;23&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;16.7.3 原子更新防并发&lt;a href=&quot;#1673-原子更新防并发&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;如果两个请求同时来，分别读到 &lt;code&gt;loginFailCount=4&lt;/code&gt;，分别 &lt;code&gt;+1&lt;/code&gt; 后写回 &lt;code&gt;5&lt;/code&gt;，就成了”丢失更新”。本项目用 SQL 直接 &lt;code&gt;count = count + 1&lt;/code&gt; 解决：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;update&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;incrementLoginFailCount&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;UPDATE sys_user&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;SET login_fail_count = login_fail_count + 1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;WHERE id = #{userId}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;update&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;数据库的 UPDATE 操作本身原子，不需要 Java 加锁。&lt;/p&gt;&lt;p&gt;⚠️ &lt;strong&gt;设计取舍&lt;/strong&gt;：锁定到期时间用绝对时间（&lt;code&gt;lockedUntil&lt;/code&gt;），不是计数器倒数。这样服务器重启不丢状态。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;16.8 越权防护&lt;a href=&quot;#168-越权防护&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;16.8.1 越权的两种类型&lt;a href=&quot;#1681-越权的两种类型&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;水平越权&lt;/strong&gt;：用户 A 操作用户 B 的资源（A 删了 B 的文章）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;垂直越权&lt;/strong&gt;：普通用户做了管理员才能做的事（普通用户调用 &lt;code&gt;/api/admin/banUser&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;16.8.2 项目的统一处理&lt;a href=&quot;#1682-项目的统一处理&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;每个修改/删除接口必须做”所有权检查”：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;deletePost&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; postId) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; currentUserId &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;SecurityUtils&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getCurrentUserIdOrNull&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (currentUserId &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;401&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;请先登录&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;BlogPost&lt;/span&gt;&lt;span&gt; post &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;blogPostMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;selectById&lt;/span&gt;&lt;span&gt;(postId);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (post &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;404&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;文章不存在&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// 关键：只有作者或管理员能删&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;SysUser&lt;/span&gt;&lt;span&gt; currentUser &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;sysUserMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;selectById&lt;/span&gt;&lt;span&gt;(currentUserId);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getUserId&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;equals&lt;/span&gt;&lt;span&gt;&lt;span&gt;(currentUserId)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;ADMIN&quot;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;equals&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;currentUser&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getRole&lt;/span&gt;&lt;span&gt;&lt;span&gt;())&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;403&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;无权操作&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;blogPostMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;deleteById&lt;/span&gt;&lt;span&gt;(postId);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;16.8.3 SecurityUtils 取当前用户&lt;a href=&quot;#1683-securityutils-取当前用户&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;SecurityUtils.getCurrentUserIdOrNull()&lt;/code&gt; 从 SecurityContext 中提取当前登录用户 ID。这个方法&lt;strong&gt;不抛异常，返回 null&lt;/strong&gt;，方便调用方按需处理（公共接口允许游客访问）。&lt;/p&gt;&lt;p&gt;⚠️ &lt;strong&gt;审查清单&lt;/strong&gt;：本项目从 v1.20 开始建立了”敏感接口审查清单”，每次发版前对所有写接口逐一确认权限校验是否健全。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;16.9 文件上传安全&lt;a href=&quot;#169-文件上传安全&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;16.9.1 三道关卡&lt;a href=&quot;#1691-三道关卡&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;文件上传是攻击重灾区，本项目设三道关：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;类型白名单&lt;/strong&gt;：只允许图片（jpg/png/gif/webp）和视频（mp4/avi）&lt;/li&gt;
&lt;/ol&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;static&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;final&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Set&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; ALLOWED_IMAGE_EXT &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Set&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;of&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;jpg&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;jpeg&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;png&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;gif&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;webp&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;/span&gt;&lt;span&gt;ALLOWED_IMAGE_EXT&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;contains&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;ext&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toLowerCase&lt;/span&gt;&lt;span&gt;&lt;span&gt;())&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;throw&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;400&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;不支持的文件类型&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;大小限制&lt;/strong&gt;（&lt;code&gt;application.yml&lt;/code&gt;）：&lt;/li&gt;
&lt;/ol&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;mybatis-plus&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;servlet&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;multipart&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;enabled&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;max-file-size&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;500MB&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;max-request-size&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;500MB&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;重命名防路径穿越&lt;/strong&gt;：用 UUID 重新生成文件名，避免攻击者上传 &lt;code&gt;../../../etc/passwd&lt;/code&gt; 这种恶意路径：&lt;/li&gt;
&lt;/ol&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; newName &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;UUID&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;randomUUID&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;toString&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;replace&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;-&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;.&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; ext&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;File&lt;/span&gt;&lt;span&gt; target &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;File&lt;/span&gt;&lt;span&gt;&lt;span&gt;(uploadDir&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; newName)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;16.9.2 静态资源访问配置&lt;a href=&quot;#1692-静态资源访问配置&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;WebMvcConfig&lt;/code&gt; 把上传目录映射成 HTTP 路径：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Override&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;addResourceHandlers&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;ResourceHandlerRegistry&lt;/span&gt;&lt;span&gt; registry) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; projectRoot &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;System&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getProperty&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;user.dir&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; uploadPath &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Paths&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(projectRoot, uploadBasePath).&lt;/span&gt;&lt;span&gt;toString&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;registry&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;addResourceHandler&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/uploads/**&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;addResourceLocations&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;file:&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; uploadPath &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;/&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;⚠️ &lt;strong&gt;生产环境最佳实践&lt;/strong&gt;：上传文件不放在应用服务器，而是放对象存储（OSS、S3、MinIO），独立域名，并配置不允许执行任何脚本（即便上传成功了也无法被解析为可执行）。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;第 16 章 实战清单&lt;a href=&quot;#第-16-章-实战清单&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt; 所有 SQL 用 &lt;code&gt;#{}&lt;/code&gt; 而不是 &lt;code&gt;${}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt; 入库前调用 &lt;code&gt;HtmlSanitizer&lt;/code&gt; 过滤 HTML&lt;/li&gt;
&lt;li&gt; 密码用 &lt;code&gt;BCryptPasswordEncoder(12)&lt;/code&gt; 加密&lt;/li&gt;
&lt;li&gt; 实体的密码字段加 &lt;code&gt;@JsonIgnore&lt;/code&gt;&lt;/li&gt;
&lt;li&gt; JWT 密钥放 &lt;code&gt;.env&lt;/code&gt;，启动校验&lt;/li&gt;
&lt;li&gt; 用户登出实现黑名单撤销&lt;/li&gt;
&lt;li&gt; Refresh Token 一次性轮换&lt;/li&gt;
&lt;li&gt; 登录失败 5 次锁 15 分钟&lt;/li&gt;
&lt;li&gt; 所有写接口校验所有权&lt;/li&gt;
&lt;li&gt; 文件上传校验类型 + 重命名&lt;/li&gt;
&lt;/ul&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h1&gt;第 17 章 性能优化技巧&lt;a href=&quot;#第-17-章-性能优化技巧&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h1&gt;&lt;p&gt;性能优化的第一原则：&lt;strong&gt;不要过早优化，先测出瓶颈再动手&lt;/strong&gt;。但理解常见优化套路能帮你写出”开箱性能就不差”的代码。&lt;/p&gt;&lt;section&gt;&lt;h2&gt;17.1 数据库优化&lt;a href=&quot;#171-数据库优化&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;17.1.1 索引设计&lt;a href=&quot;#1711-索引设计&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;本项目数据库设计的索引清单：&lt;/p&gt;




















































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;表&lt;/th&gt;&lt;th&gt;索引字段&lt;/th&gt;&lt;th&gt;类型&lt;/th&gt;&lt;th&gt;原因&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;sys_user&lt;/code&gt;&lt;/td&gt;&lt;td&gt;username&lt;/td&gt;&lt;td&gt;UNIQUE&lt;/td&gt;&lt;td&gt;登录查找 + 唯一约束&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;sys_user&lt;/code&gt;&lt;/td&gt;&lt;td&gt;email&lt;/td&gt;&lt;td&gt;UNIQUE&lt;/td&gt;&lt;td&gt;注册校验 + 唯一约束&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;blog_post&lt;/code&gt;&lt;/td&gt;&lt;td&gt;user_id&lt;/td&gt;&lt;td&gt;NORMAL&lt;/td&gt;&lt;td&gt;查”我的文章”&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;blog_post&lt;/code&gt;&lt;/td&gt;&lt;td&gt;category_id&lt;/td&gt;&lt;td&gt;NORMAL&lt;/td&gt;&lt;td&gt;按分类筛选&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;blog_post&lt;/code&gt;&lt;/td&gt;&lt;td&gt;create_time&lt;/td&gt;&lt;td&gt;NORMAL&lt;/td&gt;&lt;td&gt;按时间排序&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;blog_like&lt;/code&gt;&lt;/td&gt;&lt;td&gt;(user_id, post_id)&lt;/td&gt;&lt;td&gt;UNIQUE&lt;/td&gt;&lt;td&gt;防重复点赞 + 复合索引&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;blog_post_tag&lt;/code&gt;&lt;/td&gt;&lt;td&gt;(post_id, tag_id)&lt;/td&gt;&lt;td&gt;PRIMARY&lt;/td&gt;&lt;td&gt;关联表主键&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;17.1.2 复合索引顺序很关键&lt;a href=&quot;#1712-复合索引顺序很关键&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;复合索引 &lt;code&gt;(user_id, post_id)&lt;/code&gt; 能加速：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;WHERE&lt;/span&gt;&lt;span&gt;&lt;span&gt; user_id &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; ? &lt;/span&gt;&lt;/span&gt;&lt;span&gt;AND&lt;/span&gt;&lt;span&gt;&lt;span&gt; post_id &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; ?     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-- 全索引命中&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;WHERE&lt;/span&gt;&lt;span&gt;&lt;span&gt; user_id &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; ?                     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-- 命中前缀&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;但&lt;strong&gt;不能&lt;/strong&gt;加速：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;WHERE&lt;/span&gt;&lt;span&gt;&lt;span&gt; post_id &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; ?  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-- 用不到这个索引（最左前缀原则）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;⚠️ &lt;strong&gt;经验法则&lt;/strong&gt;：复合索引把”区分度高”和”经常单独查”的字段放前面。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;17.1.3 别用 select *&lt;a href=&quot;#1713-别用-select-&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// 不好&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;BlogPost&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; posts &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;blogPostMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;selectList&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// 更好（只查需要的字段）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;LambdaQueryWrapper&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;BlogPost&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; wrapper &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LambdaQueryWrapper&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;wrapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;select&lt;/span&gt;&lt;span&gt;(BlogPost&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getId, BlogPost&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getTitle, BlogPost&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getCreateTime);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;文章正文 &lt;code&gt;content&lt;/code&gt; 字段往往几 KB，列表页根本用不到，每次都查浪费带宽和内存。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;17.2 N+1 查询问题&lt;a href=&quot;#172-n1-查询问题&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;17.2.1 什么是 N+1&lt;a href=&quot;#1721-什么是-n1&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;最经典的性能坑。假设你查 100 篇文章，然后挨个查作者：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// 反面示例&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;BlogPost&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; posts &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;blogPostMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;selectList&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;&lt;span&gt;);&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// 1 条 SQL&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;BlogPost&lt;/span&gt;&lt;span&gt; post &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; posts) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;SysUser&lt;/span&gt;&lt;span&gt; author &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;sysUserMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;selectById&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getUserId&lt;/span&gt;&lt;span&gt;&lt;span&gt;());&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// 100 条 SQL&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setAuthor&lt;/span&gt;&lt;span&gt;(author);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// 一共 1 + 100 = 101 条 SQL&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;17.2.2 解决：批量查询 + Map 组装&lt;a href=&quot;#1722-解决批量查询--map-组装&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;BlogPost&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; posts &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;blogPostMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;selectList&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// 1. 收集所有 userId&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;Set&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; userIds &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;posts&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stream&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;(BlogPost&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getUserId)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;collect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Collectors&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toSet&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// 2. 一次查出所有作者&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;SysUser&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; authors &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;sysUserMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;selectBatchIds&lt;/span&gt;&lt;span&gt;(userIds);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// 3. 转 Map 方便查找&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;Map&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;SysUser&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; authorMap &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;authors&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stream&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;collect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Collectors&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toMap&lt;/span&gt;&lt;span&gt;(SysUser&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span&gt;getId, u &lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt; u));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// 4. 组装&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;BlogPost&lt;/span&gt;&lt;span&gt; post &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; posts) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setAuthor&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;authorMap&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getUserId&lt;/span&gt;&lt;span&gt;()));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// 总共 2 条 SQL，无论文章多少&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;⚠️ &lt;strong&gt;检测方法&lt;/strong&gt;：开启 SQL 日志（见 17.4），如果一个接口打印出几十上百条 SELECT，多半是 N+1。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;17.3 分页优化&lt;a href=&quot;#173-分页优化&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;17.3.1 PaginationInnerInterceptor&lt;a href=&quot;#1731-paginationinnerinterceptor&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;MyBatis Plus 提供了分页插件：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Configuration&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MybatisPlusConfig&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Bean&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MybatisPlusInterceptor&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;mybatisPlusInterceptor&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;MybatisPlusInterceptor&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;interceptor&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MybatisPlusInterceptor&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;interceptor&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;addInnerInterceptor&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;PaginationInnerInterceptor&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;DbType&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;MYSQL&lt;/span&gt;&lt;span&gt;));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; interceptor;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;使用方式：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;Page&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;BlogPost&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; page &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Page&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// 第 1 页，每页 10 条&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;Page&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;BlogPost&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; result &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;blogPostMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;selectPage&lt;/span&gt;&lt;span&gt;(page, &lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;17.3.2 深分页问题&lt;a href=&quot;#1732-深分页问题&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; * &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; blog_post &lt;/span&gt;&lt;span&gt;LIMIT&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1000000&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;MySQL 必须扫描前 100 万条才能跳过去取后 10 条。解决思路：&lt;strong&gt;用主键过滤&lt;/strong&gt;&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;-- 已知上一页最后一条 id 是 1000000&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; * &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; blog_post &lt;/span&gt;&lt;span&gt;WHERE&lt;/span&gt;&lt;span&gt;&lt;span&gt; id &lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;1000000&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LIMIT&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;10&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;这种叫”游标分页”或”keyset pagination”，性能恒定。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;17.4 缓存策略&lt;a href=&quot;#174-缓存策略&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;17.4.1 Spring Cache 注解（已规划）&lt;a href=&quot;#1741-spring-cache-注解已规划&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Cacheable&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;value&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;post&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;key&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;#postId&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BlogPost&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;getPostById&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; postId) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;blogPostMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;selectById&lt;/span&gt;&lt;span&gt;(postId);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;CacheEvict&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;value&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;post&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;key&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;#postId&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;deletePost&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; postId) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;blogPostMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;deleteById&lt;/span&gt;&lt;span&gt;(postId);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;@Cacheable&lt;/code&gt;：先查缓存，没有就执行方法并缓存结果。
&lt;code&gt;@CacheEvict&lt;/code&gt;：执行方法后清除缓存。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;17.4.2 项目现状&lt;a href=&quot;#1742-项目现状&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;本项目目前用 ConcurrentHashMap 做内存缓存（如黑名单、防刷指纹），尚未集成 Redis。&lt;strong&gt;v1.31 规划接入 Redis&lt;/strong&gt;，作为：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;JWT 黑名单（自动 TTL）&lt;/li&gt;
&lt;li&gt;热门文章列表缓存&lt;/li&gt;
&lt;li&gt;限流计数器&lt;/li&gt;
&lt;/ul&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;17.5 减少重复计算&lt;a href=&quot;#175-减少重复计算&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;17.5.1 阅读量增加：CAS 替代 select-then-update（v1.14 修复）&lt;a href=&quot;#1751-阅读量增加cas-替代-select-then-updatev114-修复&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;v1.14 之前的代码（典型的 TOCTOU 漏洞，Time-of-check to time-of-use）：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// 反面示例&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;BlogPost&lt;/span&gt;&lt;span&gt; post &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;blogPostMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;selectById&lt;/span&gt;&lt;span&gt;(postId);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setViewCount&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getViewCount&lt;/span&gt;&lt;span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;blogPostMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;updateById&lt;/span&gt;&lt;span&gt;(post);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// 100 个并发请求来时，会丢失大量计数&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;修复后用 SQL 原子更新：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;update&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;incrementViewCount&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;UPDATE blog_post&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;SET view_count = view_count + 1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;WHERE id = #{postId}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;update&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;17.5.2 防刷：ConcurrentHashMap 缓存指纹&lt;a href=&quot;#1752-防刷concurrenthashmap-缓存指纹&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;阅读量也不能让同一用户狂刷。用 IP+userId 作为 key 缓存：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;final&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Map&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; viewFingerprint &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ConcurrentHashMap&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;addView&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt;&lt;span&gt; postId&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt;&lt;span&gt; userId&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; ip) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt; key &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; postId &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;:&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; userId &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;:&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; ip&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; lastView &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;viewFingerprint&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(key);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;long&lt;/span&gt;&lt;span&gt; now &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;System&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;currentTimeMillis&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;&lt;span&gt; (lastView &lt;/span&gt;&lt;span&gt;==&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;||&lt;/span&gt;&lt;span&gt; now &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; lastView &lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;60&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;1000&lt;/span&gt;&lt;span&gt;) {  &lt;/span&gt;&lt;span&gt;// 1 分钟内不重复计数&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;blogPostMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;incrementViewCount&lt;/span&gt;&lt;span&gt;(postId);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;viewFingerprint&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;put&lt;/span&gt;&lt;span&gt;(key, now);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;⚠️ ConcurrentHashMap 会无限增长，需要定期清理（项目有定时任务清理过期 key）。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;17.6 异步处理&lt;a href=&quot;#176-异步处理&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;17.6.1 通知发送异步化&lt;a href=&quot;#1761-通知发送异步化&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;用户点赞文章时，需要给作者发通知。如果同步发，主流程就被拖慢：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// 同步（不好）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;likePost&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt;&lt;span&gt; postId&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; userId) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;blogLikeMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;insert&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BlogLike&lt;/span&gt;&lt;span&gt;(userId, postId));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;notificationService&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;sendLikeNotification&lt;/span&gt;&lt;span&gt;&lt;span&gt;(postId, userId);&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// 阻塞！&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;本项目用 &lt;code&gt;@TransactionalEventListener&lt;/code&gt; 异步处理：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Service&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BlogLikeService&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Autowired&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ApplicationEventPublisher&lt;/span&gt;&lt;span&gt; eventPublisher&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Transactional&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;likePost&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;postId&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;userId&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;blogLikeMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;insert&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BlogLike&lt;/span&gt;&lt;span&gt;(userId, postId));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// 发布事件（事务提交后才会触发）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;eventPublisher&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;publishEvent&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;LikeEvent&lt;/span&gt;&lt;span&gt;(postId, userId));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Component&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;NotificationListener&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Async&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;TransactionalEventListener&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;phase&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;TransactionPhase&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;AFTER_COMMIT&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;onLike&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;LikeEvent&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;event&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;notificationService&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;sendLikeNotification&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;event&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getPostId&lt;/span&gt;&lt;span&gt;(), &lt;/span&gt;&lt;span&gt;event&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getUserId&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;两个关键点&lt;/strong&gt;：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AFTER_COMMIT&lt;/code&gt;：事务提交后才发通知。否则点赞插入失败时，通知却已经发了。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Async&lt;/code&gt;：放进线程池异步执行，不阻塞主流程。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;记得在 main 类加 &lt;code&gt;@EnableAsync&lt;/code&gt;。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;17.7 锁的优化&lt;a href=&quot;#177-锁的优化&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;17.7.1 细粒度锁&lt;a href=&quot;#1771-细粒度锁&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;如果用全局锁，所有用户的所有点赞操作排队执行 —— 性能灾难。本项目按 &lt;code&gt;postId&lt;/code&gt; 分锁：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;final&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ConcurrentHashMap&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;ReentrantLock&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; lockMap &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ConcurrentHashMap&lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ReentrantLock&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;getLock&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; postId) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;lockMap&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;computeIfAbsent&lt;/span&gt;&lt;span&gt;(postId, k &lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ReentrantLock&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;likePost&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt;&lt;span&gt; postId&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; userId) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;ReentrantLock&lt;/span&gt;&lt;span&gt; lock &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;getLock&lt;/span&gt;&lt;span&gt;&lt;span&gt;(postId)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;lock&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;lock&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;try&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// 临界区&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;finally&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;lock&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;unlock&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;不同文章的并发互不影响。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;17.7.2 防锁内存泄漏&lt;a href=&quot;#1772-防锁内存泄漏&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;ConcurrentHashMap 越来越大怎么办？v1.20 加了清理逻辑：每次 &lt;code&gt;getLock&lt;/code&gt; 时检查锁是否长期空闲，空闲就清掉。或者简单粗暴：用 Guava 的 &lt;code&gt;Striped&amp;lt;Lock&amp;gt;&lt;/code&gt;，固定数量的锁随机映射。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;17.8 批量操作&lt;a href=&quot;#178-批量操作&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// 反面：一条一条插入&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; tagId &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; tagIds) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;blogPostTagMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;insert&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BlogPostTag&lt;/span&gt;&lt;span&gt;(postId, tagId));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// 10 个标签 = 10 条 SQL，10 次网络往返&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// 正面：批量插入&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;blogPostTagMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;batchInsert&lt;/span&gt;&lt;span&gt;(postId, tagIds);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;insert&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;batchInsert&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;INSERT INTO blog_post_tag (post_id, tag_id) VALUES&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;foreach&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;collection&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;tagIds&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;item&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;tagId&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;separator&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;,&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;(#{postId}, #{tagId})&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;foreach&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;insert&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;一条 SQL 解决，性能提升数十倍。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;17.9 只读事务的好处&lt;a href=&quot;#179-只读事务的好处&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Transactional&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;readOnly&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;BlogPost&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;getPostById&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt; postId) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;blogPostMapper&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;selectById&lt;/span&gt;&lt;span&gt;(postId);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;readOnly = true&lt;/code&gt; 告诉数据库和 Spring：“这是只读，不要做脏检查、不要锁行”。MySQL 可以路由到从库。&lt;/p&gt;&lt;p&gt;⚠️ 写操作必须用 &lt;code&gt;@Transactional(rollbackFor = Exception.class)&lt;/code&gt; 保证异常时回滚（默认只回滚 RuntimeException，但加 &lt;code&gt;rollbackFor = Exception.class&lt;/code&gt; 更稳）。&lt;/p&gt;&lt;hr /&gt;&lt;section&gt;&lt;h3&gt;第 17 章 实战清单&lt;a href=&quot;#第-17-章-实战清单&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt; 高频查询字段建索引&lt;/li&gt;
&lt;li&gt; 列表查询不带 select *&lt;/li&gt;
&lt;li&gt; 关联查询警惕 N+1&lt;/li&gt;
&lt;li&gt; 分页用 MyBatis Plus 的 Page&lt;/li&gt;
&lt;li&gt; 计数器用 SQL &lt;code&gt;+1&lt;/code&gt; 而不是 read-modify-write&lt;/li&gt;
&lt;li&gt; 通知/邮件用 &lt;code&gt;@Async&lt;/code&gt; + &lt;code&gt;@TransactionalEventListener&lt;/code&gt;&lt;/li&gt;
&lt;li&gt; 锁尽量细粒度&lt;/li&gt;
&lt;li&gt; 批量插入用 foreach&lt;/li&gt;
&lt;li&gt; 只读方法加 &lt;code&gt;readOnly = true&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h1&gt;第 18 章 测试与调试&lt;a href=&quot;#第-18-章-测试与调试&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h1&gt;&lt;section&gt;&lt;h2&gt;18.1 三层测试金字塔&lt;a href=&quot;#181-三层测试金字塔&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;/\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;       &lt;/span&gt;&lt;/span&gt;&lt;span&gt;/E2E\          少量端到端测试（启动整个应用，模拟真实请求）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;/----\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;     &lt;/span&gt;&lt;/span&gt;&lt;span&gt;/  集成  \       中等量集成测试（多个组件配合）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;/---------\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;   &lt;/span&gt;&lt;/span&gt;&lt;span&gt;/   单元测试  \    大量单元测试（单个类/方法）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;/-------------\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;




























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;类型&lt;/th&gt;&lt;th&gt;速度&lt;/th&gt;&lt;th&gt;覆盖范围&lt;/th&gt;&lt;th&gt;写多少&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;单元测试&lt;/td&gt;&lt;td&gt;毫秒级&lt;/td&gt;&lt;td&gt;单个方法&lt;/td&gt;&lt;td&gt;70%&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;集成测试&lt;/td&gt;&lt;td&gt;秒级&lt;/td&gt;&lt;td&gt;多个组件&lt;/td&gt;&lt;td&gt;20%&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;端到端测试&lt;/td&gt;&lt;td&gt;分钟级&lt;/td&gt;&lt;td&gt;整个系统&lt;/td&gt;&lt;td&gt;10%&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;18.2 Spring Boot Test 基础&lt;a href=&quot;#182-spring-boot-test-基础&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;本项目自带的测试类 &lt;code&gt;EduProjectApplicationTests.java&lt;/code&gt;：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;SpringBootTest&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;EduProjectApplicationTests&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Test&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;contextLoads&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// 这一个测试，能验证整个 Spring 容器启动正常&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;@SpringBootTest&lt;/code&gt; 会启动整个 Spring 上下文，加载所有 Bean。这是”集成测试”的入门级。&lt;/p&gt;&lt;section&gt;&lt;h3&gt;18.2.1 模拟 HTTP 请求：MockMvc&lt;a href=&quot;#1821-模拟-http-请求mockmvc&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;SpringBootTest&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;AutoConfigureMockMvc&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;PostControllerTest&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Autowired&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;MockMvc&lt;/span&gt;&lt;span&gt; mockMvc&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Test&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;getPostList_shouldReturn200&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;throws&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Exception&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;mockMvc&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;perform&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/api/post/list&quot;&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;andExpect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;isOk&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;                &lt;/span&gt;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;andExpect&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;jsonPath&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;$.code&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;value&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;200&lt;/span&gt;&lt;span&gt;));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;MockMvc&lt;/code&gt; 不会真起 HTTP 服务器，但能模拟整个 Spring MVC 流程。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;18.3 单元测试 JUnit 5&lt;a href=&quot;#183-单元测试-junit-5&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;HtmlSanitizerTest&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;HtmlSanitizer&lt;/span&gt;&lt;span&gt; sanitizer&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;BeforeEach&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;setUp&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;sanitizer &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;HtmlSanitizer&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Test&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;shouldRemoveScriptTag&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;dirty&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot;&amp;lt;p&amp;gt;hello&amp;lt;/p&amp;gt;&amp;lt;script&amp;gt;alert(1)&amp;lt;/script&amp;gt;&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;String&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;clean&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;sanitizer&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;sanitizeRichText&lt;/span&gt;&lt;span&gt;(dirty);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;assertFalse&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;clean&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;contains&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&amp;lt;script&amp;gt;&quot;&lt;/span&gt;&lt;span&gt;));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;assertTrue&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;clean&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;contains&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&amp;lt;p&amp;gt;hello&amp;lt;/p&amp;gt;&quot;&lt;/span&gt;&lt;span&gt;));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Test&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;shouldHandleNull&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;assertNull&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;sanitizer&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;sanitizeRichText&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;null&lt;/span&gt;&lt;span&gt;));&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;




























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;注解&lt;/th&gt;&lt;th&gt;作用&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;@Test&lt;/code&gt;&lt;/td&gt;&lt;td&gt;标记测试方法&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;@BeforeEach&lt;/code&gt;&lt;/td&gt;&lt;td&gt;每个测试前执行（初始化）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;@AfterEach&lt;/code&gt;&lt;/td&gt;&lt;td&gt;每个测试后执行（清理）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;@BeforeAll&lt;/code&gt;&lt;/td&gt;&lt;td&gt;整个测试类前执行一次&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;@DisplayName&lt;/code&gt;&lt;/td&gt;&lt;td&gt;给测试起人类可读名字&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;常用断言：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;assertEquals&lt;/span&gt;&lt;span&gt;&lt;span&gt;(expected&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; actual)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;assertTrue&lt;/span&gt;&lt;span&gt;&lt;span&gt;(condition)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;assertNull&lt;/span&gt;&lt;span&gt;&lt;span&gt;(value)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;assertThrows&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;BusinessException&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; () &lt;/span&gt;&lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;service&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;somethingThatThrows&lt;/span&gt;&lt;span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;18.4 接口测试方式&lt;a href=&quot;#184-接口测试方式&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;18.4.1 Knife4j&lt;a href=&quot;#1841-knife4j&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;本项目集成了 Knife4j，访问 &lt;code&gt;http://localhost:8825/api/doc.html&lt;/code&gt; 即可看到全部接口文档。&lt;strong&gt;点击”调试”就能直接发请求&lt;/strong&gt;，不需要 Postman。&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;这是给团队、给前端、给测试的”自助点餐机”。&lt;/p&gt;&lt;/blockquote&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;18.4.2 Postman&lt;a href=&quot;#1842-postman&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;适合：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;团队共享一套接口集合&lt;/li&gt;
&lt;li&gt;写自动化测试用例&lt;/li&gt;
&lt;li&gt;设置环境变量（dev / prod 切换）&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;18.4.3 curl&lt;a href=&quot;#1843-curl&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;最快的命令行工具：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 登录&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;curl&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-X&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;POST&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;http://localhost:8825/api/auth/login&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;     &lt;/span&gt;&lt;span&gt;-H&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Content-Type: application/json&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;     &lt;/span&gt;&lt;span&gt;-d&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;{&quot;username&quot;:&quot;admin&quot;,&quot;password&quot;:&quot;admin123&quot;}&apos;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 带 token 访问&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;curl&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;http://localhost:8825/api/post/list&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;     &lt;/span&gt;&lt;span&gt;-H&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;Authorization: Bearer eyJhbGc...&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;18.5 调试技巧&lt;a href=&quot;#185-调试技巧&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;18.5.1 IDEA 断点调试&lt;a href=&quot;#1851-idea-断点调试&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;在代码行号旁点一下，红点出现 = 普通断点&lt;/li&gt;
&lt;li&gt;用 Debug 方式启动（小蜘蛛图标）&lt;/li&gt;
&lt;li&gt;请求触发后，程序停在断点，可以一步步看变量&lt;/li&gt;
&lt;li&gt;F8 = 单步执行下一行；F7 = 进入方法内部；F9 = 继续直到下一个断点&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;&lt;strong&gt;条件断点&lt;/strong&gt;：右键断点设置 &lt;code&gt;userId == 1L&lt;/code&gt;，只有满足条件才停。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;18.5.2 日志：SLF4J + Logback&lt;a href=&quot;#1852-日志slf4j--logback&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Spring Boot 默认就用 SLF4J + Logback。本项目规范：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;private&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;static&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;final&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Logger&lt;/span&gt;&lt;span&gt; log &lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;LoggerFactory&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getLogger&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;MyService&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// 或者用 Lombok：&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Slf4j&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;debug&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;调试信息: {}&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;, obj);&lt;/span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// 开发环境打印&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;info&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;用户 {} 登录成功&quot;&lt;/span&gt;&lt;span&gt;, username);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;warn&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;用户 {} 登录失败次数: {}&quot;&lt;/span&gt;&lt;span&gt;, username, count);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;数据库异常&quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt;, e);&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;// 异常对象单独传，会打印堆栈&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;⚠️ &lt;strong&gt;必须用 &lt;code&gt;{}&lt;/code&gt; 占位符而不是字符串拼接&lt;/strong&gt;：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// 不好（即使 debug 关闭了，字符串也会被拼接）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;debug&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;用户 &quot;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toJson&lt;/span&gt;&lt;span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&quot; 登录&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;// 好（debug 关闭时根本不会执行 toJson()）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;debug&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;用户 {} 登录&quot;&lt;/span&gt;&lt;span&gt;, user);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;18.5.3 日志级别&lt;a href=&quot;#1853-日志级别&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;




























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;级别&lt;/th&gt;&lt;th&gt;用途&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;TRACE&lt;/td&gt;&lt;td&gt;最详细，几乎不用&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;DEBUG&lt;/td&gt;&lt;td&gt;开发调试用&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;INFO&lt;/td&gt;&lt;td&gt;正常业务流程关键节点&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;WARN&lt;/td&gt;&lt;td&gt;可疑但还能继续运行&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;ERROR&lt;/td&gt;&lt;td&gt;出错了，必须排查&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;生产环境通常 INFO 级别。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;18.6 数据库调试&lt;a href=&quot;#186-数据库调试&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;18.6.1 开启 SQL 日志&lt;a href=&quot;#1861-开启-sql-日志&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;application.yml&lt;/code&gt; 配置：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;mybatis-plus&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;configuration&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;log-impl&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;${MYBATIS_SQL_LOG:org.apache.ibatis.logging.stdout.StdOutImpl}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;通过 &lt;code&gt;.env&lt;/code&gt; 切换：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 开启&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;MYBATIS_SQL_LOG=org.apache.ibatis.logging.stdout.StdOutImpl&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 关闭（生产）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;MYBATIS_SQL_LOG=org.apache.ibatis.logging.nologging.NoLoggingImpl&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;开启后控制台会打出每一条 SQL + 参数 + 耗时，是排查 N+1 和慢查询的神器。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;18.6.2 查看慢查询&lt;a href=&quot;#1862-查看慢查询&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;MySQL 自身有慢查询日志：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;SHOW VARIABLES &lt;/span&gt;&lt;span&gt;LIKE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;slow_query_log%&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;SET&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;GLOBAL&lt;/span&gt;&lt;span&gt;&lt;span&gt; slow_query_log &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&apos;ON&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;SET&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;GLOBAL&lt;/span&gt;&lt;span&gt;&lt;span&gt; long_query_time &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;;  &lt;/span&gt;&lt;span&gt;-- 超过 1 秒就记录&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;之后用 &lt;code&gt;EXPLAIN&lt;/code&gt; 分析慢 SQL：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;EXPLAIN &lt;/span&gt;&lt;span&gt;SELECT&lt;/span&gt;&lt;span&gt; * &lt;/span&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; blog_post &lt;/span&gt;&lt;span&gt;WHERE&lt;/span&gt;&lt;span&gt;&lt;span&gt; user_id &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;重点看 &lt;code&gt;type&lt;/code&gt;（最好是 ref/range，最差是 ALL）和 &lt;code&gt;key&lt;/code&gt;（用了哪个索引）。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;18.7 常见 Bug 排查&lt;a href=&quot;#187-常见-bug-排查&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;18.7.1 启动报错&lt;a href=&quot;#1871-启动报错&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;报错&lt;/th&gt;&lt;th&gt;原因&lt;/th&gt;&lt;th&gt;解决&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;Port 8825 already in use&lt;/code&gt;&lt;/td&gt;&lt;td&gt;端口被占&lt;/td&gt;&lt;td&gt;改 SERVER_PORT 或杀进程&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;Failed to configure DataSource&lt;/code&gt;&lt;/td&gt;&lt;td&gt;DB_PASSWORD 为空&lt;/td&gt;&lt;td&gt;检查 .env&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;JWT secret cannot be null&lt;/code&gt;&lt;/td&gt;&lt;td&gt;JWT_SECRET 没配&lt;/td&gt;&lt;td&gt;检查 .env&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;ClassNotFoundException&lt;/code&gt;&lt;/td&gt;&lt;td&gt;依赖冲突&lt;/td&gt;&lt;td&gt;mvn dependency 查重复&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;18.7.2 运行时&lt;a href=&quot;#1872-运行时&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;NullPointerException&lt;/strong&gt;：用 IDEA 的”NullAway”插件预防，或用 Optional&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SQLSyntaxErrorException&lt;/strong&gt;：往往是字段名写错、SQL 关键字冲突（如 &lt;code&gt;order&lt;/code&gt; 是关键字）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DuplicateKeyException&lt;/strong&gt;：唯一索引冲突，正常处理（捕获 + 友好提示）&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;18.7.3 JWT 401 排查&lt;a href=&quot;#1873-jwt-401-排查&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;按以下顺序检查：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;请求头是否带了 &lt;code&gt;Authorization: Bearer xxx&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;token 是否过期（解码 &lt;a href=&quot;https://jwt.io&quot; target=&quot;_blank&quot;&gt;https://jwt.io&lt;/a&gt; 看 exp）&lt;/li&gt;
&lt;li&gt;token 是否在黑名单（用户登出过）&lt;/li&gt;
&lt;li&gt;JWT_SECRET 是否变了（变了所有旧 token 都失效）&lt;/li&gt;
&lt;li&gt;时钟是否同步（服务器时间偏差大会判定过期）&lt;/li&gt;
&lt;/ol&gt;&lt;hr /&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;第 18 章 实战清单&lt;a href=&quot;#第-18-章-实战清单&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt; 至少为核心 Service 写单元测试&lt;/li&gt;
&lt;li&gt; 用 Knife4j 走通每个接口&lt;/li&gt;
&lt;li&gt; 日志全部用 &lt;code&gt;{}&lt;/code&gt; 占位符&lt;/li&gt;
&lt;li&gt; 生产关闭 SQL 日志&lt;/li&gt;
&lt;li&gt; 学会用 EXPLAIN 看执行计划&lt;/li&gt;
&lt;li&gt; 常用快捷键：F8 单步、F7 进入&lt;/li&gt;
&lt;/ul&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h1&gt;第 19 章 部署上线&lt;a href=&quot;#第-19-章-部署上线&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h1&gt;&lt;section&gt;&lt;h2&gt;19.1 打包流程&lt;a href=&quot;#191-打包流程&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;在项目根目录执行：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;mvn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;clean&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;package&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;完成后会在 &lt;code&gt;target/&lt;/code&gt; 目录下生成：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;target/&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;└── edu_project-0.0.1-SNAPSHOT.jar  # 这就是部署用的包&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;这个 jar 是 Spring Boot 的 “fat jar”，包含了所有依赖 + 内嵌 Tomcat，单文件就能跑。&lt;/p&gt;&lt;p&gt;如果想跳过测试加速：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;mvn&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;clean&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;package&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-DskipTests&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;19.2 服务器准备（Linux）&lt;a href=&quot;#192-服务器准备linux&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;19.2.1 安装 JDK 21&lt;a href=&quot;#1921-安装-jdk-21&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Ubuntu / Debian：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;apt&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;update&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;apt&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;install&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;openjdk-21-jdk&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;java&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-version&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;# 验证&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;CentOS / RHEL：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;yum&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;install&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;java-21-openjdk-devel&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;19.2.2 安装 MySQL 8&lt;a href=&quot;#1922-安装-mysql-8&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;apt&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;install&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;mysql-server-8.0&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;systemctl&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;start&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;mysql&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;mysql_secure_installation&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;19.2.3 创建数据库&lt;a href=&quot;#1923-创建数据库&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;CREATE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;DATABASE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;campus_blog&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;DEFAULT&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;CHARACTER&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SET&lt;/span&gt;&lt;span&gt; utf8mb4 &lt;/span&gt;&lt;span&gt;COLLATE&lt;/span&gt;&lt;span&gt; utf8mb4_unicode_ci;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;CREATE&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;USER&lt;/span&gt;&lt;span&gt; &apos;&lt;/span&gt;&lt;span&gt;campus_blog&lt;/span&gt;&lt;span&gt;&apos;@&lt;/span&gt;&lt;span&gt;&apos;%&apos;&lt;/span&gt;&lt;span&gt; IDENTIFIED &lt;/span&gt;&lt;span&gt;BY&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;YourStrongPassword2026!&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;GRANT&lt;/span&gt;&lt;span&gt; ALL PRIVILEGES &lt;/span&gt;&lt;span&gt;ON&lt;/span&gt;&lt;span&gt; campus_blog.* &lt;/span&gt;&lt;span&gt;TO&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;campus_blog&apos;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;&apos;%&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;FLUSH PRIVILEGES;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;然后导入项目自带的”数据库表.sql”建表。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;19.3 环境变量配置&lt;a href=&quot;#193-环境变量配置&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;把 &lt;code&gt;.env.example&lt;/code&gt; 复制为 &lt;code&gt;.env&lt;/code&gt;：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;cp&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;.env.example&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;.env&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;nano&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;.env&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;生产环境必须修改的项：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;DB_HOST&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;127.0.0.1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;DB_PORT&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;3306&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;DB_NAME&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;campus_blog&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;DB_USERNAME&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;campus_blog&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;DB_PASSWORD&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;YourStrongPassword2026!&lt;/span&gt;&lt;span&gt;         &lt;/span&gt;&lt;span&gt;# 强密码！&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;JWT_SECRET&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;A_Very_Long_Random_String_64_chars_or_more_DON_T_use_default&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;JWT_EXPIRATION&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;86400000&lt;/span&gt;&lt;span&gt;                      &lt;/span&gt;&lt;span&gt;# 24 小时&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;JWT_REFRESH_EXPIRATION&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;604800000&lt;/span&gt;&lt;span&gt;             &lt;/span&gt;&lt;span&gt;# 7 天&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;SERVER_PORT&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;8825&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;CORS_ALLOWED_ORIGINS&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;https://campus-blog.com&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;# 具体域名！不要 *&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;⚠️ &lt;strong&gt;铁律&lt;/strong&gt;：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.env&lt;/code&gt; 必须在 &lt;code&gt;.gitignore&lt;/code&gt; 中，永远不能提交&lt;/li&gt;
&lt;li&gt;JWT_SECRET 用 &lt;code&gt;openssl rand -base64 64&lt;/code&gt; 生成&lt;/li&gt;
&lt;li&gt;数据库密码必须强密码&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;19.4 启动应用&lt;a href=&quot;#194-启动应用&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;19.4.1 nohup 启动（简单）&lt;a href=&quot;#1941-nohup-启动简单&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;nohup&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;java&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-jar&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;edu_project-0.0.1-SNAPSHOT.jar&lt;/span&gt;&lt;span&gt; &amp;gt; &lt;/span&gt;&lt;span&gt;app.log&lt;/span&gt;&lt;span&gt; 2&amp;gt;&amp;amp;1 &amp;amp;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;参数解释：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;nohup&lt;/code&gt;：忽略挂起信号，关闭 ssh 后程序仍然跑&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;gt;&lt;/code&gt;：标准输出重定向到 app.log&lt;/li&gt;
&lt;li&gt;&lt;code&gt;2&amp;gt;&amp;amp;1&lt;/code&gt;：错误流也写到 app.log&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;amp;&lt;/code&gt;：后台运行&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;查看日志：&lt;code&gt;tail -f app.log&lt;/code&gt;
停止程序：&lt;code&gt;ps -ef | grep edu_project&lt;/code&gt; 找到 PID 后 &lt;code&gt;kill &amp;lt;PID&amp;gt;&lt;/code&gt;&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;19.4.2 systemd 托管（推荐）&lt;a href=&quot;#1942-systemd-托管推荐&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;创建 &lt;code&gt;/etc/systemd/system/campus-blog.service&lt;/code&gt;：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;[Unit]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;Description&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;Campus Blog Forum&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;After&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;network.target mysql.service&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;[Service]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;Type&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;simple&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;appuser&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;WorkingDirectory&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;/opt/campus-blog&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;EnvironmentFile&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;/opt/campus-blog/.env&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;ExecStart&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;/usr/bin/java -Xmx512m -jar /opt/campus-blog/edu_project-0.0.1-SNAPSHOT.jar&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;Restart&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;always&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;RestartSec&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;StandardOutput&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;append:/var/log/campus-blog/app.log&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;StandardError&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;append:/var/log/campus-blog/error.log&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;[Install]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;WantedBy&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;multi-user.target&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;启用：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;systemctl&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;daemon-reload&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;systemctl&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;enable&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;campus-blog&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;systemctl&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;start&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;campus-blog&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;systemctl&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;campus-blog&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;systemd 优势：开机自启、崩溃自动重启、集中管理日志。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;19.5 反向代理：Nginx&lt;a href=&quot;#195-反向代理nginx&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;让用户访问 80/443 端口，由 Nginx 转发到 8825：&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;server&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;   &lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;listen &lt;/span&gt;&lt;span&gt;80&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;   &lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;server_name &lt;/span&gt;&lt;span&gt;campus-blog.com www.campus-blog.com;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;# HTTP 跳 HTTPS&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;301&lt;/span&gt;&lt;span&gt; https://$&lt;/span&gt;&lt;span&gt;server_name&lt;/span&gt;&lt;span&gt;$&lt;/span&gt;&lt;span&gt;request_uri&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;server&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;   &lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;listen &lt;/span&gt;&lt;span&gt;443&lt;/span&gt;&lt;span&gt; ssl http2;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;   &lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;server_name &lt;/span&gt;&lt;span&gt;campus-blog.com;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;   &lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;ssl_certificate &lt;/span&gt;&lt;span&gt;    /etc/letsencrypt/live/campus-blog.com/fullchain.pem;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;   &lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;ssl_certificate_key &lt;/span&gt;&lt;span&gt;/etc/letsencrypt/live/campus-blog.com/privkey.pem;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;# 前端静态文件&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;location&lt;/span&gt;&lt;span&gt; / {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;       &lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;root &lt;/span&gt;&lt;span&gt;/var/www/campus-blog-frontend;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;       &lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;try_files &lt;/span&gt;&lt;span&gt;$&lt;/span&gt;&lt;span&gt;uri&lt;/span&gt;&lt;span&gt; $&lt;/span&gt;&lt;span&gt;uri&lt;/span&gt;&lt;span&gt;/ /index.html;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;# 后端 API&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;23&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;location&lt;/span&gt;&lt;span&gt; /api/ {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;24&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;       &lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;proxy_pass &lt;/span&gt;&lt;span&gt;        http://127.0.0.1:8825/api/;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;25&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;       &lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;proxy_set_header &lt;/span&gt;&lt;span&gt;  Host              $&lt;/span&gt;&lt;span&gt;host&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;26&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;       &lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;proxy_set_header &lt;/span&gt;&lt;span&gt;  X-Real-IP         $&lt;/span&gt;&lt;span&gt;remote_addr&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;27&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;       &lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;proxy_set_header &lt;/span&gt;&lt;span&gt;  X-Forwarded-For   $&lt;/span&gt;&lt;span&gt;proxy_add_x_forwarded_for&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;28&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;       &lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;proxy_set_header &lt;/span&gt;&lt;span&gt;  X-Forwarded-Proto $&lt;/span&gt;&lt;span&gt;scheme&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;29&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;30&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;       &lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;client_max_body_size &lt;/span&gt;&lt;span&gt;500m&lt;/span&gt;&lt;span&gt;;  &lt;/span&gt;&lt;span&gt;# 大文件上传&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;31&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;32&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;section&gt;&lt;h3&gt;19.5.1 HTTPS 证书&lt;a href=&quot;#1951-https-证书&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;用 Let’s Encrypt 免费证书：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;apt&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;install&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;certbot&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;python3-certbot-nginx&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;certbot&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--nginx&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-d&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;campus-blog.com&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-d&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;www.campus-blog.com&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;自动续期：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;certbot&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;renew&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--dry-run&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;19.6 跨域配置（CORS）&lt;a href=&quot;#196-跨域配置cors&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;本项目 CORS 通过环境变量配置：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;cors&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;allowed-origins&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;${CORS_ALLOWED_ORIGINS:http://localhost:8080,http://127.0.0.1:8080}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;⚠️ &lt;strong&gt;生产环境绝对不能用 &lt;code&gt;*&lt;/code&gt; 通配符&lt;/strong&gt;：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 错误&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;CORS_ALLOWED_ORIGINS&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 正确&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;CORS_ALLOWED_ORIGINS&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;https://campus-blog.com,https://www.campus-blog.com&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;带 Cookie 的请求（&lt;code&gt;credentials: true&lt;/code&gt;）配合 &lt;code&gt;*&lt;/code&gt; 是浏览器禁止的；安全角度也禁止任意网站调你的接口。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;19.7 监控与日志&lt;a href=&quot;#197-监控与日志&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;section&gt;&lt;h3&gt;19.7.1 日志切割&lt;a href=&quot;#1971-日志切割&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;让 Logback 自动按日期切割：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;appender&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;FILE&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;ch.qos.logback.core.rolling.RollingFileAppender&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;file&lt;/span&gt;&lt;span&gt;&amp;gt;logs/app.log&amp;lt;/&lt;/span&gt;&lt;span&gt;file&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;rollingPolicy&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;fileNamePattern&lt;/span&gt;&lt;span&gt;&amp;gt;logs/app-%d{yyyy-MM-dd}.%i.log.gz&amp;lt;/&lt;/span&gt;&lt;span&gt;fileNamePattern&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;maxFileSize&lt;/span&gt;&lt;span&gt;&amp;gt;100MB&amp;lt;/&lt;/span&gt;&lt;span&gt;maxFileSize&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;maxHistory&lt;/span&gt;&lt;span&gt;&amp;gt;30&amp;lt;/&lt;/span&gt;&lt;span&gt;maxHistory&lt;/span&gt;&lt;span&gt;&amp;gt;           &lt;/span&gt;&lt;span&gt;&amp;lt;!-- 保留 30 天 --&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;totalSizeCap&lt;/span&gt;&lt;span&gt;&amp;gt;10GB&amp;lt;/&lt;/span&gt;&lt;span&gt;totalSizeCap&lt;/span&gt;&lt;span&gt;&amp;gt;     &lt;/span&gt;&lt;span&gt;&amp;lt;!-- 总大小上限 --&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;rollingPolicy&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;encoder&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;pattern&lt;/span&gt;&lt;span&gt;&amp;gt;%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger - %msg%n&amp;lt;/&lt;/span&gt;&lt;span&gt;pattern&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;encoder&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;appender&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;19.7.2 健康检查&lt;a href=&quot;#1972-健康检查&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;加入依赖：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&amp;gt;org.springframework.boot&amp;lt;/&lt;/span&gt;&lt;span&gt;groupId&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&amp;gt;spring-boot-starter-actuator&amp;lt;/&lt;/span&gt;&lt;span&gt;artifactId&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;dependency&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;访问 &lt;code&gt;http://localhost:8825/api/actuator/health&lt;/code&gt; 返回：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;&quot;status&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;UP&quot;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;可以接入监控系统（Prometheus + Grafana），自动发现服务挂掉。&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;19.8 数据库备份&lt;a href=&quot;#198-数据库备份&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;每日全备份脚本 &lt;code&gt;backup.sh&lt;/code&gt;：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;#!/bin/bash&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;DATE&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;$(&lt;/span&gt;&lt;/span&gt;&lt;span&gt;date&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+%Y%m%d&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;BACKUP_DIR&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;/var/backup/mysql&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;mkdir&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-p&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;$BACKUP_DIR&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;mysqldump&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-u&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;campus_blog&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-p&lt;/span&gt;&lt;span&gt;&apos;YourStrongPassword2026!&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;campus_blog&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;--single-transaction&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;--routines&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;| &lt;/span&gt;&lt;span&gt;gzip&lt;/span&gt;&lt;span&gt; &amp;gt; &lt;/span&gt;&lt;span&gt;$BACKUP_DIR&lt;/span&gt;&lt;span&gt;/campus_blog_&lt;/span&gt;&lt;span&gt;$DATE&lt;/span&gt;&lt;span&gt;.sql.gz&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 保留最近 30 天&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;find&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;$BACKUP_DIR&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-name&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;*.sql.gz&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-mtime&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+30&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-delete&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;加入 crontab 每天凌晨 3 点跑：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;0 3 * * * /opt/scripts/backup.sh&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;19.9 版本升级流程&lt;a href=&quot;#199-版本升级流程&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;本地拉新代码、测试通过&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mvn clean package -DskipTests&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;scp 把 jar 上传到服务器&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sudo systemctl stop campus-blog&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;备份旧 jar：&lt;code&gt;mv app.jar app.jar.bak&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;替换新 jar&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sudo systemctl start campus-blog&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tail -f /var/log/campus-blog/app.log&lt;/code&gt; 确认启动成功&lt;/li&gt;
&lt;li&gt;用 curl 跑下健康检查&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;⚠️ &lt;strong&gt;重要：升级前一定先备份数据库&lt;/strong&gt;。如果新版本改了表结构，回滚需要数据库也能回。&lt;/p&gt;&lt;hr /&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;19.10 Docker 容器化部署&lt;a href=&quot;#1910-docker-容器化部署&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;项目支持 Docker 部署，适合小型项目快速上线。&lt;/p&gt;&lt;section&gt;&lt;h3&gt;Dockerfile（项目根目录）&lt;a href=&quot;#dockerfile项目根目录&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 阶段一：构建&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; maven:3.9-eclipse-temurin-21-alpine &lt;/span&gt;&lt;span&gt;AS&lt;/span&gt;&lt;span&gt; builder&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;WORKDIR&lt;/span&gt;&lt;span&gt; /app&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;COPY&lt;/span&gt;&lt;span&gt; pom.xml .&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;RUN&lt;/span&gt;&lt;span&gt; mvn dependency:go-offline&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;COPY&lt;/span&gt;&lt;span&gt; src ./src&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;RUN&lt;/span&gt;&lt;span&gt; mvn clean package -DskipTests&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 阶段二：运行&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;FROM&lt;/span&gt;&lt;span&gt; eclipse-temurin:21-jre-alpine&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;WORKDIR&lt;/span&gt;&lt;span&gt; /app&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;COPY&lt;/span&gt;&lt;span&gt; --from=builder /app/target/edu_project-*.jar app.jar&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 变量由 docker-compose 的 .env 注入&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;ENV&lt;/span&gt;&lt;span&gt; JWT_SECRET=placeholder&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;ENV&lt;/span&gt;&lt;span&gt; DB_HOST=db&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;ENV&lt;/span&gt;&lt;span&gt; SERVER_PORT=8825&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;EXPOSE&lt;/span&gt;&lt;span&gt; 8825&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;ENTRYPOINT&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;&quot;java&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;-jar&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;app.jar&quot;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;docker-compose.yml&lt;a href=&quot;#docker-composeyml&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;version&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&apos;3.8&apos;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;services&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;db&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;image&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;mysql:8.0&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;restart&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;always&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;environment&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;MYSQL_ROOT_PASSWORD&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;${MYSQL_ROOT_PASSWORD}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;MYSQL_DATABASE&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;${DB_NAME}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;volumes&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;mysql_data:/var/lib/mysql&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;./init.sql:/docker-entrypoint-initdb.d/init.sql:ro&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;ports&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;&quot;3306:3306&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;networks&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;blog-net&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;16&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;17&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;app&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;18&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;build&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;19&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;restart&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;always&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;20&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;depends_on&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;21&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;db&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;22&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;condition&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;service_healthy&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;23&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;env_file&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;24&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;.env&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;25&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;environment&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;26&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;DB_HOST&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;db&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;27&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;DB_PORT&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;3306&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;28&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;DB_NAME&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;${DB_NAME}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;29&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;DB_USERNAME&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;root&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;30&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;DB_PASSWORD&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;${MYSQL_ROOT_PASSWORD}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;31&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;ports&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;32&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;&quot;8825:8825&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;33&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;networks&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;34&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;blog-net&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;35&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;36&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;volumes&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;37&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;mysql_data&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;38&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;39&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;networks&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;40&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;blog-net&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;41&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;driver&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;bridge&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;部署步骤&lt;a href=&quot;#部署步骤&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 1. 复制环境变量模板&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;cp&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;.env.example&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;.env&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;3&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 编辑 .env，填入真实密码（生产环境务必修改）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;4&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;5&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 2. 启动&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;6&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;docker&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;compose&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;up&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-d&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--build&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;7&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;8&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 3. 查看日志&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;9&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;docker&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;compose&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;logs&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-f&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;app&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;10&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;11&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 4. 健康检查&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;12&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;curl&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;http://localhost:8825/api/doc.html&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;13&lt;/div&gt;&lt;/div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;14&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 5. 停止&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;15&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;docker&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;compose&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;down&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;span&gt;展开&lt;/span&gt;&lt;span&gt;收起&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;⚠️ 生产环境使用 Docker 部署时，建议：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.env&lt;/code&gt; 不要提交到 Git（已在 &lt;code&gt;.gitignore&lt;/code&gt; 中）&lt;/li&gt;
&lt;li&gt;MySQL 数据通过 &lt;code&gt;volumes&lt;/code&gt; 持久化，防止容器重启丢数据&lt;/li&gt;
&lt;li&gt;考虑使用 Docker Swarm 或 Kubernetes 做多节点部署&lt;/li&gt;
&lt;/ul&gt;&lt;hr /&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;第 19 章 实战清单&lt;a href=&quot;#第-19-章-实战清单&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt; mvn clean package 打包成功&lt;/li&gt;
&lt;li&gt; 服务器装好 JDK 21 + MySQL 8&lt;/li&gt;
&lt;li&gt; .env 配好且不泄露&lt;/li&gt;
&lt;li&gt; systemd 托管应用&lt;/li&gt;
&lt;li&gt; Nginx + HTTPS 反向代理&lt;/li&gt;
&lt;li&gt; CORS 限定具体域名&lt;/li&gt;
&lt;li&gt; 日志切割&lt;/li&gt;
&lt;li&gt; 数据库每日备份&lt;/li&gt;
&lt;li&gt; 升级前备份&lt;/li&gt;
&lt;/ul&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h1&gt;附录 A：常见问题 FAQ&lt;a href=&quot;#附录-a常见问题-faq&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h1&gt;&lt;section&gt;&lt;h3&gt;Q1：启动报错 &lt;code&gt;JWT secret cannot be null&lt;/code&gt;&lt;a href=&quot;#q1启动报错-jwt-secret-cannot-be-null&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：&lt;code&gt;.env&lt;/code&gt; 文件没创建，或者没配 &lt;code&gt;JWT_SECRET&lt;/code&gt;。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;1&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;cp&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;.env.example&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;.env&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;2&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;span&gt;# 编辑 .env，填入 JWT_SECRET（至少 32 字符）&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div&gt;&lt;/div&gt;&lt;div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;如果用 IDEA 启动还是报错，可能是 IDEA 没读取 .env。&lt;code&gt;DotenvConfig.load()&lt;/code&gt; 会自动从 &lt;code&gt;user.dir&lt;/code&gt; 找 .env，确认 IDEA 的 Working Directory 设的是项目根目录。&lt;/p&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;Q2：Lombok 不生效，&lt;code&gt;@Slf4j&lt;/code&gt; 报错找不到 log&lt;a href=&quot;#q2lombok-不生效slf4j-报错找不到-log&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：IDEA 没装 Lombok 插件，或没开 annotation processing。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;IDEA → Settings → Plugins，搜 Lombok 安装&lt;/li&gt;
&lt;li&gt;Settings → Build → Compiler → Annotation Processors → Enable&lt;/li&gt;
&lt;/ol&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;Q3：Connection refused，连不上数据库&lt;a href=&quot;#q3connection-refused连不上数据库&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;按顺序排查：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;MySQL 服务起没起：&lt;code&gt;systemctl status mysql&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;防火墙拦没拦：3306 端口&lt;/li&gt;
&lt;li&gt;MySQL 监听地址：&lt;code&gt;bind-address&lt;/code&gt; 改成 0.0.0.0&lt;/li&gt;
&lt;li&gt;用户授权：&lt;code&gt;GRANT ALL ON campus_blog.* TO &apos;campus_blog&apos;@&apos;%&apos;;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;.env 里 DB_HOST/DB_PORT/DB_USERNAME/DB_PASSWORD 是否对&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;mysql -h IP -P 3306 -u campus_blog -p&lt;/code&gt; 在命令行试连&lt;/li&gt;
&lt;/ol&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;Q4：Token expired&lt;a href=&quot;#q4token-expired&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;正常情况&lt;/strong&gt;：默认 access token 24 小时过期。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;解决方案&lt;/strong&gt;：&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;调用 &lt;code&gt;/api/auth/refresh&lt;/code&gt; 刷新（前端拦截 401 自动刷）&lt;/li&gt;
&lt;li&gt;或重新登录&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;Q5：上传大文件报 413 Request Entity Too Large&lt;a href=&quot;#q5上传大文件报-413-request-entity-too-large&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;两层限制都要改：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;Spring Boot：&lt;code&gt;application.yml&lt;/code&gt; 里 max-file-size、max-request-size&lt;/li&gt;
&lt;li&gt;Nginx：&lt;code&gt;client_max_body_size 500m;&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;&lt;/section&gt;&lt;section&gt;&lt;h3&gt;Q6：跨域报错 CORS&lt;a href=&quot;#q6跨域报错-cors&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;错误信息形如 &lt;code&gt;No &apos;Access-Control-Allow-Origin&apos; header is present&lt;/code&gt;。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;排查&lt;/strong&gt;：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;后端是否配置了 CorsFilter（项目自带）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CORS_ALLOWED_ORIGINS&lt;/code&gt; 是否包含前端域名&lt;/li&gt;
&lt;li&gt;是否用了 &lt;code&gt;*&lt;/code&gt; 配合 &lt;code&gt;withCredentials&lt;/code&gt;（不允许）&lt;/li&gt;
&lt;li&gt;预检请求 OPTIONS 是否通过：浏览器 Network 面板看&lt;/li&gt;
&lt;/ol&gt;&lt;hr /&gt;&lt;/section&gt;&lt;/section&gt;
&lt;section&gt;&lt;h1&gt;附录 B：项目术语表&lt;a href=&quot;#附录-b项目术语表&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h1&gt;


















































































































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;术语&lt;/th&gt;&lt;th&gt;全称&lt;/th&gt;&lt;th&gt;通俗解释&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;IoC&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Inversion of Control&lt;/td&gt;&lt;td&gt;控制反转：对象不再自己 new 依赖，由 Spring 容器注入&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;DI&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Dependency Injection&lt;/td&gt;&lt;td&gt;依赖注入：IoC 的具体实现方式（@Autowired）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;AOP&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Aspect-Oriented Programming&lt;/td&gt;&lt;td&gt;面向切面编程：把日志、事务等横切关注点抽出来&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;ORM&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Object-Relational Mapping&lt;/td&gt;&lt;td&gt;对象关系映射：实体类 ↔ 数据库表&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;JWT&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;JSON Web Token&lt;/td&gt;&lt;td&gt;用 JSON + 签名表示的 Token，自包含，前后端分离常用&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;BCrypt&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;-&lt;/td&gt;&lt;td&gt;一种慢哈希算法，专为密码存储设计，自动加盐&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;CSRF&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Cross-Site Request Forgery&lt;/td&gt;&lt;td&gt;跨站请求伪造：诱骗用户在已登录站点发请求&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;XSS&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Cross-Site Scripting&lt;/td&gt;&lt;td&gt;跨站脚本：注入脚本被浏览器执行&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;CORS&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Cross-Origin Resource Sharing&lt;/td&gt;&lt;td&gt;跨源资源共享：浏览器同源策略的放行机制&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;CRUD&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Create/Read/Update/Delete&lt;/td&gt;&lt;td&gt;增删改查&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;DTO&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Data Transfer Object&lt;/td&gt;&lt;td&gt;数据传输对象：接口入参/出参用&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;VO&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;View Object&lt;/td&gt;&lt;td&gt;视图对象：返回前端用，不暴露内部字段&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Entity&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;-&lt;/td&gt;&lt;td&gt;实体类：直接对应数据库表&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;CRUD&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Create/Read/Update/Delete&lt;/td&gt;&lt;td&gt;增删改查&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;TOCTOU&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Time-of-check to time-of-use&lt;/td&gt;&lt;td&gt;检查时与使用时之间的并发漏洞&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;CAS&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Compare-And-Swap&lt;/td&gt;&lt;td&gt;比较并交换，无锁原子操作&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;CDN&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Content Delivery Network&lt;/td&gt;&lt;td&gt;内容分发网络，加速静态资源&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;SSL/TLS&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;-&lt;/td&gt;&lt;td&gt;HTTPS 加密协议&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;REST&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Representational State Transfer&lt;/td&gt;&lt;td&gt;一种 API 设计风格，URL 表示资源&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Bean&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;-&lt;/td&gt;&lt;td&gt;Spring 容器管理的对象&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Bootstrap&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;-&lt;/td&gt;&lt;td&gt;Spring Boot 启动过程&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;hr /&gt;&lt;/section&gt;
&lt;section&gt;&lt;h1&gt;后记：你的成长之路&lt;a href=&quot;#后记你的成长之路&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h1&gt;&lt;p&gt;恭喜！你坚持读完了这本书。从环境搭建、Java 基础、Spring Boot 核心，到安全加固、性能优化、上线部署 —— 你已经走完了一个 Java 后端开发者最重要的入门旅程。&lt;/p&gt;&lt;p&gt;但这只是起点。&lt;/p&gt;&lt;section&gt;&lt;h2&gt;接下来学什么&lt;a href=&quot;#接下来学什么&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;按推荐顺序：&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Redis&lt;/strong&gt;：缓存、分布式锁、消息队列入门、限流&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;消息队列&lt;/strong&gt;：RabbitMQ / Kafka，解决削峰填谷、系统解耦&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分布式基础&lt;/strong&gt;：CAP 理论、最终一致性、分布式事务（Seata）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;微服务&lt;/strong&gt;：Spring Cloud（Nacos、OpenFeign、Sentinel、Gateway）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;容器化&lt;/strong&gt;：Docker、Docker Compose、Kubernetes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DevOps&lt;/strong&gt;：Jenkins / GitHub Actions 自动化部署&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据库进阶&lt;/strong&gt;：分库分表（ShardingSphere）、读写分离&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JVM 调优&lt;/strong&gt;：垃圾回收、内存模型、性能诊断（Arthas、JProfiler）&lt;/li&gt;
&lt;/ol&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;推荐学习资源&lt;a href=&quot;#推荐学习资源&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Spring 官方文档&lt;/strong&gt;：&lt;a href=&quot;https://spring.io/projects/spring-boot&quot; target=&quot;_blank&quot;&gt;https://spring.io/projects/spring-boot&lt;/a&gt; —— 任何中文教程都比不上官方文档权威&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Spring Boot 示例&lt;/strong&gt;：&lt;a href=&quot;https://github.com/spring-projects/spring-boot/tree/main/spring-boot-samples&quot; target=&quot;_blank&quot;&gt;https://github.com/spring-projects/spring-boot/tree/main/spring-boot-samples&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Awesome Java&lt;/strong&gt;：&lt;a href=&quot;https://github.com/akullpp/awesome-java&quot; target=&quot;_blank&quot;&gt;https://github.com/akullpp/awesome-java&lt;/a&gt; —— 整理了海量优秀 Java 库&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OWASP&lt;/strong&gt;：&lt;a href=&quot;https://owasp.org/&quot; target=&quot;_blank&quot;&gt;https://owasp.org/&lt;/a&gt; —— 安全方面的圣经&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MySQL 实战 45 讲&lt;/strong&gt;（极客时间）—— 把 MySQL 真正讲透了&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GitHub Trending&lt;/strong&gt;：每天去看 Java 区，跟最新潮流&lt;/li&gt;
&lt;/ul&gt;&lt;/section&gt;&lt;section&gt;&lt;h2&gt;一些过来人的话&lt;a href=&quot;#一些过来人的话&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;不要囤教程&lt;/strong&gt;：买了 100 个课程不如把 1 个项目做完。本书的项目就是最好的练习对象，把它跑起来、改起来、加新功能。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;拥抱开源&lt;/strong&gt;：在 GitHub 上读优秀项目源码（如 mall、若依、jeecg-boot），看看大公司怎么写代码。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多写、多改、多重构&lt;/strong&gt;：第一个版本永远是丑的。看到 v1.10 → v1.34 一路修复痕迹了吗？真实项目就是这样长大的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;保持好奇&lt;/strong&gt;：碰到不懂的报错，别只复制百度，看完报错的英文 stack trace 和官方文档，你会比 90% 的人都强。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;别怕错&lt;/strong&gt;：你写的每一行 bug，都是未来不再写 bug 的资本。&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;最后送你一句话，是本项目 &lt;code&gt;CLAUDE.md&lt;/code&gt; 里的”开发荣耻”，希望你也能在工作中践行：&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;以瞎清接口为耻，以认真查询为荣。
以模糊执行为耻，以寻求确认为荣。
以创造接口为耻，以复用现有为荣。
以跳过验证为耻，以主动测试为荣。
以破坏架构为耻，以遵循规范为荣。
以假装理解为耻，以诚实无知为荣。
以盲目修改为耻，以谨慎重构为荣。
以忘记更新文档为耻，以及时更新文档为荣。&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;—— 祝你在 Java 的世界里走得长远，写出让自己骄傲的代码。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;全书完。&lt;/strong&gt;&lt;/p&gt;&lt;/section&gt;&lt;/section&gt;</content:encoded></item></channel></rss>