ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于SpringBoot的医院药品管理系统:从启动到避坑全解析

基于SpringBoot的医院药品管理系统:从启动到避坑全解析 简介这份基于SpringBoot的医院药品管理系统设计与实现毕业设计资源面向计算机相关专业的学生适用于课程作业、毕业设计或中小型医院药品管理项目参考。系统覆盖用户注册登录、角色权限管理、药品信息录入与分类、实时库存监控与报警、出入库管理、采购申请与审批等完整业务模块药品信息包含名称、规格、生产厂家、有效期等核心字段能帮助读者理解前后端配合方式与后台管理流程。压缩包共415个文件、24.08MB包含109个Java后端源码、54个Vue前端页面、161个SVG图标资源以及SQL数据库脚本、XML配置、打包运行脚本和演示视频等目录结构按功能划分便于定位代码并快速启动项目。目前已有59人学习下载适合对照SpringBootVue开发实践梳理药品库存、角色授权等核心逻辑也可为毕业设计说明书提供结构参考。1. 医院药品管理系统靠什么撑起一张处方药的完整流转一个药品从采购申请到入库、出库、发到患者手里中间要过审批、库存校验、批次记录好几道关。这套基于 SpringBoot 的医院药品管理系统就是把「药品信息—库存—采购—角色权限」这条链路一次性串起来药剂师提交采购申请管理员审核生成订单入库自动增加库存出库自动扣减库存低于阈值就亮报警整个过程每个操作都留痕。它不是只做增删改查的演示品而是把医院药房日常的账实管理逻辑做成了可落地的小型业务系统。适合两类人一是拿它做毕业设计或课程设计的在校生跑通之后改改表结构、换套前端皮肤就能答辩二是想快速搭一套药品管理后台给医院科室或药店做内部演示的开发者。系统是 SpringBoot 做后端、Vue 打包后由后端统一伺服的前后端一体项目下载解压后按脚本启动即可访问你不用另起 Node 服务。下面我把启动流程、数据模型、权限设计和踩过的坑逐一拆开讲。2. 三分钟跑起来从安装依赖到浏览器看到登录页拿到压缩包后先别急着看代码按 1-install.bat → 2-run.bat 的顺序走完整个系统就能在本地跑起来。这个项目的前端是 Vue 构建产物index.html、chunk-vendors.js 这类文件由 SpringBoot 当作静态资源直接伺服所以不需要你本地装 Node.js 或单独跑前端工程这是它省事的地方。2.1 三个批处理脚本的职责与执行顺序压缩包里有三个脚本1-install.bat、2-run.bat、3-build.bat。它们的职责是拆开的install 负责初始化环境run 负责启动服务build 负责重新打包前端产物。日常使用只碰前两个。REM 1-install.bat —— 首次部署时执行初始化数据库和依赖 echo off echo [1/3] 正在检查 Maven 环境... mvn -v nul 21 || (echo 未检测到 Maven请先安装并配置环境变量 pause exit /b 1) echo [2/3] 正在初始化数据库表结构和基础数据... mvn spring-boot:run -Dspring-boot.run.profilesinit echo [3/3] 环境初始化完成请执行 2-run.bat 启动系统 pause这里的关键动作是第二步以init这个 profile 启动一次 SpringBoot目的不是跑业务而是触发项目里的数据初始化逻辑建表、写入默认管理员账号和药品分类、生成基础库存记录。跑完之后数据库里已经有表结构和最小数据集后续正常启动就不会再重复初始化。接着是日常启动脚本REM 2-run.bat —— 日常启动入口 echo off echo 正在启动医院药品管理系统... call mvn spring-boot:run这个脚本会以前端分离模式启动——后端打包好的 Vue 静态资源已经位于src/main/resources/static目录下SpringBoot 内置 Tomcat 会把它们直接映射到根路径/。启动过程中如果看到Tomcat started on port 8080之类的日志说明 Web 容器起来了。打开浏览器访问http://localhost:8080看到的第一个页面就是登录页。默认管理员账号密码通常在初始化 SQL 或文档说明里拿到手先翻一下README或sql目录下的初始化脚本。2.2 端口、数据库与关键配置项环境跑起来之前建议先看一眼application.yml很多启动失败的问题都出在这。常见的配置结构如下server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/hospital_drug?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update show-sql: true h2: console: enabled: true两个最容易出问题的点一是 MySQL 版本项目默认用的是 MySQL 8.x对应驱动是com.mysql.cj.jdbc.Driver如果你的环境是 MySQL 5.7要改成com.mysql.jdbc.Driver二是ddl-auto: update这个配置——它允许 Hibernate 在启动时自动更新表结构对演示很方便但生产环境千万别这么干后面避坑章节我会细说。serverTimezoneAsia/Shanghai也要重点确认MySQL 8 默认时区是 UTC不写这个参数系统会直接抛时区相关的 SQLException。若你已经把数据库建好了但启动时报时区错误先改这个连接串比在 MySQL 里执行SET GLOBAL time_zone 8:00更快。2.3 重新构建前端什么时候才需要跑 3-build.bat如果你想改前端页面的样式、调整页面布局或者替换掉默认的登录页 Logo那就必须重新走一次构建流程先改 Vue 源码src 目录再打包输出到 SpringBoot 的 static 目录。3-build.bat 做的事情本质上就是 npm 构建核心命令如下npm install npm run buildnpm run build会执行 Vue 项目的打包脚本生成新的 index.html、css、js 文件并输出到dist目录。打包完需要把dist里的内容复制到 SpringBoot 的src/main/resources/static目录部分模板项目已经配置了自动拷贝然后重新执行 2-run.bat。注意如果你只改了后端 Java 代码跑 3-build.bat 没有意义它只负责前端资源。3. 药品业务闭环从分类管理到库存报警的数据流转系统把药品业务拆成了「药品信息—分类—库存—出入库记录」四个层次。理解这套数据模型是后续改功能、加字段的基础。药品信息是最上层的「主数据」分类是可选的挂靠维度库存表管实时数量出入库记录表管每一次数量变动的明细。四张表互相配合组成了药品流转的闭环。3.1 药品信息字段与分类的设计逻辑药品信息表是整个系统的主数据表字段设计很有代表性药品名称、规格、生产厂家、有效期、批准文号、存储条件、药品分类 ID。我建议你拿到项目后先打开实体类Drug.java看一眼通常会看到类似这样的定义Entity Table(name drug) public class Drug { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(nullable false, length 100) private String drugName; // 药品通用名 Column(length 50) private String specification; // 规格如 0.25g*24粒 Column(length 50) private String manufacturer; // 生产厂家 Column(length 20) private String approvalNumber; // 批准文号国药准字H开头 ManyToOne JoinColumn(name category_id) private DrugCategory category; // 关联药品分类 Column private Integer lowStockThreshold; // 库存报警阈值 Column private LocalDate expiryDate; // 有效期截止日期 }ManyToOne关联的是分类表。分类表的逻辑分成三级处方药、非处方药OTC、中药饮片。分类的好处不只是查询时按类别过滤更重要的是后续的权限控制——比如普通药剂师可以浏览非处方药但修改处方药价格需要管理员权限这个限制就是在服务层通过分类字段判断的。lowStockThreshold这个字段是关键中的关键它是库存报警阈值。每一条药品记录都可以单独设置阈值比如某个常备药设定为 200 盒库存低于 200 时系统自动进入待补货列表。设计成「每药品一阈值」而不是「全系统统一阈值」是为了应对不同药品消耗速度差异大的现实——感冒灵一个月消耗 2000 盒某种急救药可能半年才用 30 支统一设阈值必然失真。3.2 库存实时监控与出入库记录账实相符的账是怎么记的库存表和出入库流水表是分开的这个设计值得专门讲一下。-- 库存表核心字段 CREATE TABLE drug_stock ( id BIGINT PRIMARY KEY AUTO_INCREMENT, drug_id BIGINT NOT NULL, current_quantity INT NOT NULL, -- 当前库存数量 safety_quantity INT, -- 安全库存线 location VARCHAR(50), -- 存放货位如 A-03-02 updated_time DATETIME, FOREIGN KEY (drug_id) REFERENCES drug(id) ); -- 出入库流水表核心字段 CREATE TABLE stock_flow ( id BIGINT PRIMARY KEY AUTO_INCREMENT, drug_id BIGINT NOT NULL, flow_type VARCHAR(10) NOT NULL, -- INBOUND / OUTBOUND quantity INT NOT NULL, -- 本次变动数量 source_order_id BIGINT, -- 来源采购订单ID operator_id BIGINT, -- 操作人哪个用户执行的 flow_time DATETIME );两张表的分工是库存表只保存「当前快照」流水表记录「每一次变动」。为什么拆开假设没有流水表某天库存数量对不上你根本不知道是哪一次入库登记错了有了流水表盘亏时按时间倒序逐条比对流水就能定位到具体是哪一笔操作出了问题。这套设计叫做「流水驱动库存」进销存系统的常见做法是库存表只做加减法流水表负责留痕最终对账以流水为准。入库和出库对应的业务场景分别是药品采购入库验收、药房窗口发药出库。入库时找采购订单号出库时找处方单号。注意出库操作一定要校验库存是否充足否则会出现负数库存。真实的业务里这里还涉及并发控制后面避坑章节会专门讲。3.3 库存报警的实现方式与调参建议系统里库存报警的触发逻辑通常是定时任务核心代码一般长这样Scheduled(cron 0 */30 * * * ?) // 每半小时检查一次 public void checkStockAlert() { ListDrugStock stocks stockRepository.findAll(); ListStockAlert alerts new ArrayList(); for (DrugStock stock : stocks) { if (stock.getCurrentQuantity() stock.getSafetyQuantity()) { StockAlert alert new StockAlert(); alert.setDrugId(stock.getDrugId()); alert.setCurrentQuantity(stock.getCurrentQuantity()); alert.setAlertTime(LocalDateTime.now()); alerts.add(alert); } } alertRepository.saveAll(alerts); // 同时把报警数据推送给前端轮询接口 }Scheduled是 Spring 自带的轻量级定时任务注解cron表达式0 */30 * * * ?表示每半小时整点执行一次?是日字段的占位符表示不需要指定具体哪一天。项目中实际报警间隔可以在application.yml里通过配置项调整比如改成0 */5 * * * ?就是 5 分钟检查一次。新手常犯的错误是不知道这个 cron 在项目里能改导致演示时库存报警迟迟不出现等得心焦。stock: alert: cron: 0 */15 * * * ? # 改成每15分钟检查一次 notify-role: ADMIN # 报警推送对象我一般建议演示前把 cron 改短比如每 5 分钟或每 1 分钟一次这样演示「库存报警」功能时不用干等半小时。如果项目的报警不是通过定时任务而是通过前端轮询那你需要关注的是后端查询接口的返回频率改法不同但殊途同归。4. 用户权限与采购审批三张表如何撑起一套权限链药品管理系统不像普通的后台管理系统那样人人平等它必须区分角色管理员管药品信息、药剂师管日常出入库、医生只能查看药品信息不能改库存。这套控制逻辑建立在用户表、角色表、权限表或用户-角色关联表之上配合 Spring Security 的过滤器链实现。底层的数据模型是标准 RBAC基于角色的访问控制设计理解它之后给系统新增角色或者调整权限边界就变得很简单。4.1 用户注册登录与角色权限的核心模型用户管理模块不是一张用户表就完事正常项目至少会拆四张表-- 用户表 CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(255) NOT NULL, -- BCrypt 加密后存储 real_name VARCHAR(50), role_id BIGINT, enabled TINYINT DEFAULT 1, created_time DATETIME ); -- 角色表 CREATE TABLE sys_role ( id BIGINT PRIMARY KEY AUTO_INCREMENT, role_code VARCHAR(30) UNIQUE NOT NULL, -- ADMIN / PHARMACIST / DOCTOR role_name VARCHAR(50) ); -- 权限表 CREATE TABLE sys_permission ( id BIGINT PRIMARY KEY AUTO_INCREMENT, perm_code VARCHAR(50) UNIQUE NOT NULL, -- drug:add / stock:outbound perm_name VARCHAR(50) ); -- 角色-权限关联表 CREATE TABLE sys_role_permission ( role_id BIGINT NOT NULL, permission_id BIGINT NOT NULL, PRIMARY KEY (role_id, permission_id) );密码字段的存储方式值得注意必须是 BCrypt 加密后的密文绝对不能是明文。Spring Security 的BCryptPasswordEncoder每次加密结果不同登录时用matches方法校验这样即使数据库泄露攻击者也拿不到可直接用的密码。我在一些课程设计代码里见过password字段直接存明文的做法如果是自己写代码千万别省这一步。RBAC 模型的权限判断逻辑是用户登录成功后后台查询该用户角色再根据角色查出对应权限码列表存入本次会话的SecurityContext或 Redis 缓存里。每次请求到达 Controller 时Spring Security 的过滤器拦截请求解析访问目标需要的权限码比对当前用户权限列表匹配失败返回 403。请求拦截的配置长这样Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers(/login, /css/**, /js/**).permitAll() .antMatchers(/api/drug/**).hasAnyRole(ADMIN, PHARMACIST) .antMatchers(/api/stock/inbound).hasRole(PHARMACIST) .antMatchers(/api/stock/outbound).hasRole(PHARMACIST) .antMatchers(/api/purchase/approve).hasRole(ADMIN) .anyRequest().authenticated() .and() .formLogin() .loginPage(/login) .defaultSuccessURL(/index) .and() .logout() .logoutUrl(/logout); return http.build(); }antMatchers的匹配顺序是按声明顺序从上到下执行的Spring Security 用的是第一条匹配规则优先所以更具体的路径要写在更泛化的路径前面。比如 /api/drug/**写在/api/drug/delete前面没问题但要反过来写就会出问题。.hasAnyRole(ADMIN, PHARMACIST)表示只要用户角色是 ADMIN 或 PHARMACIST 就能访问多个角色之间是「或」的关系。.anyRequest().authenticated()兜底表示除以上规则外所有请求都必须登录后才能访问。4.2 采购申请与审批的状态流转采购管理模块的业务流是药剂师提交采购申请包含药品名称、数量、预估金额、供应商→ 管理员登录后台查看申请列表 → 点击「审核通过」→ 系统自动生成采购订单 → 药品到货后进行入库操作 → 采购订单状态置为「已完成」。整个流程在数据库层面就是状态字段的流转CREATE TABLE purchase_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(30) UNIQUE, -- 单号PO20250613001 drug_id BIGINT, quantity INT, supplier VARCHAR(100), applicant_id BIGINT, -- 提交人 apply_time DATETIME, status TINYINT DEFAULT 0, -- 0待审核 1已通过 2已入库 3已驳回 approver_id BIGINT, -- 审核人 approve_time DATETIME );status字段从 0 到 3 单向流转每一步操作都由对应的服务层方法控制。这里有一个比较好的实践审核操作不能只改状态还要记录approver_id和approve_time形成完整的操作审计。这样以后如果要查「这批药是谁审批的、什么时候审批的」直接从表里就能拉出来。我在做药品管理系统时遇到过这种需求使用方要求每一个操作节点都能追溯后来就把这种「操作留痕」的思路推广到了所有状态流转的业务上。采购申请生成订单之后订单和后续入库是对接的入库操作需要选择「对应采购订单」系统自动把订单数量和入库数量做比对防止超量入库。这也是账实相符的一部分。演示时可以走一遍完整流程用药剂师账号提交采购申请 → 退出登录 → 用管理员账号审核通过 → 再入库 → 查看库存增加、流水表新增一条 INBOUND 记录。5. 避坑指南启动失败、数据不对、权限 403 的常见翻车现场跑这套系统时最容易出问题的不是业务代码而是环境配置和几个隐蔽的数据坑。下面按「现象 → 原因 → 解决」整理 6 条踩过的坑覆盖启动、数据、权限三个层面。5.1 启动报错MySQL 时区问题导致无法建表现象执行 2-run.bat 后控制台抛出The server time zone value EST is unrecognized或Could not create connection to database server应用启动失败。原因MySQL 8.x 默认时区是系统时区很多服务器上是 UTC 或 EST而 JDBC 连接串没有显式声明时区驱动在解析时间字段时通不过校验。这是 MySQL 8 新版 JDBC 驱动最常见的启动错误。解决在application.yml的数据库连接 URL 末尾加上参数url: jdbc:mysql://localhost:3306/hospital_drug?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai改完重启。如果你用的是 MySQL 5.7还需要把驱动类从com.mysql.cj.jdbc.Driver换成com.mysql.jdbc.Driver同时把 pom.xml 里的 mysql-connector-java 版本降到 5.x。5.2 启动报错Tomcat 端口被占用现象控制台报Port 8080 was already in use应用起不来。原因本机已经有其他程序占用了 8080 端口或者上一次 SpringBoot 进程没有正常退出实际还在后台挂着。解决先找到占用进程Windows 下执行netstat -ano | findstr :8080 taskkill /PID 进程号 /F如果你想换端口改application.yml里的server.port为 8081 或其他空闲端口同时确认数据库连接串没被影响。5.3 前端页面能开但接口数据加载不出来现象浏览器能打开登录页CSS 和 JS 加载正常但登录后页面表格空白打开控制台看到接口请求报 404 或 500。原因最常见的情况是后端服务根本没有连上数据库或者接口路径与前端调用路径不一致。SpringBoot 对前端静态资源和后端 API 做了分离映射前端打包时调用的 baseURL 如果写成了别的端口前后端就对不上。解决先看浏览器 Network 里接口请求的 URL是不是http://localhost:8080/api/...。如果端口对得上再用 curl 直接测后端接口curl http://localhost:8080/api/drug/list返回 JSON 说明后端正常问题出在前端静态资源版本和接口不匹配重新 build 一次前端返回 500 则看后端日志重点排查 SQL 语句或数据库连接。我在处理类似问题时还会顺手确认一下ddl-auto是否为update——如果项目后来手动改过数据库表结构实体类与表不一致时也会抛错。5.4 中文乱码药品名称变成问号现象药品信息录入中文名称后页面上显示一串???数据库里也是乱码。原因数据库表或连接串的字符集没有指定 UTF-8。MySQL 5.7 默认字符集是 latin1不支持中文。解决改数据库连接串加上characterEncodingutf8注意不是utf-8带横杠再检查表本身的字符集ALTER TABLE drug CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;最后重启应用。如果还乱码检查一下前端页面 meta 标签charsetutf-8是否还在——有时打包后会被覆盖掉。5.5 权限 403明明有管理员账号但按钮还是点不了现象用管理员账号登录进去后台某些操作按钮如删除药品、审批采购申请可以显示但点击执行时提示「没有权限」或直接返回 403。原因多数情况下是用户角色关联数据出了问题——数据库里该用户的role_id指向的角色编码和代码里hasRole(ADMIN)拼写不一致。比如代码里写的是ADMIN但角色表里存的是adminSpring Security 匹配失败。解决查数据库SELECT u.username, r.role_code FROM sys_user u LEFT JOIN sys_role r ON u.role_id r.id;确认role_code值和代码里hasRole参数大小写一致。另外一个隐蔽的坑是hasRole(ADMIN)在 Spring Security 底层会自动拼成ROLE_ADMIN如果角色表里存的是ADMIN而不是ROLE_ADMIN也会失败。解决办法是在角色表里用ROLE_ADMIN作为编码或者用hasAuthority(ADMIN)替代hasRole后者不拼接前缀。5.6 库存出库出现负数现象执行出库操作后库存变成负数药品数量对不上。原因出库校验时只判断了quantity 0但没有校验当前库存是否充足或者两个用户同时操作同一药品出库产生了竞态条件——前一个还没扣完后一个已经读取到旧数量通过了校验。解决业务层加两个策略。第一个是代码层校验public synchronized void outbound(Long drugId, int quantity) { DrugStock stock stockRepository.findByDrugId(drugId); if (stock.getCurrentQuantity() quantity) { throw new BusinessException(库存不足当前仅有 stock.getCurrentQuantity()); } stock.setCurrentQuantity(stock.getCurrentQuantity() - quantity); // 写流水表... }第二个是数据库层兜底——把更新语句带上条件UPDATE drug_stock SET current_quantity current_quantity - 5 WHERE drug_id 1 AND current_quantity 5;受影响行数为 0 时说明库存不足事务回滚。这样即使在并发场景下数据库的行锁也会保证不会扣成负数。我一般在课程设计里会把这两层都加上毕竟展示的时候当面演示库存负数很尴尬。6. 让接口自己会说话用 Postman 做一次 5 分钟的接口回归系统跑通后在答辩前做一次 API 快速巡检能发现 60% 以上的隐藏问题。这里分享一个我常用的方法用 Postman 建一个带环境变量的集合把核心接口按「药品—库存—采购—权限」的顺序串成一条链用自动断言的脚本跑完整个流程哪里断了直接定位到接口省得在页面上一个个点。先建环境变量把 baseUrl 和 token 设为变量{ baseUrl: http://localhost:8080, token: }然后用登录接口获取 token 并自动写入环境变量。登录接口的 tests 脚本如下const res pm.response.json(); if (res.code 200 res.data.token) { pm.environment.set(token, res.data.token); console.log(登录成功token 已写入环境变量); } else { console.error(登录失败请检查账号密码或服务状态); }接着是药品列表查询接口测试脚本断言返回不为空pm.test(药品列表接口正常, function () { const res pm.response.json(); pm.expect(res.code).to.eql(200); pm.expect(res.data.list).to.be.an(array); pm.expect(res.data.list.length).to.be.greaterThan(0); });测试脚本里pm.test是 Postman 的断言方法第一个参数是断言名称会在测试结果面板显示第二个参数是回调函数里面用pm.expect做断言。.to.eql是相等断言.to.be.an(array)是类型断言.to.be.greaterThan(0)是数值断言。数组和数值断言是关键——药品列表返回空数组不代表接口报错但列表数据为 0 说明初始化脚本可能没跑成功需要返回去检查数据库。然后按业务链路继续串创建药品 → 修改药品 → 入库 → 出库 → 提交采购申请 → 管理员审核 → 查询采购单状态。每一步都把上一步返回的关键 ID 写入环境变量形成依赖链// 创建药品接口的 Tests const res pm.response.json(); if (res.data res.data.id) { pm.environment.set(drugId, res.data.id); }跑完这条链之后后端日志能覆盖全部核心业务方法调用数据库里的数据也会被完整写入一遍。如果某一步断言失败优先看 Postman 控制台打印的后端异常堆栈——SpringBoot 默认会把异常信息写入响应体里面有精确到哪一行代码出错的信息配合show-sql: true的配置如果没开把这个配置临时打开再重启一次就能看到 SQL 执行的具体情况判断是 SQL 写错还是参数传错。演示前一天晚上我会完整跑一遍这个 Postman 集合再关机避免第二天现场出问题。从那以后我每次接手类似的 SpringBoot 管理系统都会先花一刻钟建这么一套接口巡检集改完代码跑一遍比在页面上手工点十几次快得多也踏实得多。这套系统值得下载的地方就在于它不是一个空壳后台而是把用户、权限、药品、库存、采购的业务闭环做完整了——你在此基础上加功能、改业务、做扩展都有现成的骨架和代码范式可以参考希望帮到你。本文还有配套的精品资源点击获取
返回列表