
去年有个学弟要准备毕业设计找我聊选题说不想做那种烂大街的学生管理系统图书馆管理系统。我反问他你身边有没有什么真实的需求是大家一直在抱怨、但一直没人认真解决的他想了想说他们学校有个流浪动物救助组织至今靠一个共享表格管理流浪猫狗信息——照片挤在聊天记录里领养状态靠肉眼跟踪疫苗和绝育记录散落各处想统计这个月救助了几只、领养出去几只根本没法快速查。于是我们花了大概两周时间从零梳理业务做了一套基于 SpringBoot Vue 的宠物爱心组织管理系统。技术栈很常见后端 SpringBoot MyBatis-Plus MySQL前端 Vue Element Plus前后端分离。但真正让我觉得这个项目值得写一篇文章说说的不是用了什么高深框架而是业务建模这个环节——尤其是一张领养审核表背后的状态流转设计以及从救助登记到回访记录的整体闭环。这篇文章不是照着源码逐行讲解而是把设计思路、表结构、前后端关键实现、启动运行的步骤、以及答辩时容易被追问的点全部串一遍。无论你是要拿它做毕设、课设还是单纯想入门 Java 全栈项目都可以把它当一份第二份说明书来看。1. 为什么需要这样一套系统从校园流浪动物救助场景说起1.1 纸质登记和共享表格撑不住的高频痛点先别急着聊代码先聊业务。宠物爱心组织的日常其实远没有外人想的那么有爱就行它是一堆琐碎事务的集合。一只流浪猫被发现在教学楼后面谁来登记谁负责带去体检疫苗打没打绝育做了没有暂时寄养在哪个志愿者家里同时有三位领养人在咨询怎么判断先给谁看猫领养之后说好了三个月定期回访回访记录放哪我见过不少组织前期用共享表格填的人一多就会出现三种情况第一同一个宠物被登记了两遍因为不同人不同时间上报第二状态信息更新不及时表格里显示待领养实际早就被领走第三数据统计基本靠猜年度总结想写成功领养率完全导不出准确数字。这些痛点的本质是数据分散、状态割裂、没有角色区分。管理系统的第一个任务就是把这堆散落的信息收拢到一个地方让每只动物都有一个档案。1.2 系统的业务闭环从发现流浪宠物到回访记录我设计这套系统时没有一上来就列功能清单而是先画业务闭环。大家记住一个原则管理系统的核心是流程不是页面。页面只是流程的外壳。这个系统的完整闭环是发现流浪动物志愿者在系统中登记救助信息包含发现地点、照片、初步健康状况。宠物进入收容环节录入体检、疫苗、驱虫、绝育等健康档案分配寄养人。确认宠物状态稳定后在平台上上架进入待领养列表。领养人浏览宠物信息提交领养申请。管理员或工作人员审核申请安排线下看猫/回访。回访通过后签署领养协议宠物状态变为已领养。领养后按约定进行定期回访填写回访记录。围绕这条闭环系统的功能模块就自然浮现了用户管理、宠物救助登记、收容与健康档案管理、领养申请与审核、回访记录、公告资讯、数据统计。模块不是拍脑袋想出来的是业务倒推出来的。这也是答辩时老师最爱问的问题之一——为什么你的系统需要这些模块答案就藏在闭环里。2. 技术选型逻辑SpringBootVueMySQL的取舍2.1 为什么说SpringBoot是当前Java毕设/课设的核心选择先聊后端。现在很多教程还在教 SSMSpring SpringMVC MyBatis手写 XML 配置但实际开发中SpringBoot 几乎是事实标准了。它最核心的价值是自动配置内嵌 Tomcat不需要单独部署容器依赖直接通过 starter 引入配置集中在 application.yml 文件里改完不用重启调试半天。对学生来说SpringBoot 还有一个隐性好处社区资料极其丰富遇到异常答案一搜一大把。而且答辩时你不能只知道用得能讲清楚原理。最常被问的SpringBoot 为什么能自动配置核心就一句话启动类上的SpringBootApplication里包含EnableAutoConfiguration它会根据 classpath 下的依赖自动装配对应的 Bean配合Conditional条件注解实现按需加载。把这句话理解透比背十篇框架教程都管用。2.2 前后端分离的工程化收益与Vue的选择后面前端选 Vue核心考虑是渐进式和组件化。Vue 不像某些重型框架那样要求你上来就学一整套它可以从简单的{{ data }}插值开始也可以拿 Vite 搭完整工程。配合 Element Plus 这类组件库后台管理系统的页面搭建速度非常快——表格、表单、弹窗、分页都是现成的把重点放在业务逻辑而不是样式上。前后端分离带来的工程化收益也很明显。前端工程和后端工程是两个独立项目接口联调通过 HTTP 完成。好处是职责清晰后端只提供 API不关心页面长什么样前端只管渲染和交互不关心数据从哪来。对于毕设展示来说这种结构也更容易讲清楚你可以说我做了两个工程一个负责数据服务一个负责用户界面比传统的 JSP 页面有说服力多了。2.3 MySQL为什么够用以及哪些技术没必要硬上数据存储我选了 MySQL没有悬念。这个系统的数据结构高度结构化宠物信息、用户信息、申请记录都是典型的二维表用关系型数据库管理最自然。MySQL 免费、大学课程全覆盖、Navicat 可视化工具导入导出顺手加上 MyBatis-Plus 之后单表增删改查基本不用手写 SQL开发效率提升非常明显。这里我要多说一句克制选型的重要性。有些同学喜欢给项目硬加 Redis、ES、消息队列美其名曰技术亮点。但实际效果往往是Redis 只用来存了一个 token消息队列一个没发出去反而在答辩现场被老师两句就问穿了。选型逻辑应该是当前规模下够用且我能自圆其说。MySQL 就是对这个系统而言最合理的答案。如果真想往上加我会在最后一部分给大家列扩展路线。3. 数据库设计与业务状态流转这个系统的心脏3.1 角色权限体系与功能矩阵权限体系我设计了三种角色管理员、工作人员志愿者、普通用户领养人。为什么不是管理员用户两种因为实际业务里登记救助信息、录入健康档案、审核领养申请这些操作通常由多个志愿者协同完成如果只有单一管理员角色功能边界会很模糊代码里也容易出现一个接口所有人能调的情况。角色和功能的关系可以用一张功能矩阵表说清楚功能模块管理员工作人员普通用户用户管理全部权限查看无宠物救助登记新增/编辑/删除新增/编辑无健康档案录入新增/编辑新增/编辑查看领养申请审核/驳回审核/回访提交/取消回访记录查看/导出新增/编辑无公告资讯发布/编辑发布查看数据统计全量查看按权限查看无这张表在需求文档里一放整个系统的边界就清晰了。代码层面通过在后端接口做权限拦截配合前端菜单栏控制两层面双保险。3.2 核心表结构和字段设计要点来看表设计。重点是以下八张表sys_user用户表pet_info宠物信息表adopt_apply领养申请表health_record健康档案表foster_record寄养记录表follow_up_record回访记录表notice公告表apply_log审核日志表。pet_info表是核心中的核心我用它举例说明字段设计思路CREATE TABLE pet_info ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 宠物ID, pet_name VARCHAR(50) DEFAULT 待取名 COMMENT 宠物名称, pet_type TINYINT COMMENT 类型1-猫2-狗3-其他, breed VARCHAR(50) COMMENT 品种, age_month INT COMMENT 年龄按月计算, gender TINYINT COMMENT 性别1-公0-母, is_neutered TINYINT DEFAULT 0 COMMENT 是否绝育0-否1-是, vaccine_status VARCHAR(255) COMMENT 疫苗记录JSON或逗号分隔, health_desc TEXT COMMENT 健康描述, discover_location VARCHAR(255) COMMENT 发现地点, status TINYINT DEFAULT 0 COMMENT 状态0-待收容1-收容中2-待领养3-已领养4-已离世, cover_image VARCHAR(255) COMMENT 封面图URL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT宠物信息表;两个设计细节值得展开。第一个是status字段不要用字符串描述而用数字枚举理由后面状态机部分会说。第二个是vaccine_status这种一组选项的字段可以用逗号分隔存储比如 1,3,5 代表三针疫苗已打方便前端回显也方便统计如果未来要做复杂条件筛选再拆子表也不迟。3.3 领养申请的状态机设计六个状态与流转规则我前面说一张领养审核表背后的状态流转是重点现在细讲。很多新手写领养模块就在表里放一个status字段然后前端用一个字段存字符串比如待审核已通过。看着没问题一旦逻辑变复杂就会乱套。我给adopt_apply表设计的状态枚举是这样一组值状态值含义允许流转到0待审核1、51初审通过待线下回访2、52回访通过待领养3、53已领养44回访完成流程终结—5已拒绝/已取消流程终结—这套状态机的逻辑在代码里就是一张switch或者映射表核心约束是状态只能按照规则跳转不能从待审核直接变成已领养。为什么强调这个因为在真实业务里跳过了线下回访就直接放猫出去是对宠物不负责的表现在系统层面状态跳跃也会导致数据统计口径混乱比如回访通过率没法算。3.4 为什么还要一张apply_log审核日志表另一个容易被忽略但很加分的细节是审核日志表。每次领养申请发生状态变化都往apply_log里插一条记录内容是申请ID、操作人ID、变更前状态、变更后状态、操作时间、备注说明。它有两个作用。第一是可追溯性真出了问题能查出来是谁在什么时候把申请从待审核改成了已拒绝备注那里还能写清拒绝理由。第二是答辩素材老师问你的系统如何保证操作可追溯你直接把这个表的设计逻辑讲出来比现场编有说服力得多。实现上也很简单就是在状态变更方法上顺手insert一下不需要额外引入审计框架。4. 后端核心实现接口设计、鉴权、文件上传与业务逻辑4.1 统一响应体与全局异常处理不管哪个后端接口返回给前端的数据格式最好统一。我自己习惯用一个Result类结构就三个字段code、message、data。public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.code 200; r.message success; r.data data; return r; } public static T ResultT error(Integer code, String message) { ResultT r new Result(); r.code code; r.message message; return r; } }配合一个RestControllerAdvice全局异常处理器把业务异常、参数校验异常、未登录异常统一转成这个格式。前端代码就可以非常简洁后端永远返回相同结构前端只需要判断code是否等于 200就能拿到data。省掉了每个接口单独写错误判断的重复代码。4.2 三种角色共存的接口鉴权方案JWT加拦截器接口鉴权我推荐用 JWTJSON Web Token配合 Spring 拦截器来实现不推荐传统 Session。原因有两个第一前后端分离后前端工程和后端工程可能部署在不同的端口甚至不同机器上Session 的会话状态不好处理第二JWT 是无状态的后端不用在服务器上存会话扩展性更好。我的实现思路分三步登录成功后后端用JwtUtil生成 token 返回给前端。前端把 token 存在localStorage每次请求在请求头里加上Authorization: Bearer token。后端写一个AuthInterceptor拦截所有/api/**请求解析 token 并取出当前用户信息放进ThreadLocal中供 Controller 层随时取用。核心拦截器伪代码如下public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { token token.substring(7); Long userId JwtUtil.parseToken(token); if (userId ! null) { UserContext.set(userId); return true; } } response.setStatus(401); return false; } }角色权限这块我在接口上用RequireRole(ADMIN)这样的自定义注解配合拦截器统一校验比在每个方法里写一堆if (当前用户角色不是管理员) return error优雅得多。具体实现就是在拦截器里根据注解要求从数据库查角色对比当前用户权限。这套逻辑不复杂但能让答辩老师一眼看到你理解了鉴权是分层设计这件事。4.3 领养审核的业务代码事务与状态不分离领养审核是最容易写错业务逻辑的地方我拿一个核心方法adoptApplySuccess举例它要做两件事把申请状态改成已领养同时把对应宠物的状态改成已领养。这两步必须在一个事务里如果申请状态改了宠物状态没改数据就不一致了。Transactional(rollbackFor Exception.class) public void adoptApplySuccess(Long applyId) { AdoptApply apply adoptApplyMapper.selectById(applyId); if (apply null || apply.getStatus() ! 2) { throw new BusinessException(当前申请状态不允许领养); } // 更新申请状态回访通过 - 已领养 apply.setStatus(3); adoptApplyMapper.updateById(apply); // 更新宠物状态待领养 - 已领养 PetInfo pet petInfoMapper.selectById(apply.getPetId()); pet.setStatus(3); petInfoMapper.updateById(pet); // 写入审核日志 applyLogMapper.insert(LogBuilder.build(applyId, 2, 3, 领养成功)); }注意那段状态校验它是整个方法的关键防线。比如apply.getStatus() ! 2意味着只有回访通过待领养的申请才能进入领养环节。这道校验配合数据库设计里那张表状态枚举能挡住绝大多数乱调接口产生的问题。这就是我前面强调状态机设计的原因——它不止是文档上的理论最终会落到每一处数据变更的入口上。4.4 宠物照片上传本地存储与静态资源映射宠物照片是这个系统里让人头疼又绕不开的部分。救助登记要传图、健康档案可能也要配图、封面图还得在列表页展示。我在后端做了一个通用的文件上传接口接收MultipartFile保存到本地磁盘的upload/pet/目录文件名用UUID 原文件后缀避免重名。spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB web: resources: static-locations: classpath:/static/,file:${upload.path}/重点在upload.path这个配置。它指定了一个服务器上的绝对路径同时通过静态资源映射把/upload/**请求映射到实际磁盘目录。这样的话前端拿到上传接口返回的相对路径比如/upload/pet/xxx.jpg直接拼接访问地址就能在浏览器里显示图片。整个过程不需要额外引入 FastDFS 或 OSS对课设项目完全够用不过我会在文章最后说说为什么生产环境要考虑更可靠的方案。5. 前端实现与前后端联调组件拆分、接口封装、边界情况5.1 axios二次封装与token注入前端的核心基础设施是请求封装。直接在每个组件里写axios.get不是不行但一旦要统一处理 token、错误码、401 跳转就会陷入大量重复代码。我的做法是建一个request.js文件对 axios 做二次封装import axios from axios const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器自动携带token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) // 响应拦截器统一处理响应码 request.interceptors.response.use( response { const res response.data if (res.code 200) { return res.data } if (res.code 401) { router.push(/login) return Promise.reject(new Error(未登录)) } ElMessage.error(res.message) return Promise.reject(new Error(res.message)) }, error { ElMessage.error(网络请求异常) return Promise.reject(error) } ) export default request这里有两个容易被忽略的细节。第一baseURL写/api配合开发环境的 Vite 代理把请求转发到后端避开了跨域问题生产环境再通过 Nginx 做一下路径转发。第二响应拦截器里统一处理后端返回的错误信息前端业务代码就不用手动try...catch每个请求了。5.2 页面与组件拆分宠物列表页和领养审核页的实操思路前端最核心的页面是宠物列表和领养审核。宠物列表页我拆成了三个部分筛选栏组件、宠物卡片组件、分页组件。宠物卡片是一个典型的重用组件因为它会在待领养列表我的收藏后台搜索等多个页面出现抽成一个PetCard.vue传一个pet对象进去内部展示照片、名字、状态标签、详细按钮非常省事。领养审核页我推荐用弹窗型设计。列表展示待审核的申请记录点击审核按钮弹出一个抽屉组件内容分三块申请人的基本信息与联系方式、宠物的完整档案含健康记录、以及审核操作区通过/驳回/备注。这里最需要注意的是弹窗组件不要直接修改列表页的源数据。比较稳妥的做法是弹窗确认审核动作后重新请求一次当前列表接口让服务端数据回流而不是在本地偷偷改状态。这一设计能避免很多前后端状态不同步的 bug。5.3 前端路由权限控制先看token再看角色前端路由不打算做成几百行的动态权限体系我用的方案是静态路由 菜单栏的角色过滤。因为系统一共就三种角色后台页面对工作人员和管理员基本都是开放的只有用户管理数据统计这类模块需要管理员权限。具体实现是利用vue-router的全局前置守卫router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token to.path ! /login) { next(/login) return } if (to.meta.roles !to.meta.roles.includes(role)) { next(/403) return } next() })meta.roles在路由定义时写死守卫函数从本地读取当前用户的角色字符串做判断。这套方案好处是直观、容易理解遇到实际需求变化时调整一份路由配置即可不会出现页面按钮藏了但接口还能调的尴尬情况。当然我也在答辩资料里准备了一个补充说明如果需要按后台动态分配菜单权限就得改成从后端获取路由表再做动态注册这个可以作为扩展点回答老师的追问。6. 从源码到答辩环境准备、启动步骤、避坑指南与扩展方向6.1 环境准备与初始化数据库先把环境说清楚。我推荐使用下面这套组合兼容性最稳软件推荐版本备注JDK1.8 或 111.8 最稳妥避免高版本造成的编译兼容问题Maven3.6配合阿里云镜像加速依赖下载MySQL5.7 或 8.05.7 教学环境最多8.0 注意驱动和时区配置Node.js16 或 18过高版本可能导致某些依赖安装报错前端包管理npm 或 pnpm国内建议配 镜像源数据库初始化有两种方式第一种命令行执行mysql -uroot -p pets.sql一次性灌入全部表结构和初始数据第二种用 Navicat 等客户端新建数据库再执行 SQL 文件。注意pets.sql里可能包含创建数据库的语句如果已经手动建库记得先把建库语句去掉或改成USE语句。6.2 后端和前端的启动步骤后端启动相对简单两个命令二选一# 开发模式 mvn spring-boot:run # 打包后运行 mvn clean package -DskipTests java -jar target/pets-admin.jar前端需要分三步# 1. 安装依赖建议加镜像源参数 npm install --registryhttps://registry.npmmirror.com # 2. 启动开发服务 npm run dev # 3. 浏览器访问 # 默认地址通常是 http://localhost:5173运行阶段最容易遇到的问题是 Maven 依赖下载慢、npm install卡住、端口被占用。依赖慢就换镜像源端口被占用就改application.yml里的server.port或 Vite 配置里的port。这些都不算 bug但很影响心态提前知道能省不少时间。6.3 答辩时高频问题与回答方向答辩老师最喜欢问的问题往往是为什么这么做和如果条件变了怎么办。我把高频问题整理成表方便你提前准备问题回答方向为什么用 JWT 而不用 Session前后端分离跨域场景下无状态便于横向扩展服务端不存储会话并发提交两次领养申请怎么办前端按钮 loading 防重复点击后端对同一宠物存在未完结申请做唯一约束图片存在本地生产环境有什么问题磁盘扩容、备份难、多实例共享问题生产建议使用对象存储状态字段为什么用数字而不是字符串节省存储、避免枚举值混乱和拼写错误、便于做统计配合前端口径转换统计报表的 SQL 怎么写的用GROUP BYDATE_FORMAT按年月汇总领养数再通过 ECharts 展示Transactional能否解决所有事务问题自调用失效、异常被捕获不抛出时失效、非公开方法失效其中Transactional 失效场景这个点很多同学会在答辩现场被问到。我建议至少背下三个场景同类内部调用不经过代理对象、方法不是 public 时失效、异常被 catch 吞掉后不会触发回滚。能答出这几条基本就说明你理解事务的本质而不只是写了注解。6.4 项目还可以往哪些方向扩展如果学有余力我会建议在源码基础上做一两个有亮点的扩展而不是推倒重来。第一个方向是缓存优化。宠物列表页的热度很高每次请求都查数据库虽然不是扛不住但可以把热点宠物数据放入 Redis设置五分钟过期时间。这个改动小、思路明确答辩时能展示你对性能问题的敏感度。第二个方向是接入消息通知。比如领养申请审核状态变化后通过邮件或短信通知申请人。后边配置一个简单的异步事件监听器接口一触发就发通知既不影响主流程又显得项目完整度高。第三个方向是微信小程序端。把现有的前端页面改造成小程序服务端接口基本可以直接复用工作量集中在 JS 适配层和登录体系切换上。这属于能吹得很大的扩展方向适合想冲刺高分的同学。6.5 跑起来之后别放过的一个自测脚本最后分享一个我自己的小习惯每跑通一个模块就写一个临时脚本批量造点假数据。比如写完领养审核流程不要只测一条数据而是循环造二十条申请记录把状态分别重置到不同环节再看你的状态机是不是每个入口都校验到位了。我就曾经用这种方式测出一个 bug当宠物状态是已领养时理论上不能再有新申请提交但新增申请接口忘了校验宠物状态导致一只猫同时有两个有效申请。这种问题用肉眼难发现用批量数据一测就暴露。这种自测脚本并不会体现在最终源码里但它能让你对系统的状态流转理解得更深答辩时讲起来也更自信。说到底源码是拿来跑的不是拿来摆着好看的。把一条完整的业务流程从登录走到最后一步回访完成你才算是真正掌握了这套系统。