ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java Web动物领养平台:SpringBoot+Vue3前后端分离实战源码

Java Web动物领养平台:SpringBoot+Vue3前后端分离实战源码 最近整理了一套 Java Web 动物领养平台的前后端分离源码技术栈是 SpringBoot2 Vue3 MyBatis-Plus MySQL8.0整套项目从数据库表设计到权限控制从宠物图片上传到 Nginx 部署都配套了完整的文档。简单说这套系统解决的是传统救助站信息不透明、领养流程难追踪的问题前端面向普通用户浏览和申请领养后端面向管理员审核和发布动物信息带角色权限、审核状态流转、图片管理等完备模块。如果你是刚学完 SpringBoot 和 Vue 基础想找一套能真正跑起来的项目练手或者需要做一个结构完整、演示效果好、答辩有内容的毕设这篇内容可以帮你在最短时间内把整套系统的设计思路、核心代码和坑点一次理清。这套系统不是什么花哨的重型架构而是把日常开发中最常见的组件组合起来做出一个真实的业务闭环。我写这篇复盘的时候刻意把每个关键决策背后的原因都解释了一遍——为什么用 MyBatis-Plus 而不是 JPA为什么前端要用状态流转而不是简单改个字段为什么 MySQL8.0 的时区配置能折腾一下午。下面按开发顺序逐层拆。1. 为什么是这套技术栈方案选型的底层逻辑很多同学拿到项目先问能不能直接开写我的建议是先把技术栈选型的理由想清楚。这套系统的选型并不是随便拼的每层都有明确的现实考量。1.1 前后端分离架构下的职责边界传统 JSP 时代的做法是页面和后端代码写在一起改样式和加字段都挤在一个项目里后期维护成本极高。动物领养平台这种项目前台要展示宠物卡片、做搜索筛选后台要管理大量表单和数据列表交互已经远超 JSP 模板引擎能舒服处理的范围。所以这套系统采用标准的前后端分离模式Vue3 负责路由、页面渲染、数据交互SpringBoot2 只提供 RESTful JSON 接口二者通过 axios 收发数据。分工一旦明确开发效率提升非常明显。后端可以只专注于业务逻辑和数据库操作按接口文档一个个完成接口前端可以同时做页面用 Mock 数据先跑通界面。我实际开发时是先画接口列表再同步开工差不多三天出第一版可演示页面。前后端分离还带来一个好处以后如果想加一个小程序端或者 H5 端后端接口完全不用改直接复用。1.2 SpringBoot2、Vue3、MyBatis-Plus 的组合优势技术选型上有个常见误区总觉得版本越新越好。这套系统选 SpringBoot2 而不是 SpringBoot3核心原因是生态兼容和教育场景的现实需求。SpringBoot2 对 JDK8 的支持非常成熟绝大多数高校机房、公司遗留系统、云服务器环境还停留在 JDK8如果用 SpringBoot3 强制要求 JDK17光环境装好就要劝退一半人。SpringBoot2 本身就是业界覆盖面最广的版本遇到问题百度一下基本都是这个版本的解决方案开发成本和学习成本都更低。Vue3 相比 Vue2 最大的变化是组合式 API 和 script setup 语法一个组件内的数据、方法、生命周期可以集中写在一起逻辑复用从 mixin 变成 composable functions代码更简洁。配合 Element Plus 组件库做后台管理的表单、表格、弹窗页面几乎是搭积木一周时间完全够把管理端页面全部搓出来。选 Vue3 而不选 Vue2 是因为 Vue3 已经是绝对的默认选择生态组件已经成熟没必要在新的学习项目里用老框架。再单独说 MyBatis-Plus。这个项目里大量操作是单表 CRUD比如用户信息修改、公告增删改、分类管理MyBatis-Plus 提供 BaseMapper 的通用方法不需要写 XML mapper一个继承就能拿到 insert/update/delete/selectPage 全套能力。配合 LambdaQueryWrapper 可以写类型安全的条件构造器动态拼接 where 条件不再需要反复拼接 SQL 字符串。它不排斥手写 SQL复杂联表查询还是在 XML 里写二者可以共存。这套体系的实践经验是不要让 MyBatis-Plus 做超出它能力范围的事单表交给它多表联查自己写既快又稳。1.3 MySQL8.0 相比旧版本的几个关键变化数据库选的 MySQL8.0而不是停留在 MySQL5.7。主要有几个理由MySQL8.0 默认字符集是 utf8mb4能存 emoji 表情和生僻字对于用户昵称、宠物描述这类文本非常重要5.7 版本安装时如果忘记设字符集很容易出现中文乱码MySQL8.0 自带窗口函数 ROW_NUMBER()、RANK() 等做数据统计和排行榜很顺手JSON 类型变得更强大如果以后想在动物表直接存一个疫苗记录的 JSON 字段扩展非常方便。但升级到 8.0 也伴随适配代价最典型的是默认认证插件从 mysql_native_password 变成了 caching_sha2_password老版本的 JDBC 驱动无法连接必须使用 mysql-connector-j 8.x同时在 JDBC URL 里加上 allowPublicKeyRetrievaltrue 和 useSSLfalse。另一个是时区问题MySQL8.0 对时间处理更严格JDBC URL 少了 serverTimezone 参数就直接报错。这些问题不是绝症但确实是每个迁移到 8.0 的人都会踩到的坎后面避坑章节我再展开。2. 数据库设计与 MyBatis-Plus 落地数据库设计是这套系统里最值得讲的部分。表面上看就是几张表但表之间怎么关联、状态怎么流转、字段怎么设计能兼顾查询效率和扩展性直接决定了后面写的代码是行云流水还是到处打补丁。2.1 领养平台的表结构设计一个完整的动物领养平台核心表至少包含这几张用户表、动物分类表、动物信息表、领养申请表、公告表。我实际建表的思路是先画流程再建表普通用户注册登录浏览动物 - 提交领养申请 - 管理员审核申请 - 审核通过后动物标记为已领养。每一步对应一个表或一个状态字段。用户表设计要点是区分角色一般用 role 字段值可以是 ADMIN 或 USER不用单独建权限表因为系统的角色只有两种权限控制做细粒度以后再说。密码字段存的是 BCrypt 加密后的密文千万不要存明文哪怕只是练手项目这也是底线要求。动物信息表是核心表字段包括分类 id、品种、年龄、性别、是否绝育、疫苗状态、描述、封面图 URL。注意年龄字段我建议用整数存月份而不是存3岁这种字符串这样后续做按年龄筛选时才能用数据库排序展示时再格式化成人类可读的文字比如8个月。领养申请表是整个系统的业务枢纽它把用户和动物关联起来。字段有 animal_id、user_id、申请理由、居住情况、联系地址、状态。状态只取 0 待审核、1 已通过、2 已驳回干净清晰。不需要在申请表里冗余宠物名称和用户昵称后续列表查询时写一条 join 就能查出来如果为了性能想冗余也不是不行但要注意冗余字段更新的同步成本。公告表比较简单标题、内容、类型、发布时间用于前台首页展示领养须知和平台公告。2.2 通用 CRUD 服务的封装思路有了表结构后端代码可以顺着 MyBatis-Plus 的套路瞬间写出大部分接口。每个实体对应一个 Mapper 接口集成 BaseMapper 后单表增删改查方法直接可用。Service 层继承 ServiceImpl像这样public interface AnimalService extends IServiceAnimal { } RequiredArgsConstructor Service public class AnimalServiceImpl extends ServiceImplAnimalMapper, Animal implements AnimalService { }这样做的直接收益是AnimalController 里想新增一个动物直接调用 animalService.save(animal)想删除animalService.removeById(id)想分页查询animalService.page(page, wrapper)。这些通用方法已经带了基础的逻辑处理和参数校验不用自己造轮子。更重要的是条件查询。领养平台首页和列表页有大量组合筛选按分类、按状态、按关键词搜索品种。这种动态 SQL 如果用 XML 手写每个接口都要写一长串 where 拼接的if判断。用 MyBatis-Plus 的 LambdaQueryWrapper 写起来舒服很多PageAnimal page animalService.page( new Page(current, size), new LambdaQueryWrapperAnimal() .eq(StringUtils.hasText(category), Animal::getCategoryId, category) .like(StringUtils.hasText(keyword), Animal::getName, keyword) .in(ObjectUtil.isNotEmpty(ageRange), Animal::getAgeMonth, ageList) .eq(Animal::getStatus, 1) .orderByDesc(Animal::getCreateTime));注意这里筛选条件是可领养状态status 等于 1避免把已领养和已下架的小动物展示给用户。LambdaQueryWrapper 的好处是直接引用实体类方法比 QueryWrapper 传字符串category_id更安全后期字段重命名时编译器能帮你发现错误。2.3 逻辑删除、自动填充与分页的实战配置三个配置是 MyBatis-Plus 使用中必须掌握的。第一个是逻辑删除。动物下架和删除是两回事管理员误删一条动物不应该真的从数据库抹掉而是通过逻辑删除标记。在实体类的 deleted 字段上加 TableLogic 注解之后所有 select 和 update 会自动追加AND deleted 0删除操作自动变成 update。配置完成后开发时要时刻记住逻辑删除对唯一索引有影响比如用户表设置了唯一索引 uk_username (username)用户注销后被逻辑删除的记录还在表里再次注册同名用户会报唯一键冲突解决方法是把 deleted 字段加进联合唯一索引比如 UNIQUE KEY uk_username_deleted (username, deleted)后面的避坑章节我再详细说。第二个是自动填充。create_time、update_time 这类字段每个表都要有每个 insert 和 update 都要维护。不要每处手动 set而是实现 MetaObjectHandlerComponent public class MybatisPlusMetaHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } }对应的实体字段上要加TableField(fill FieldFill.INSERT)和fill FieldFill.INSERT_UPDATE注解两个配置缺一不可只写了 MetaObjectHandler 而忘记字段注解填充一样不生效。第三个是分页插件。不要自己用 limit 手写分页MyBatis-Plus 的 PaginationInnerInterceptor 能自动生成 count 语句和 limit 语句Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination new PaginationInnerInterceptor(DbType.MYSQL); pagination.setOverflow(false); pagination.setMaxLimit(100L); interceptor.addInnerInterceptor(pagination); return interceptor; } }设置 MaxLimit 可以防止恶意请求把页码传到非常大导致数据库压力变大这是一个被很多人忽略的小细节。我在实际项目里把它设成 100配合前端的固定分页大小既安全又省事。3. Vue3 前端工程与核心模块实现前端部分是我觉得这套系统最有观赏价值的部分。宠物领养平台对视觉和交互的要求天然比普通管理系统高做出来的页面既有美观度又有业务复杂度Vue3 在这个场景的优势非常明显。3.1 Vite 工程化与 Axios 响应拦截项目初始化直接用官方脚手架npm create vue3选上 Vue Router、Pinia、ESLint、Prettier 这几个预设生成的目录结构很干净。Vite 的开发服务器启动速度比 Webpack 快了一个量级几乎是秒开热更新响应也快这对开发体验的提升是实打实的。网络请求层我用 axios 封装了一层统一实例。不要在每个页面里散落 axios.get必须统一拦截请求和响应。我的封装思路是三个部分baseURL 统一用/api前缀生产环境由 Nginx 转给后端request 拦截器自动往 header 里塞 Tokenresponse 拦截器统一处理后端返回的数据结构。后端约定所有接口返回格式是{ code: 200, data: ..., msg: ... }前端在拦截器里判断 code 不等于 200 时弹出 ElMessage 提示并 reject这样业务代码里完全不需要关心提示逻辑只需拿到干净的 data。开发环境的跨域问题靠 Vite 的 server.proxy 解决而不是在后端代码里随便开跨域server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, } } }这样前端请求/api/animal/list会被开发服务器转发到http://localhost:8080/api/animal/list页面里看不到任何跨域报错。生产环境则交给 Nginx保持同样的路径规则全链路一致。3.2 领养流程的前后端闭环从用户视角来看整个流程是浏览首页和宠物列表 - 点击某个宠物进入详情页 - 提交领养申请 - 在我的申请里看到进度 - 管理员审核后看到结果。这个流程每一环都是在操作具体的业务状态而不只是展示静态页面。首页实现上宠物列表使用卡片式布局封面图、品种名称、年龄、绝育状态几个关键字段显示在卡片上。列表页的筛选条件通过 URL query 管理比如分类变化时 router.push 新参数组件内根据 route.query 发起请求这样用户刷新页面后筛选条件还在回退按钮也能正常工作。详情页除了展示动物的完整信息还有当前状态标签可领养、已领养、审核中、已下架状态不同领养按钮要么可点击要么置灰并显示原因。用户提交申请时前端要做两步校验第一步判断是否登录没登录就跳转登录页并带上 redirect 参数登录后自动跳回来第二步判断是否已经申请过这只宠物这个校验后端接口里也要做。我当时的做法是后端在申请表上建联合唯一索引 uk_apply (animal_id, user_id)重复申请直接抛异常返回您已经提交过申请比先查后插的代码判断更可靠。用户的我的申请列表用 tabs 切换待审核、已通过、已驳回三类记录每条记录显示宠物封面图、申请时间、处理时间、驳回原因。这里前端要特别注意驳回原因字段可能为空取数据时要给默认值不然页面上会出现 undefined 这种难看的字符。3.3 后台管理中的权限控制与动态菜单后台管理的权限控制核心是两件事能不能进这个页面、能不能调用这个接口。前端做的是页面控制后端做的是接口控制两者缺一不可。前端权限控制在路由层实现。路由需要拆成两部分公共路由包含登录页、注册页、首页和列表页需要登录后才能访问的路由放在一个专门的数组中通过路由守卫统一拦截。管理员专属页面更严格在路由 meta 上标注 role: ADMIN守卫里判断当前用户角色不匹配就跳 403 页面。router.beforeEach((to) { const token localStorage.getItem(token) if (to.path ! /login !token) { return { path: /login, query: { redirect: to.fullPath } } } if (to.meta.role to.meta.role ! userStore.userInfo?.role) { return /403 } })登录成功后端返回 token 和用户信息包含角色 role 字段前端把用户信息存到 Pinia 里。菜单结构我用 el-menu 的 :default-active 属性绑定当前路由路径实现和路由的联动高亮。主菜单包括动物管理、领养审核、用户管理、公告管理、数据统计。普通用户登录后只显示我的申请和个人中心所以菜单数据是根据角色动态生成的数组而不是全部写死在前端模板里。这样以后加一个志愿者角色只需要改菜单配置和路由 meta不用动模板。后台的表格页全部使用 el-table 加 el-pagination 的分页结构查询条件区和操作区分离。审核领养申请页是后台最核心的页面列表展示申请时间、用户、宠物、联系电话、申请理由操作列有两个按钮通过和驳回驳回时弹窗填写驳回原因。这个界面交互非常简单但背后的状态变更逻辑要处理到位具体在第 5 章的 5.4 节讲。4. 文件存储、环境配置与部署上线一个系统写完能本地跑起来只是第一步真正要在服务器上用起来涉及图片存哪里、数据库怎么初始化、前端后端怎么部署三个问题。这一章我按实际部署的完整顺序讲。4.1 宠物图片上传的取舍宠物领养平台没有图片等于没有灵魂图片上传是必须的功能。上传方案有两条主流路线本地磁盘存储和对象存储。对象存储的优势是容量弹性大、访问延迟低但对这个项目来说又增加了一个云服务依赖还要处理鉴权和费用问题。所以我的建议是本地磁盘存储就好只需要特别处理几件事。后台上传接口接收 MultipartFile把文件保存到服务器上一个独立目录用 UUID 重命名文件避免同名覆盖同时保留原始文件后缀用于识别图片类型。文件名不要用时间戳加简单随机数并发上传可能撞名。保存时要记录文件相对路径到数据库比如/upload/2025/07/abc.jpeg而不是完整 URLhttp://xxx.com/upload/...后者一旦服务器 IP 或域名变了库里存的数据全部作废。SpringBoot 默认不会直接对外暴露磁盘文件需要配置资源映射Configuration public class WebMvcConfig implements WebMvcConfigurer { Value(${upload.path}) private String uploadPath; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadPath); } }注意 addResourceLocations 必须写成file:前缀并且 uploadPath 末尾要带/比如/home/animal/upload/。如果不带末尾斜杠路径拼接会错位。这个细节我见过太多人踩了。4.2 MySQL8.0 环境准备与初始化本地开发时我推荐用 Docker 装 MySQL8.0比 Windows 直接安装的姿势干净得多卸载也方便。一条命令就能拉起来docker run -d --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD123456 \ -e TZAsia/Shanghai \ -v /opt/mysql/data:/var/lib/mysql \ mysql:8.0启动后等十几秒容器健康检查过了再导入 SQL 文件。导入命令用 docker exec 配合重定向docker exec -i mysql8 mysql -uroot -p123456 /opt/animal.sql这里要注意三点。第一初始化的 MYSQL_ROOT_PASSWORD 环境变量只在容器首次创建数据目录时生效如果之前用别的密码跑过容器想改密码需要清理挂载的 data 卷删掉容器重新建或者进容器手动改。第二启动参数里的 TZAsia/Shanghai 可以直接把 MySQL 的时区设为东八区从根源上规避 JDBC 时区问题比在 URL 里加 serverTimezone 更稳妥。第三导入 SQL 后立即检查默认字符集执行SHOW VARIABLES LIKE character_set_%;确认 character_set_server 是 utf8mb4再插入一条中文测试数据验证编码。数据库连接 URL 我最终用的是这一串jdbc:mysql://localhost:3306/animal?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue很多教程里漏掉了 allowPublicKeyRetrieval导致 MySQL8.0 连接时报 Public Key Retrieval is not allowed。这个参数配合 caching_sha2_password 认证插件是必须的记住别落。4.3 前后端打包与 Nginx 部署部署到服务器的完整链路可以概括为后端打成 jar 包用 nohup 跑起来前端 npm run build 生成 dist 目录扔给 NginxNginx 负责托管静态页面并把/api请求转发给后端。前后端在同服务器的部署方式下完全不需要后端开启 CORS由于同源了跨域问题因此不复存在。后端打包命令mvn clean package -DskipTests打出来的 jar 放在 target 目录传到服务器后用 nohup 启动nohup java -jar animal-admin.jar --server.port8080 app.log 21 启动日志一定要看SpringBoot 启动成功后控制台会打印 Tomcat started on port 8080。此时用 curl 测试一个接口比如curl -X POST http://localhost:8080/api/auth/login -H Content-Type: application/json -d {username:admin,password:123456}能返回 JSON 说明后端 OK。前端打包前需要确认 .env.production 文件里的 VITE_API_BASE_URL 是/api这样请求路径在当前域名下给 Nginx 转发留接口。dist 目录拷到服务器的/var/www/html后Nginx 配置如下server { listen 80; server_name animal.example.com; root /var/www/html; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /upload/ { alias /home/animal/upload/; } location / { try_files $uri $uri/ /index.html; } }location / 里的 try_files 是 SPA 框架刷新不 404 的关键。Vue Router 用 history 模式时前端路由像/pet/12在后端文件系统里是不存在的nginx 不配 try_files刷新直接 404。配了try_files $uri $uri/ /index.htmlnginx 找不到物理文件时会把请求重写到 index.html由前端路由接管页面就能正常显示。5. 常见问题排查与避坑实录这一章是我最想分享的因为这套系统里几乎所有的时间都耗费在几个看起来很小的问题上。我按技术类别整理成坑位清单每一条都是实际运行中遇到过的有的坑甚至花了一天才定位。5.1 MySQL8.0 连接认证与时区引得坑先说连接认证。MySQL8.0 默认认证插件是 caching_sha2_password 而不是 5.7 时代的 mysql_native_password。这条变化带来的直接后果是如果你的 pom 文件里还写着 mysql-connector-java 5.1.x连接时大概率报Unable to load authentication plugin caching_sha2_password。解决方法是把驱动换成 MySQL 官方 8.x 版本坐标是com.mysql:mysql-connector-jSpringBoot2 的依赖管理已经适配了 8.x 版本直接依赖即可。然后说时区。MySQL8.0 对时区处理更严格JDBC 连接如果没指定 serverTimezone会出现The server time zone value Öйú±ê׼ʱ¼ä is unrecognized这一长串乱码其实是 GBK 编码的中国标准时间。解决办法是 URL 加上serverTimezoneAsia/Shanghai或者在数据库执行SET GLOBAL time_zone 8:00我强烈建议双管齐下数据库层面用 Docker 启动参数-e TZAsia/ShanghaiJDBC 层面加 serverTimezone。第三个相关问题是安全校验参数。URL 中useSSLfalse和allowPublicKeyRetrievaltrue几乎也是一种必需项。MySQL8.0 默认协议需要公钥交换客户端首次连接时请求服务器公钥被禁止就会出现 Public Key Retrieval is not allowed。这两个参数是社区验证过的最稳组合。5.2 MyBatis-Plus 隐藏的坑MyBatis-Plus 确实省了不少代码但它的隐藏设定也有不少。我遇到的第一个坑是逻辑删除与唯一索引的冲突。给用户表加了 username 唯一索引、同时加了逻辑删除字段 deleted这会导致一个用户被逻辑删除后再次用同一个用户名注册时数据库仍能查到那条 deleted1 的记录唯一索引被占用注册直接报 DuplicateEntry。解决方式在前面提过把 deleted 字段加进联合唯一索引。这个坑在 MySQL8.0 下表现尤其明显因为 8.0 的 Unique Key 默认不区分 NULL 值处理起来要小心。第二个坑是自动填充不生效。新手经常写了 MetaObjectHandler 但忘了在实体字段上加TableField(fill FieldFill.INSERT)或者 fill 的值写错导致 create_time 一直是 NULL。调试这类问题有两个手段开启 MyBatis-Plus 的 SQL 日志输出配置logging.level.com.example.mapperdebug看 insert 语句里有没有 create_time另一种是直接在 MetaObjectHandler 内打断点确认填充方法有没有被调用。我实测的话90% 的情况是字段注解没写。第三个坑是 LambdaQueryWrapper 报字段不存在。实体类里的驼峰属性名和数据库下划线列名映射失败时查询会报 Unknown column。默认配置下 MyBatis-Plus 开启了驼峰命名自动转换createTime会自动转成create_time但如果数据库列名不规范比如叫create_time_new自动转换就失效了要么给实体字段加 TableField(create_time_new)要么规范数据库列名。团队协作时统一命名规范比什么都重要。5.3 前端跨域与 SPA 刷新 404跨域问题在不同阶段长相不一样。开发阶段最常见的报错是 CORS policy: No Access-Control-Allow-Origin header于是有人在后端加一个全局 CORS 过滤器把*放开。但开发时如果你已经用了 Vite proxy后端再开 CORS就会造成请求被代理转发后 Origin 被双重处理反而产生奇怪的问题。正确姿势是开发环境只用 Vite proxy后端不需要也不能开 CORS或者只允许本地前端地址生产环境同源部署后端更不需要 CORS。我的经验是这个项目里后端根本没有写任何 CORS 配置全部交给代理层解决干净利落。SPA 刷新 404 的问题同样高频。Nginx 配置里 location / 一定要有try_files $uri $uri/ /index.html;很多从 Vue2 时代过来的人习惯用 hash 路由规避这个问题但 hash 路由的 URL 带 # 很不美观分享链接地址带一长串 hash。用 history 路由加正确 Nginx 配置才是正规方案。配好以后要实际刷新多级路径测试比如刷新 /animal/list 和 /admin/apply/detail/1确认都回到 index.html。5.4 领养审核业务上容易遗漏的点领养审核看起来简单实际写代码时有个很容易遗漏的操作顺序问题。管理员点击通过按钮时前端只发一个请求但这个请求背后要完成三件事把这条申请的状态从待审核改成已通过、把对应动物状态改成已领养、把这条动物的其他待审核申请全部标记为已驳回。这三步必须放在同一个数据库事务里否则可能出现申请状态是已通过、动物却还是可领养的情况其他用户还能继续申请业务逻辑就直接崩了。Transactional(rollbackFor Exception.class) public void approveApply(Long applyId) { AdoptionApply apply applyService.getById(applyId); Assert.notNull(apply, 申请不存在); Assert.state(apply.getStatus() 0, 申请已处理); applyService.update( new LambdaUpdateWrapperAdoptionApply() .eq(AdoptionApply::getId, applyId) .set(AdoptionApply::getStatus, 1) .set(AdoptionApply::getHandleTime, LocalDateTime.now())); animalService.update( new LambdaUpdateWrapperAnimal() .eq(Animal::getId, apply.getAnimalId()) .eq(Animal::getStatus, 1) .set(Animal::getStatus, 2)); applyService.update( new LambdaUpdateWrapperAdoptionApply() .eq(AdoptionApply::getAnimalId, apply.getAnimalId()) .eq(AdoptionApply::getStatus, 0) .set(AdoptionApply::getStatus, 2) .set(AdoptionApply::getRejectReason, 该宠物已被领养)); }这里还有一个高并发隐藏问题同一只宠物同时有两个管理员在审核或者两个用户同一秒提交申请单纯的事务并不能完全防止一猫多主。更稳妥的做法是在第三步更新动物状态时WHERE 条件里带上AND status 1然后检查 update 的返回值如果影响行数为 0说明动物已经被人领走了马上抛异常。这种基于条件更新的乐观方案比先查后改成胜率更高。还有一点事务方法要放在独立的 Service 类里不能自调用否则 Transactional 失效——这种基础问题看似低级实际操作中我身边的人踩过不止一次。整套系统从选型到部署跑一趟下来最大的体会就是项目能跑通不是靠某个炫技的框架能力而是靠每一步都选最稳的方案、把边界条件处理干净。SpringBoot2 加 Vue3 这个组合虽然不是最新潮的但它在 JDK8 环境下的兼容性、资料的丰富程度、遇到问题能搜到解决方案的确定性对学习和练手来说都是最优的。我在开发过程中踩过 MySQL8.0 认证插件和时区的连环坑也调试过一台机器上 CORS 双重处理的诡异表现把这些整理成文希望你能一次避过。这套系统的后续扩展空间其实很大。现有接口完全可以直接复用新增一个消息通知表审核结果产生时给用户发邮件或短信通知用 ECharts 在后台管理页做领养趋势统计、动物分类占比可视化甚至把前台页面基于 uni-app 改写一遍直接打包成微信小程序编码量不大但有极好的传播场景。最后分享一个小技巧项目里的 doc 目录一定要留一份带测试数据的 SQL 脚本我每次在新环境部署都会用到它——有测试数据才能快速确认界面渲染、分页、筛选都正常否则面对一张空表根本看不出问题在哪。
返回列表