SpringBoot医院药品管理系统设计与实现:从业务链路到库存批次核心解析
2026/9/8 15:22:26 网站建设 项目流程

想做 SpringBoot 医院药品管理系统的人,我这两年见过太多。后台私信里问“这一类毕设/项目怎么设计才靠谱”的人也不少,说实话它和图书管理、学生管理系统那种纯 CRUD 系统有本质区别:药品有批号、有有效期、有拆零规则,库存一变就会牵扯单据、批次、流水,业务上稍微想得不严谨,后面全是补窟窿的活。这篇文章就把我理解中的“基于 SpringBoot 的医院药品管理系统的设计与实现”完整拆一遍,从业务流程、数据库设计到后端核心实现,再到部署和避坑,尽量讲得直白,且每一步都告诉你“为什么这么做”。

这个项目适合谁看?如果你是正在做类似题目、准备做相关系统开发,或者想从 SpringBoot 入门到真正做一个有业务深度的管理系统,这篇文章基本可以当成一份开发前的提纲来用。我会把常见但容易翻车的点都拎出来说,不会只停留在“增删改查”层面,而是把药品管理系统真正的难点——批次库存、效期预警、单据流水、权限控制——讲透。

1. 先梳理业务流程:医院药品管理到底管什么

1.1 药品“从进到出”的主链路

很多人拿到这个题目,第一反应就是建表、写 CRUD,实际上最大的坑恰恰在这里。如果不先把药品在医院的完整流转路径理清楚,你建立的表结构大概率会在做到一半时被推翻。

药品从供应商送货开始,进入药库,这是“入库”;药库再按照各药房的申领单把药品发到门诊药房、住院药房,这是“出库”;药房给患者发药或者科室领用,库存继续往下走;中间还穿插着退货、报损、盘点、效期处理、近效期退回等操作。所以项目里的基础核心不是“药品表”,而是一条供应链:采购-入库-申领-出库-发药/退药-盘点-报表。

在设计系统时,我强烈建议你至少把“库存”分成药库库存和药房库存两个层级来看。哪怕你第一版不想做得太重,也最好在库存表里加一个“库存类型/库房”字段,否则后续一旦要支持门诊药房和住院药房分开管理,就只能把原有表推翻重来。药库是全院药品的蓄水池,药房是面向患者发药的末端库存,这两者的操作权限和业务单据应当分开,这是医院药品系统在逻辑上区别于普通进销存系统的关键。

1.2 把系统角色和使用场景先对齐

角色设计也不建议一上来就做得很复杂,核心角色有三个基本够了:

  • 系统管理员:维护用户、角色、菜单权限、数据字典,一般不直接操作药品库存。
  • 药库管理员(库管员):负责采购入库、供应商管理、库存批次维护、报损等。
  • 药师/药房人员:负责药房申领、退药、发药记录、日常库存查询。

这里要提醒一句:别把“医生开处方”功能做为重头戏。开处方通常属于 HIS 系统里的门诊/住院医生工作站范畴,你这个系统名字是“药品管理”,重点在药品流通过程而不是诊疗行为。真要为了演示闭环,可以保留一个“处方发药登记”的简单模块,药师录入处方号或者患者信息后,扣减药房库存并生成发药记录即可,不要陷进复杂的处方编辑器里。这个边界一旦守住,后面的工作量和答辩逻辑都会清爽很多。

1.3 技术选型与模块划分:SpringBoot为中心的落地组合

从技术路线来看,SpringBoot 是这类管理系统的绝对主力。原因是它自动装配能力强、starter 生态丰富,内置 Tomcat,开发者只需要关注业务代码,不需要像 SSM 时代那样做大量 XML 配置。简单说一下自动装配的原理:SpringBoot 启动时读取META-INF/spring/...AutoConfiguration.imports里的自动配置类,再根据当前类路径上的依赖和你配置的条件来装配对应的 Bean,简单理解就是“依赖里有什么,框架就自动给你把该配的配好”。

我的推荐版本组合是这样一套:Spring Boot 2.7.18 + JDK 8 + MyBatis-Plus + MySQL 8.0 + Sa-Token(或 JWT)+ Knife4j,前端可选 Vue 3 + Element Plus。前后端分离是当前主流,但如果不想碰前端,也可以用 Thymeleaf 做服务端渲染,只不过搜索热词里既然大量是 SpringBoot + Vue 前后端分离的方案,我会建议按前后端分离来设计模块。

模块划分可以列成这个表格:

模块后端重点前端页面
用户与权限RBAC、登录认证、接口鉴权登录页、用户管理、角色管理
药品建档药品字典维护、状态校验药品列表、新增/编辑表单
供应商管理供应商资料维护供应商列表
采购入库采购单、验收、批次生成采购单登记、入库审核
药房申领/出库申领单、出库扣减库存申领登记、出库单
库存与效期批次库存、有效期预警批次库存列表、预警看板
盘点与报损盘点单、差异调整、报损单盘点任务、报损登记
报表统计出入库统计、库存金额图表展示、Excel 导出

2. 数据库设计是核心:多少张表、哪些字段值得雕琢

2.1 药品主数据和批次数据必须分开

这是整套系统数据库设计里最重要的一条原则。很多初学的人会把药品名称、规格、库存数量、生产日期、有效期全部塞进同一张 drug 表,这个设计短期能跑,后期一遇到不同供应商送来的同一种药,批号不同、效期不同、价格不同,立刻完全失控。

正确的做法是把“药品基础信息”和“药品批次库存”分开。前者存药品的通用属性,比如药品编码、药品名称、通用名、规格、剂型、生产厂家、批准文号、包装单位、零售价等;后者才存某个批次的实际库存,比如批次号、生产日期、有效期、入库单价、库存数量、所在库房。举一个实际场景:药库里有 100 盒阿莫西林,其中 60 盒有效期到明年 5 月,另外 40 盒下个月就到期。如果效期字段写在药品主表上,先进先出和效期预警根本没法做。药品批次表就是要解决“同一药品不同效期”的问题。

药品主表常用字段如下,供参考:

CREATE TABLE drug_info ( id BIGINT PRIMARY KEY AUTO_INCREMENT, drug_code VARCHAR(64) NOT NULL COMMENT '药品编码', drug_name VARCHAR(128) NOT NULL COMMENT '药品名称', common_name VARCHAR(128) COMMENT '通用名', specification VARCHAR(128) COMMENT '规格', dosage_form VARCHAR(64) COMMENT '剂型', manufacturer VARCHAR(255) COMMENT '生产厂家', approval_number VARCHAR(128) COMMENT '批准文号', package_unit VARCHAR(32) COMMENT '包装单位,如盒/瓶', min_unit VARCHAR(32) COMMENT '最小单位,如片/支', conversion_rate INT COMMENT '包装单位到最小单位的换算比', retail_price DECIMAL(10,2) COMMENT '零售价', status TINYINT DEFAULT 1 COMMENT '1启用 0停用', deleted TINYINT DEFAULT 0 COMMENT '逻辑删除' );

这里先不展开每条字段,但有几个点值得在开发时就确认好:药品编码要唯一;批准文号在大多数情况下也应当唯一;状态字段要比直接删除更安全;单价和金额一律用 DECIMAL,不能用 Double。别小看这几个规定,等做到采购入库和报表的时候就会知道有多重要。

2.2 业务单据与库存流水成对设计

采购入库单、药房申领单、出库单、盘点单,这是“单据层”;而药品批次库存的变化是“结果层”。不要让业务代码直接UPDATE drug_stock_batch SET stock_qty = stock_qty + 100,没有任何单据地改库存,将来连“为什么库存多了 100”都查不出来。

建议每张核心单据都有主表和明细表,比如采购入库单主表存单号、供应商、入库时间、入库人、审核状态,明细表存该单涉及的药品、批次、数量、单价。同时建一张统一的stock_log库存流水表,每次库存发生变化都要往流水里插入一条记录,包含药品 ID、批次 ID、库房、变动类型、关联单号、变动前数量、变动数量、变动后数量、操作人、操作时间。

这样设计的意义在于“账实相符、过程可追溯”。以后库存对不上时,直接查流水就能还原整个药品去向;盘点时发现盘盈盘亏,也能通过流水追到具体是哪张单子引起的。它是整套系统在数据和业务上能站住脚的核心支撑。

2.3 效期字段、预警SQL与定时任务配合

效期相关的字段必须在批次库存表里:生产日期production_date、有效期至expire_date。查询预警的逻辑其实非常简单,用 MySQL 的DATEDIFF计算“距过期还有多少天”,然后按业务需要分几个档位展示。

SELECT di.drug_name, di.specification, dsb.batch_no, dsb.stock_qty, DATEDIFF(dsb.expire_date, CURDATE()) AS remain_days FROM drug_stock_batch dsb LEFT JOIN drug_info di ON dsb.drug_id = di.id WHERE dsb.stock_qty > 0 AND DATEDIFF(dsb.expire_date, CURDATE()) BETWEEN 0 AND 30 ORDER BY remain_days ASC;

这个 SQL 能直接把未来 30 天内到期的批次找出来,页面里按“已过期、30天内到期、90天内到期”分组展示即可。如果想要每天自动提醒,可以用 SpringBoot 自带@Scheduled定时任务,每天执行一次查询,把近效期批次写入一张预警记录表。注意定时任务要加上开关配置,避免测试环境也天天发消息。

2.4 药品拆零与单位换算如何建模

医院发药经常会“整进零出”。比如药品以“盒”为单位入库,但患者可能只需要 14 片,这种需求直接影响库存表设计。如果你想省事,只存一个包装单位,最后在发药时会发现数量完全对不上。

较好的做法是引入双单位设计。系统内部库存统一以“最小单位”来存,展示时再根据换算比例显示成盒/瓶。建表时需要有package_unitmin_unitconversion_rate三个字段。举个例子,一盒阿司匹林 30 片,入库录入 100 盒时,后台自动换算成100 * 30 = 3000片库存;药师发药发掉 14 片,库存剩 2986 片;界面上显示给患者看时,再除以 30,表现为“99盒+16片”。这样做计算逻辑统一,不会出现“整盒和零片混在一起不知道怎么加”的问题。

3. SpringBoot后端实现:从工程骨架到核心模块

3.1 别小看包结构与统一返回体

项目结构一定要干净,主流做法是controller/service/mapper/entity/dto/vo/common/config这种分层的包结构。实体类只负责和表字段对应,不要直接在 Controller 里返回实体,而应该用 DTO/VO 做数据交互。这样做的目的是防止把数据库里的多余字段直接暴露给前端,将来改动字段时也不至于影响接口。

几乎每一个像样的 SpringBoot 管理系统都会做统一返回体。推荐一个很轻量的Result<T>:包含codemessagedata三个字段,并提供success()error()静态方法。配合全局异常处理器,把业务异常、参数校验异常等统一转成规范格式,这样前端处理接口时就只面对一种结构。代码上很简单,但会省掉后期大量联调沟通。

还建议使用@RestControllerAdvice做全局异常捕获。项目里自定义一个BizException,在 Service 层遇到“库存不足”“药品已停用”等情况直接抛出业务异常,由全局处理器转成 JSON 返回。这套机制如果等到后期再补,会出现大量try-catch埋在业务代码里的情况,可维护性很差。

3.2 登录和权限:用轻量RBAC解决复杂权限

权限设计不需要做成大而全的框架,核心是 RBAC:用户关联角色,角色关联菜单或权限点,后端接口做鉴权。技术选型上,我见过很多项目强行用 Spring Security + 一堆过滤器配置,最后卡在放行路径、密码加密、会话管理上,反而把业务开发时间挤掉了。对于这种管理系统,我更推荐轻量级方案,比如 Sa-Token 或者手写 JWT 拦截器,把用户登录后生成 token,前端每次请求带上,后端拦截器解析出用户信息即可。

如果项目要求用 Spring Security,也建议只保留最基础认证流程,不要堆砌复杂配置。无论用什么框架,都要记住权限校验必须放在后端,不能只靠前端把按钮隐藏就以为安全了。后端接口上直接加权限注解,比如新增药品需要drug:add权限,这样即使有人拼接 URL 也能被拦截。

一个常见小坑是接口文档与鉴权冲突。用 Knife4j 或 Swagger 调试时,如果不把/doc.html/webjars/**/v3/api-docs/**这些路径加到白名单,就会一直返回 401,看起来非常像后端接口崩溃。这块要放在最开始的过滤配置里直接放行,因为是开发期文档地址,不属于业务接口。

3.3 药品管理CRUD里藏着大量边界校验

药品基础信息的增删改查看似常规,真正写起来要注意的边界不少。新增药品时,药品编码和批准文号要做唯一校验;已存在相同编码时,应当给前端明确提示,不能直接抛数据库唯一键冲突的异常给用户看。

删除药品尽量不要物理删除。一旦药品已经发生过采购入库、发药出库,数据之间就有引用关系,硬删会导致历史单据统计错误。更稳妥的写法是“停用”策略,把状态置为 0,查询时默认只查启用状态的药品,历史单据依然能通过完整药品信息还原。库存为零并且没有业务的药品,可以再做物理清理,但这是上线后的运维操作,不适合作为日常功能。

编辑药品时也要小心并发覆盖。虽然管理系统一般并发不高,但两个人同时打开同一个编辑页并先后保存,后保存的人可能覆盖先保存的人修改的内容。最简单的做法是在表单中携带版本号或者更新时间,更新 SQL 里加上where version = 传入的版本,影响行数为 0 就提示“数据已被他人修改,请刷新后重试”。这个设计不但上得了台面,也真的能避免数据脏写。

3.4 库存变动提供唯一出口,并发扣减才安全

库存操作是药品管理系统的命脉,最容易出问题的逻辑是“多个 Service 各自操作库存”。比如采购入库的 Service 写一段库存增加逻辑,发药模块又写了一段库存扣减逻辑,两段逻辑不一致时就会出大问题。正确做法是所有涉及库存变化的模块,比如采购入库、药房申领、退药、报损、盘点调整,都调用同一个库存变更服务。

核心方法大致是这样的思路:

@Transactional(rollbackFor = Exception.class) public void changeStock(StockChangeParam param) { // 1. 根据库房、药品、批次找到批次库存 // 2. 写库存流水 stock_log // 3. 执行条件更新的扣减或增加 }

扣减库存时特别推荐用条件更新来保证并发安全,而不是先查询再判断再更新。上面那段逻辑落到 SQL 上可以是:

UPDATE drug_stock_batch SET stock_qty = stock_qty - #{qty} WHERE id = #{batchId} AND stock_qty >= #{qty}

这个 SQL 的意思是:只有当前库存足够时,UPDATE 才会成功,否则影响行数为 0。代码层只需要判断updateCount > 0,不成立就抛“库存不足”异常。它天然防止了多个并发请求把库存扣成负数的问题。这个思想在答辩环节也是一个很亮眼的设计点。

这段逻辑必须注意事务边界。如果库存变更方法和调用它的方法在同一个类内部直接调用,Spring 的 AOP 代理会失效,@Transactional可能不生效。解决办法是把库存变更服务单独放到一个类里,通过注入的方式调用;或者使用AopContext.currentProxy(),但我更推荐前者,代码更直观。

3.5 统计报表、效期预警与导出Excel

药品管理系统做到最后,统计报表的作用是提供管理视角。不要小看出入库统计,一个“本月各药房领用数量 TOP10”的 SQL,直接按药品维度做GROUP BY聚合,要比在 Java 内存里循环求和高效得多。日期统计上,按月趋势通常是把时间按DATE_FORMAT(create_time, '%Y-%m')分组,前端可以对数组直接渲染折线图。

效期预警除了 SQL 查询,还可以做一个独立的预警看板,以红黄绿区分紧急程度:红色代表 30 天内到期,黄色代表 90 天内到期,绿色是正常批次。红色批次需要注意是否要退回供应商或是报损,不能让它一直占用正常库存。

Excel 导出如果时间充裕可以加,常用方案是 Apache POI 或 EasyExcel。导出并不难,但要认真处理大批量数据时的内存占用,统一用流式写出,避免一次性把几万条数据塞进内存。对于系统演示来说,只要能把当前库存列表和出入库明细导出成 Excel,亮点已经足够。

4. SpringBoot实操细节:配置、常见问题与部署

4.1 application.yml、连接池和常见启动异常

配置文件是整个后端的地基。很多新手 SpringBoot 项目启动报错,不是代码问题,而是连接数据库时配置少了一些关键参数。比如 MySQL 8 的驱动类名已经变成com.mysql.cj.jdbc.Driver,老的com.mysql.jdbc.Driver在较新版本驱动中已经不能使用。URL 里我一般还会带上useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai,避免出现中文乱码和时区偏差。

server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/hospital_drug?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password hikari: minimum-idle: 5 maximum-pool-size: 20 connection-timeout: 30000 mybatis-plus: global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 configuration: map-underscore-to-camel-case: true

HikariCP 是 SpringBoot 2.x 的默认连接池,如果只是做系统开发,默认参数基本够用,但我还是建议通过配置把最大连接数写在明面上,避免以后排查问题时还要去想默认值。MyBatis-Plus 不需要额外配置实体类映射,它默认会把下划线字段映射成驼峰,前提是开启map-underscore-to-camel-case

如果你还没有创建数据库,别忘记先执行建库语句,例如CREATE DATABASE hospital_drug DEFAULT CHARACTER SET utf8mb4;。utf8mb4 比 utf8 能存更多字符,尤其当数据里出现特殊符号或表情时不至于报错。

4.2 IDE里创建SpringBoot项目时的版本选择

搜索热词里出现“springboot版本太高”不是没道理的。Spring Boot 3.x 基于 JDK 17,并将javax改成jakarta,如果你用的是网上大量基于 2.x 的教程,很多依赖和代码就是不对应的。第一次做这种管理系统,我个人的建议是稳一点,选 Spring Boot 2.7.18 + JDK 8。它是 2.x 的最终维护版本,资料多、坑少,绝大多数学校机房或本地环境也都能跑。等到你确实理解了依赖和包名差异之后,再升级才更从容。

在 IDEA 中创建项目时,Spring Initializr 默认可能给你选最新的 Boot 3.x,需要手动把 Server URL 切到可用的阿里云镜像,或者直接改成https://start.spring.io并在 Dependencies 里选择 Web、MyBatis、MySQL 驱动等。生成的工程如果无法启动,先看启动类位置,建议放在根包下,保证扫描范围包含所有 Controller 和 Service。

4.3 接口联调、权限放行与跨域

前后端分离项目里,接口联调阶段两个典型问题最明显:跨域和鉴权放行。跨域的表现是前端请求在浏览器里报 CORS 错误,而后端日志里根本没有输出。解决方式可以加一个 WebMvcConfigurer 配置允许跨域,指定允许的来源、方法、请求头。也可以在后端接口上加@CrossOrigin,但全局配置比一个接口一个接口加省事得多。

权限放行也很关键。登录接口、验证码接口、静态资源、Knife4j 相关路径必须放行;剩下需要登录的接口统一拦截。这里我踩过的坑是写拦截器时把路径匹配规则写错,导致登录接口自己也被拦住,前端永远拿不到 token。建议在代码里把路径规则拆出来并写单元测试或直接手工验证,不然排查起来太费时间。

4.4 Docker Compose一键部署前后端

如果想把系统部署到服务器上给别人演示,不建议用java -jar手动跑,也不建议把 MySQL 直接装在宿主机里。用 Docker Compose 把 MySQL、后端、Nginx 组合在一起是最省心的。一个典型的多阶段 Dockerfile 如下:

FROM maven:3.8-openjdk-8 AS builder WORKDIR /build COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests FROM openjdk:8-jre-slim WORKDIR /app COPY --from=builder /build/target/hospital-drug-system.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]

正式环境里前端打包成 dist 后放在 Nginx 容器,Nginx 再反代到后端的 8080 端口。用 Docker Compose 编排的好处是一次启动所有组件,而且环境一致,避免出现“本地能跑服务器不能跑”的尴尬。部署前记得把数据库脚本单独导入 MySQL 容器,不要把数据库初始化逻辑硬塞到 SpringBoot 启动流程里。

5. 开发中让我印象深刻的经验与可扩展点

5.1 真正耗时的地方:业务对账和代码重构

如果把这个项目的开发过程拆开看,真正耗时间的不是登录和 CRUD,而是“业务对账”。明明两张入库单都录了,批次库存总数却对不上;盘点差异到底是该调库存还是该生成报损单;退药之后批次效期怎么重新计算。这些业务规则如果一开始没有在纸上完整画清楚,就会边写边改,改到后面自己都容易蒙。

我建议动手前用思维导图或者普通文本把每个操作对应的库存变化、单据状态变化列出来。比如“药房向药库申领药品”就包含:新增申领单、药库审核通过、扣减药库批次库存、增加药房批次库存、写入两条库存流水。这些步骤想清楚后再写代码,整个后端 Service 会非常清晰。

5.2 给下一个做同类系统的人几条建议

能复用模板就不要从零搭前端。Element Plus 或若依这种现成后台管理模板,布局和权限框架已经完备,重点把后端业务写稳,整体效率会高出很多。

不要让“表面功能多”掩盖“业务逻辑松”。如果采购入库和库存流水没有关联,哪怕页面做得再花哨,评审时一问对不上账就会被质疑。宁可少做一个图表,也要把库存变动链条做扎实。

不要用真实数据去做演示。测试一批模拟药品数据时,建议统一编造明显的测试名称和批号,比如“测试用阿莫西林胶囊(勿用)”,避免演示时误把演示系统当成能直接投入生产环境的系统,安全边界一定要清楚。

5.3 这套系统还能往哪些方向做得更深

从设计实现角度来看,系统完成到这里已经算是有完整业务闭环的系统,但如果后续想继续扩展,其实还有不少方向可以做深。例如在采购入库时引入“药品追溯码”管理,通过扫码记录每一盒药的流向;在发药模块中接入院内 HIS 的处方数据,实现电子处方自动扣减库存;或者通过异步消息队列处理单据审批通知,把药库和药房之间的操作响应做成实时提醒。

我个人印象最深的一点是,这种系统表面上是一个 SpringBoot 管理平台,实际考验的是供应链思维和数据一致性意识。把药品批次库存当作基础账本,把每一笔变动都留下流水记录,系统才不会在复杂业务面前散架。做完这套药品管理系统后,再去看进销存、仓库管理、物料管理类的项目,很多设计可以互通。希望这份拆解能让你少踩我踩过的那些坑,把核心精力放到真正有价值的事情上。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询