最近把一套流浪动物救助平台从零到一完整做了出来技术栈用的是 SpringBoot2 Vue3 MyBatis-Plus MySQL8.0从需求梳理、数据库设计到前后端联调、部署验证整个链路走完最大的感受是这类 Java Web 项目最耗时间的往往不是写代码而是把业务状态和权限边界想清楚。平台本身解决的是流浪动物信息分散、领养流程不透明、回访跟踪难的问题普通用户可以在线浏览待领养动物、提交领养申请、发布寻宠启事救助站管理员负责审核申请、维护动物档案和救助记录系统管理员做整体权限和公告配置。如果你正准备做毕业设计或者想拿一套完整的全栈实战项目练手这篇内容能帮你把从数据库建表到跑通第一个接口的弯路省掉一大半。1. 整体思路与需求拆解1.1 这套平台到底要解决什么问题救助流浪动物这件事线下信息通常只靠朋友圈和群聊传播领养人看不到动物真实状态救助人也很难跟踪后续情况。平台要做的就是把这些分散的数据集中起来形成一条可追踪的流程动物入站、信息发布、用户申请、审核评估、签订协议、回访反馈。我一开始就把这个系统定位成多角色协作平台而不是简单的动物信息展示站。因为如果没有审核和回访机制领养申请就只是一个表单实际业务价值很低。做 Java Web 项目时最容易犯的错就是只把 CRUD 做完就以为结束了真正让系统有生命力的是动物从“待领养”到“已完成回访”这条状态链路的完整性。1.2 角色划分和核心业务流系统里我规划了三种角色分别对应实际场景里的三方人角色核心操作权限边界普通用户浏览动物、领养申请、寻宠发布、捐赠登记、收藏关注只能维护自己的数据和申请记录救助站管理员动物档案录入、审核领养申请、回访记录、救助登记可操作救助站范围内的数据系统管理员用户管理、公告发布、分类字典、全局配置全部权限核心流程我建议先画出来再动手写代码救助站管理员录入一条动物信息状态是“待展示”系统管理员或救助站管理员审核后状态变更为“待领养”用户看到后提交领养申请此时申请状态是“待审核”救助站管理员进行初审和终审通过后进入“待签订协议”用户在线确认或线下签完协议动物状态改为“已领养”最后在回访周期内填写回访记录整个流程关闭。这套流程也直接影响数据库设计。前期如果只是建几个表做增删改查后期逻辑会越写越乱先把每个操作对应哪个角色、哪个状态变更方向想清楚写出来的接口才能干净。1.3 功能模块清单实际项目中我拆成了七个子模块避免把 Controller 写成一个大杂烩动物管理分页筛选、新增、编辑、上下架、图片上传动物状态和领养状态分开存储。领养申请管理提交申请、审核列表、状态流转、回访记录。救助记录管理记录救助位置、救助经过、图片以及后续关联的动物档案。寻宠启事管理用户发布丢失宠物信息平台管理员可置顶或下架。捐赠管理捐赠意向登记、管理员确认、接收记录公示。用户中心登录注册、个人资料、我的申请、我的收藏、我的寻宠。系统管理用户列表、角色分配、公告发布、字典分类。其中比较容易忽略的是“救助记录”和“动物档案”的关联。一条救助记录可能对应一只后来入站的动物也可能是无主动物直接上报所以我在两张表之间用外键留了可空字段而不是做死一对一的耦合。2. 技术选型与架构设计2.1 为什么不是 SpringBoot3而是 SpringBoot2做这种从零开始的实战项目或者交毕业设计稳定和资料丰富度是第一位的。SpringBoot2 生态成熟基于 JDK8 跑起来没有版本冲突随便查一个报错都能找到解决方案。SpringBoot3 确实有新特性但依赖场景变了很多比如 javax 改 jakarta很多旧教程直接不适用。我这套用的是 SpringBoot 2.7.x配合 MyBatis-Plus 3.5.x 和 MySQL 8.0 驱动。选 2.7 而不是 2.4 或 2.5是因为 2.7 是 2.x 系列的持续维护版本对 Java 8 兼容性仍然很好而且 springdoc / knife4j 这类文档组件适配这个版本更顺畅。同时引入 starter-web、starter-validation、lombok、mysql-connector-java、mybatis-plus-boot-starter、jjwt 做 token 管理这些都是老组合了不会踩奇怪的兼容性问题。2.2 MyBatis-Plus 带来的效率提升标题里特意出现 MyBatis-Plus说明这个项目目标就是减少手写单表 SQL。MyBatis-Plus 的核心能力是几件套BaseMapper 的通用 CRUD、条件构造器、分页插件、逻辑删除、自动填充。我实际用下来单表操作真的不用写 XML。比如动物列表的筛选操作只需要构造一个 LambdaQueryWrapper把 species、status、keyword 这些条件拼进去底层自动转 SQL。但要注意多表关联查询还是得自己写MyBatis-Plus 没有把 join 做成自动化不需要为此感觉沮丧反而这样更能控制查询性能。我可以直接说这项目里 60% 的 Mapper 方法都是空的因为 BaseMapper 已经够用了剩下 40% 集中在领养申请与用户、动物信息的多表关联以及统计类的报表查询。用 MyBatis-Plus 还有一个好处它的分页插件很容易配置只需要在容器里注册一个 MybatisPlusInterceptor再添加分页拦截器即可。2.3 前端为何选 Vue3而不是 Vue2Vue3 已经是目前前端岗位的主流要求毕业设计里用 Vue3 也很常见。组合式 API 在组织业务代码时更灵活同一个页面里的搜索条件、列表数据、分页逻辑可以放在一个 setup 里很清晰地展开不用像选项式 API 那样强行拆 data、methods、computed。配合 Element Plus 组件库后台管理界面的表格、表单、弹窗基本可以靠组件拼出来。我用 Vite 做构建工具开发环境冷启动非常快不用像 webpack 那样等半天。状态管理选 Pinia相比 Vuex 的 mutation 和 getter 层层写Pinia 的写法接近普通对象对新手友好很多。路由用 vue-router4做权限控制的思路是前端根据登录用户角色先只渲染默认路由登录成功后再把动态路由 addRoute 进去实现菜单级别的权限隔离。2.4 前后端分离的整体架构整个系统的访问链路是这样的浏览器请求 Vue3 页面页面里的 Axios 请求通过 Vite 代理转发到 SpringBoot 后端后端 Controller 接收请求Service 处理业务Mapper 访问 MySQL8.0文件上传则单独走一个本地静态路径后端把上传后的图片 URL 返回给前端拼接展示。前后端分离的好处是职责明确。前端只管页面交互和状态控制后端只暴露 JSON 接口文档用 knife4j 生成打开 /doc.html 就能直接看接口定义和模拟请求。生产环境中前端构建成 dist 目录交给 NginxNginx 再配置反向代理把 /api 开头的请求转发到后端服务同时解决跨域问题。3. 表结构设计与核心细节3.1 核心表清单与字段规划数据库设计的核心是把业务对象拆清楚再把状态字段从业务字段里独立出来。我建的主要表有sys_user用户表保存账号、密码、昵称、手机号、头像、角色标识、状态。animal动物信息表保存编号、名称、物种、品种、性别、年龄、健康状况、疫苗、绝育情况、图片列表、当前状态。adopt_application领养申请表保存申请用户、动物、姓名、电话、住址、申请理由、家庭情况、状态、审核备注。rescue_record救助记录表保存救助人、地点、描述、图片、后续关联动物 ID。lost_found寻宠启事表保存宠物信息、丢失时间地点、特征描述、联系方式。donate_record捐赠记录表保存捐赠人、类型、金额或物资信息、确认状态。follow_record关注表关联用户和动物记录关注时间。sys_notice公告表标题、内容、发布时间。字段命名统一用下划线风格比如 create_time、update_time避免前端拿到几个驼峰字段又映射不上。日期类字段全部用 datetime不用 timestamp因为 timestamp 有时区限制处理跨时区项目时会莫名少 8 小时。金额字段如果用 double 会出精度问题这里捐赠金额我用 DECIMAL(10,2)配合 BigDecimal 类型处理。3.2 领养申请的状态流转设计状态流转是这个项目最值得聊的部分。领养申请不是简单地挂一个“未通过/已通过”真实场景有多个阶段我设计了六个状态0待审核1初审通过2已拒绝3待签订协议4已领养5已完结后端更新状态时不能直接无条件 UPDATE我用乐观锁思路处理并发问题update 语句里加上当前状态条件例如把初审通过改成待签订协议时必须是status 1并且status 1如果影响行数是 0说明这条记录已经被其他人操作过了立即抛出异常提示刷新页面。用 MyBatis-Plus 的 UpdateWrapper 写起来非常简单只要把 eq(status, 当前状态) 放在 update 条件里即可。动物本身的状态也要和领养申请区分开。一个动物可以处于“待领养”此时它的领养申请可能是待审核一旦申请流程走到“已领养”动物状态必须同步修改。我在事务里同时更新这两张表避免流程走到一半出现状态不一致。3.3 逻辑删除和自动填充后端开启 MyBatis-Plus 逻辑删除后删除操作变成 UPDATE deleted 1查询自动过滤 deleted 0。配置很简单在实体类字段TableLogic加注解即可。但有个坑如果数据库表里有唯一索引比如手机号逻辑删除后不能重复插入同手机号的用户因为记录还留在表中唯一索引照样生效。解决方法是给唯一字段联合 deleted 建组合唯一索引或者删除时在用户名后加随机后缀否则还会注册失败。create_time和update_time我不想在每个 Service 里手动 set直接利用 MyBatis-Plus 的字段自动填充。实体类字段上标注TableField(fill FieldFill.INSERT)和TableField(fill FieldFill.INSERT_UPDATE)再写一个实现MetaObjectHandler的类在 insertFill 和 updateFill 方法里自动填入当前时间。这一步容易漏的是配置文件里要开启自动填充而且实体类字段如果叫 createTime数据库列叫 create_time映射关系别写反。4. 关键功能实现与代码片段4.1 用 MyBatis-Plus 做动物分页查询动物列表是最主要的前端页面带物种筛选、名称模糊查询和分页。Controller 接收 pageNum、pageSize、species、keywordService 里创建一个PageAnimal再用 LambdaQueryWrapper 拼接条件public PageAnimal pageAnimals(int pageNum, int pageSize, String species, String keyword) { PageAnimal page new Page(pageNum, pageSize); LambdaQueryWrapperAnimal wrapper new LambdaQueryWrapper(); wrapper.eq(StringUtils.hasText(species), Animal::getSpecies, species) .and(StringUtils.hasText(keyword), w - w .like(Animal::getName, keyword) .or().like(Animal::getAnimalNo, keyword)) .eq(Animal::getStatus, 1) .orderByDesc(Animal::getCreateTime); return animalMapper.selectPage(page, wrapper); }分页插件必须注册否则 selectPage 只会查出全部数据然后前端看到的分页参数是错的Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }分页插件默认支持 limit 优化返回的 total 是自动 count 查询的。这里有个小建议查询条件里不要直接把 entity 整对象传进去否则会出现某些字段不知不觉成为 where 条件的情况逻辑很难排查。很多 MyBatis-Plus 的“坑”都是从这个点开始的。4.2 多表联查用 VO 和 XMLMyBatis-Plus 的 BaseMapper 不负责 join需求到了这一步就得写 XML。领养申请列表需要关联用户表的昵称和动物表的名称我在项目中新建了一个 AdoptApplicationVO避免让 VO 继承实体对象造成字段混乱。select idselectAdoptPage resultTypecom.example.vo.AdoptApplicationVO SELECT a.id AS id, a.animal_id AS animalId, a.user_id AS userId, a.real_name AS realName, a.phone AS phone, a.reason AS reason, a.status AS status, u.nickname AS userNickname, u.avatar AS userAvatar, an.name AS animalName, an.images AS animalImages, a.create_time AS createTime FROM adopt_application a LEFT JOIN sys_user u ON a.user_id u.id LEFT JOIN animal an ON a.animal_id an.id where if teststatus ! null AND a.status #{status} /if if testkeyword ! null and keyword ! AND (u.nickname LIKE CONCAT(%, #{keyword}, %) OR an.name LIKE CONCAT(%, #{keyword}, %)) /if /where ORDER BY a.create_time DESC /select联表查询有几个注意点一是字段别名要和 VO 的驼峰属性对应最好设置map-underscore-to-camel-casetrue二是只 select 需要的列不要无脑SELECT a.*, u.*三是筛选条件尽量放在 where 里能减少一大部分数据回表。在 Service 层把联查结果包装成统一 Page 对象时直接使用 MyBatis-Plus 的Page泛型指定为 VO 即可分页插件对自定义 SQL 同样生效。4.3 登录鉴权与 JWT 拦截器系统包含三种角色前端路由必须根据角色动态渲染后端接口也必须校验登录状态和角色权限。我这里没有直接引入 Spring Security因为对一个纠错量可控的项目轻量级 JWT 拦截器已经足够也好解释。登录接口验证用户名密码后生成一个 tokentoken 里带上 userId、role、过期时间用 HMAC 签名。后端写一个 AuthInterceptor请求进入 Controller 前先解析 header 里的 Authorizationpublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { throw new BizException(401, 未登录或登录已过期); } Claims claims JwtUtil.parseToken(token.replace(Bearer , )); Long userId Long.valueOf(claims.get(userId).toString()); String role claims.get(role).toString(); UserContext.set(new LoginUser(userId, role)); return true; }UserContext 使用 ThreadLocal 保存当前登录用户Controller 或 Service 需要当前用户时直接UserContext.get()请求结束一定要在 afterCompletion 里 remove否则线程池复用会串数据。拦截器注册时需要排除登录、注册、验证码和静态资源路径同时配置跨域registry.addInterceptor(authInterceptor()) .addPathPatterns(/api/**) .excludePathPatterns(/api/auth/login, /api/auth/register, /api/animal/list, /api/animal/detail/**, /api/notice/**, /upload/**, /doc.html);Windows 下 ThreadLocal 的坑我踩过多次这里特别强调千万记得清理不然高并发请求时会出现用户 A 登录状态下查询结果显示用户 B 的数据。4.4 Vue3 封装 Axios 请求和路由控制前端所有请求都走同一个 axios 实例。我把它放在src/utils/request.js里baseURL 直接用/api开发时由 Vite 代理转发。请求拦截器从 localStorage 拿出 token加到 header响应拦截器统一处理后端返回的 code如果 code 是 401就清空用户数据并跳转登录页。request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message) if (res.code 401) { router.push(/login) } return Promise.reject(new Error(res.message)) } return res }, error { ElMessage.error(error.message || 网络异常) return Promise.reject(error) } )路由权限用动态路由实现默认路由只有登录、注册和首页登录成功后根据角色加载菜单配置。前端保存一份路由映射表后端接口返回该角色的菜单标识列表。使用router.addRoute把允许的页面注册进去同时刷新浏览器时通过sessionStorage恢复路由。这个方案比每次都从后端请求菜单要快但要注意刷新时路由尚未恢复就跳转需要处理全局守卫的等待逻辑。4.5 图片上传实现上传功能在整个系统里很重要动物的多图展示、寻宠启事、捐赠凭证都需要。后端用 MultipartFile 接收文件限制扩展名和大小PostMapping(/api/upload) public ResultString upload(RequestParam(file) MultipartFile file) throws IOException { if (file.isEmpty()) { throw new BizException(请选择文件); } String originalFilename file.getOriginalFilename(); String ext StringUtils.getFilenameExtension(originalFilename); if (!Arrays.asList(jpg, jpeg, png, gif, webp).contains(ext.toLowerCase())) { throw new BizException(不支持的文件类型); } if (file.getSize() 5 * 1024 * 1024) { throw new BizException(文件大小不能超过5MB); } String folder upload/ LocalDate.now().toString(); File dir new File(uploadPath, folder); if (!dir.exists()) { dir.mkdirs(); } String fileName UUID.randomUUID().toString().replace(-, ) . ext; file.transferTo(new File(dir, fileName)); return Result.success(/upload/ folder / fileName); }上传路径不要用项目根目录的绝对路径最好在配置文件里指定一个外部目录比如/data/upload这样重新部署时不会丢图片。后端需要配置静态资源映射把/upload/**指向这个目录否则前端访问图片会 404。5. 运行环境与部署注意事项5.1 环境准备清单软件推荐版本用途JDK1.8 及以上运行 SpringBootMaven3.6.x后端依赖管理MySQL8.0.x主数据库Node.js16 或 18前端开发与构建IDEIDEA 或 VS Code开发调试5.2 MySQL8.0 安装与初始化MySQL8.0 安装方式很多Windows 上最省事的方案是官方 zip 版。解压后创建一个 my.ini核心配置是端口、字符集和数据目录[mysqld] port3306 basedirD:/mysql-8.0.40-winx64 datadirD:/mysql-8.0.40-winx64/data character-set-serverutf8mb4 collation-serverutf8mb4_general_ci default_authentication_pluginmysql_native_password然后以管理员身份运行 cmd进入 bin 目录执行mysqld --initialize-insecure生成 data 目录并免密初始化再执行mysqld --install安装服务net start mysql启动服务。此时 root 密码为空登录后执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码; FLUSH PRIVILEGES;很多人连接 8.0 报Public Key Retrieval is not allowed连接 URL 需要加上allowPublicKeyRetrievaltrue同时因为默认认证方式的关系我直接把驱动对应的 URL 参数也一起写进配置不然新装的 MySQL 会逼着你先跑一次 SSL 流程。项目里我推荐建立专用账号不要直接用 root保证安全性也方便后期重新造数据。初始化数据库时直接执行项目 doc 目录下的init.sql和data.sql里面包含建库、建表和测试数据。数据库名称我用animal_adoption字符集选择utf8mb4排序规则utf8mb4_general_ci或utf8mb4_unicode_ci都可以中文乱码问题基本不会出现。如果你用 Docker 安装 MySQL8.0命令也简单docker run -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD123456 -e MYSQL_DATABASEanimal_adoption mysql:8.0。但注意容器里的时区默认是 UTC进入容器执行SET GLOBAL time_zone 8:00否则数据库里的时间会比北京时间少 8 小时。5.3 后端配置文件和启动application.yml 里最关键的几处server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/animal_adoption?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0url 里的serverTimezoneAsia/Shanghai必须和服务端时区一致否则拿到的 Date 会差 8 小时。useSSLfalse是因为本地开发没必要做 SSL 加固。allowPublicKeyRetrievaltrue是解决 MySQL8.0 使用 caching_sha2_password 认证时可能出现的问题。后端启动方式很简单Maven 依赖加载完直接运行主类 Application。此时打开http://localhost:8080/doc.html能看到基于 knife4j 的接口文档页面所有 Controller 接口都列在左侧可以直接在页面上调试登录、查询等接口。5.4 前端启动和打包进入前端目录执行npm install安装依赖时建议锁定具体版本不要用^去给大版本浮动否则不同机器安装出来的依赖不一致容易出现编译器报错。开发环境执行npm run dev如果后端跑在 8080前端在 vite.config.js 里跳过代理server: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }打包时执行npm run build产物在 dist 目录。部署到 Nginx 时一个常见问题是前端初始路径不错但刷新页面后 404。因为 vue-router 默认使用 history 模式刷新时 Nginx 找不到对应的真实文件。需要在 nginx.conf 里加一句location / { try_files $uri $uri/ /index.html; }同时将/api请求转发到后端location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }如果前端部署在子路径比如http://ip/animal/还需要在 vite.config.js 里配置base: /animal/全局的路由 base 也改成/animal/否则所有资源都会从根路径加载。6. 常见问题与排查技巧实录6.1 MySQL8.0 连接失败先排查这几个地方“连接被拒绝”、“Cant connect to MySQL server (10061)”这类错误我通常按顺序检查四步服务是否启动、端口是否被占用、账号是否授权、驱动版本是否匹配。Windows 上 MySQL 服务没启动时在服务管理里把 MySQL 设为自动启动即可。端口被占用时执行netstat -ano | findstr 3306看到进程 PID再用任务管理器结束异常进程。还要注意一个新手容易踩的坑MySQL8.0 驱动类名是com.mysql.cj.jdbc.Driver老驱动com.mysql.jdbc.Driver虽然也能连但会有警告建议统一改成新驱动。驱动版本选 8.0.33 或以上不要用 5.x 系列否则allowPublicKeyRetrieval参数根本不认识。6.2 MyBatis-Plus 分页失效或者总数错误分页失效的大概率原因是没注册分页插件这时selectPage实际只是执行了一次全量查询Page 对象里有数据但 total 不对。另一种情况是分页插件注册了但MapperScan扫描的包路径不对插件没有生效。启动日志里看到PaginationInnerInterceptor相关输出能帮助你判断。另外如果自定义 XML 里写了多个参数必须用Param(page)传给 Page 参数分页插件才能在生成的 SQL 上自动追加 limit。还有一种更隐蔽的问题在自定义 SQL 中手动写了LIMIT又同时传入了 Page插件会在原 SQL 上再次追加 limit导致 SQL 语法错误。写 selectPage 的 XML 时除非特殊情况一定不要在 SQL 里手动控制 limit。6.3 Vue3 开发时接口跨域或请求带不上 tokenVite 代理配置没生效时浏览器 Network 里看到请求地址是http://localhost:8080/api/...而不是http://localhost:3000/api/...。axios baseURL 一定要写成/api而不是http://localhost:8080这样才能走上代理。请求带不上 token 的常见原因是请求拦截器没有正确从 localStorage 读取 token或者代码中为某个接口直接写了绝对地址导致 axios 实例的拦截器失效。调试时可以打印 header确认Authorization: Bearer xxx已携带。6.4 图片上传后前端打不开上传成功但图片 URL 404优先看后端有没有配置静态资源映射。SpringBoot 里我加了如下配置才能访问本地上传目录Configuration public class WebMvcConfig implements WebMvcConfigurer { Value(${upload.path}) private String uploadPath; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceHandler(uploadPath); } }Windows 上传路径要注意反斜杠问题配置文件里最好写成upload.pathD:/data/upload不要带末尾的反斜杠否则拼接时会叠成两个。Linux 部署时还要检查目录权限chmod -R 755 /data/upload否则应用账号没有写权限。6.5 Element Plus 表单校验在 Vue3 中不生效表单明明 rules 写好了但点击提交时 el-form 没有提示。这个坑通常因为 FormItem 的prop没有对应到v-model的字段名比如 rules 里写的是phoneFormItem 里 prop 写的是telephone就校验不上。另一个原因是model绑定的对象和v-model绑定字段不是同一个响应式对象。用组合式 API 时我建议直接在reactive里维护 formData给 el-form 绑定:modelformData每个 el-form-item 的 prop 和 formData 的 key 保持一致。日期类型的校验如果需要指定范围可以直接用 validator 函数不过一般项目里自带的日期范围校验已经够用了。收尾我就不再重复一遍功能清单了只分享一句实际感受这套 Java Web 项目里最花时间的地方不是 SpringBoot 或 Vue3 的写法而是把“申请状态”和“角色权限”这两件事想明白。我做的过程中把数据库表设计改了三轮前两轮都是因为状态流转没梳理清楚导致后面写接口时反复返工。如果你也准备上手类似平台我的建议是先在文档里画一张状态流转图每一步谁操作、能不能跳过、操作后通知谁全部列出来再开始敲代码。还有一点小技巧用 MyBatis-Plus 的自动填充字段时一定记得写 MetaObjectHandler只加注解不写处理器插入数据时字段会一直空着而且日志里不会报错排查起来特别迷惑。把这几个点提前处理好后面的开发会顺畅很多。