做毕设选题的同学,十个里有七个会问:有没有那种技术栈主流、业务场景清楚、做起来不难但又不会被导师问倒的项目。医院药品管理系统就是这类选题里很稳的一个。它是一个典型的 Java 全栈项目,前端用 Vue,后端用 SpringBoot,数据库用 MySQL,业务上围绕药品的入库、出库、库存、效期、供应商和角色权限展开,既有增删改查的整体感,又有预警、流水、状态控制这些能拿出来讲的细节。
我前阵子帮一位同学把类似的一套系统从课程设计改成毕业设计,前后花了一周多。这篇文章就把这套 SpringBoot + Vue 医院药品管理系统从设计思路、表结构、核心代码到运行和答辩的完整路径写出来,给正在选型或者已经拿到源码但不知道怎么讲的同学做个参考。内容会尽量贴实际操作,少说虚的。
1. 项目到底在做什么:医院药品管理系统核心业务拆解
1.1 一个医院药品管理系统要管什么
很多人一听“药品管理系统”第一反应是“不就是药品增删改查吗”,真细拆起来会发现,它要管的链路比想象中长。药品从供应商送到医院,先进药库,再根据科室领用或者患者处方发放到药房,最后消耗掉。这一路涉及药品基本档案、进货批次、库存变化、效期管理、供应商结算、操作人员权限,任何一个环节都可能是题目里“可展开的功能”。
一套能拿去答辩的药品管理系统,至少要有这些模块:药品基础信息管理,包括药品编码、名称、分类、规格、生产厂家、批准文号、单位、进价、售价;药品分类维护,方便按类别筛选和统计;供应商管理,记录供货单位、联系人、电话、状态;采购入库和出库/退药出库,每次操作都要生成流水;库存批量管理,要记录批号、生产日期、有效期;库存预警,低于下限或快过期的药品要提示;用户管理和角色权限,不同岗位只能看到和操作对应菜单。
这套系统的价值不在于某个页面写得多炫,而在于它把“药品从哪来、现在在哪、还剩多少、什么时候过期”这条主线讲清楚了。对毕设而言,这条主线比花哨的图表更能撑起答辩。
1.2 为什么说这个选题最适合毕设和课设
毕设选题最难把握的度是:不能太简单,也不能复杂到一个人做不完。药品管理系统正好在中间偏上的位置。
业务场景够真实。医院信息化是当前行业热点,药品管理又是其中最贴近日常的部分,导师一听就知道你在做什么,不会觉得你是随便选了一个管理系统。同时数据关系有层次:药品分类和药品是一对多,药品和批次是一对多,每一次出入库都对应多条流水,这种关系足够你展示外键和关联查询能力,又不会像电商订单那么复杂。
更重要的是,它能自然引出“事务”“并发”“预警”这些进阶概念。库存类系统都必须处理同一个问题:一次入库或者出库,不只是改一个表,而是一组数据要同时变化。这正好是数据库事务最经典的讲解场景。只要能把这部分讲明白,答辩里的系统设计分基本就稳了。
1.3 拿到源码后的第一件事:先分清“能用”和“能讲”
很多同学下载源码后第一件事是急着点击运行,页面出来了就以为万事大吉。我建议反过来,先打开数据库脚本,把表和表之间的关系看懂,再回到代码里确认每个页面操作对应哪个接口、改了哪些表。代码可以在短时间内跑通,但“讲得顺”才是毕设拿高分的关键。
我带的这个同学最开始也是一样的,页面很漂亮,但我在模拟答辩时问他“库存预警怎么实现的”,他愣了几秒后说“就是查询库存小于最小库存的数据”。这个回答不算错,但不够有深度。后面我们把整个入库流程重新梳理了一遍,他才真正意识到这个系统里最有价值的不是界面,而是数据联动和事务控制。
2. 技术选型为什么是 SpringBoot + Vue + MySQL 而不是其他组合
2.1 SpringBoot 把后端复杂度压到了最低
做毕设最怕的不是业务逻辑难,而是环境配置和框架整合浪费时间。SpringBoot 在这方面的优势非常明显:内嵌 Tomcat,不用单独部署;自动配置机制能大幅减少 XML 配置;Starters 依赖把常用的包都聚合好了,需要什么就引什么。以前用 SSM 要先配 web.xml、Spring 容器、MyBatis 事务管理器,现在一个 spring-boot-starter-web 基本就搞定。
更实际的一点是,SpringBoot 已经成为 Java 岗位的基础要求。不管以后做后端还是做全栈,简历上写“熟悉 SpringBoot”至少不会拉低印象分。用它来做药品管理系统,既完成学业任务,也算一次项目经验的积累。
2.2 Vue 作为前端框架,开发和演示都很顺手
这套系统用的是 Vue,配合 Element UI 这类组件库,页面搭建效率极高。药品列表用表格组件,入库和出库用弹窗表单,统计区域用卡片展示,基本不需要写什么复杂的前端交互。Vue 的响应式机制让数据和页面自动同步,修改库存后页面立刻更新,答辩现场演示效果更直观。
前后端分离的结构对毕设也很友好。后端专心出接口,前端专心出页面,两边可以分开调试,也可以在答辩时重点展示接口返回的数据结构。前端代码结构清楚的话,还能讲一点“组件化开发”“接口统一封装”的点,这在项目里虽然不复杂,但对找工作有用。
2.3 为什么不用 SpringCloud 或者更重的微服务架构
有些同学想把项目做“大”,非要加 SpringCloud、消息队列、Redis 缓存,结果是自己给自己挖坑。药品管理系统本质是一个单体业务系统,已经有库存、权限、预警这些完整逻辑,再拆成多个微服务只会增加部署成本,答辩时反而容易被追问“你这个服务拆分到底解决了什么问题”。
技术选型要有说服力,不要唯“新”是举。用 SpringBoot + Vue + MySQL 的理由很充分:业务规模不需要微服务,单体架构足够快速开发和演示;数据关系明确,MySQL 的关系模型最合适;Vue 让前端展示直观,也符合当前主流的前后端分离实践。等以后真需要扩展,再把库存服务单独拆出去也不迟。
3. 核心表结构设计:药品主表、批次库存、流水表
3.1 表结构与业务关系怎么组织
数据库设计是这套系统的灵魂。很多源码在功能上没问题,但表和表之间的关系绕来绕去,非常影响理解和答辩。我建议按业务主线来建表,不要一上来堆二十张表。
最核心的是 sys_user、drug、drug_category、supplier、stock_batch、stock_log 这几张。sys_user 管登录用户,drug 管药品信息,drug_category 管分类,supplier 管供应商,stock_batch 管批次库存,stock_log 管每次出入库的流水。如果要做到药品出库去向可查,可以再增加 outbound_order 和 outbound_order_item,关联领用科室或者患者处方号。这张表清单足够支撑系统功能,也方便在答辩时画出整洁的 ER 图。
3.2 药品主表设计的几个关键字段
药品主表是整个系统的中心,设计时要区分“药品基础属性”和“库存属性”。药品编码、名称、规格、厂家、批准文号属于基础属性,一般不会随业务变化;当前库存、库存下限、库存上限属于库存属性,会频繁更新。
CREATE TABLE `drug` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `drug_code` varchar(50) NOT NULL COMMENT '药品编码', `drug_name` varchar(200) NOT NULL COMMENT '药品名称', `category_id` bigint(20) DEFAULT NULL COMMENT '药品分类ID', `specification` varchar(200) DEFAULT NULL COMMENT '规格', `manufacturer` varchar(200) DEFAULT NULL COMMENT '生产厂家', `approval_number` varchar(100) DEFAULT NULL COMMENT '批准文号', `unit` varchar(20) DEFAULT NULL COMMENT '单位', `purchase_price` decimal(10,2) DEFAULT NULL COMMENT '采购价', `sale_price` decimal(10,2) DEFAULT NULL COMMENT '零售价', `stock` int(11) NOT NULL DEFAULT 0 COMMENT '当前库存', `min_stock` int(11) NOT NULL DEFAULT 10 COMMENT '库存下限', `max_stock` int(11) DEFAULT 1000 COMMENT '库存上限', `status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '状态 0停用 1启用', `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_drug_code` (`drug_code`), KEY `idx_drug_name` (`drug_name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='药品信息表';药品编码加唯一索引,这是药品身份的关键,同一药品不能重复建档。药品名称建普通索引,用来支持模糊搜索;分类 ID 做外键关联 drug_category。这里要特别注意,不要把生产日期、有效期直接放到 drug 表里,因为同一药品可能进多个批次,有效期和批号必须跟批次走。
3.3 批次库存与流水表:库存预警的核心
真正让库存系统变专业的是批次库存表和流水表。
CREATE TABLE `stock_batch` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `drug_id` bigint(20) NOT NULL COMMENT '药品ID', `batch_no` varchar(100) NOT NULL COMMENT '批号', `produce_date` date DEFAULT NULL COMMENT '生产日期', `expire_date` date DEFAULT NULL COMMENT '有效期至', `quantity` int(11) NOT NULL DEFAULT 0 COMMENT '批次库存', PRIMARY KEY (`id`), UNIQUE KEY `uk_drug_batch` (`drug_id`, `batch_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='药品批次库存表';同一个药品可能同时存在多个批号,出库或发药时应优先处理最先过期的批次,这是医院药房里很常见的规则。stock_batch 表以药品 ID 和批号做联合唯一索引,既防止重复录入,也能在入库时通过唯一键判断是新增批次还是累加库存。
流水表则更加关键。千万不要只改 drug 表的 stock 字段,而是在每次入库、出库、盘点时写一条记录:药品、批号、数量变化、操作类型、操作人、操作时间。这样一旦数据对不上,能根据流水回溯。对毕设答辩来说,这个设计本身就是亮点,可以说明“我的系统不只记录结果,还能追踪过程”。
预警功能就是针对当前库存和效期做筛查。当前库存小于等于 min_stock,就提示库存不足;按日期筛选到期时间小于某个阈值的批次,就形成效期预警。这段逻辑不需要复杂算法,重点是表结构要支持,而上面的设计正好能满足。
4. 关键代码实现:SpringBoot 后端与 Vue 前端怎么联动
4.1 后端项目结构与分层思路
一套适合学习和答辩的后端项目,结构一定要清楚,不要所有代码堆在 Controller 里。我的习惯是分成 controller、service、mapper、entity、config、common 这六层。
com.example.hospital ├── controller │ ├── DrugController.java │ ├── StockController.java │ └── LoginController.java ├── service │ ├── StockInService.java │ └── impl │ └── StockInServiceImpl.java ├── mapper │ ├── DrugMapper.java │ ├── StockBatchMapper.java │ └── StockLogMapper.java ├── entity │ ├── Drug.java │ ├── StockBatch.java │ └── StockLog.java ├── config │ └── WebConfig.java └── common ├── Result.java └── BizException.javaController 只负责接收参数和返回结果,Service 里写业务规则,Mapper 只处理数据库操作。这个分层在答辩时一定能用上,比如导师问“入库逻辑写在哪个方法里”,你可以直接说在 StockInServiceImpl 的 inbound 方法里,从哪个包哪一行开始讲,一听就知道你对代码熟悉。
4.2 数据库连接配置和登录鉴权
启动后端前,必须把 application.yml 里的数据库连接改成自己本地的配置。
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&useSSL=false username: root password: 123456 mybatis-plus: configuration: map-underscore-to-camel-case: true这里的 url 一定要带 characterEncoding=utf8,否则后面查出来的中文可能乱码。MySQL 8 还要带 serverTimezone,不然会因为时区问题启动报错。
登录鉴权不一定要上全套 Spring Security,对毕设来说用拦截器加 JWT 足够。用户登录成功后生成 token,前端每次请求都放到请求头里,后端拦截器校验 token 后再放行。用 Spring Security 也行,但配置复杂,而且答辩时容易被追问原理。用 JWT 加拦截器,代码量少,逻辑也好讲:以 token 判断登录状态,以角色标识控制菜单权限。
4.3 药品入库接口:事务、批次、库存联动
入库是整个库存系统里最值得深讲的功能。很多同学的代码是这样写的:把药品库存加一下,再插一条记录,完事。这个做法在演示时可能没毛病,但被问到“库存本身和批次数据不一致怎么办”就答不上来。
合理的入库流程应该是:判断药品是否存在,按药品 ID 加批号查询批次;批次存在就把数量累加,批次不存在就新建批次;同步更新 drug 表当前库存,最后写库存流水。这三个动作必须在一个事务里完成,任何一个失败都要回滚,否则库存数据就错了。
@Service public class StockInServiceImpl implements StockInService { @Resource private DrugMapper drugMapper; @Resource private StockBatchMapper stockBatchMapper; @Resource private StockLogMapper stockLogMapper; @Override @Transactional(rollbackFor = Exception.class) public void inbound(InboundDTO dto) { Drug drug = drugMapper.selectById(dto.getDrugId()); if (drug == null) { throw new BizException("药品不存在"); } StockBatch batch = stockBatchMapper.findByDrugIdAndBatchNo( dto.getDrugId(), dto.getBatchNo()); if (batch == null) { batch = new StockBatch(); batch.setDrugId(dto.getDrugId()); batch.setBatchNo(dto.getBatchNo()); batch.setProduceDate(dto.getProduceDate()); batch.setExpireDate(dto.getExpireDate()); batch.setQuantity(dto.getQuantity()); stockBatchMapper.insert(batch); } else { batch.setQuantity(batch.getQuantity() + dto.getQuantity()); stockBatchMapper.updateById(batch); } drug.setStock(drug.getStock() + dto.getQuantity()); drugMapper.updateById(drug); StockLog stockLog = new StockLog(); stockLog.setDrugId(dto.getDrugId()); stockLog.setBatchNo(dto.getBatchNo()); stockLog.setQuantity(dto.getQuantity()); stockLog.setType("IN"); stockLog.setOperatorId(dto.getOperatorId()); stockLogMapper.insert(stockLog); } }这段代码的关键点在 @Transactional。只要有这一步,系统在同一时刻出现多个请求时,事务就能保证要么全部执行成功,要么全部回滚。如果还想更严谨一点,可以在事务开始后对 drug 表执行 select for update 做行锁,避免两个人同时操作同一种药品导致库存覆盖。讲到这里,答辩分数已经不会低了。
4.4 前端页面与接口联调
前端我用 Vue + Element UI 来举例。页面大部分是表格加弹窗,调用后端接口时统一用 axios 封装好的 request 对象。
<el-dialog title="药品入库" :visible.sync="dialogVisible" width="500px"> <el-form :model="inboundForm" label-width="90px"> <el-form-item label="药品"> <el-select v-model="inboundForm.drugId" filterable> <el-option v-for="item in drugList" :key="item.id" :label="item.drugName" :value="item.id" /> </el-select> </el-form-item> <el-form-item label="批次号"> <el-input v-model="inboundForm.batchNo" /> </el-form-item> <el-form-item label="生产日期"> <el-date-picker v-model="inboundForm.produceDate" type="date" value-format="yyyy-MM-dd" /> </el-form-item> <el-form-item label="有效期至"> <el-date-picker v-model="inboundForm.expireDate" type="date" value-format="yyyy-MM-dd" /> </el-form-item> <el-form-item label="入库数量"> <el-input-number v-model="inboundForm.quantity" :min="1" /> </el-form-item> </el-form> <div slot="footer"> <el-button @click="dialogVisible = false">取消</el-button> <el-button type="primary" @click="submitInbound">确认入库</el-button> </div> </el-dialog>提交入库时,前端把表单数据 POST 给后端,后端返回成功后重新加载当前页表格和库存数据。这里有个很容易踩的坑,就是日期格式不一致。后端 LocalDate 默认可能接收不到前端传来的日期字符串,所以前端日期组件要加 value-format="yyyy-MM-dd",后端用 String 接收后再转 LocalDateTime,或者直接用 @JsonFormat 注解统一格式。
5. 从源码到可运行:环境搭建、启动与常见问题
5.1 环境准备与版本建议
运行这套系统前,先把基础环境装好。后端建议 JDK 1.8 或者 11,Maven 3.6 以上,数据库 MySQL 5.7 或 8.0,前端 Node.js 14 以上。版本不要追求最新,稳定就好,尤其是 JDK 版本太高容易遇到第三方依赖兼容性问题。
如果是第一次跑项目,建议按顺序来,先数据库,再后端,最后前端。不要三个环境同时乱试,出了问题很难定位。
5.2 后端启动步骤
打开 SQL 文件,执行里面的建库建表语句,生成 hospital_drug 数据库。然后把 application.yml 里的用户名密码改成你自己本地的数据库账号。后端项目用 Maven 导入依赖后,直接运行启动类。
mvn clean package -DskipTests java -jar target/hospital-drug-1.0.0.jar如果只是为了开发调试,直接用 IDE 里 Run 的方式启动也可以。启动成功后看到 Spring Boot 的端口号,再用浏览器访问后端接口地址确认是否返回数据。这里提醒一下,Spring Boot 2.x 默认端口是 8080,如果本机 8080 被占用,可以临时把 server.port 改成 8081,但要记得同步改前端代理配置。
5.3 前端启动与代理配置
前端项目安装依赖是个重头戏,尤其在国内网络环境下,npm install 经常卡住。建议先换镜像源。
npm config set registry https://registry.npmmirror.com npm install npm run serve启动后 Vue 默认跑在 8081 端口,但后端接口在 8080,直接请求会跨域。最简单的处理方式是在 vue.config.js 里配置代理,让所有 /api 开头的请求都转发到后端的 8080。
module.exports = { devServer: { port: 8081, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }后端接口统一以 /api 开头,前端请求也统一带 /api,这样就绕开了跨域问题,部署到服务器时也能用同样的规则做 Nginx 代理。
5.4 常见问题速查表
| 现象 | 常见原因 | 处理方式 |
|---|---|---|
| 后端启动报 Access denied for user | 数据库密码或用户名配置不对 | 修改 application.yml 中的 username/password |
| 启动时提示端口被占用 | 8080 端口已被其他程序占用 | 换端口,或找到占用进程后关闭 |
| 数据库连接超时或连接不上 | MySQL 服务没启动 | 确认 MySQL 服务已启动,检查 3306 端口 |
| 前端请求接口 404 | 后端接口地址与前端口径不一致 | 检查 @RequestMapping 和 axios url 是否匹配 |
| 中文乱码 | 数据库连接字符串缺字符集配置 | url 中加 characterEncoding=utf8 |
| 前端 npm install 卡住 | 网络源太慢 | 使用 npmmirror 镜像源 |
| 接口返回 401 | token 失效或未传请求头 | 检查登录状态,确认请求拦截器注入了 token |
| 查询列表时报 SQL 字段不存在 | 数据库表结构与实体类字段不一致 | 检查表字段和实体类映射 |
这些问题大多不是代码本身有问题,而是环境和路径不匹配。排错顺序建议从前端控制台看请求地址,再到后端日志看报错信息,最后到数据库确认数据,基本能覆盖九成情况。
6. 答辩准备:把库存链路讲透就赢了一半
6.1 模拟几个高频问题
答辩时老师不只会看页面,更会问你为什么这么设计。药品管理系统的高频问题基本集中在库存和权限这两块。
第一个高频问题:库存不足是怎么预警的?不要只回答“查询 stock 小于 min_stock 的数据”,可以补充说,drug 表里冗余设计了当前库存和库存下限,入库和出库事务里更新库存后,前端会调用预警查询接口,把 stock 小于等于 min_stock 的药品列出来,同时按批次有效期筛选临期药品。
第二个高频问题:为什么需要批次库存表?因为同一种药品可能有多个生产批号,不同批号的生产日期和有效期不同。如果不按批次管理,出库时很难做到先进先出或者效期优先。讲这个点的时候,顺势提到 stock_batch 表上联合唯一索引是为了防止重复批次,效果会非常好。
第三个高频问题:为什么出库和入库要用事务?因为一次操作要同时改多张表,比如药品主表库存、批次表数量、流水表记录,任何一个失败都会导致数量对不上,所以用 @Transactional 保证整体成功或整体失败。如果再能提起“并发场景下可以加乐观锁或行锁”,基本就超出预期了。
6.2 讲透代码比背文档管用
答辩前我建议把整个项目按模块写一遍“口述稿”,不是背代码,而是讲清楚这个模块处理了什么数据、为什么这么处理、还有没有改进空间。比如药品入库,你可以讲输入参数是什么,查询了哪些表,条件怎么判断,最后有什么结果;讲到批次不存在怎么处理,库存怎么累加,流水怎么记录,再补充事务保证一致性。这段话如果你能不看代码快速讲出来,答辩基本没问题。
还有一个容易被忽略的地方,就是数据库脚本里的初始化数据。演示系统最好提前准备好管理员账号和几种不同库存状态的药品数据:一种库存正常、一种低于下限、一种快过期。这样现场演示库存预警时不会被“没有数据”卡住。
说实话,这套系统的代码量不算大,真正能不能在答辩时站住脚,靠的是你把“入库、出库、库存预警”这条链路讲透。我遇到过不少同学,页面做得很好看,但被问到“库存到底怎么扣的”就开始含糊。这里给一个最实用的训练方法:把 drug、stock_batch、stock_log 三张表之间的关系在纸上画出来,然后对着入库接口的代码讲一遍。讲顺了,你就不只是“会用源码”,而是真的掌握了这个项目。