news 2026/10/27 7:48:06

Spring Boot宠物医院管理系统毕设实战:部署、编码与答辩

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot宠物医院管理系统毕设实战:部署、编码与答辩

简介:这套基于Spring Boot与Vue的宠物医院管理系统毕业设计源码,面向Java技术学习者、高校毕设学生及课程设计人员,可用于快速搭建一个前后端分离的完整管理平台,是初学全栈开发的参考范例。压缩包共1167个文件、34.89MB,核心包含127个Java后端业务代码、211个Vue前端页面组件,以及151个JS脚本与109个CSS样式文件,另有170个XML配置和43个JAR依赖包,并附带SQL数据库初始化脚本,目录结构完整清晰。已有1753人学习下载;部署环境要求明确,包括JDK1.8、MySQL5.7、Tomcat7与Maven3.3.9等,按配套说明即可分别启动后台管理端与前台展示端,方便快速验收与演示。系统覆盖宠物档案管理、挂号就诊、药品管理等典型业务模块,代码分层清晰,注释规范,既可作为毕业设计答辩项目,也适合课程实训或二次开发扩展;遇到部署或运行问题,可随时联系博主获得及时解答。

1. 这是毕设里最“脏”的一类项目:宠物医院管理系统的真实用途

很多人在选题时看到“基于Spring Boot的宠物医院管理系统”就直接跳过了——觉得宠物医院太小众,不如图书管理、酒店管理来得“正规”。但做过的人都知道,这恰恰是毕设里最实用的一类:它有明确的多角色权限(管理员、医生、前台)、有典型的业务流(挂号、就诊、开药、结算)、有文件上传(宠物照片、病例附件),还牵扯一对多和多对多的表关系。你答辩时能讲的东西,比“用户管理+增删改查”那种项目多出一倍。这个标题对应的zip里,装的就是一套可以直接运行的Spring Boot单体应用,前后端分离或不分离都有可能,主流方案是Spring Boot + MyBatis Plus + Vue/Thymeleaf + MySQL。适合谁?适合那些想要一个“功能齐全但代码量可控”的毕设选题、或者想快速复现一个完整业务闭环当作练手项目的人。接下来我从解压开始,一步一步拆。

2. 拆开这个zip:Spring Boot宠物医院管理系统的目录结构与技术选型

拿到zip先别急着解压跑起来。先搞清楚里面的工程结构是什么样的,这决定了你后面改代码、换数据库、加功能的工作量。这一章我们从技术选型逻辑讲到资源文件清单,最后落到数据库设计,把整个项目的骨架先立住。

2.1 为什么是Spring Boot + MyBatis Plus而不是SSH:毕设项目的技术选型逻辑

如果你打开zip发现pom.xml里依赖是MySQL Connector、MyBatis Plus、Lombok、Spring Boot Starter Web这套组合,那恭喜你,这是目前最“好讲”的技术栈。Spring Boot 2.7.x版本是当前毕设项目的绝对主流,稳定、资料多、遇到报错随便一搜就有答案。相比十年前流行的SSH(Struts + Spring + Hibernate),Spring Boot把配置压缩到极致,你不需要写一堆XML配置文件,一个application.yml就能搞定数据源、端口、MyBatis映射路径。这对毕设的意义很大——三个月后你要答辩,代码里的每一步你都能说清楚来龙去脉,不会出现“这段配置是网上复制来的但我不知道干嘛”的尴尬。

MyBatis Plus在这个项目里承担的是数据访问层的“减负”角色。宠物医院的表结构并不复杂,但涉及大量单表CRUD操作,MyBatis Plus的BaseMapper直接内置了insert、deleteById、selectPage这些方法,你在Service层几乎不用手写SQL。而多表关联查询,比如“查询某个主人名下的宠物列表并带上最近的就诊记录”,这种需求用注解式SQL或者XML里的自定义SQL搞定。选MyBatis Plus而不是JPA,还有一个实际原因:毕设答辩时,老师大概率会问“你怎么控制SQL的”,MyBatis Plus的SQL日志配置打开后,每一条执行的SQL都打印在控制台,你把日志贴到论文附录里,比空口讲“JPA自动生成”要有说服力得多。

前端部分,如果压缩包里带了Vue项目,那走的是前后端分离方案——Vue开发环境跑8080,Spring Boot跑8081或9090,生产环境把Vue打包后的dist目录扔进Spring Boot的static目录实现合并部署。如果是Thymeleaf,那更简单,服务端渲染,所有页面都在resources/templates下面。判断压缩包属于哪种方案,看根目录里有没有package.json或者vue.config.js,有就是分离式,没有就是模板渲染。两种方案各有优劣:分离式的项目结构清晰、前后端职责分得开,但答辩需要额外讲解Vue的proxy配置如何转发请求;模板渲染部署简单,但页面代码和服务端代码混在一起,后期加功能时定位文件稍微麻烦。我一般建议答辩方向偏后端就把Vue项目放进来展示“全栈能力”,方向偏部署运维就选Thymeleaf版本,两三个命令就能跑起来。

2.2 压缩包里应该有的东西:从pom.xml到前端资源的文件清单

解压zip后,你大概率会看到两类目录并列:后端工程目录(比如pet-hospital)和数据库脚本目录(或者sql脚本就放在根目录下),有的打包者会把完整的Vue前端项目单独放一个文件夹。先别急着看代码,按下面的顺序梳理一遍,确保项目是完整的。首先打开pom.xml确认核心依赖是不是齐了:spring-boot-starter-parent(父工程统一管理版本)、spring-boot-starter-web(MVC和Tomcat)、mybatis-plus-boot-starter、mysql-connector-java、lombok。如果还有其他东西,比如spring-boot-starter-security或者shiro,说明项目做了登录鉴权框架的集成;如果有spring-boot-starter-mail,那可能带邮件通知功能。这些额外依赖是你论文里“技术亮点”的素材,但也意味着环境配置更复杂。

后端工程的包结构一般是这样:controller(接收HTTP请求)、service(业务逻辑接口)、service.impl(接口实现)、mapper(MyBatis Plus的持久层接口)、entity(对应数据库表的实体类)、config(拦截器、跨域配置、MyBatis Plus分页插件注册)、common或者utils(统一返回结果类、JWT工具类、异常处理器)。你拿到项目后重点检查config包和common包,统一返回结果类如果写得规范,controller层每个方法都会返回Result类包装的JSON数据——这个类在答辩时是加分项。如果整个项目没人封装返回结果,所有controller直接返回Map或裸对象,代码质量就偏低了,你后续整合前端对接时要花额外时间。

数据库脚本通常是pet_hospital.sql或者init.sql,用Navicat连接MySQL后直接执行即可。脚本里如果带了INSERT语句,那就有初始账号密码(管理员、医生、前台各一个),解压密码或默认密码通常在readme.txt或者项目根目录的说明文档里。前端资源如果是分离式的Vue项目,压缩包里会有src目录(页面组件)、public目录(静态资源)、package.json。拿到压缩包后的第一件事,是确认前端项目的devServer配置里的proxy目标端口——如果前端写死请求http://localhost:8081,而你的后端在9090,即使后端起来了前端也调不通接口。

2.3 数据库设计:宠物、主人、挂号、医嘱四张核心表怎么建

宠物医院管理系统和图书管理系统的最大差别在表关系上。图书管理是“书-借阅记录-读者”三条主链,而宠物医院至少需要八到十张表才能撑起一个完整闭环。核心表逃不过这四张:pet(宠物档案)、pet_owner(宠物主人,如果你那个项目的登录角色是“用户”而不是“主人”,表名可能叫sys_user,那pet这张表通过owner_id关联到用户表)、appointment(挂号预约)、medical_record(就诊记录/医嘱)。只看表名看不出深度,你要明白这些表为什么这样设计,答辩时才能讲清楚。

pet表的关键字段:id、pet_name、pet_type(猫/狗/其他,用字符串还是枚举值)、breed(品种)、age、gender、owner_id(外键关联主人表)、avatar(宠物照片路径)、create_time、update_time。这里有个经常被毕设项目忽略的点:宠物和主人是一对多关系,一个主人可以带三只猫来看病,所以pet表必须存owner_id,反过来主人表不能存pet_ids这种逗号分隔的字段——这违反第一范式,代码里处理起来也恶心。

appointment表是业务流的核心。字段应该有:id、pet_id、doctor_id(接诊医生)、appointment_time(预约时间)、status(状态字段:0-待就诊、1-已完成、2-已取消)、remark、create_time。状态字段用int还是varchar?我建议用int,理由很简单:MyBatis Plus的实体类里对应的是Integer,前端下拉框选项值直接绑数字,比字符串判断更简洁。

medical_record表的重点在“医嘱和费用怎么关联”。常见设计是medical_record主表(record_id、pet_id、doctor_id、diagnosis、create_time),下面再挂medical_item子表(医嘱明细:项目名称、单价、数量),最后通过record_id关联到settlement表做费用结算。这么拆的好处是:处方里的每一项可以单独核算金额,如果宠物主人只付了检查费没付药费,你按record_id去结算表里一查状态就知道欠费,不用把整个JSON字符串塞在一个字段里。很多毕设项目图省事,把诊断和药品明细用逗号拼成一个字符串存进medical_record的一个字段——功能上跑得通,答辩时老师一句“你怎么查询某个药品的销售统计”就能把你问住。数据库设计是一个项目的底子,你在复现压缩包时,先花半小时把脚本的表结构和字段注释看一遍,比直接启动项目踩坑要有用得多。

3. 把项目跑起来:从解压到浏览器看到登录页的完整步骤

这一章解决的是“怎么让这个项目在我电脑上活起来”的问题。不熟悉Spring Boot运行机制的人,最容易卡在环境版本不匹配和配置项改错这两个点上。我按实际操作的顺序,把每一步拆开。

3.1 环境准备:JDK、Maven、MySQL的版本匹配

压缩包里的pom.xml如果用的是Spring Boot 2.7.x,那对应的JDK必须是8到11之间的版本。用JDK 17跑Spring Boot 2.7.x会有兼容性问题,常见报错是Caused by: java.lang.reflect.InaccessibleObjectException,这是因为JDK 17的模块化系统默认禁止反射访问内部API。所以第一步先确认你的JDK版本,命令行输入java -version,如果是17甚至21,建议直接装一个JDK 8。装两个JDK并不冲突,你只需要在IDE里把项目的JDK切到8,系统环境变量保持默认也行,但命令行里跑mvn命令时要注意jvm用的是哪个版本——这个坑我踩过,IDE里正常启动,命令行打包就报错,最后发现是PATH里JDK版本不对。

Maven版本选择:Spring Boot 2.7.x的依赖解析对Maven没有硬性要求,Maven 3.6+即可。确认Maven用的是阿里云镜像而不是默认中央仓库,否则下载spring-boot-starter-parent这堆依赖能卡你没脾气。修改镜像的方式:找到Maven安装目录下的conf/settings.xml,把mirror节点换成阿里云仓库地址,这个操作比换Maven版本本身更有用。

数据库建议用MySQL 5.7或8.0。如果你的压缩包里的SQL脚本有utf8mb4字符集设置,那MySQL 8.0更合适,注意创建用户时授权命令要写成mysql_native_password方式,因为Spring Boot的JDBC驱动和MySQL 8.0默认的caching_sha2_password认证方式有时会有兼容问题。如果启动后报错Authentication plugin 'caching_sha2_password' cannot be loaded,直接改MySQL用户的认证插件或者在数据库连接URL后加上allowPublicKeyRetrieval=true。

提示:连接URL务必写上useSSL=false和serverTimezone=Asia/Shanghai两个参数,前者避免MySQL 8.0的SSL握手警告,后者解决时区报错。

3.2 修改application.yml:数据库连接、端口、文件上传路径三个必改项

打开后端工程的src/main/resources/application.yml,最核心的配置块有三个。第一个是spring.datasource:你本地的数据库名、用户名、密码全部要改。第二个是server.port:如果8080端口被占用,改成9090或者其他空闲端口。第三个是文件上传路径——宠物照片、病历附件这些东西要存到磁盘,配置一个绝对路径,别用相对路径的./upload,因为项目你是用命令行启动还是用IDE启动,工作目录不一样,相对路径会导致上传成功后图片404。改配置的模板如下:

server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/pet_hospital?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: yourpassword mvc: static-path-pattern: /** web: resources: static-locations: file:E:/pet-hospital-upload/ servlet: multipart: max-file-size: 10MB max-request-size: 20MB mybatis-plus: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.pethospital.entity configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

这段配置里最值得说两个地方。static-locations配的是自定义磁盘路径,它的作用是让Spring Boot接管URL映射——当浏览器访问http://localhost:8080/upload/xxx.jpg时,实际读取的是E盘那个目录的文件。这个机制困扰过很多人,他们以为上传的文件被Spring Boot存进了项目内存,其实没有,Spring Boot从来不帮你在项目内部存文件,它只提供URL到磁盘路径的映射关系。另一个是Log-impl: StdOutImpl,这行配置打开后,控制台会打印MyBatis Plus执行的所有SQL语句。第一次启动项目时建议打开,方便你确认数据库操作有没有走对;正式交付前把它关掉,否则答辩演示时不光项目页面会被你看到,后台SQL也暴露了。

配置改完后,去数据库里执行SQL脚本创建数据库和表结构。这里给一个小建议:在Navicat里创建数据库时,字符集选择utf8mb4,排序规则选utf8mb4_general_ci。项目里的SQL脚本大概率已经带了CREATE DATABASE语句,但Navicat执行时选择数据库不严谨会导致表建到错误库下面,你从application.yml里配置的库名和实际建表的库名要保持一致——这一步错了,后面启动无限报错。

3.3 初始化数据库:执行SQL脚本的正确顺序和常见报错

SQL脚本的执行环境和方式有讲究。如果是用Navicat直接打开脚本执行,注意先检查脚本开头有没有USEpet_hospital; 或者CREATE DATABASE语句。有的脚本写得规范,你只管整个文件执行;有的脚本只写了CREATE TABLE没有创建库,你得自己先建库再切到目标库执行。同一个脚本执行两次,第二次会报“Table already exists”——这不算污染,因为宠物医院这种小项目脚本里很少写DROP TABLE IF EXISTS,你自己手动把表删掉重跑一次即可。

关联表结构建好后,下一步是执行INSERT语句。如果你的系统带了管理员账号又是用MD5加密的,此时密码落库值是一串32位十六进制字符串,比如e10adc3949ba59abbe56e057f20f883e——这对应明文密码123456。如果你怎么看都看不出这个密文到底代表什么,直接用这个密文值做登录测试,不要手动改数据库把密码改成明文,因为登录逻辑里会对比加密后的值而不是明文。实在想换密码,在登录页注册一个新账号,新密码的密文会被正确写入,用它登录即可。

启动项目的方式建议用IDE里的Spring Boot启动类(Application.java)右键运行,而不是命令行mvn spring-boot:run。原因很简单:IDE运行能看到彩色控制台输出,报错信息可以直接点击跳转代码行。启动成功的标志不是Tomcat started on port 8080,而是“Started PetHospitalApplication in x.xxx seconds”。如果你看到日志停在某个地方超过10秒没动静,多半是数据库连接卡住了——检查MySQL服务有没有启动、账号密码对不对、防火墙有没有放开3306端口。要快速验证数据库能不能连,用Navicat试一下;Navicat能连,Spring Boot连不上,就去查连接URL和驱动版本。

4. 核心模块实现:宠物档案、在线挂号、医嘱开药怎么做

这个章节切入到实际功能代码,目标不是通读整个项目源码,而是抓住宠物医院管理系统里最有业务代表性的三个模块——宠物档案、挂号、医嘱结算——讲清楚它们是怎么落地的。每一个模块都牵涉到“表关系→实体设计→Mapper层→Service方法”这条链路,熟悉这条链路,你在答辩时面对任意功能提问都能往下讲。

4.1 宠物档案:一对多表关系下的增删改查与懒加载问题

宠物档案模块的数据库表设计在2.3已经说过,owner_id是外键。那么实体类上怎么体现这个关系?常见方案是Pet实体类里加一个petOwner字段,用@TableField(exist = false)标注,代表这个字段不在pet表里,是通过关联查询查询出来的。当查询宠物列表时,需要在Service层手动把主人信息塞进返回结果,MyBatis Plus的BaseMapper不会自动帮你做这个跨表填充。

@Service public class PetServiceImpl extends ServiceImpl<PetMapper, Pet> implements PetService { @Autowired private PetOwnerMapper petOwnerMapper; @Override public List<PetVO> listPetsWithOwner() { List<Pet> pets = this.list(); List<PetVO> result = new ArrayList<>(); for (Pet pet : pets) { PetVO vo = new PetVO(); BeanUtils.copyProperties(pet, vo); PetOwner owner = petOwnerMapper.selectById(pet.getOwnerId()); vo.setOwnerName(owner == null ? "未知主人" : owner.getName()); vo.setOwnerPhone(owner == null ? "" : owner.getPhone()); result.add(vo); } return result; } }

这段代码解决的核心问题是“一对多关系下,如何把主表的记录和关联表的字段拼装成前端需要的视图对象”。注意两点:第一,列表循环里做N+1查询——每查一条宠物记录都要额外执行一条主人查询,数据量上去了性能会崩,毕设里数据量通常只有几十条,这样写最简单直观。第二,BeanUtils.copyProperties是Spring自带的拷贝工具,实体类Pet和视图类PetVO字段名完全一致才能拷贝成功。如果你在VO里加了petTypeName这种实体里没有的字段,需要手动设置。这个模块的答辩点在于讲解一对多和多表拼装的过程,比单纯说“我用了MyBatis Plus的CRUD接口”得分高。

4.2 挂号与候诊队列:状态机字段的设计与更新时机

挂号模块是整个系统的业务中转站。用户(宠物主人)选择医生和时段提交挂号,前台或医生在后台更新状态。status字段的流转是这里最容易出错的部分,流程是:待就诊(0)→ 已就诊(1)或已取消(2),已经取消的单子不能变回待就诊,已经完成的不能再次取消。用状态机的思维去约束更新操作,比在Service层里随手setStatus要严谨。

@Override @Transactional public boolean cancelAppointment(Integer appointmentId, Integer userId) { Appointment appointment = this.getById(appointmentId); if (appointment == null) { throw new ServiceException("挂号记录不存在"); } // 校验归属权:用户只能取消自己的挂号 if (!appointment.getUserId().equals(userId)) { throw new ServiceException("不能操作他人的挂号记录"); } // 状态机校验:只有待就诊状态可以取消 if (appointment.getStatus() != 0) { throw new ServiceException("当前状态不可取消"); } appointment.setStatus(2); return this.updateById(appointment); }

这段代码最值得借鉴的一点是updateById之前的状态检查——三层校验:记录存在性、归属权、状态可流转性。很多毕设项目在这里只做一层存在性校验,导致用户把别人家的宠物挂号取消掉,或者重复取消同一单导致前端状态混乱。逻辑说明:@Transactional注解必须加在public方法上,而且要保证抛出的异常没有被同类内部方法吃掉,否则事务失效。如果你在同一个Service类里写了另一个方法调用了cancelAppointment,而这个方法内部catch了异常,事务照样会回滚失败。状态机在代码里用int常量表达已经是够清晰的做法,如果你想把状态机做得更优雅,可以定义一个枚举类AppointmentStatus,定义可流转的状态列表和校验逻辑——但那是加分项,毕设阶段用if判断就够了。

4.3 医嘱与费用结算:事务注解放在哪一层才不会失效

医嘱模块是宠物医院管理系统里最典型的多表事务场景:写入一条就诊记录,往明细表插入处方项,改掉挂号状态,更新宠物最后就诊时间。四个操作必须全成功或全回滚。如果你的项目里是这么写的,事务肯定没问题。但很多新手会犯的一个错误是把这些操作分散在四个Service方法里,然后在一个Controller方法里依次调用——Controller层顺手加了一个@Transactional,看起来也对,但Spring的事务代理机制只对通过代理对象调用的方法生效,同类里的内部互调,事务是保不住的。

@Service public class MedicalRecordServiceImpl extends ServiceImpl<MedicalRecordMapper, MedicalRecord> { @Autowired private MedicalItemMapper medicalItemMapper; @Autowired private AppointmentMapper appointmentMapper; @Transactional(rollbackFor = Exception.class) public Integer createRecordAndSettlement(MedicalRecord record, List<MedicalItem> items, Integer appointmentId) { // 1. 插入医嘱主表 this.save(record); // 2. 插入医嘱明细表 for (MedicalItem item : items) { item.setRecordId(record.getId()); medicalItemMapper.insert(item); } // 3. 更新挂号状态为已完成(状态值 1) Appointment appointment = new Appointment(); appointment.setId(appointmentId); appointment.setStatus(1); appointmentMapper.updateById(appointment); return record.getId(); } }

看懂这段代码的关键在rollbackFor = Exception.class这个参数。Spring的默认事务回滚策略是只回滚RuntimeException,如果你在代码里抛的是Exception子类或者自定义的CheckedException,不加rollbackFor,方法执行完业务照样被提交,数据看起来没事但状态没改对。做题时遇到这种隐蔽的问题,排查起来极其痛苦。另外,图省事在主表插入后不拿到自增主键ID就往明细表塞recordId,会导致外键关联断裂,MyBatis Plus的save方法内置了主键回填——只要你的实体类主键字段标了@TableId(type = IdType.AUTO),save之后record.getId()一定有值,这一步不少新手踩过,主键没回填以为是MyBatis Plus的问题,最后查起代码发现主键字段根本没加注解。

5. 避坑:毕设项目从源码到通过答辩的常见问题排查

读者反馈最多的一个问题就是“按教程做了还是跑不起来”。这一章把我在复现这种项目时遇到的高频故障整理出来,按“现象→原因→解决”的格式写。这五条覆盖启动阶段、运行阶段、部署阶段最常见的坑,遇到问题先来这里对照一遍,能少走三个小时的弯路。

5.1 启动报错“Invalid bound statement (not found)”

现象:Spring Boot启动成功,但调用某一个Mapper接口方法时报org.apache.ibatis.binding.BindingException: Invalid bound statement (not found)。原因:MyBatis Plus的Mapper接口和XML文件没有正确绑定。要么是Mapper接口的方法名在XML里找不到对应的id,要么是XML文件的namespace写错了,要么是application.yml里的mapper-locations配置路径没覆盖到XML文件实际位置。解决:第一步检查Mapper接口文件的包路径——如果把Mapper接口写在com.pethospital.mapper,而XML文件放在resources/mapper目录,那application.yml里的mapper-locations必须是classpath:mapper/*.xml。第二步打开XML文件看namespace是否和接口全限定名一致。第三步确认XML里的statement id和接口方法签名一致,参数类型不写也可以,但方法名和id必须能对上。

5.2 前端页面404:静态资源被拦截的三种可能

现象:项目启动正常,后端接口用Postman测试通,但浏览器访问http://localhost:8080/index.html返回404。原因有几种:第一种是你的项目里配置了拦截器或过滤器,把所有请求拦下来让你登录,还没登录所以404或者302跳转;第二种是Spring MVC的静态资源映射方式——如果你的项目把页面放在static目录下,同时自定义了WebMvcConfigurer并重写了addResourceHandlers,可能把默认的/**映射覆盖掉了;第三种是前后端分离项目,你把Vue打包后的dist文件放到了static目录但没放对层级,Spring Boot识别不到index.html。解决:先看控制台有没有RequestInterceptor拦截日志,有就是拦截器问题,放行静态资源;没有就去检查static目录下是不是真的存在index.html,并且路径是classpath:/static/index.html。

5.3 图片上传后无法访问:路径映射与磁盘存储的错位

现象:图片上传接口返回成功,数据库里也有文件路径比如/upload/2024/01/xxx.jpg,但浏览器访问这个地址报404。原因:文件被保存到了项目运行的临时目录或者一个相对路径下,而Spring Boot启动时工作目录变化导致这个相对路径指向的物理位置不对。解决:给你的上传路径配置成一个绝对路径,比如E:/pet-hospital-upload/,然后把application.yml里的spring.web.resources.static-locations配置成file:E:/pet-hospital-upload/。这一步改完后记得要重启项目,因为static-locations是在启动时读取的。另外,如果你把文件存的是Windows路径,但代码里用了硬编码的斜杠,URL拼接可能会有问题,统一用File.separator或者直接拼“/”就好——URL里始终是正斜杠,磁盘存储用反斜杠还是正斜杠取决于操作系统。

5.4 分页查询翻车:PageHelper和MyBatis Plus分页插件冲突

现象:项目一开始用PageHelper做分页,运行正常;后来因为其他模块需要MyBatis Plus特性,引入了MyBatis Plus,分页莫名其妙失效——要么不管传第几页都返回全部数据,要么查出来的总数不对。原因:两个分页插件的拦截器同时生效,MyBatis Plus的分页插件在SQL解析时先拦截了,或者PageHelper拦截器把MyBatis Plus的Executor方法给包装了一次,导致分页SQL没有正确拼上LIMIT。解决:不要同时用两套分页方案,统一成MyBatis Plus的Page对象。删掉PageHelper的依赖和拦截器配置,在使用时把IPage page = new Page<>(current, size);传给selectPage方法。如果你的项目里大量代码用了PageHelper.startPage的方式,改动量可能会大一些,但在答辩前统一成一套方案是值得的——否则分页逻辑的bug很难跟评委解释清楚。

5.5 时间字段显示成时间戳:JSON序列化配置

现象:数据库的create_time字段在Navicat里看是正常的2024-01-01 12:00:00,但前端通过接口拿到的JSON却是一个数字串,比如1704067200000。原因:Spring Boot默认使用Jackson库做JSON序列化,而LocalDateTime或Date类型的默认序列化方式在某些配置下会输出时间戳。解决:在application.yml里加一段格式化的全局配置,或者用注解@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")标到实体字段上。全局配置更省事但要注意会不会影响其他类型字段。时间格式化这个事看着小,但在答辩演示时非常显眼——前端表格里的时间列显示一堆数字肯定难看,而且数字格式的日期时间对评委来说会直接影响对项目规范的判断。

6. 答辩前值得做的三个加分改造:从“能用”到“能讲”

如果系统已经跑通,功能都能正常操作,你还有余力,这个章节提供三个投入产出比高的改造方向。它们不是必做项,但做了之后你的项目会从“功能齐全”升级成“有设计感”,答辩时老师对你的第一印象会差很多。

6.1 用AOP实现操作日志:一套注解完成登录、增删改的记录

宠物医院管理系统的后台需要记录谁在什么时候操作了哪条数据,比如“医生王某某修改了宠物编号为3的档案”。自己手动在每次Service方法前后写日志代码工作量巨大,用AOP切面加自定义注解,一套方案就能搞定。先定义一个@OperationLog注解,标注了该注解的方法会被切面拦截,在方法执行前或执行后把操作者、方法名、传入参数、耗时写入日志表。核心代码配置如下:

@Aspect @Component public class OperationLogAspect { @Autowired private OperationLogMapper operationLogMapper; @Around("@annotation(com.pethospital.aspect.OperationLog)") public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.currentTimeMillis(); // 放行方法并拿到返回值 Object result = joinPoint.proceed(); long end = System.currentTimeMillis(); // 从Spring Security或Session中拿当前登录用户 String username = SecurityUtils.getUsername(); // 解析方法信息 MethodSignature signature = (MethodSignature) joinPoint.getSignature(); String methodName = signature.getName(); String className = joinPoint.getTarget().getClass().getName(); // 组装日志记录 OperationLogEntity logEntity = new OperationLogEntity(); logEntity.setOperation(methodName); logEntity.setOperator(username); logEntity.setCostMs(end - start); logEntity.setCreateTime(LocalDateTime.now()); operationLogMapper.insert(logEntity); return result; } }

这个切面用了@Around环绕通知,意思是目标方法执行前后都能插入逻辑——执行业务前可以打开始时间,执行成功后打结束时间,如果你在保存日志时失败,注意不要影响主流程,要把日志写入包在try-catch里吞掉异常。代码里的SecurityUtils.getUsername()是Spring Security的API,如果你的项目用的是Session或JWT,换成通过RequestContextHolder拿HttpServletRequest再去取用户信息。

答辩时讲这个切面的亮点是“非侵入式”,你不需要改动任何Service方法,新增需要记录日志的操作时只加一个@OperationLog注解。如果老师继续追问“怎么记录请求参数”,你在joinPoint.getArgs()里获取参数列表后转成JSON字符串存到日志里即可。答辩紧张时,这套代码很容易理清楚自己的思路——方法名、类名、参数、执行耗时,四个维度就是操作日志的全部。

6.2 给挂号模块加一个简单的预约冲突检测

宠物医院管理系统的原始版本做预约时,往往只检查挂号时段是否已被同一只宠物重复预约,但忽略了一位医生同一时段是不是已经被安排了好几个病人。加一个冲突检测是成本很低的优化,却能让答辩现场的演示多一个亮点——你怎么处理并发场景下的数据一致性。

@Override @Transactional public boolean createAppointment(Appointment appointment) { // 查询同一医生、同一时间段的已有预约 LambdaQueryWrapper<Appointment> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Appointment::getDoctorId, appointment.getDoctorId()) .eq(Appointment::getAppointmentTime, appointment.getAppointmentTime()) .eq(Appointment::getStatus, 0); Long count = this.count(wrapper); // 并发场景下 count 和 insert 之间可能有时间差,需要数据库唯一索引兜底 if (count > 0) { throw new ServiceException("该医生当前时段已有预约,请选择其他时间"); } appointment.setStatus(0); return this.save(appointment); }

这段代码的核心思想不是数据库层面的强约束,而是应用层的事前校验。count=0就允许插入的逻辑在真正的并发场景里不够严谨——两个请求同时读到count=0,然后先后插入,就会出现一个医生同一时间被约了两次。为了让这个逻辑更完整,可以把appointment_time加doctor_id加上status字段加一个联合唯一索引,这三个字段组合值相同就不允许重复插入,数据库会直接抛DuplicateEntry异常,代码里catch异常转换成友好的业务提示。这种“应用层校验+数据库唯一索引兜底”的设计是真正的生产思维,比单纯if判断有说服力得多。

因为宠物医院管理系统的并发量不大,你不需要使用Redis分布式锁这类重型方案,但要能在答辩时说出“为什么用联合索引而不是Redis锁”——因为成本低、无外部依赖、对当前系统的并发规模完全够用,这已经超出大多数毕设项目的深度了。

6.3 写测试用例:Service层单元测试怎么规划

如果论文里有“系统测试”章节,与其贴截图,不如贴单元测试代码和测试覆盖率。Spring Boot对JUnit 5的支持非常友好,写Service层测试只需要在测试类上注解@SpringBootTest和@Transactional。@Transactional的作用是每个测试方法结束后自动回滚,不会污染数据库。三个核心测试方法就能覆盖挂号模块的核心链路——创建预约、取消预约、创建医嘱并结算。

@SpringBootTest @Transactional class AppointmentServiceTest { @Autowired private AppointmentService appointmentService; @Test void testCreateAndCancelAppointment() { Appointment appointment = new Appointment(); appointment.setPetId(1); appointment.setDoctorId(2); appointment.setAppointmentTime(LocalDateTime.now()); boolean createResult = appointmentService.createAppointment(appointment); assertTrue(createResult); boolean cancelResult = appointmentService.cancelAppointment(appointment.getId(), 1); assertTrue(cancelResult); // 已取消的挂号不能重复取消 assertThrows(ServiceException.class, () -> appointmentService.cancelAppointment(appointment.getId(), 1)); } }

测试代码不是给系统增加负担,而是给自己留一个“方不方便改代码”的定心丸——后面改状态机逻辑或者调整表结构时,跑一遍测试就知道有没有改坏。答辩时你告诉评委“我的Service层有三个核心测试用例,覆盖了状态机的正常流转和边界情况”,比口头描述系统功能稳定可靠得多。

最后说一个我做这类项目一直保持的习惯:每改完一个模块的代码就重新跑一遍那一个模块的测试,把改动标记在论文的设计章节对应的段落旁边。答辩前一周,顺着测试用例去回顾每个功能的实现细节,比从头看代码效率高。希望这篇实操笔记能帮你把这个毕设项目从解压到答辩的路走得顺一些,省下些没用但折腾人的时间,把精力留到真正能讲清楚的技术点上。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 7:47:48

AI开源知识库全链路管理技术与实践

1. AI开源知识库全链路知识管理能力解析最近两年&#xff0c;AI技术正在深刻改变知识管理的方式。作为一名长期关注知识工程的技术从业者&#xff0c;我完整经历了从传统Wiki到智能知识库的演进过程。今天想和大家深入聊聊现代AI开源知识库的全链路能力——这不仅仅是简单的文档…

作者头像 李华
网站建设 2026/10/27 7:47:16

易方虚拟主机销售系统 EfangIsale 7.0:老 ASP.NET 源码部署与上线避坑指南

简介&#xff1a;一套面向IDC服务商及ASP.NET开发者的虚拟主机在线销售系统源码&#xff0c;对应易方虚拟主机销售系统EfangIsale 7.0版本&#xff0c;覆盖域名管理、备案录入、主机开通、交易结算等典型销售环节&#xff0c;可作为商业项目二次开发或学习ASP.NET服务端架构的参…

作者头像 李华
网站建设 2026/10/27 7:47:01

揭秘Harness:同一模型为何判若两人

这阵子帮几个朋友调模型应用&#xff0c;发现一个特别有意思的现象&#xff1a;大家本地都部署了同一个开源模型&#xff0c;但有人觉得"这模型真聪明"&#xff0c;有人骂"这模型就是个傻子"。同样的模型&#xff0c;差距咋就这么大呢&#xff1f;排查到最…

作者头像 李华
网站建设 2026/10/27 7:46:48

基于AI Agent与飞书机器人的车载HMI自动化测试平台实践

1. 为什么做这套平台&#xff1a;车载HMI自动化测试的痛点与目标车载HMI自动化测试这个事儿&#xff0c;做过的人都知道痛在哪&#xff1a;一个中等规模的智能座舱项目&#xff0c;光蓝牙、导航、语音、仪表这几个域的回归测试&#xff0c;手工点下来就是好几个小时&#xff0c…

作者头像 李华
网站建设 2026/10/27 7:46:09

有限差分法求解雷诺方程:MATLAB气体静压轴承仿真源码精度与实战

简介&#xff1a;利用有限差分法求解雷诺方程、开展气体静压轴承特性分析的MATLAB源码项目&#xff0c;面向从事流体润滑数值计算的研究人员、机械设计工程师&#xff0c;以及具有初步编程基础的高校学生。项目核心是采用有限差分法离散稳态雷诺方程&#xff0c;通过迭代计算获…

作者头像 李华
网站建设 2026/10/27 7:45:58

小模型Agent工具调用优化与ATLAS架构解析

1. 小模型Agent工具调用能力的现状与挑战在AI领域&#xff0c;小模型Agent因其轻量化和高效性正受到越来越多的关注。不同于动辄数百亿参数的大语言模型&#xff0c;小模型通常指参数量在10亿以下的轻量级模型。这类模型在边缘设备、嵌入式系统和实时应用中展现出独特优势&…

作者头像 李华