1. 宠物医院管理系统的需求边界:别把毕设做成通用增删改查
做这个项目之前,我一直在想一个问题:宠物医院管理系统,和网上满天飞的“XX管理系统”到底差在哪里?后来我去某宠物诊所蹲了半天,看完前台、护士和医生的日常工作,才意识到这类系统的核心不在“管理”两个字,而在“医疗流程”。
那天的真实场景是这样的:前台用一本厚Excel登记宠物主信息,疫苗记录散落在纸质病历本里,狗三联疫苗到底打没打第三针,要翻完整个本子才能确认。医生开完处方之后,护士要手动记下下次疫苗日期,经常有宠物主被漏掉提醒。整个诊所的数字化程度约等于零,但业务量又确实不小,需要一个能跑通“建档—预约—接诊—诊疗—疫苗—回访”的闭环系统。这才是标题里“萌宠医疗健康档案管理平台”和“疫苗接种管理系统”的真正含义:它不是一个PRJ增删改查玩具,而是一个贴近真实业务状态的平台。
这个项目做下来,我觉得它最适合三类人:一是计算机相关专业的毕业设计选题,尤其是分布式、前后端分离方向的同学;二是想转Java后端、需要一个完整项目练手的开发者;三是小诊所或宠物店希望自建管理系统、但又没有预算买商业SaaS的个体经营者。无论你是哪一类,核心诉求都一样:把散落在Excel和纸质单据里的数据,变成一套结构化的、可查询、可追踪、可提醒的在线档案。
需求拆解阶段我列了一个比较实在的功能清单:
- 宠物主与宠物档案管理:宠物主信息、宠物基本信息、过敏史、绝育情况、体重变化记录
- 预约与排班:在线预约、现场挂号的候诊队列、医生排班
- 诊疗管理:病历记录、症状、诊断结果、处方明细、费用结算
- 疫苗管理:疫苗批次、接种记录、下次接种日期计算、到期自动提醒
- 库存管理:疫苗库存、常用药品库存,出库自动扣减
- 权限角色:管理员、医生、护士/前台、宠物主(前台用户)四类角色
- 报表统计:接诊量趋势、疫苗到期分布、热门宠物品种分布
如果你只是想“交个毕设”,那第6条和第7条可以简化;但如果你是给真实诊所做系统,那我建议一条都别省。很多所谓宠物医院管理系统做出来不好用,不是代码不行,而是业务链没打通。比如疫苗记录和库存没关联,护士给宠物打完针之后还要单独去库存表减数,多一步操作就多一个出错机会。
还有一点值得强调:系统里的“状态”很重要。预约有“待就诊—已到达—诊疗中—已完成—已取消”,诊疗记录有“初诊—复诊—复查”,订单有“待支付—已支付—已退款”。状态枚举要在数据库层和前端同时约束,不然就会出现“预约还没完成就已经开出了处方”这种逻辑漏洞。我在设计时把状态流转当成一个重要参与者,后面代码实现时才没跑偏。
2. 技术选型:SpringBoot + MyBatis Plus 的毕业设计配置
技术栈的选择,我直接套用了当下Java后端项目里最成熟、最不容易翻车的组合:SpringBoot 2.7 + MyBatis Plus 3.5 + MySQL 8.0 + Redis + Sa-Token,前端用 Vue3 + Element Plus + ECharts。
先解释一下为什么是这套组合,而不是更“新潮”的Spring Cloud微服务或者纯JPA方案。
SpringBoot 2.7是目前兼容性最稳的版本,JDK8环境下跑起来零障碍,网上几乎所有的坑都有人踩过了。有些同学一上来就选SpringBoot 3.x + JDK17,确实新,但很多老教程、老依赖不兼容,遇到问题查资料的时间比你写代码的时间还长。毕业设计的第一原则是稳定交付,不是秀新技术。
MyBatis Plus对比原生MyBatis,最大的好处是单表CRUD不用写SQL,自带分页插件、逻辑删除、自动填充。宠物档案、预约、疫苗记录这类表,90%的操作都是单表读写,用MP能省掉大量重复的Mapper XML。但多表关联查询还是得自己写SQL,这一点不矛盾:单表交给框架,多表交给手写SQL,效率和质量都兼顾。
鉴权我用的是Sa-Token而不是Spring Security。原因很直接:Sa-Token的API设计对新手极其友好,登录、登出、权限校验就几个方法调用,不像Spring Security那样要配一堆Filter和SecurityConfig。宠物医院这种系统权限不算复杂,Sa-Token够用且代码更简洁。
Redis在这个系统里的定位有三个:存储登录Token、缓存预约时间段的锁状态、存储疫苗提醒的待办任务缓存。如果你不想引入Redis,也可以用MySQL做个简单的token表,但疫苗提醒的并发扫描任务一多,MySQL做分布式锁会很别扭,所以我还是建议把Redis加上,成本不高,但架构上显得完整。
前后端分离是我推荐的方案。Vue3 + Element Plus做后台管理界面非常快,ECharts画报表也是现成的。如果你前端基础薄弱,直接用Thymeleaf模板渲染也可以,但这样项目演示时视觉冲击力会弱不少。毕设答辩时,一套看得过去的UI,能让评委对项目的印象分肉眼可见地提高。
环境版本我整理了一张表,照着配基本不会出问题:
| 组件 | 版本 | 说明 |
|---|---|---|
| JDK | 1.8 | 兼容性最好的选择,避免高版本带来的坑 |
| SpringBoot | 2.7.x | 稳定,文档多,插件适配全 |
| MyBatis Plus | 3.5.x | 单表CRUD零SQL |
| MySQL | 8.0 | 支持JSON字段,性能足够 |
| Redis | 6.x或7.x | Token缓存、预约锁、提醒任务 |
| Sa-Token | 1.34+ | 轻量鉴权,比Security好上手 |
| Vue | 3.x | 组合式API,配合Element Plus |
| Node.js | 16+ | 前端构建环境 |
项目骨架我建议用Maven多模块还是单模块?如果是真实诊所用,可以拆成common、system、business三个模块,方便后续扩展。如果是毕设,单模块就够了,别为了模块化而模块化,评委会问你“为什么拆模块”,你要是答不上来反而扣分。我在这个项目里用的是单模块,但包名按业务拆清楚:controller、service、mapper、entity、dto、vo、task,结构一目了然。
3. 数据模型设计:七张核心表串起萌宠医疗健康档案
数据模型是整个系统最容易提前出问题的地方。很多同学拿到需求就直接建表,建到一半发现缺字段,又回头改表,表结构改一次,关联的Mapper和前端页面就要跟着动,非常痛苦。我这次是先画ER图再建表,前后调整了三次才定稿,核心表是七张。
第一张是宠物主表,我命名为pet_owner。字段包含宠物主ID、姓名、电话、微信昵称、会员卡号、地址、创建时间。宠物主和宠物是一对多关系,一个宠物主可以养三只猫两只狗,所以宠物表必须单独建。
第二张是宠物表pet,这是整个系统的核心资产表。字段包括:宠物ID、宠物主ID、宠物名称、品种、物种(猫/狗/其他)、性别、出生日期、体重、是否绝育、过敏史、疫苗接种状态、头像URL、备注。关键点在于“过敏史”不能做成单字段,要支持多条过敏源,我用了冗余存储的方式:主表存一个过敏史摘要字段,同时建一张pet_allergy明细表,这样列表页和详情页都能快速展示,又不会丢细节。
第三张是预约表pet_appointment。字段:预约ID、宠物ID、医生ID、预约日期、时间段、症状描述、状态、创建时间。时间段的存储我用date + time_slot两个字段,time_slot存的是“09:00-10:00”这种字符串。有人会问为什么不用开始时间和结束时间两个字段?因为前端展示时间段列表更直观,而且排班规则通常是固定时间段而不是任意起止时间,字符串字段够用,查询也简单。
第四张是诊疗记录表diagnosis_record。字段:记录ID、宠物ID、医生ID、接诊日期、主诉、诊断结果、处理意见、下次复查日期、创建时间。病历这类数据属于医疗档案,只允许追加,不允许物理删除。我在业务层把删除接口直接屏蔽了,只保留“作废”状态位,最大程度避免误删。
第五张是处方明细表prescription_item。字段:明细ID、诊疗记录ID、药品ID、药品名称、数量、单价、用法用量。这里有个经验:药品名称要用快照字段存一份,不能只关联药品ID,否则药品名称改了之后历史处方上的名称也变了,不符合医疗记录的可追溯原则。
第六张是疫苗接种记录表vaccine_record。字段:记录ID、宠物ID、疫苗ID、接种日期、下次接种日期、接种医生ID、疫苗批次号、不良反应备注、状态、创建时间。这表是整个“疫苗接种管理系统”的心脏,下次接种日期是疫苗提醒功能的数据来源。
第七张是疫苗与药品库存表vaccine_stock和drug_stock。疫苗库存字段:疫苗ID、疫苗名称、生产企业、批号、进货日期、有效期、库存数量、预警阈值。库存和疫苗记录要联动:每生成一条接种记录,对应库存减一。我在数据库层面没做触发器,而是在Service层的事务里同时更新疫苗记录和库存,避免了触发器维护困难的问题。
除了这七张核心表,还有后台用户表sys_user和角色权限表,我沿用了常用的RBAC设计:用户表、角色表、权限表、用户角色关联表、角色权限关联表。宠物主端和后台管理系统分开登录逻辑,但共用一个用户体系,通过user_type区分内部用户和前端宠物主。
建表时我特别强调几个约束:
- 所有表都带
deleted逻辑删除字段,配合MyBatis Plus的@TableLogic,查询自动过滤已删除记录 - 所有业务表都带
create_by、create_time、update_time,审计信息在出问题的时候能溯源 - 业务表中关键字段比如疫苗ID、药品ID,外键逻辑约束不物理建外键,降低表之间的耦合度,用索引提升查询效率
补充一段建表SQL示例,让大家感受一下关键设计:
CREATE TABLE `pet` ( `id` bigint NOT NULL AUTO_INCREMENT, `owner_id` bigint NOT NULL COMMENT '宠物主ID', `pet_name` varchar(50) NOT NULL COMMENT '宠物名称', `species` varchar(20) NOT NULL COMMENT '物种: CAT/DOG/OTHER', `breed` varchar(50) DEFAULT NULL COMMENT '品种', `gender` tinyint DEFAULT NULL COMMENT '1公 2母', `birth_date` date DEFAULT NULL COMMENT '出生日期', `weight` decimal(5,2) DEFAULT NULL COMMENT '体重kg', `sterilized` tinyint DEFAULT 0 COMMENT '是否绝育', `photo_url` varchar(255) DEFAULT NULL COMMENT '头像路径', `allergy_summary` varchar(500) DEFAULT NULL COMMENT '过敏史摘要', `vaccine_status` tinyint DEFAULT 0 COMMENT '0未接种 1部分接种 2已完成', `deleted` tinyint DEFAULT 0, `create_by` bigint DEFAULT NULL, `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_owner_id` (`owner_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='宠物档案表';vaccine_status这个字段不存具体值,而是通过计算最新接种记录得出,一般是在宠物详情页实时算:先查该宠物的所有疫苗记录,如果全部完成就标记为“已完成”,否则显示“未完成”和下次接种时间。这样做的原因是状态值不能依赖人工更新,人工更新一定会漏。
4. 核心业务落地:从预约接诊到疫苗接种提醒的完整链路
数据模型定了之后,干活就顺了。这一章我挑几个核心业务场景讲透,包括登录鉴权、预约流程、诊疗流程和疫苗提醒,这些做好了,系统整体就立住了。
4.1 登录鉴权与四类角色权限控制
后台用户表和宠物主表分开,但登录入口我统一放在一个AuthController里。Sa-Token的用法很简单:登录成功之后调用StpUtil.login(userId),后续接口通过StpUtil.checkPermission("system:user:add")校验权限。管理员拥有全部权限,医生和护士只能操作自己相关的业务,宠物主只能看到自己的宠物档案和预约记录。
权限配置我建议做细一点,哪怕当前用不到。比如疫苗记录的删除权限只给管理员,医生只能新增和修改,这样即使接口被猜到ID,没有权限也调不动。我在项目里用注解@SaCheckPermission("vaccine:record:add")挂在Controller方法上,直观有效。
4.2 宠物档案的新建与修改流程
宠物主在前台注册后,可以自助添加宠物档案,也可以由前台工作人员在后台代为录入。录入时要注意几个联动逻辑:
- 宠物年龄不是直接存年龄值,而是存出生日期,展示时计算年龄,这样过一年年龄自动更新
- 体重变化要记录下来,我单独建了一张
pet_weight_record,前端画体重趋势曲线用 - 过敏史录入时用多选标签,点击保存后写入过敏明细表
宠物档案修改有一个小设计:宠物名称、头像、体重这些基础信息允许宠物主自助修改,但过敏史、绝育状态这类医疗相关字段需要医生审核后修改,避免宠物主误操作影响诊疗。这个业务规则在答辩时也是一个很好的“业务思考”加分点。
4.3 预约与候诊流程的状态机实现
预约流程是最容易写乱的地方。我画了一个状态机(不是代码强行限定,而是靠数据库字段加Service层逻辑控制):
- 宠物主选择医生和日期,提交预约,状态为
PENDING - 到店后前台确认,状态变为
ARRIVED - 医生开始接诊,状态变为
IN_CONSULTATION - 诊疗完成,状态变为
FINISHED - 如果宠物主取消或超时未到,状态变为
CANCELED或NO_SHOW
状态流转的核心代码在AppointmentServiceImpl里,每次流转之前先校验当前状态是否合法。比如CANCELED状态不能直接跳到FINISHED,必须有IN_CONSULTATION的过程。这种防御性编程虽然多写几行,但能挡住前端恶意请求。
时间冲突检测也放在Service层。提交预约时,先查询同一医生同一日期同一时间段是否有未取消的预约,如果有就返回“该时段已被预约”的提示。并发场景下会出现两人同时提交成功的情况,我后面专门讲这个坑和解决方案。
4.4 诊疗记录与处方开立
诊疗记录表单我分为三个区域:主诉、诊断、处理意见。医生在接诊页面选宠物后,系统自动带出历史疫苗记录和过敏史,这个细节很实用,医生开处方之前一眼就能看到这只宠物能不能用某种药。
处方开立时,前端弹出药品选择框,药品从药品表中搜索,选中后自动带出默认单价和用法用量。提交时后端要做三件事:
- 保存诊疗记录
- 保存处方明细
- 扣减药品库存
这三件事必须在一个事务里,要么都成功,要么都失败。我用了@Transactional(rollbackFor = Exception.class),并且把库存扣减放在最后,因为库存不足时直接抛异常,整个事务回滚,不会出现病历存上了但库存没扣的尴尬。
4.5 疫苗接种管理:从手工登记到自动提醒
疫苗记录的核心功能是“到期提醒”。先看实现逻辑:
- 每次接种完成时,根据疫苗类型和首免/加强针规则,计算下一次接种日期存入
next_vaccine_date - 定时任务每天扫描
next_vaccine_date在最近7天内的宠物,生成提醒记录 - 提醒方式先做站内信,宠物主登录小程序或公众号时能看到提醒;短信和公众号模板消息预留接口
定时任务我用Spring自带的@Scheduled,没有额外引入Quartz,因为场景就一个每天固定的扫描任务,足够用了。示例代码:
@Component public class VaccineRemindTask { @Scheduled(cron = "0 30 8 * * ?") public void scanDueVaccine() { LocalDateTime today = LocalDateTime.now(); LocalDateTime weekLater = today.plusDays(7); List<VaccineRecord> dueList = vaccineRecordMapper.selectList( new LambdaQueryWrapper<VaccineRecord>() .between(VaccineRecord::getNextVaccineDate, today, weekLater) .eq(VaccineRecord::getStatus, "NORMAL")); for (VaccineRecord record : dueList) { // 生成站内信提醒,以宠物主ID为接收人 messageService.sendVaccineRemind(record.getOwnerId(), record.getPetId(), record.getNextVaccineDate()); } } }这里有一个业务细节:不同疫苗的接种周期不一样。常见的是每年一针加强疫苗,但有的疫苗首免要打两针甚至三针,每针间隔21天或28天。我把疫苗表里加了一个interval_days字段,首免第二针的日期就是第一针日期加interval_days,如果超过两针就继续累加。这样即使业务规则变化,只需要调整疫苗配置表,不用改代码。
4.6 报表与云端化的落地
报表我用ECharts画了三张核心图:近30天每日就诊量柱状图、疫苗到期倒计时列表、宠物品种占比饼图。数据都是从业务表统计出来的,写好SQL就行。比如每日就诊量就是按date(create_time)分组统计。
“云端诊疗”在这个项目里的含义是:系统部署在公网服务器上,宠物主通过浏览器或小程序端即可远程访问,查看宠物档案和预约信息。真正意义上的视频问诊功能我给预留了接口模块,但没有实际做。如果要做,需要引入WebRTC或集成第三方音视频服务,技术上复杂度会明显上升,作为毕业设计在答辩时讲清楚“未来扩展方向”就够了,不用强行做完。
5. 实测踩坑记录:时区、文件路径与并发预约的三个大坑
这个项目我从原型到完整跑通花了两周,期间踩了三个比较典型的坑,写出来给大家避雷。这些坑不亲自动手做一遍,光看文档很难预知。
5.1 LocalDateTime与MySQL时区错乱
第一个坑是日期时间差8小时。本地开发环境没问题,部署到服务器之后,数据库存的时间比实际时间晚了8小时。原因很简单:MySQL连接的serverTimezone参数没配置对,同时Java侧使用了LocalDateTime,而JDBC驱动在解析时间时要跟数据库会话时区做换算。
解决方案是在数据库连接串上显式指定时区:
spring: datasource: url: jdbc:mysql://localhost:3306/pet_hospital?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false另外还要注意Jackson序列化LocalDateTime的格式,默认序列化出来是数组格式,不好看也不方便前端处理。我在配置里加了一段:
@Bean public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() { return builder -> builder .serializers(new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))) .deserializers(new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); }这样前后端传输时间都是yyyy-MM-dd HH:mm:ss字符串,清晰直观。
5.2 MyBatis Plus逻辑删除与唯一索引冲突
第二个坑是逻辑删除导致的唯一索引冲突。我给宠物主表加了phone唯一索引,用户注销后通过逻辑删除标记,但再次用同一手机号注册时,插入报Duplicate entry错误,因为老数据还在表里,只是被逻辑删除了。
解决方案有三种:
- 设计表时不加唯一索引,靠代码查重
- 把逻辑删除字段拼进唯一索引(MySQL 8.0支持函数索引,比如
INDEX(phone, deleted)) - 注销时把手机号改成一个带时间戳的占位符
我最后用的是第三种,注销时将手机号置为deleted_ + 时间戳,这样既能保留唯一索引校验新用户,又不影响历史数据。虽然直接改业务字段有点粗暴,但在这个场景下最实用。
5.3 宠物头像上传的静态资源路径问题
第三个坑是图片上传。宠物档案里有头像字段,我一开始把图片传到了本地项目的target/classes/upload目录下,结果重新打包部署后图片全没了,而且前端访问不到。
正确做法是在配置文件里指定外部存储路径,然后配置静态资源映射:
app: upload: path: /data/pet_hospital/upload/@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceHandler("file:" + uploadPath + "/"); } }这样图片存在服务器外部目录,项目重启不丢,前端直接通过/upload/xxx.jpg访问。真实部署时也可替换成云存储,把上传接口返回的URL改成云存储的路径,业务层不需要大改。
5.4 并发预约时间冲突的边界问题
最后一个是并发预约冲突。上面提到同一时段只能有一个预约,但高并发下两个请求同时查到没有预约记录,然后同时插入,就都成功了。我在开发环境用JMeter模拟20个并发请求,同一时段成功创建了3条预约。
解法我最终采用了数据库唯一约束兜底:给pet_appointment表加一个uk(doctor_id, appointment_date, time_slot, status)的部分唯一索引,但因为有状态字段,这个方案行不通。退而求其次,用Redis分布式锁:
String lockKey = "appointment:" + doctorId + ":" + date + ":" + timeSlot; boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", Duration.ofSeconds(10)); if (!locked) { throw new BusinessException("该时间段正在被其他人预约,请重试"); } try { // 查询并保存预约 } finally { redisTemplate.delete(lockKey); }Redis锁方案简单有效,每秒上千的并发也扛得住,作为毕设项目来说足够体面。答辩时可以顺带讲讲Redis锁的过期时间设置、加锁解锁的原子性,这些都是加分点。
6. 把项目交付成一门“看得见”的毕设:演示、文档与扩展建议
纯技术实现做完只算完成一半。如果你这是毕业设计,还要考虑如何把项目清晰地呈现给评委。我见过太多代码写得不错但演示混乱的同学,最后成绩反而不如代码一般但PPT讲得清楚的人。这不公平,但很现实。
演示阶段一定要准备一套“剧本”。不要进系统乱点,而是按业务故事线走:先演示宠物主注册并添加宠物档案,然后前台预约医生,医生接诊录入病历和处方,再给宠物打疫苗并生成下次接种提醒,最后展示定时任务自动生成的提醒消息和报表。每个步骤之前想好要切换的页面和要强调的技术点,控制在8到10分钟以内。
文档方面,除了常见的需求规格说明书、设计说明书、数据库设计文档,我还建议准备两样东西:
- ER图和业务流程图,用画图工具画出来放到文档附录里
- 接口文档,至少把核心模块的RESTful接口整理成表格,写明请求方式、请求参数和响应结果
答辩被问频率最高的几个问题是:为什么选择SpringBoot;MyBatis Plus比MyBatis好在哪里;预约冲突怎么解决;疫苗提醒如何保证不遗漏。这几个问题我都在这篇博文里讲到了,提前组织好语言,答起来会顺畅很多。
如果你后续想让这个项目再上一个台阶,比较实际的扩展方向有三个。第一个是加一个小程序端,宠物主在微信里查档案和接收疫苗提醒,使用体验比浏览器好太多;第二个是把疫苗提醒对接公众号模板消息或短信服务,真正解决触达问题;第三个是引入简单的视频问诊,预约成功后生成一个视频房间链接,医生和宠物主远程沟通。这三个方向任意做一个,项目的完整度和竞争力都能明显提升。
最后分享一个实际使用中的小细节:疫苗提醒的定时任务,我建议执行时间设在早上8点到9点之间,而不是凌晨跑批。这个时间段宠物主刚起床,看到提醒消息后回复率最高。我用的是0 30 8 * * ?这个cron表达式,实测下来,早上推送的提醒被宠物主看到的比例比下午推送高了一大截。别小看这个时间点,业务系统的价值往往就体现在这种细节里。