SpringBoot宠物医院管理系统毕设:技术栈、核心业务与避坑指南
简介面向Java毕设与课程设计人群的宠物医院管理系统基于SpringBootVue实现前后端分离提供可运行源码与SQL脚本覆盖后台管理与前台展示两套入口适合作为毕业设计、课程设计、工程实训或初期项目立项也可直接修改用于二次开发。压缩包共1167个文件约34.89MB其中含211个vue页面组件、127个java后端类、170个xml配置文件以及前端js/css、图片、jar依赖等资源SQL脚本可快速初始化MySQL 5.7数据库开发环境基于JDK1.8、Tomcat7、Maven3.3.9搭建。目前已有1753人学习浏览。项目前后台访问路径已内置环境配置说明清晰便于快速启动体验从表结构设计到接口调用形成完整链路目录结构便于按模块检索遇到运行问题可与博主沟通解决。1. 宠物医院管理系统SpringBoot 毕设源码到手后先搞清楚的三件事如果你正抱着这份「基于 SpringBoot 的宠物医院管理系统」源码包发愁大概率是两种情况要么毕设选题刚定想找个现成项目改一改要么项目下下来了但 IDE 里一直跑不起来被环境问题卡了一整天。这个项目是 Java 方向很典型的业务型毕设前端 Vue、后端 SpringBoot、MySQL 存数据、JWT 做登录业务上覆盖宠物建档、挂号、病历、药品、收费几乎把答辩会问的高频点都占满了。这篇笔记不聊虚的直接按技术栈、业务拆解、跑起来、避坑这个顺序把这份资源从压缩包变成你答辩时能讲清楚的系统。新手按步骤走能复现熟手重点看第 5 章的边界与坑。2. 技术栈与项目骨架SpringBoot 分层、MyBatis-Plus 与 Vue 目录怎么对2.1 技术栈定版为什么这套组合是毕设的稳妥答案这套系统的后端主框架是 SpringBoot持久层用 MyBatis-Plus前端 Vue 配 Element Plus数据库 MySQL 8。为什么这套组合在毕设场景里最稳三个理由。第一SpringBoot 在 Java 岗位需求里的占比很高答辩时被问「为什么不用 SSM」也有现成话术可讲第二MyBatis-Plus 把单表 CRUD 几乎全包了你能把时间省下来做业务逻辑而不是天天写重复的 insert 和 selectById第三社区资料最多随便一个报错扔进搜索引擎都有前人的解决方案这点对赶工期的毕设来说比技术先进性重要得多。版本对齐是第一个要处理好的事。我一般建议 SpringBoot 用 2.7.x 配 JDK 8这个组合最成熟MyBatis-Plus、druid、JWT 这些常用库都不需要额外适配。如果资源包给的是 SpringBoot 3.x那必须配 JDK 17因为 3.x 全面切到了 jakarta 命名空间很多老版本的第三方依赖会直接 NoClassDefFoundError。下面这组版本是我实测过比较稳的搭配。组件稳妥版本说明JDK8 或 17看 SpringBoot 主版本2.7 用 83.x 用 17SpringBoot2.7.183.x 能跑但第三方库兼容是个坑详见第 5 章MyBatis-Plus3.5.3提供 BaseMapper 通用 CRUD 和分页插件MySQL8.05.7 也行但建议统一用 8.0 避免驱动版本差异Node.js16 或 18对应 Vue 3 和 Vite 的版本要求Vue3.2 Element Plus如果资源里是 Vue 2调整点在第 5 章2.2 项目骨架后端三层分包与前端 views/api 的对应关系拿到源码后第一步不是急着点启动而是先把目录结构读一遍。后端是标准的三层分包controller 层只做参数接收和结果返回service 层写业务逻辑mapper 层负责 SQLentity 实体类和数据库表一一对应。前端的结构一般长这样pet-hospital ├── backend # SpringBoot 后端工程 │ ├── src/main/java/com/pet/hospital │ │ ├── controller # 接口层/api/xx 的入口 │ │ ├── service # 业务层事务和状态流转在这里 │ │ ├── mapper # MyBatis-Plus 的 Mapper 接口 │ │ ├── entity # 数据库表对应的实体 │ │ ├── config # 拦截器、跨域、Swagger 配置 │ │ └── utils # JWT、日期、统一返回结果等工具 │ └── src/main/resources │ ├── application.yml # 数据源、端口、MyBatis-Plus 配置 │ └── mapper # 复杂 SQL 的 XML 文件 └── frontend # Vue 前端工程 └── src ├── api # 每个模块一个 js 文件封装 axios 请求 ├── views # 页面组件按模块分文件夹 ├── router # 路由表配置页面路径和权限 └── utils # axios 实例、token 拦截器前后端对应关系有个规律后端一张表对应一个 entity、一个 mapper、一个 service、一个 controller前端 api 目录下的 js 文件按后端 controller 的模块路径命名。比如后端有AppointmentController映射/api/appointment前端就有一个appointment.js封装挂号相关的所有请求。熟悉这个规律后你改一个功能点就知道要去哪个文件改不会像无头苍蝇一样翻代码。2.3 数据库设计八张表把挂号、病历、药品、收费串成闭环先看 SQL 脚本里的表结构这比看代码更高效。这个系统的核心表是八张用户、宠物主人、宠物档案、挂号、病历、药品、库存流水、收费单。它们的关系是一条完整的业务链宠物主人带宠物来医院前台创建挂号单医生接诊后写病历、开药药房扣库存最后收费台生成收费单。表名核心字段作用userid、username、password、role管理员和医生账号role 区分权限pet_ownerid、name、phone、address宠物主人信息petid、owner_id、pet_name、species宠物档案species 记录猫狗等种类appointmentid、pet_id、doctor_id、status挂号单status 是状态机核心medical_recordid、appointment_id、diagnosis、prescription病历记录诊断和处方medicineid、name、spec、stock、warn_stock药品库存和低库存预警线stock_logid、medicine_id、type、quantity出入库流水type 区分入库出库chargeid、appointment_id、amount、pay_status收费单pay_status 区分已付未付有几个设计点在答辩时会被问到提前想好。appointment 表的 status 字段用了整型而非字符串0 待接诊、1 就诊中、2 已完成、3 已取消用整型是为了排序和聚合方便这是状态机的典型做法。charge 表里冗余了 pet_name 和 owner_name 字段正常设计第三范式是不该冗余的但这里是为了收费报表展示时少几次联表查询——冗余换性能是报表场景下的合理取舍不是设计失误。3. 核心业务拆解登录鉴权、挂号流转、库存扣减与收费统计的实现3.1 登录鉴权JWT 签发、拦截器与「行级权限」登录这块是答辩必问点核心是 JWT 的签发和校验。密码在数据库里不应该存明文常见做法是用 BCrypt 加密存储登录时把用户输入的密码加密后和库里的密文比对。校验通过后签发 tokentoken 里带上 userId 和 role后续接口通过拦截器解析 token 拿到当前用户身份。// JwtUtil.java - token 签发与解析 public class JwtUtil { // 密钥在 application.yml 里配置毕设环境硬编码这里也可接受但别提交到公开仓库 private static final String SECRET pet-hospital-secret-key; // 过期时间 24 小时单位毫秒 private static final long EXPIRE 24 * 60 * 60 * 1000; // 签发 token把用户 id 和角色放进去 public static String generateToken(Long userId, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() EXPIRE)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } // 解析 token返回 Claims从中取 userId 和 role public static Claims parseToken(String token) { return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); } }登录成功后的 controller 返回统一结果前端把 token 存到 localStorage后续每次请求在 axios 拦截器里加到 header。后端拦截器负责拦截/api/**请求放行/api/auth/login和/api/auth/register其余都要校验 token解析失败直接返回 401。这里有个细节值得你答辩时主动讲医生查看病历列表时service 层会把当前登录用户的 id 拼进查询条件只查doctor_id 当前用户的记录这就是行级权限的雏形不是单纯靠前端隐藏按钮而是在后端数据层面做了隔离。3.2 挂号流转一个 status 字段如何撑起状态机挂号和就诊是整个系统的核心流程代码量不大但状态流转的逻辑必须清楚。创建挂号单时校验宠物存在、校验当天是否已经挂过号然后插入一条 status 为 0 的记录。医生接诊时把 status 从 0 改成 1完成就诊时改成 2同时生成病历和收费单。// AppointmentService.java - 状态流转的核心逻辑 Service public class AppointmentService { Autowired private AppointmentMapper appointmentMapper; // 创建挂号状态默认 0待接诊 Transactional public Appointment createAppointment(Appointment appointment) { // 校验宠物档案存在避免脏数据 if (petMapper.selectById(appointment.getPetId()) null) { throw new BusinessException(宠物档案不存在请先建档); } appointment.setStatus(0); // 预约日期默认今天时间默认当前时间 appointment.setAppointDate(LocalDate.now()); appointmentMapper.insert(appointment); return appointment; } // 接诊只有待接诊状态才能流转到就诊中 Transactional public void startTreat(Long id) { Appointment appointment appointmentMapper.selectById(id); if (appointment null || appointment.getStatus() ! 0) { throw new BusinessException(当前状态不可接诊); } appointment.setStatus(1); appointmentMapper.updateById(appointment); } }Transactional注解在这里是关键创建挂号和后续的插入病历、生成收费单如果任何一个环节失败前面写入的数据要能整体回滚。状态字段的修改用 updateById 而不是直接写 update status 1 where id ?是为了让 MyBatis-Plus 只更新非 null 字段避免把其他字段意外覆盖成 null。这个判断逻辑你答辩时主动讲出来比被问到时再解释效果好得多。3.3 库存扣减先查再扣不是银弹但够答辩用药品开出处方后要扣库存这个场景最容易踩的坑是超卖——两个医生同时开同一种药库存只剩一盒两个处方都扣减成功了。毕设级别的实现先查再扣加事务就够了代码如下。// StockService.java - 出库扣减 Transactional public void deductStock(Long medicineId, Integer quantity) { // 第一步查当前库存 Medicine medicine medicineMapper.selectById(medicineId); if (medicine null) { throw new BusinessException(药品不存在); } // 第二步判断库存是否充足 if (medicine.getStock() quantity) { throw new BusinessException(库存不足当前库存 medicine.getStock()); } // 第三步扣减并写流水 medicine.setStock(medicine.getStock() - quantity); medicineMapper.updateById(medicine); StockLog log new StockLog(); log.setMedicineId(medicineId); log.setType(2); // 2 表示出库 log.setQuantity(quantity); stockLogMapper.insert(log); }这段代码查两次数据库在并发量大的时候理论上会有超卖问题。答辩被追问怎么优化标准回答是两条一是把 update 语句改成条件更新update medicine set stock stock - #{num} where id #{id} and stock #{num}影响行数为 0 表示库存不足二是引入乐观锁给表加 version 字段更新时带上 version 并自增。这两种方案的 SQL 差异和适用场景要提前想清楚口头能讲明白比写出来更重要。低库存预警的实现也很直接查stock warn_stock的药品列表在首页做一个醒目的提示条。3.4 收费统计一条 group by SQL 顶一个报表页收费统计是前端可视化页面的数据来源。日营收报表的核心 SQL 就一条按支付时间的天维度做聚合同时把支付状态限定为已支付避免把未付款的单子算进营收里。-- ChargeMapper.xml - 按日统计营收 SELECT DATE_FORMAT(pay_time, %Y-%m-%d) AS stat_date, COUNT(*) AS order_count, SUM(amount) AS total_amount FROM charge WHERE pay_status 1 AND pay_time #{startDate} AND pay_time #{endDate} GROUP BY DATE_FORMAT(pay_time, %Y-%m-%d) ORDER BY stat_date ASCMapper 接口里对应方法是ListDailyStatVO selectDailyStat(Param(startDate) String startDate, Param(endDate) String endDate)返回结果直接映射成前端图表要的数据结构。这里有个参数陷阱日期字符串传进 XML 前要保证格式是yyyy-MM-dd HH:mm:ss否则边界数据会少一天。另外报表 SQL 里不要用DATE_FORMAT(pay_time, %Y-%m-%d)做范围过滤条件应该直接对 pay_time 做范围判断否则索引会失效数据量大了之后报表页会明显变慢。这一段的取舍逻辑讲清楚答辩时实现细节这一块就稳了。4. 从零跑起来数据库初始化、SpringBoot 配置与 Vue 联调全流程4.1 环境准备版本对齐比什么都重要复现这套项目的第一步是检查本机环境。后端需要 JDK、Maven、MySQL前端需要 Node.js 和 npm。每个工具装完先命令行验证一下版本比直接进 IDE 报错再回头查高效得多。java -version # 需要 8 或 17看项目 pom.xml 里 parent 版本 mvn -v # 3.6 以上即可3.8 或 3.9 都行 node -v # 16 或 18Vue 3 配合 Vite 对 node 版本有下限要求 npm -v # 跟着 node 走 mysql --version # 8.0 最稳5.7 也行版本确认后用 IDEA 导入后端项目选择 Maven 的方式导入等待依赖下载完成。这块最常见的坑是 Maven 镜像问题依赖卡住不动或者下载一半失败优先在~/.m2/settings.xml里配阿里云镜像。前端工程用 IDEA 或 VSCode 打开都行先npm install装依赖装的过程如果出现 node-gyp 报错或者 sass 编译失败多半是 Node 版本太高换 Node 16 重试。4.2 数据库初始化建库导数据编码一步错全局乱数据库是整套系统的基础导入顺序和编码一定要走对。先建库再导数据库名建议和项目配置保持一致我一般习惯直接命令行操作比可视化工具更不容易漏步骤。# 建库utf8mb4 是必须的能存 emoji 和生僻字 mysql -uroot -p -e CREATE DATABASE IF NOT EXISTS pet_hospital DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; # 导入 SQL 脚本路径按资源包实际位置改 mysql -uroot -p pet_hospital sql/pet_hospital.sql导入完成后用show tables;验证一下八张核心表都在就说明成功了。为什么特别强调 utf8mb4因为 MySQL 8 默认就是 utf8mb4但很多 SQL 脚本是从旧环境导出的建表语句里写的是DEFAULT CHARSETutf8中文没问题一旦涉及 emoji 或特殊符号就会报「Incorrect string value」错误。另外如果你拿到的是拆分的 SQL 文件先导表结构再导数据否则外键约束会报错。4.3 后端配置与启动application.yml 的五个必改项后端工程的application.yml是启动前唯一必须改的文件核心是五处端口、数据源地址、数据库账号、数据库密码、JWT 密钥。其他配置像 MyBatis-Plus 的驼峰映射和逻辑删除一般保持默认就能用不用动。# application.yml - 关键配置项 server: port: 8080 # 后端端口前端代理会指向这里 spring: datasource: url: jdbc:mysql://localhost:3306/pet_hospital?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root # 改成你本地的数据库账号 password: 123456 # 改成你本地的数据库密码 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: map-underscore-to-camel-case: true # 数据库下划线字段映射成驼峰属性 global-config: db-config: logic-delete-field: deleted # 逻辑删除字段删除走 update 而不是 delete jwt: secret: pet-hospital-secret-key # 生产环境要用强随机数毕设随便填但别和别人相同这里最容易翻车的是 jdbc URL 少了characterEncodingutf8和serverTimezoneAsia/Shanghai。第一个少了会中文乱码第二个少了在 MySQL 8 下会报时区错误启动直接失败。改完配置启动项目在项目根目录执行mvn spring-boot:run看到Started Application in x.xxx seconds日志并且没有异常堆栈就说明后端起来了。验证方式是浏览器访问http://localhost:8080/api/auth/login的 GET 会返回 405 或 401说明接口服务在线。提示IDEA 里直接跑启动类更方便但要确认启动类的包路径没有被 IDEA 错误地标记成普通目录。4.4 前端启动与跨域联调代理配置与首屏验证前端启动前先确认两件事依赖装好了代理配好了。开发环境下前端端口和后端不一样直接请求会触发跨域常见做法是在vue.config.js里配 devServer 代理把/api开头的请求转发到后端 8080。// vue.config.js - 开发环境代理 module.exports { devServer: { port: 3000, proxy: { /api: { target: http://localhost:8080, // 转发到后端 changeOrigin: true // 修改请求来源避免后端校验 origin 时报错 } } } };配置好后先npm install然后npm run serve。看到编译完成并输出本地访问地址后打开浏览器进登录页用资源包里的初始账号登录。登录成功后 F12 打开 Network 面板随便点一个菜单如果请求的Request URL是http://localhost:3000/api/xx且响应 200说明代理生效、前后端联调通了。如果看到跨域报错优先检查代理配置有没有生效而不是去后端加CrossOrigin——那个只能解决部分场景不是根治方案。5. 毕设避坑实录版本冲突、打包 404、中文乱码与答辩追问5.1 SpringBoot 版本太高MyBatis-Plus 和 druid 一起翻车现象项目导入后启动控制台直接抛NoClassDefFoundError: javax/servlet/...或者ClassNotFoundException: javax.annotation.Resource重启几次都一样。原因SpringBoot 3.x 把 Java EE 的javax.*全面换成了jakarta.*而资源包里很多第三方依赖老版本 MyBatis-Plus、druid、fastjson还是按javax.*编译的。版本对不上类加载必然失败。解决不用管资源包里的 pom 写的什么直接把 SpringBoot 降到 2.7.18同时确保 JDK 是 8 或 11 而不是 17。改法是在 pom.xml 的 parent 里把版本改成2.7.18然后把所有依赖里的版本删掉让父工程统一管理重新导入让 Maven 重新拉取。降级后再启动这类类加载错误基本全部消失。5.2 Vue 打包放进 SpringBoot 的 static 目录刷新页面 404现象用npm run build把前端打包后dist 目录扔进后端的 static 目录访问首页没问题但进入某个子页面后按 F5 刷新直接 404。原因vue-router 默认用的是 history 模式路由路径是真实的 URL比如/appointment/list刷新时浏览器带着这个路径请求后端后端没有对应的 controller 或静态资源自然返回 404。这是前后端分离项目打单包部署的经典问题。解决两个方案选一个。一是把 vue-router 改成 hash 模式路径会变成/#/appointment/list刷新时锚点部分不会发送到后端改动最小二是在后端加一个兜底 controller把所有非/api开头的路径转发到 index.html。注意我一般用hash模式改动小、零副作用。兜底转发方案要考虑静态资源路径的冲突新手容易配出问题。5.3 数据库中文乱码问题不在代码而在连接参数现象页面保存的宠物名字进数据库变成???或者表格里显示乱码。原因三层乱码源排查——数据库和表不是 utf8mb4、jdbc URL 缺少characterEncodingutf8、前端请求没有主动声明 UTF-8。其中 jdbc URL 缺参数是最隐蔽的代码看起来完全没问题数据进去就是花的。解决先用命令行show variables like character_set_database;确认库编码再用ALTER DATABASE pet_hospital CHARACTER SET utf8mb4;修正。然后回到第 4 章的 jdbc URL确保三个参数完整useUnicodetrue、characterEncodingutf8、serverTimezoneAsia/Shanghai。改完重启后端再测一轮新增数据这轮就能通。5.4 端口被占、token 莫名失效这两类问题最耗时间现象启动后端报Port 8080 was already in use或者前端操作到一半跳回登录页。原因8080 被其他 Java 进程或别的服务占用排查命令是 Windows 用netstat -ano | findstr 8080macOS/Linux 用lsof -i:8080。token 失效则是 JWT 过期时间太短或者重启后端后 SECRET 变了导致之前签发的 token 全部校验失败。解决端口问题杀进程或者改端口我习惯在 application.yml 里改成 8081 图省事。token 问题把JwtUtil里的过期时间调到 24 小时同时确认 SECRET 是固定字符串而不是每次启动随机生成。这个坑坑过很多人SECRET 写成随机的后果就是后端一重启所有在线用户全部掉线。6. 答辩前 48 小时接口自测清单、演示脚本与高频追问预案6.1 用 Postman 把核心接口按业务链跑一遍答辩前一天我一般会打开 Postman把系统的六条核心接口按真实业务顺序跑一遍防止演示时现场翻车。自测顺序就按用户的操作路径来登录拿 token、建档、挂号、接诊、收费、查报表。每条接口用什么参数、预期返回什么整理成一张小表。步骤接口预期结果1POST /api/auth/login返回 token24 小时内有效2POST /api/pet宠物档案插入成功返回 id3POST /api/appointment挂号单生成status 为 04PUT /api/appointment/{id}/start状态变为 1可填病历5POST /api/charge收费单生成金额正确6GET /api/charge/stat/daily累计金额与第 5 步一致如果跑完这条链所有数据都对得上答辩演示基本不会出大问题。别忘了测一条异常路径比如让库存不足的药品强行出库确认返回的是友好提示而不是一条红色报错堆栈——这个细节评委很吃。6.2 五分钟演示脚本与高频追问预案演示时间有限我习惯把路径固定成登录页展示 JWT 鉴权进入工作台后先建一个宠物档案然后走完挂号到收费的完整闭环最后打开统计报表页指着一目了然的数据说「这就是从 charge 表按日期聚合出来的」。手势要稳演示过程中别切换窗口提前把浏览器缩放调好。追问预案准备三条就够。为什么用 SpringBoot 不用 SSM——自动配置减少样板代码、内嵌 Tomcat 部署简单、生态资料最多为什么用 MyBatis-Plus——单表 CRUD 免写 SQL、自带分页插件、代码生成器能快速生成基础代码token 过期了怎么办——前端 axios 拦截器收到 401 后清 localStorage 跳登录页后端拦截器统一抛业务异常两层都有兜底。这次写这套系统拆解我自己最深的教训是版本对齐拿到任何毕设源码第一步排查的一定是 JDK、SpringBoot、MySQL 三者版本是否匹配而不是去看业务代码。从那以后我每次复现别人的项目都强制自己先花十分钟过一遍环境矩阵再点启动按钮。希望帮到你。本文还有配套的精品资源点击获取