SpringBoot医院药品管理系统实战:数据模型、库存预警与部署全解析 📅 发布时间:2026/9/11 10:02:06 👁 浏览次数: 简介基于SpringBoot的医院药品管理系统定位为计算机专业毕业设计参考项目面向有一定Java基础的学生核心功能覆盖用户信息、员工信息、药品信息和公告信息的日常维护员工信息支持模糊查询和类型筛选药品信息可按条件检索并执行新增、修改和删除较完整地体现了小型信息管理系统的分析与设计过程。整个资源包包含416个文件压缩后体积约25.97MB源码以Java、Vue为主配合SVG图标、XML配置、SQL脚本、论文文档和演示视频并提供安装、启动用的bat文件可帮助开发者快速跑通前后端全流程。目前已有242人次学习下载。项目目录层次清楚前后端模块界限明确对于正在开展毕业设计或希望上手SpringBoot框架的读者既能直接作为功能基线也能从中学习数据库建模、接口分层和页面交互等关键环节。1. SpringBoot医院药品管理系统先看清这个zip里装的是什么以“基于SpringBoot的医院药品管理系统”为题的源码每年在毕业设计、课程设计和外包交付里能见到几十个版本。骨架大同小异SpringBoot 做后端接口MySQL 存药品、供应商、库存和出入库记录前端要么挂一个 Vue 后台要么直接用 Thymeleaf 模板核心功能绕不开药品信息维护、药房发药、采购入库、库存查询和效期预警。这类系统看着门槛不高真正上手跑通后翻车点反而集中库存对不上账、批次效期没有字段存、定时预警没配 cron、价格用浮点类型导致精度丢失。我建议拿到源码后不要急着启动。先把 SQL 脚本里的十几张表看一遍看清药品表、批次库存表、流水表之间的关系等于先把系统骨架摸透了。后面写的接口、定时任务和报表都挂在这套模型上。所以这篇内容按“数据模型 → 后端实现 → 库存业务 → 版本配置 → 验收脚本”这条路线展开覆盖从解压 .zip 到部署演示的完整路径。2. 拆数据模型SpringBoot药品管理系统的表结构设计2.1 模块划分为什么药房系统比普通后台多一层约束医院药品管理系统的数据模型核心词汇只有一个批次。普通商品管理系统按 SKU 管理库存就够了药品不行。同一种药不同生产批号、不同有效期价格可能相同但分发优先级不同近效期批次必须优先出库。所以数据库里不能只有 drug 表和 stock 数量字段必须拆出批次库存。我一般把功能拆成五个模块基础数据药品、供应商、批次库存、出入库流水、预警记录、系统用户。表控制在十张以内权限不用做到按钮级一个用户表和角色字段足够应付答辩和演示。下面直接看核心建表语句这是整个系统能跑起来的地基。2.1.1 药品基本信息表通用名和商品名不要混在一个字段CREATE TABLE drug_info ( id BIGINT PRIMARY KEY AUTO_INCREMENT, drug_code VARCHAR(32) NOT NULL COMMENT 药品编码院内唯一, drug_name VARCHAR(64) NOT NULL COMMENT 药品通用名, trade_name VARCHAR(64) COMMENT 商品名可为空, spec VARCHAR(32) COMMENT 规格如0.25g*24粒, unit VARCHAR(8) COMMENT 基本单位盒/瓶/支, dosage_form VARCHAR(16) COMMENT 剂型片剂/胶囊/注射液, manufacturer VARCHAR(64) COMMENT 生产厂家, approval_number VARCHAR(32) COMMENT 批准文号, retail_price DECIMAL(10,2) COMMENT 零售价, status TINYINT DEFAULT 1 COMMENT 状态 1启用 0停用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_drug_code (drug_code), KEY idx_name (drug_name) ) ENGINEInnoDB COMMENT药品基本信息表;药品表和普通商品表最大的差异在于批准文号字段国药准字号是药品身份的唯一标识。这里 drug_code 是院内编码用来做条码和表单关联approval_number 是药监部门的批准文号两者不能互相替代。规格字段建议用字符串因为“0.25g*24粒”这种格式包含数值和单位拆开存反而难处理。价格字段必须用 DECIMAL(10,2)不能图省事用 DOUBLE否则后续对账会出现 0.01 元级别的误差医院场景对钱很敏感。2.1.2 批次库存表库存数字别放在药品表里CREATE TABLE drug_stock ( id BIGINT PRIMARY KEY AUTO_INCREMENT, drug_id BIGINT NOT NULL COMMENT 药品ID关联drug_info.id, batch_no VARCHAR(32) NOT NULL COMMENT 生产批号, stock_qty DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT 当前库存数量, expiry_date DATE NOT NULL COMMENT 有效期至, location VARCHAR(32) COMMENT 货位编码如A1-03, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_drug_batch (drug_id, batch_no), KEY idx_expiry (expiry_date) ) ENGINEInnoDB COMMENT药品批次库存表;这里三个细节直接决定系统好不好用第一库存按“药品 批号”维度唯一同一种药多个批次就多行记录不会把不同效期的货混在一起数。第二加了 expiry_date 并建索引效期预警就是一条范围查询不用全表扫描。第三预留了 version 字段后面扣库存做乐观锁要用避免两个人同时发药导致库存扣成负数。很多二手源码只给一张 drug 表加一个 stock 字段表面上能做增删改查一到“近效期药品列表”就写不出来。拿到这种项目后我一般直接补一张 drug_stock 表再写同步脚本把旧数据迁进去。2.2 出入库流水表让每一步操作都留痕CREATE TABLE stock_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, drug_id BIGINT NOT NULL, batch_no VARCHAR(32) NOT NULL, change_type TINYINT NOT NULL COMMENT 1入库 2出库 3报损 4盘点调整, change_qty DECIMAL(12,2) NOT NULL COMMENT 变动数量入库正数出库负数, before_qty DECIMAL(12,2) NOT NULL, after_qty DECIMAL(12,2) NOT NULL, operator_name VARCHAR(32) COMMENT 操作人, remark VARCHAR(128), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT药品出入库流水表;流水表的用途不是记账而是对账。库存汇总出错时通过 before_qty 和 after_qty 可以还原每一步操作。change_type 用数字枚举比字符串省空间查询也快。这里有一个很多人忽视的点业务代码里不要直接改 drug_stock 的 stock_qty 后随手 insert 一条流水两个动作必须包在同一个事务里否则会出现“流水有了库存没扣”或者反过来的脏数据。2.3 从 zip 还原项目骨架先对上目录结构拿到 .zip 解压后标准 Maven 结构长这样hospital-drug/ ├── pom.xml ├── src/main/java/com/example/hospital/ │ ├── controller/ # 接口层 │ ├── service/ # 业务层 │ ├── mapper/ # MyBatis-Plus Mapper │ ├── entity/ # 数据库实体 │ ├── config/ # 拦截器、跨域配置 │ └── HospitalApplication.java ├── src/main/resources/ │ ├── application.yml │ └── sql/ # 初始化脚本 └── target/ # Maven打包产物看目录的意义在于判断代码质量。如果 controller 里直接写 SQL 操作数据库说明项目没有分层后期加效期预警这种功能要重构如果有 service 层但没有 mapper 接口目录可能是 JPA 写的。绝大多数毕业设计用 MyBatis-Plus实体类与表字段一一对应。3. 后端实现SpringBoot常用注解与接口层写法3.1 统一返回结构Controller 里不裸返回 Map医院系统前端不是只有一个人写统一返回结构能减少前后端联调成本。我习惯的项目里永远有一个 Result 类所有接口返回它包一层。Data public class ResultT { private Integer code; private String msg; private T data; public static T ResultT ok(T data) { ResultT r new Result(); r.setCode(200); r.setMsg(success); r.setData(data); return r; } public static T ResultT error(String msg) { ResultT r new Result(); r.setCode(500); r.setMsg(msg); return r; } }统一返回结构解决的问题很直接前端 axios 拦截器只看 code业务数据全在 data 里不分页接口和分页接口的返回格式一致不需要每个接口单独对接。code 用 200 和 500 两个值就够不需要搞出一套复杂的错误码体系那只会让调用方记不住。3.2 Service 层用 MyBatis-Plus 少写一半代码SpringBoot 整合 MyBatis-Plus 是这个场景的标配。实体类继承 Model 或直接加 TableName 注解后Mapper 接口继承 BaseMapper 就拥有了单表 CRUD不需要写 XML。Service public class DrugServiceImpl extends ServiceImplDrugInfoMapper, DrugInfo implements DrugService { Override public PageResultDrugInfo pageQuery(DrugQuery query) { LambdaQueryWrapperDrugInfo wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(query.getKeyword()), DrugInfo::getDrugName, query.getKeyword()) .eq(query.getStatus() ! null, DrugInfo::getStatus, query.getStatus()) .orderByDesc(DrugInfo::getCreateTime); PageDrugInfo page new Page(query.getPageNum(), query.getPageSize()); this.page(page, wrapper); return PageResult.of(page); } }这段代码里有三个值得关注的参数。第一个是 LambdaQueryWrapper 里的 like 条件第一个参数是 boolean为 true 才拼接这个条件这样 keyword 为空时 SQL 不会多出多余的 WHERE 条件。第二个是 getStatus 的 eq 判断前端不传状态时默认查全部。第三个是 Page 对象MyBatis-Plus 的物理分页不需要手写 LIMIT配置分页插件后自动拼接。分页查询是后台管理系统的核心高频接口写得好不好直接影响列表页响应速度。3.3 参数校验和登录拦截小系统也要做的两道卡口RestController RequestMapping(/api/drug) public class DrugController { PostMapping public ResultString add(RequestBody Valid DrugInfo drug) { drugService.save(drug); return Result.ok(新增成功); } PutMapping(/{id}) public ResultString update(PathVariable Long id, RequestBody Valid DrugInfo drug) { drug.setId(id); drugService.updateById(drug); return Result.ok(修改成功); } }实体类的字段上补两个注解就能完成基础校验NotBlank 用在 drugCode 和 drugName 上NotNull 用在 retailPrice 上。SpringBoot 的 spring-boot-starter-validation 会自动拦截校验失败请求返回 400 和字段错误信息不用自己在方法里写 if else 判空。拦截器这一层演示项目不需要引入 Spring Security 全家桶一个 HandlerInterceptor 加一个 WebMvcConfigurer 就能挡住未登录请求。把登录接口放行其余 /api/** 请求校验请求头里的 token查到用户就放行。这套做法的信息量刚好覆盖答案SpringBoot 常用注解能力展示足够又不会把工程量推高一截。4. 药品库存的三大业务场景预警、效期与事务4.1 库存预警把“库存不足”和“近效期”当成两类任务库存预警在药品管理系统里不是一个小功能它直接决定了这个系统是“演示玩具”还是“能用的工具”。常见做法是写一个定时任务每天凌晨扫一次库存表把低库存和近效期的药品生成预警记录。Component public class StockAlarmTask { Resource private DrugStockMapper stockMapper; Resource private AlarmLogMapper alarmLogMapper; Scheduled(cron 0 0 2 * * ?) public void checkStock() { // 1. 低库存库存数量小于安全阈值DIY系统里阈值先写死 ListDrugStock lowStocks stockMapper.selectList( new LambdaQueryWrapperDrugStock() .le(DrugStock::getStockQty, 10)); // 2. 近效期有效期距离今天小于90天 LocalDate deadline LocalDate.now().plusDays(90); ListDrugStock expiryStocks stockMapper.selectList( new LambdaQueryWrapperDrugStock() .le(DrugStock::getExpiryDate, deadline)); // 3. 去重后写入预警日志表 saveAlarms(lowStocks, 库存不足); saveAlarms(expiryStocks, 近效期); } }定时任务的 cron 表达式 “0 0 2 * * ?” 表示每天凌晨 2 点执行选这个时间点是避开白天发药高峰。要开启定时任务必须在启动类上加 EnableScheduling这是 SpringBoot 定时任务最常见的遗漏点。预警日志表记录药品、类型、阈值、实际值和处理状态前端在首页展示未处理数量。阈值写死在代码里只适合演示。实际项目我会加一张 alarm_config 表按药品分类配置不同的低库存阈值抗生素类要求更严格外用药可以宽松些。这块改动量不大但演示时能讲出的业务理解明显不一样。4.2 有效期管理近效期药品怎么查最快SELECT d.drug_name, s.batch_no, s.stock_qty, s.expiry_date, DATEDIFF(s.expiry_date, CURDATE()) AS remain_days FROM drug_stock s LEFT JOIN drug_info d ON s.drug_id d.id WHERE s.expiry_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 90 DAY) ORDER BY s.expiry_date ASC;这条 SQL 是效期预警的核心把所有 90 天内到期的批次捞出来按到期日排序。DATEDIFF 算出的 remain_days 可以给前端用来渲染“剩余X天”的标签。为什么不在 Java 代码里算有效期差额因为数据库做日期范围计算走索引更快还能直接配合 LIMIT 做分页。需要注意 expiry_date 字段类型必须是 DATE 而不是 DATETIME否则 BETWEEN 的边界值会把当天的数据漏掉。这是实际运行中常见的坑查出来的结果比预期少几行以为是定时任务没跑其实是字段精度问题。4.3 库存扣减与事务并发下别用一条 update 硬扛发药出库是整个系统里并发风险最高的操作。两个收费窗口同时给同一个批次扣库存如果代码只做“先查库存够不够再 UPDATE 扣减”必然出现超卖。Transactional(rollbackFor Exception.class) public void reduceStock(Long stockId, BigDecimal qty, String operator) { DrugStock stock stockMapper.selectById(stockId); if (stock null) { throw new BusinessException(批次库存不存在); } if (stock.getStockQty().compareTo(qty) 0) { throw new BusinessException(库存不足); } // 乐观锁扣减版本号匹配才更新成功 int rows stockMapper.reduceStockByVersion(stockId, qty, stock.getVersion()); if (rows 0) { throw new BusinessException(操作冲突请重试); } // 写入流水表与扣减在同一事务 saveRecord(stock, qty, operator); }对应 XML 里的 UPDATEUPDATE drug_stock SET stock_qty stock_qty - #{qty}, version version 1 WHERE id #{stockId} AND version #{version}reduceStockByVersion 返回影响行数0 说明版本号对不上有人已经改过这行数据。抛异常让前端提示用户重试。Transactional 保证扣库存和写流水要么都成功要么都回滚。为什么用乐观锁不用悲观锁医院药品系统的并发量没有到秒杀级别悲观锁的 SELECT FOR UPDATE 虽然更简单但会锁住整行记录影响其他窗口发药。乐观锁让冲突发生在最后一刻绝大多数请求能直接成功。这里给新手一个建议compareTo 比较 BigDecimal 不要用 equalsequals 会比较精度0.9 和 0.90 会被判定为不相等而 compareTo 只比数值。5. 从 zip 到可运行SpringBoot 版本、配置与打包实战5.1 SpringBoot 版本怎么选先看 JDK 再看依赖很多解压出来的项目启动失败问题不在代码而在版本错配。拿到项目先看 pom.xml 里 SpringBoot 的 parent 版本。场景推荐组合原因毕业设计演示SpringBoot 2.7.x JDK 8环境问题最少教程最多新开工项目SpringBoot 3.2.x JDK 17长期维护Spring 官方新特性支持二次改造老项目保持原有版本升版本不如重写热词里“springboot版本太大”说的就是这种场景SpringBoot 3.x 把 javax 包替换成了 jakarta老代码里的 import javax.servlet 全部报错MyBatis-Plus 版本也要升到 3.5.3 以上才能兼容。如果机器只装了 JDK 8SpringBoot 3.x 直接启动不了这属于版本和运行环境不匹配不是代码 bug。5.2 application.yml 配置清单把外部依赖一次配齐server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/hospital_drug?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver data: redis: host: localhost port: 6379 database: 0 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto logic-delete-field: deleted参数说明都要对上serverTimezone 必须加否则 MySQL 8.x 驱动会报时区错误map-underscore-to-camel-case 开启后数据库的 drug_name 自动映射到实体 drugName 字段id-type 为 auto 表示主键自增。Redis 配置在演示项目里如果不是必须可以先注释掉减少一个外部依赖。log-impl 开发时打开SQL 会打印到控制台部署时删掉这行避免日志刷屏。5.3 Idea 创建项目超时和依赖下载慢的解决口径Idea 内建的 Spring Initializr 偶尔初始化超时这不是代码问题是网络到初始化服务的链路不稳定。处理方式有两种一是把初始化服务的 URL 换成镜像地址二是在本机已有的项目基础上直接改 pom.xml。Maven 依赖下载慢是另一码事在 settings.xml 里配一个阿里云镜像仓库多数依赖能秒下。# 打 jar 包跳过测试 mvn clean package -DskipTests # 后台运行指定端口 java -jar target/hospital-drug-0.0.1-SNAPSHOT.jar --server.port8080打包失败时先看控制台有没有 test 相关报错-DskipTests 是跳过编译测试代码-Dmaven.test.skiptrue 是跳过测试代码编译和运行后者更彻底。很多源码里带着测试类其中依赖的测试数据库根本没建不跳过测试就会一直装不上。SpringBoot 默认内嵌 Tomcatjar 包直接跑不需要外置容器。如果要把服务放到内网服务器长期运行用 nohup 加日志重定向比如 nohup java -jar app.jar app.log 21 方便事后排查。6. 验收前夜演示数据脚本与常见问题自查6.1 用一批临期药品把整个闭环演示出来空数据库演示没有说服力自己动手造一批带业务含义的数据比点半天新增按钮效率高得多。INSERT INTO drug_info (drug_code, drug_name, spec, unit, dosage_form, manufacturer, approval_number, retail_price) VALUES (GMP20240001, 阿莫西林胶囊, 0.25g*24粒, 盒, 胶囊剂, 示例制药厂, 国药准字H20003263, 12.80), (GMP20240002, 布洛芬缓释胶囊, 0.3g*20粒, 盒, 缓释胶囊, 示例制药厂, 国药准字H19990011, 19.50), (GMP20240003, 头孢呋辛酯片, 0.25g*12片, 盒, 片剂, 示例制药厂, 国药准字H20000463, 26.00); INSERT INTO drug_stock (drug_id, batch_no, stock_qty, expiry_date, location, version) SELECT id, 20240101, 5, DATE_ADD(CURDATE(), INTERVAL 30 DAY), A1-01, 0 FROM drug_info WHERE drug_code GMP20240001;这段脚本的效果是三分钟内造出低库存药品5 盒低于阈值 10和 30 天内的近效期批次定时任务跑一次就能在前端看到预警列表。演示时可以直接讲“这个批次的阿莫西林还有 30 天到期库存低于安全线系统自动生成两条预警”比现场点选药品再手动登记更有说服力。6.2 上线前自查表几个看代码就能发现的问题检查项判断标准常见问题分页查询SQL 没有 select *手写 LIMIT 容易漏参数金额字段全部用 BigDecimalDOUBLE 会导致对账差一分钱时间处理使用 LocalDateTimejava.util.Date 在 JSON 序列化时格式混乱幂等控制出库接口带唯一业务单号重复提交会扣两次库存敏感端点Actuator 相关端点不对外暴露生产环境开放 health 以外的端点存在信息泄露风险密码存储使用 BCrypt 加密明文密码在演示环境还好交付前必须改SpringBoot 的 heapdump 端点曾经是一个敏感信息泄露点原因就是 management.endpoints.web.exposure.include 配置成了 *把 heapdump、env 等端点全暴露出来。自查办法很简单开发环境需要看指标就配 include 只有 health、info生产环境直接留空。6.3 验证接口用一条 SQL 直接检查预警结果SELECT alarm_type, COUNT(*) AS cnt FROM alarm_log WHERE handle_status 0 GROUP BY alarm_type;这条 SQL 能在演示前确认预警任务是否执行成功。如果 count 为 0优先排查两件事启动类有没有 EnableScheduling以及 batch_no 字段存的数据和预期是否一致。整个系统跑通后把药品新增、入库、发药、预警处理这四个动作各做一遍数据的闭环就完整了。本文还有配套的精品资源点击获取