
SpringBoot宠物医院管理系统做完了前后端加起来小半年时间源码在文末有说明。这套系统解决的是宠物医院最头疼的三件事挂号排队乱、病历纸质堆、药房库存对不上。如果你是做毕业设计、课程设计或者自己开宠物诊所想上一套轻量级信息管理系统这篇内容可以直接帮你少走一大半弯路。我先把这套系统的核心思路、技术选型、关键实现和部署踩坑记录完整写下来从建表到跑通全流程都有不绕弯子。1. 项目定位与整体设计思路1.1 宠物医院管理到底管什么很多人一听管理系统就想到高大上的数字化平台但宠物医院和综合医院完全是两个量级。我在设计这套系统之前蹲过几家宠物诊所的日常运营发现真实痛点非常具体前台小妹上班第一件事是翻纸质挂号本医生开完处方要手写三份单子一份给主人、一份给药房、一份存档药房小哥对库存全靠记忆月底盘点和进货完全对不上账。这才明白宠物医院管理系统的核心不是电子化而是把医院日常经营中反复出现的流程动作固化成系统规则。基于这个定位我最终把系统拆成六大业务域宠物档案与主人信息、前台挂号与分诊、医生问诊与电子病历、药房库存与处方发药、住院寄养管理、经营统计分析与系统配置。每个业务域都围绕一条实际业务链路展开比如挂号-就诊-开方-划价-取药-回访这条线系统里每一步都有对应功能承接。可以说这套系统的设计原则只有一个让数据跟着业务走而不是让业务适应系统。1.2 为什么选择 SpringBoot 作为技术底座技术选型的时候我不是没犹豫过。宠物医院管理系统属于典型的中小型企业级应用业务复杂度中等但并发要求不高最重要的是开发效率和后期维护成本。SpringBoot 在这类场景下几乎是标准答案原因有三。第一是自动装配带来的开发效率提升。你只需要在 pom 里引入一个spring-boot-starter-web内嵌 Tomcat 就帮你把 Web 环境搭好了不需要像传统 SSM 框架那样写一堆web.xml、spring-mvc.xml配置。第二是生态成熟到几乎能找到所有需要的轮子。数据库用 MyBatis-Plus 操作安全认证用 JWT缓存用 Redis接口文档用 Knife4j文件存储用 MinIO这些在 SpringBoot 生态下都有非常成熟的集成方案不需要自己造轮子。第三是部署足够简单一个java -jar就能跑起来对没有专职运维的小医院来说尤其友好。另外多说一句这套系统我采用 SpringBoot 2.7.x 版本而不是最新的 SpringBoot 3.x。原因很现实SpringBoot 3 强制依赖 JDK 17 并且 Jakarta EE 命名空间变更对于很多还在用 JDK 8 的教学环境和存量服务器来说兼容成本太高。如果你的机房或教学环境已经全面 JDK 17可以直接上 3.x代码迁移成本不算大但没必要为了新而新。1.3 前后端分离还是服务端渲染这是我在项目初期纠结最久的问题。如果做服务端渲染用 Thymeleaf 模板一个人开发确实更快但那意味着每次改页面都必须重新部署后端服务而且移动端适配非常麻烦。考虑到现在的宠物医院绝大多数都希望用手机号登录、在小程序或平板端查看数据我最终选定前后端分离方案。后端负责纯 API 接口返回统一 JSON 格式前端用 Vue 2 Element UI 搭建后台管理界面独立部署在 Nginx 上。前后端通过 RESTful 风格接口通信用 JWT token 做身份校验。这套架构的好处是以后不管要做小程序端、微信端还是自助挂号大屏后端接口完全可以复用只需要新增前端应用即可。在实际开发中我踩过一个坑一上来就定义接口返回格式结果后端改了几次字段结构前端也跟着改了三次。所以强烈建议你先定死统一返回体code、message、data 三层结构再开发具体业务接口这套系统里的Result类就是干这个用的。2. 核心技术选型与设计细节2.1 数据库设计从需求到表结构宠物医院的数据量不会像电商那样爆炸但表和表之间的关系并不简单。我设计了 14 张核心表其中最关键的几张表设计思路如下。宠物档案表的核心是一主一宠多关联一个主人可能带多只宠物来就诊所以宠物表里冗余了主人 ID。这里我用了逻辑删除字段deleted而不是物理删除。原因很实际宠物医疗数据有连续追踪价值比如同一只猫的疫苗记录、过敏史、历次诊断都是关联分析的基础删掉就真的没了。逻辑删除在 MyBatis-Plus 里只需要配置一个TableLogic注解查询时自动过滤成本极低强烈建议保留。挂号记录表是业务链路的起点状态字段我设计了0待就诊、1就诊中、2已完成、3已取消、4已过号五种状态。这里有过一个设计教训一开始我把状态设计成布尔值is_finished结果业务一跑发现根本不够用比如过号患者需要重新排队、医生临时停诊需要批量取消挂号这些都要有独立状态才能支撑。所以凡是涉及流程流转的业务表状态字段一定要用可扩展的数值枚举别偷懒。处方表和处方明细表是典型的主子表结构这也是整个系统里数据一致性要求最高的地方。一张处方对应多个药品明细前端一次性提交主表信息和明细列表后端必须用事务去处理否则会出现主表写成功、明细写失败的数据脏状态。我在后面代码部分会专门讲这里的事务处理。2.2 身份认证JWT 无状态登录方案管理系统没有登录验证就是裸奔但传统 Session 方案在前后端分离架构下体验并不好跨域要配置 CORS 允许携带 CookieSession 还要考虑共享问题。所以我选了 JWTJSON Web Token方案。用户在登录接口提交账号密码后端校验通过后生成一个包含用户 ID、角色、过期时间的 token 字符串返回前端后续每个请求都在 header 里带Authorization: Bearer token后端通过拦截器统一解码校验。这样做的核心优势是服务端不需要存储会话状态水平扩展的时候不用考虑 Session 同步天然适合后面接 Nginx 负载均衡。但 JWT 也有它的问题token 一旦签发在过期之前是无法主动作废的如果有用户修改密码或管理员封禁账号旧 token 依然有效。我这套系统采用的折中方案是设置合理的过期时间默认 8 小时同时在用户表里维护update_time做被动校验。如果你后续要支撑严格的权限管理建议引入 Redis 做 token 黑名单机制把登出用户的 jti 拉黑。2.3 文件存储宠物影像与处方附件的落地方案宠物医院的影像文件X光、B超、伤口照片比较特殊单张不会特别大但数量增长很快而且需要支持医生的后续查阅对比。我不建议直接存 MySQL 的 BLOB 字段那会把数据库拖垮。系统里我做了两层方案本地服务器磁盘存储用于快速上线按日期目录保存文件同时预留了 MinIO 对象存储的集成方式便于后续扩展。本地存储的逻辑很简单前端上传文件到后端/api/file/upload后端把文件写到服务器的upload/目录文件名用 UUID 重命名避免冲突然后把访问路径存到数据库对应字段。实际使用中注意两件事一是要用绝对路径配置而不是相对路径否则用java -jar启动和工作目录启动时路径行为不一致二是 Nginx 代理层要做好上传大小限制配置否则拍个稍微大点的影像文件就会被 nginx 拦截下来。我后期把 MinIO 也接上了这里分享一句实际体会如果你手头有 NAS 或者小型服务器直接上 MinIO 是更稳妥的选择文件备份、迁移、权限控制都比磁盘路径省心得多。3. 系统核心功能模块拆解3.1 前台挂号与就诊流程联动前台挂号模块是整个系统的门面体验好不好直接决定大家愿不愿意用。我实现的流程是前台输入主人手机号查询关联宠物档案选择宠物后自动带出主人姓名和宠物基本信息选择科室、医生、号源时段提交后生成挂号记录。这里做了一点自动化处理——同一宠物如果存在就诊中或待就诊的记录系统会弹窗提醒避免重复挂号造成医生侧排队混乱。医生端接诊后在待诊列表点击接诊挂号状态自动变为就诊中同时系统自动记录接诊时间。这一步在数据上非常关键因为后面统计医生工作量、平均接诊时长都要依赖这个时间点。我建议所有涉及流程状态变化的操作都在服务端同步更新对应的时间字段而不是让前端传时间。前端传时间有两个问题一是用户可以篡改二是各客户端时钟不一致会导致统计失真。3.2 电子病历与处方管理核心业务的难点拆解电子病历是宠物医院管理系统里技术含量最高的模块难不在增删改查而在病历的持续性和结构化。我做的病历主表记录就诊时间、主诉、诊断结论、医嘱说明同时用病历明细类型字段标注是初诊还是复诊。复诊的时候系统会把历次病历按时间线拉出历史卡片医生可以快速对比病情变化。这里我强烈建议病历内容做结构化录入而非纯文本病史、体温、体重、诊断结果这些字段单独存方便后续做数据统计和病情筛选。处方管理的主子表结构上面提到过这里讲事务实现。提交处方时后端接收prescription主对象和prescriptionItems明细列表事务方法里先插入主表拿到自增 ID再遍历明细列表逐条插入。重点是插入过程中的库存预校验如果某药品库存不足直接抛异常回滚整个事务避免出现开了处方但药房发不出药的情况。这个校验逻辑必须在事务内完成否则就会出现并发场景下两个订单同时扣减同一药品库存的脏数据。3.3 药房库存与采购预警药房库存模块设计得比较朴素但很实用每种药品有库存数量、预警阈值、进价、零售价四个核心字段。发药时库存自动扣减入库时数量自动增加每次操作都写库存流水记录stock_log方便财务和库管对账。预警逻辑我放在每次库存变动的逻辑里和定时任务里双重触发一旦库存低于阈值系统生成预警记录并在首页提醒。有个数据上的坑需要特别说金额一律用整数分存储而不是浮点数元。药品单价、处方金额、统计报表金额全部以分为单位。这是因为浮点数在 MySQL 计算中会出现精度丢失比如 0.1 加 0.2 不等于 0.3。用分存储虽然写起来要多一个单位换算但统计报表对得上账、不出幺蛾子这笔账很划算。3.4 住院寄养与疫苗提醒住院寄养是很多宠物医院管理系统容易忽略的模块但实际是重要收入来源。我的住院模块围绕笼位做状态管理笼位状态有空闲、占用、已消毒、维修中。宠物办理住院时选择一个空闲笼位创建住院记录关联到看护医生和宠物档案。每天的护理操作喂食、换药、体征检查记录到住院护理日志里出院时自动结算住院天数乘以床位费加护理费。疫苗提醒的实现用 SpringBoot 的Scheduled定时任务每天凌晨跑一次查询所有宠物档案的最后一次疫苗接种时间按不同疫苗种类计算到期提醒日期把即将到期未来 30 天内或已超期的宠物筛选出来生成提醒记录并推送到医生工作台。这里注意后台定时任务默认是单线程执行的如果你的系统后面接了短信推送或者消息队列一定要给Scheduled配置线程池否则多个任务会相互阻塞。3.5 经营统计报表报表模块是给老板看的核心价值是一眼看懂今天赚不赚钱。我的统计逻辑围绕三张表挂号收入明细、药品销售明细、住院收费明细。按日、按周、按月三个时间维度聚合输出总营收、挂号收入、药品收入、住院收入、处方量、客单价等指标。这里用到 MyBatis-Plus 的聚合查询不需要额外引入复杂的报表引擎。有个业务细节值得分享药品这一块的毛利不能用售价减进价乘数量直接算因为存在临期折扣、会员折扣、整单减免这些营销活动。我的做法是在处方明细表里冗余discount_amount字段开处方时就计算当前用户的折扣并写入统计的时候直接汇总这样报表口径是唯一且可追溯的。4. 关键代码实现与配置细节4.1 项目结构初始化与基础配置项目是标准的 Maven 多层级结构按业务域分包controller、service、mapper、entity、dto、vo、config、common、utils。我强烈建议新手不要只用三层结构Controller-Service-Mapper一定要加dto入参对象和vo出参对象否则后期接口一旦出现字段调整直接改实体类会牵连数据库映射。核心依赖pom.xml配置如下parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies这里特别说一下 MyBatis-Plus 版本。3.5.x 系列对 SpringBoot 2.x 兼容性最稳不要图新用 3.5.3 以上的版本配合 mybatis-spring 的版本冲突。我踩过一个坑MyBatis-Plus 3.5.4 强制升级了 mybatis-spring 版本需要手动排除冲突依赖否则启动时会报Property sqlSessionFactory or sqlSessionTemplate are required。4.2 统一返回体与全局异常处理统一返回体是所有接口的数据契约直接决定前后端联调效率。我的ResultT结构如下Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } public static T ResultT error(String message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } }code用 HTTP 语义的简化版200 成功、500 业务失败、401 未授权。不要设计复杂的业务码体系对小型系统来说维护成本远大于收益。全局异常处理用RestControllerAdvice捕获两类异常业务异常自定义BusinessException直接返回错误信息系统异常Exception记录日志并返回系统繁忙通用提示。这里有个小细节全局异常处理里一定要对MethodArgumentNotValidException做单独处理否则前端传参数校验不通过时拿到的错误信息是 JVM 级别的英文用户体验很差。4.3 MyBatis-Plus 条件查询与分页插件配置MyBatis-Plus 的分页插件配置非常简单但很多人会漏掉MapperScan注解的扫描路径。分页插件核心是拦截器必须在配置类里声明Configuration MapperScan(com.hospital.*.mapper) public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }分页查询我用LambdaQueryWrapper写条件比字符串写字段名安全得多重构实体类字段名时不会出现运行期报错LambdaQueryWrapperPetEntity wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(keyword), PetEntity::getName, keyword) .eq(type ! null, PetEntity::getType, type) .orderByDesc(PetEntity::getCreateTime); PagePetEntity page petMapper.selectPage(new Page(current, size), wrapper);注意like条件第一参数传了StringUtils.hasText(keyword)这个布尔判断非常关键否则前端传空字符串时 SQL 会拼接出一个查不到结果的LIKE %查询返回空列表而不是全部数据。4.4 全局过滤器处理 XSS 攻击与请求安全很多人的第一个管理系统完全没有做 XSS 防护直到被嵌入脚本才发现问题。宠物医院管理系统里有大量文本录入场景病历、医嘱、主人留言如果这些字段没做转义恶意脚本就能存到数据库里在其他用户打开时执行。我实现的方案是基于OncePerRequestFilter的全局请求包装过滤器拦截所有 POST、PUT 请求把请求体里的 HTML 标签、script关键字、javascript:协议等做转义处理。核心代码如下public class XssFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { if (isJsonRequest(request)) { XssHttpServletRequestWrapper wrappedRequest new XssHttpServletRequestWrapper(request); filterChain.doFilter(wrappedRequest, response); } else { filterChain.doFilter(request, response); } } private boolean isJsonRequest(HttpServletRequest request) { return StringUtils.hasText(request.getContentType()) request.getContentType().contains(application/json); } }这里有个实际业务的坑必须说病历内容里医生会写伤口 2cm这类带小于号的文本如果无脑过滤所有符号病历内容会被错误修改。所以我的实现是只过滤危险标签白名单和危险协议前缀比如script、iframe、onerror、javascript:而不是一刀切转义所有符号。如果你的业务场景里有 Markdown 编辑需求更要注意这个度全量转义会破坏格式。另外我需要强调过滤器只是第一层防护真正的数据安全还要靠数据库预编译 SQL防止注入 参数校验限制类型长度 权限拦截。三层叠加才是一个完整的安全闭环不要以为装一个过滤器就万事大吉。4.5 定时任务与疫苗提醒的完整实现疫苗提醒模块我用Scheduled实现每天凌晨固定执行一次配合EnableScheduling开启调度。核心逻辑是根据疫苗种类计算下次应接种日期犬瘟热疫苗首次接种后间隔 21 天打第二针之后每 365 天加强一次狂犬疫苗每年一次。Component Slf4j public class VaccineReminderTask { Resource private PetRecordService petRecordService; Scheduled(cron 0 30 2 * * ?) public void execute() { ListPetRecordEntity pets petRecordService.listDueVaccinePets(30); for (PetRecordEntity pet : pets) { // 生成提醒记录 petRecordService.createReminder(pet.getId(), 疫苗到期提醒); } log.info(疫苗提醒任务完成共提醒 {} 只宠物, pets.size()); } }定时任务在生产环境要特别注意时间问题用户服务器的系统时区如果不是Asia/Shanghai凌晨 2 点半执行的定时任务可能是在早上 8 点半执行的。解决办法是在启动命令里显式指定时区-Duser.timezoneAsia/Shanghai或者在application.yml里配置spring.jackson.time-zone: GMT8客户端展示时间的时区问题也一并解决。5. 项目部署与源码使用指南5.1 源码导入与数据库初始化拿到源码后导入 Idea 的步骤是固定的File - New - Project from Existing Sources 选择根目录的pom.xmlMaven 会自动拉取依赖。这里有个常见问题Maven 依赖下载慢或者失败。解决方法是在settings.xml里配置阿里云镜像仓库不要裸连 Maven 中央仓库。另外 JDK 版本我建议用 1.8 或 11不要用 17 以上编译避免 javac 版本过高导致语法兼容问题。数据库初始化脚本在源码的sql/目录下一个.sql文件包含建库、建表和测试数据。执行时用 Navicat 或命令行均可注意先创建字符集为utf8mb4的数据库再执行脚本。utf8mb4不是可选项它决定了你能不能存宠物名字里的 emoji 和生僻字。我之前吃过亏数据库用默认utf8结果给一只名字带日文的猫建档直接报错。5.2 本地运行配置详解核心配置集中在application.yml三块内容需要按自己的环境改server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/hospital?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 password: mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0启动时如果提示连不上数据库先检查 MySQL 服务是否启动、账号密码是否匹配、端口是否被占用。如果提示 Redis 连接失败是因为登录验证和 token 缓存依赖 Redis必须先把 Redis 服务启动起来不需要改配置。另外log-impl这段配置建议本地调试时开启可以打印所有 SQL 语句方便排查问题生产环境记得关掉。5.3 我的启动步骤记录我实际跑通的完整步骤记录如下照着做不会有问题启动 MySQL 服务执行sql/hospital.sql脚本初始化数据库。启动 Redis 服务Windows 下直接运行redis-server.exeLinux 下systemctl start redis。打开 Idea导入项目后等待 Maven 依赖下载完毕在src/main/resources/application.yml中修改数据库账号密码。运行HospitalApplication.java的main方法看到Started HospitalApplication日志说明启动成功。浏览器访问http://localhost:8080/doc.html查看接口文档用脚本里内置的管理员账号admin/admin123登录。前端部分是单独的 Vue 工程启动前先npm install安装依赖再npm run serve起本地开发服务。这里注意 Vue 工程也要改一下代理配置vue.config.js里的后端地址否则接口请求会 404。改完后访问http://localhost:8081就能看到完整的管理界面。5.4 Docker 部署实战记录很多同学做完项目只在本地跑通一到要部署到服务器就懵。我把 Docker 部署方式写成一个标准操作流程。首先准备一个精简的 DockerfileFROM openjdk:8-jre-alpine MAINTAINER yourname RUN mkdir -p /app WORKDIR /app COPY hospital.jar /app/app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar, --spring.profiles.activeprod]执行构建命令docker build -t hospital-server .然后运行容器docker run -d --name hospital-server \ -p 8080:8080 \ --network host \ hospital-server用--network host模式直接让容器共享宿主机网络这样容器内连接localhost:3306就能直接访问宿主机 MySQL不用做复杂容器互联配置。这是小型项目单人部署最省心的方式。如果你要上docker-compose就把 MySQL、Redis、后端、前端四个容器编排在一起这个方案适合有多个服务要一起管理的情况。部署中有两个坑我反复遇到一是-p只映射了端口但宿主机的防火墙没开对应端口导致外部访问不通二是数据库连接串里的时区参数没写导致时间段查询数据偏移。检查顺序建议是先看容器日志再测端口连通性再查数据库连接配置。不要一上来就猜防火墙问题。5.5 源码获取与二次开发建议源码完整结构包含hospital-serverSpringBoot 后端工程、hospital-webVue 前端工程、sql数据库脚本和doc接口文档说明适合直接作为毕设或课设的基础项目。如果用于毕设答辩我建议在现有系统上做两个有区分度的扩展点一个是在药房模块增加药品批次效期管理一个是在统计报表里引入图表可视化。另外如果你拿到的源码运行报错优先检查三处Maven 依赖是否完整下载、数据库脚本是否在当前 MySQL 版本成功执行、Redis 服务是否启动。这三个占了启动失败 90% 的原因。我见过太多人项目跑不起来最后发现只是 redis 没装好或者数据库脚本执行到一半报错就中止了。6. 常见问题与排查技巧实录6.1 启动阶段高频报错速查表我把实际答疑过程中被问得最多的报错整理成一张表基本都是这几个原因报错信息根因解决方案Port 8080 was already in use端口被占用netstat -ano找到占用进程换端口或杀进程Failed to configure a DataSource数据源配置错误检查application.yml的 url、账号、密码Access denied for user rootlocalhost数据库账号无权限用 root 登录执行授权 SQLUnable to connect to RedisRedis 未启动或密码错误启动 Redis、检查spring.redis配置Unknown database hospital数据库未创建执行建库语句或重新执行 sql 脚本Invalid bound statement (not found)Mapper 接口与 XML 映射不一致检查 Mapper 接口路径和 XML namespace排查问题的时候要养成看完整堆栈的习惯不要只看第一行。SpringBoot 的报错信息其实写得很明确大多数情况下错误原因就藏在后半段。把堆栈完整贴到搜索引擎比只看第一行找答案快得多。6.2 前后端联调的经典问题联调阶段的问题主要集中在三个地方。第一个是跨域前端页面在 8081 端口后端接口在 8080 端口浏览器默认会拦截跨域请求。解决办法是在后端加全局 CORS 配置类允许指定来源和请求头开发环境也可以像上面说的用vue.config.js配置代理两种方案我都测试过建议后端 CORS 配置为主代理方案放前端兜底。第二个是 token 失效问题。前端登录后 token 存在 localStorage每次请求通过 Axios 拦截器加到 header 里。但刷新页面后 Axios 实例重新创建如果代码里没有统一从 localStorage 读取 token就会出现明明登录了请求却返回 401的情况。建议封装一个request.js在请求拦截器里统一取 token 注入。第三个是日期格式不一致。后端返回的时间默认是2024-01-15T10:30:00这种带 T 的格式前端表格直接展示会很难看。统一在配置里加spring.jackson.date-format和time-zone让所有接口返回格式统一的yyyy-MM-dd HH:mm:ss省去前端每个页面单独格式化的麻烦。6.3 二次开发中容易踩的坑如果你计划在这个系统上加功能我提供一个清单帮你避坑。第一个坑是逻辑删除带来的唯一索引冲突。假设药品表有唯一索引uk_name逻辑删除后重新插入同名药品会因为旧记录还在而报唯一索引冲突。解决办法是删除唯一索引或者在业务层先查询包含已删除记录在内的数据做判断。第二个坑是主子表事务操作。开处方、创建挂号记录、办理住院这些涉及多表写入的操作一定要在 Service 层加Transactional(rollbackFor Exception.class)注解而且要注意自调用事务失效的问题——同一个类内部 A 方法调用 B 方法B 方法的事务注解是不生效的。第三个坑是 MyBatis-Plus 的saveOrUpdate方法。这个方法的判断逻辑依赖主键是否为空如果你在业务里给实体类设置了不恰当的主键值它会走更新而不是插入分支导致数据被覆盖。建议批量新增或导入场景统一用saveBatch或显式判断主键。这些坑都属于不会报错但结果悄悄变错的类型排查起来比报错更费时间。我的习惯是在每个关键写操作后打印日志把 SQL 执行前的主键值和影响行数输出出来上线前盯着日志看几轮能拦下大部分问题。6.4 从实际使用中总结的几个优化点系统跑了一段时间后我根据实际运营反馈做了几个针对性优化这些优化点你也可以直接用在自己项目里。挂号模块增加过号重排逻辑后前台和医生的纠纷明显变少。原来的逻辑是过号直接作废主人只能重新挂号但很多主人觉得不公平排队等了半天过去 5 分钟就被取消。现在的逻辑是做状态标记而不是删除医生叫号时如果当前号码已过号系统会优先安排最近的过号患者复诊既不占新号源也能安抚情绪。药房库存扣减从提交处方即扣减调整为药房确认发药才扣减。这个改动的价值在于医生开出的处方如果主人不缴费库存不应被扣减否则会出现账面库存和实际库存不一致。原来的设计虽然代码简单但库存报表每个月都对不上改成发药确认扣减后库存正确率提升非常明显。疫苗接种提醒原来只有站内提醒后来加了每日汇总的方式。每天早上把当天需要提醒的宠物列表喂给 AI 生成个性化文案由客服统一触达虽然技术上只是多了一个查询条件但运营效率提升很大。如果你也想加这个功能只需要在定时任务里按remind_date字段筛选即可核心表结构在设计时就预留了这个扩展点。我自己的体会是管理系统这类项目功能列表是骨架真正见功夫的是业务细节的打磨。你在跑这套系统的时候一定要试着从医生、前台、药房、老板四个角色各自的角度去操作一遍只有每个角色都觉得好用系统才算真正落地。源码里所有的表结构、接口文档、前后端代码都是完整可跑的改改配置就能起服务你也可以在这个基础上去做自己的业务扩展。最后再说一句如果启动遇到问题先把日志从下往上完整读一遍再对照我上面的排查表逐项检查基本不会再卡在翻越第一座山的地方。