SpringBoot + Java的宠物医院药品管理系统,是很多同学做毕业设计时绕不开的“标准题”。我见过不少大三学生拿到类似题目后,第一反应就是上网找现成代码,下载下来跑起来就算完事。可真正到了开题答辩和系统演示的时候,连“库存批次”“先进先出”“效期预警”都解释不清楚。这套系统表面上是药品的增删改查,背后其实是宠物医院日常运营中最敏感的一条链路:药品怎么进来、怎么存放、怎么被开出去、怎么处理过期。这篇文章不会给你一堆下载链接,而是把系统从业务到代码完整拆开讲,适合正在做SpringBoot课题、或者打算用药品管理方向交作业的同学直接参考。
1. 宠物医院药品管理究竟在管什么:先理清业务再谈代码
1.1 药品管理不是简单的增删改查
宠物医院的药品管理跟你想象的小诊所药柜不是一回事。同样是阿莫西林,可能有多个厂家、多个批次、不同有效期;有些是兽用处方药,开药必须关联宠物病历和医生处方;同一只猫的用药量还要按体重计算。如果单纯建一张药品表,把所有信息堆在一起,刚开始看不出问题,等数据量到了几千条,效期预警和批次追溯就完全没法做。所以业务模型是先于代码的。
很多人习惯先翻前端模板再建数据库,这是反的。你应该先回答几个问题:入库单怎么生成?医生开的处方以什么形式保存?药房发药时扣的是哪个批次库存?过期药品是直接删除还是保留记录?这些问题的答案,直接决定你建几张表、每个表留哪些字段。
1.2 三类使用者和四条核心链路
先区分角色,再谈功能。核心角色有三类:
- 管理员:维护药品基础数据、供应商、入库审核、参数配置。
- 医生:查看库存、开处方、查看效期预警。
- 药房人员:发药、退药、报损、出入库登记。
在这套系统里,最核心的业务链路有四条:
- 采购入库:药品从供应商到达医院,生成入库单,写入批次库存。
- 库存管理:维护批号、效期、库存上下限,触发预警。
- 处方发药:医生开处方单,药房按处方扣减对应批次库存。
- 过期报损:效期临近或过期药品进入报损流程。
这些链路不用全部一步到位,但数据库设计时必须为它们预留字段。很多同学把系统做成“只记录库存数量”的玩具,根源就是只画了页面,没画业务链路。建议动手前先用Excel模拟一次:一张药品入库单进来之后,会经历哪些状态变化。你会发现同一盒药,从入库到发药,往往要牵动三四张表,这就是药品管理系统的核心价值。
2. 技术栈与项目结构:为什么SpringBoot是毕业设计的最优解
2.1 技术选型的对比思路
如果你是2026年做毕设,我不推荐再写SSM的XML配置式项目。不是说SSM不能做,而是同样的功能,SpringBoot可以少写一半配置文件,留给业务的时间更多。技术栈可以定为SpringBoot 2.7 + MyBatis Plus + MySQL,前端二选一:Vue3 + Element Plus,或者直接用Thymeleaf + Bootstrap。前者页面好看、能讲前后端分离;后者部署简单,适合不熟悉前端的同学。
选SpringBoot的原因很实际:内嵌Tomcat,不用单独配服务器;自动配置机制让数据源、JSON转换、日志这些常见问题都有默认方案;MyBatis Plus又提供代码生成器和条件构造器,能把单表CRUD的时间压缩到很小。毕设周期通常只有几个月,你需要把更多精力放在库存扣减、效期预警这类有区分度的业务上,而不是配置一个拦截器就折腾两天。
2.2 后端工程分包与启动流程
工程结构不用太复杂,按业务分包足够清晰:
com.pet.hospital ├── controller # 接口层 ├── service # 业务逻辑 │ └── impl ├── mapper # 数据访问 ├── entity # 数据库实体 ├── dto # 请求参数 ├── vo # 返回视图对象 └── config # 配置类、拦截器、定时任务启动流程也很简单:先创建数据库pet_hospital,导入初始化SQL,改一下数据源配置,启动Application主类。需要注意的一个细节是数据库连接串里的时区参数,很多人不写serverTimezone,启动后时间字段会差8小时。下面是一份可直接用的数据源配置:
spring: datasource: url: jdbc:mysql://localhost:3306/pet_hospital?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 mybatis-plus: global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0如果你的系统规模不大,完全不需要引入微服务或者Redis。毕业设计的重点是把一个场景做透,而不是技术堆砌。
2.3 前端选型的两个方向
很多同学纠结前端到底用Vue还是用模板引擎,这里直接给对比结论:
| 维度 | Vue3 + Element Plus | Thymeleaf + Bootstrap |
|---|---|---|
| 开发效率 | 前后端分离,联调成本稍高 | 单体应用,一个工程直接跑 |
| 页面效果 | 组件丰富,表格表单完善 | 中规中矩,但够用 |
| 答辩说服力 | 能讲接口设计、跨域、路由守卫 | 成本低,容易自查 |
| 部署复杂度 | 需要打包前端并配置跨域 | 打包进SpringBoot即可 |
我的建议是:如果时间比较充足,优先Vue3,因为前后端分离的架构本身就是一个讲解亮点;如果时间只剩一两周,Thymeleaf完全够用,把业务讲清楚照样能过。
3. 数据库设计:一张库存表如何承载批次、效期和处方来源
3.1 八张核心表的职责划分
数据库是一套系统的底盘,我建议你至少设计下面这些表:
| 表名 | 核心作用 | 关键字段提示 |
|---|---|---|
| user | 员工账号 | username、password、status |
| role | 角色 | role_code、role_name |
| pet_info | 宠物档案 | pet_name、owner_name、pet_type |
| drug_info | 药品基础信息 | drug_name、spec、manufacturer、retail_price |
| drug_batch | 药品批次库存 | batch_no、production_date、expire_date、available_qty |
| stock_in_order | 入库单主表 | supplier_id、order_no、total_amount |
| stock_in_item | 入库单明细 | batch_id、qty、purchase_price |
| prescription | 处方单主表 | pet_id、doctor_id、diagnosis、status |
| prescription_item | 处方明细 | drug_id、dosage、qty、batch_id |
| drug_loss | 报损记录 | batch_id、loss_qty、loss_reason |
为什么要把入库单拆成主表和明细表?因为一张入库单里往往包含多种药品,每种药品的批次信息独立。如果只用一张表,字段会出现大量冗余,统计供应商供货情况也很痛苦。同理,处方单和处方明细也要拆开,一只猫可能在一张处方里开三四种药,每种药的剂量和数量都需要单独记录。
3.2 药品表与库存批次表的最关键设计
几乎每一个把系统做崩的同学,都会犯同一个错误:在drug_info表里直接放一个stock字段。表面上看很省事,查询库存也方便,但你无法回答三个问题:这批药品什么时候过期?同一药品不同批次的进价分别是多少?如果某一批次出现问题,怎么下架?
正确的做法是把药品基础信息和批次库存拆成两张表,用drug_id关联。drug_info保存通用名、商品名、规格、生产厂商、默认零售价;drug_batch保存每一批次的专属信息,比如:
- batch_no:批次号,同一批进货同一个批号。
- production_date:生产日期。
- expire_date:有效期。
- available_qty:当前可用数量。
- purchase_price:入库价。
- sale_price:零售价。
查询当前库存时,对drug_batch按drug_id做SUM(available_qty)即可。发药时,则必须精确到批次。这套设计的本质是让“库存来源”可追溯,也是答辩时你能讲清楚的关键点。
3.3 处方单和发药记录如何串联
处方单主表记录一次诊疗的结论,处方明细记录具体药品、剂量、数量。药房发药时,再把处方明细与drug_batch关联,扣减对应批次的库存。
这里有个很多人忽略的设计:处方明细里除了drug_id,还要冗余一份drug_name和spec。为什么?药品基础信息可以被管理员修改,比如商品名从A改成B,但已经开出去的处方不能跟着变。医疗记录讲究可追溯,冗余快照虽然违反第三范式,却是业务上的正确选择。同理,发药记录中也应该记录实际发放的批次号batch_no,方便将来对某批次药品做召回。
很多同学会在这里犹豫:要不要单独建一张发药表?我的建议是建。因为一次处方可能被拆成两次发药,或者发药过程中发现某一批次库存不足,需要换批次。如果只在处方明细上加个状态字段,想查“哪批药发给了哪只宠物”会很困难。
4. 核心业务代码:库存扣减如何避免超卖和效期错误
4.1 先进效期先出的核心思路
药品库存管理的核心是FEFO,也就是效期近的先出。人用药房习惯先进先出,宠物医院药品流转速度慢,效期问题更敏感,必须优先考虑有效期。
发药时,先查当前药品所有可用的批次,按有效期升序排列,然后依次扣减。伪代码如下:
public void dispenseDrug(Long drugId, int needQty) { // 查询该药品的有效批次,按有效期升序、入库时间升序 List<DrugBatch> batches = drugBatchMapper.selectValidBatches(drugId); for (DrugBatch batch : batches) { if (needQty <= 0) { break; } int available = batch.getAvailableQty(); if (available <= 0) { continue; } int takeQty = Math.min(available, needQty); drugBatchMapper.reduceStock(batch.getId(), takeQty); needQty -= takeQty; // 写发药明细,记录batchId和batchNo } if (needQty > 0) { throw new RuntimeException("库存不足"); } }这段代码逻辑不复杂,但有一个隐藏问题:如果一个批次只有5盒,你需要发7盒,系统必须自动拆成“这个批次扣5盒,下一个批次扣2盒”。很多学生写到这里会直接用总库存减,导致明明批次数据存在,却说不清药到底是从哪个批次发的。
4.2 并发场景下的库存更新写法
扣减库存最怕两件事:超卖和重复扣减。用Java代码先查再改,在高并发场景下会出现并发覆盖问题。解决办法很简单,把扣减操作写成一条带条件的UPDATE语句:
@Update("UPDATE drug_batch SET available_qty = available_qty - #{qty} " + "WHERE id = #{batchId} AND available_qty >= #{qty}") int reduceStock(@Param("batchId") Long batchId, @Param("qty") Integer qty);当返回值是0,说明当前批次库存不足,可以立即停止并回滚事务。这种方式不需要分布式锁,在单库场景下足够可靠。整个发药方法要加@Transactional注解,保证批次扣减和发药记录写入要么全部成功,要么全部失败。
有一点要提醒:不要在循环里逐条调用SELECT再UPDATE,尽量先把符合条件的批次列表一次查出,再在内存中计算每批次应扣数量。数据量大时,逐条查询数据库会拖慢接口响应,而且事务时间越长,锁冲突的概率也越高。
4.3 过期预警定时任务
过期货品如果继续留在库里,容易误发。因此需要一个定时任务,每天扫描近效期批次,生成预警记录。SpringBoot提供的@Scheduled注解已经够用:
@Component public class ExpireRemindTask { @Scheduled(cron = "0 0 2 * * ?") public void remind() { List<DrugBatch> expiringBatches = drugBatchMapper.selectExpiringBatches(30); expiringBatches.forEach(batch -> { // 生成一条预警消息,或者调用消息接口通知药房 }); } }查出的批次判断条件是available_qty大于0,且expire_date在30天以内。为什么不用quantity?因为quantity可能包含已经出库但还没结算的历史数据,只有可用库存才需要处理。这个细节也会成为你和普通同学拉开差距的地方。
定时任务的cron表达式建议设置在凌晨,避免影响白天正常业务。如果想让预警更及时,还可以在前端登录后显示一个醒目的提示条,点击后跳到近效期列表。
5. 权限与安全:三种角色互不越权的实现细节
5.1 基于RBAC的权限模型
药品管理系统一定要做权限控制,因为医生和药房人员的职责边界很清晰。管理员负责基础数据和入库审核,医生可以开处方,药房人员可以发药和报损,但不应该能修改药品价格。
推荐使用RBAC模型,也就是用户-角色-权限。简单做法是建五张表:user、role、user_role、role_menu、menu。登录后把当前用户的权限码列表放进Session或Redis,接口调用时用拦截器校验。如果用了Spring Security,可以在配置类里开启方法级权限注解@EnableGlobalMethodSecurity,然后直接在Controller或Service方法上加@PreAuthorize,代码会更简洁。
很多同学一张用户表加一个type字段就来控制权限,确实快,但遇到“某个药房人员同时需要查看统计报表”的场景,就要临时改代码。用RBAC模型,只需要给角色分配菜单权限即可,灵活性和答辩说服力都更强。
5.2 登录态与接口鉴权的常见实现
登录接口建议使用JWT,而不是传统Session。不是因为它更安全,而是前后端分离架构下更自然。先校验用户名密码,密码用BCrypt加密存储,不要明文入库。登录成功后生成token,前端放到请求头的Authorization字段。
自定义一个拦截器,做三件事:
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 || !JwtUtil.verify(token)) { response.setStatus(401); return false; } // 解析用户信息,放入ThreadLocal UserContext.set(JwtUtil.parseUser(token)); return true; } }注意JWT并不适合保存敏感数据,里面只放userId和角色编码,过期时间建议设置2到4小时。前端路由守卫也要配合,未登录用户跳到登录页。权限验证不能只靠前端隐藏按钮,因为接口可以直接调用,必须在后端做二次校验。
6. 从开发到上线容易踩的坑:日志、时间、枚举转换
6.1 MyBatis Plus 自动填充与逻辑删除
MyBatis Plus最实用的功能是自动填充。在entity的createTime字段上加@TableField(fill = FieldFill.INSERT),再实现一个MetaObjectHandler,插入和更新时就不用手动set时间了。这能减少很多低级错误,尤其是数据库字段和时间类型不一致导致的插入失败。
逻辑删除也是一个省心配置。设置logic-delete-field: deleted后,所有查询会自动带上WHERE deleted = 0。但有一个坑:如果给drug_name字段建了唯一索引,逻辑删除后会再次插入同名的药品时,会因为唯一索引冲突插入失败。处理办法有两种,要么删除唯一索引,要么把deleted字段改成删除时间,让每个被删除的记录都保留不同的时间戳。这个问题在答辩前务必自测一遍。
6.2 LocalDateTime 序列化与前端显示
如果使用Java 8的LocalDateTime,没有额外配置时Jackson会把时间序列化成数组,前端拿到的是“2026,5,10,14,30”这种格式,非常难处理。建议在application.yml里统一配置:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8同时在实体类的日期字段上加上@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss"),双保险。数据库连接串的serverTimezone也要保持一致,否则你存进去的时间是正确的,读出来却多了8小时。这在统计报表功能中尤其明显。
6.3 库存扣减与报表统计的脏数据问题
写库存扣减时,最容易产生脏数据的场景是异常事务没有回滚。比如先扣减批次库存,再生成发药明细,第二步抛异常且没有事务,库存就平白少了。解决方法是把整个发药流程封装到一个事务方法中,并且只对异常做回滚,不要用try-catch吞掉异常。
另一个常见问题是报表统计时,把逻辑删除的数据也算进去了。统计库存金额时,要明确统计口径:以available_qty为准,乘以入库价。统计销量时,要关联发药记录,而不是用“库存总量变化”去推算。这个口径不统一,做出来的报表数据会对不上账。
7. 如果这是你的毕设:文档、演示和答辩怎么准备
7.1 演示脚本要覆盖一条完整业务闭环
很多同学答辩演示时,只点开几个列表页,说“这里可以增删改查”,讲两分钟就结束。这样很难让评委相信系统真的可用。建议准备一条完整业务闭环的演示脚本:
- 用管理员账号登录,新增一个药品基础信息。
- 选择该药品,做一次采购入库,打开入库单能看到新批次和数量。
- 切换到医生账号,为一只宠物档案开处方,选药时能看到库存余量。
- 切换到药房账号,找到该处方,点击发药,确认扣减的是哪个批次。
- 回到药品列表,确认当前库存数量已经减少。
- 把某个批次的效期手动改成临近30天,等待或触发定时任务,查看预警列表。
- 对过期批次做报损,查看报损记录。
这一套流程走下来,不到十分钟,却覆盖了三个角色、四张核心表、两条业务链。比单纯展示表格有说服力得多。
7.2 答辩时如何介绍“技术亮点”
答辩时不用贪多,讲清楚两三个亮点就够了。我的建议顺序是:批次效期设计、防超卖扣减、权限控制。
效期设计可以这样讲:“为了追溯药品来源,我把库存拆成批次表,发药时按效期优先扣减,记录每个批次去往哪只宠物。”这体现出你对业务的理解。
防超卖可以这样讲:“扣库存时用带条件的UPDATE语句,保证并发场景下库存不会变成负数,同时用事务保证发药记录和库存扣减的一致性。”这体现出你的工程设计能力。
权限控制可以这样讲:“系统采用RBAC模型,医生和药房人员的操作权限分开,前端只控制显示,后端通过拦截器对每个接口做鉴权。”这体现出你对安全性的考量。
不要主动提微服务、分布式缓存这些没用到的技术。评委如果追问,坦诚回答“当前场景并发量不高,单机事务足够,过度设计会增加复杂度但不会带来收益”即可。
最后说一个我自己的习惯:做这类系统,不要在没跑通业务链之前就急着追求界面炫酷。先把数据表建好,用一份测试Excel模拟一轮入库和发药,看库存数字能不能对得上;再开始写代码。药品管理系统最关键的不是某个页面好不好看,而是数据在哪个环节都不能丢。如果你也能做到这一点,不管最后功能多还是少,你的项目至少是完整的、能自洽的。