Spring Boot医用物资进销存系统:毕设项目核心设计与实现解析
发布时间:2026/9/10 19:53:56 作者:尧图编辑部 阅读量:1,286

每年到了毕业设计选题季后台就会涌出一批来问“什么项目好做”“什么选题容易过”的留言。今年分享的这套Spring Boot乡镇卫生所医用物资进销存系统我反复看了很多遍确实是一款性价比很高的毕设项目业务体量不大但五脏俱全技术栈主流答辩有东西可深挖代码量又不至于把人熬秃。这篇文章就从选型、需求、数据库设计、核心实现到实际开发中容易踩的坑把这套系统拆开揉碎讲清楚。无论你是刚拿到题目的应届生还是想找项目练手提升的开发者这套系统的设计思路都能给你一些参考。医用物资进销存本质上是一个带“批次 有效期 库存流水”的进销存系统比普通商品进销存多了医疗行业特有的合规约束比大型ERP又轻量得多正好卡在毕设最舒服的难度区间。1. 项目概述与需求拆解1.1 乡镇卫生所的物资管理到底难在哪很多人一听“进销存系统”第一反应就是“不就是个商品管理吗”如果只是这样想那这个毕设做出来大概率是套皮电商项目答辩时老师一问就露馅。乡镇卫生所的医用物资管理和普通超市卖货有本质区别。医用物资包括药品、一次性耗材、低值器械、高值耗材等这些东西有几大特点首先是批次管理同一款药品可能有两个批次效期不同、进价不同不能混着算其次是有效期监管药品和部分耗材过期就不能用而且过期的不能只做删除操作要有报损记录再者是领用去向跟踪卫生院的物资不是卖给消费者的而是被各科室领走的领用人和用途要能追溯。再加上乡镇卫生所信息化底子薄很多地方还在用Excel记账甚至纸质台账月底盘点对不上账是家常便饭。这套项目的价值就在于它把这些问题用一个前后端分离的Web系统落地了采购单登记、验收入库、科室领用出库、库存实时台账、近效期预警、月度统计报表形成闭环。用这套系统替代手工账本是乡镇卫生院信息化建设里很典型的一个场景。1.2 核心功能模块拆解进销存系统的核心永远围绕“进、销、存”三个字但对医用物资来说还需要加上“近效期管理”和“统计上报”这两块。我按实际需求把系统拆成六个模块基础信息管理物资分类、物资档案、供应商档案、科室档案。物资档案里必须包含通用名、规格、生产厂家、批准文号、包装单位、默认供应商等字段这些是后面所有单据的“字典数据”。采购入库管理采购计划、采购单创建、到货验收、入库单生成。这里要注意采购单和入库单是分开的采购单对应的是“订了哪些货”入库单对应的是“实际到了哪些货”实际到货数量允许与采购数量不一致。出库领用管理科室提交领用申请、库管审核、出库登记、自动扣减库存同时生成出库流水。出库异常时还要有“退库”的逆向操作否则数据就乱了。库存台账与预警实时库存查询、批次库存明细、近效期预警、低库存预警、过期物资报损登记。统计报表月度入库统计、月度出库统计、科室领用排行、库存周转情况。卫生院经常要向上级报数据这部分能直接把Excel导出做好。系统管理用户登录、角色权限管理员、库管员、普通科室用户、菜单管理、操作日志。这些模块看起来多但每个模块的表结构和工作流是相互咬合的做完一遍你对“业务系统设计”这件事的基本功就齐了。1.3 这个选题在毕设里的定位毕设选题最怕两种一种是太简单比如做个单表增删改查老师觉得没有工作量另一种是太复杂比如做个微服务电商平台代码还没写一半就发现时间不够。这套乡镇卫生所医用物资进销存系统就是一个很精准的中间档。它的业务边界清晰领域模型容易建模不会有那种“需求越做越大”的情况。同时它又有几个“加分点”一是医疗场景的合规细节效期、批次、报损体现了你对行业的思考二是包含采购、入库、出库、预警、报表的完整业务闭环论文和PPT都有充足内容可写三是技术栈主流Spring Boot MyBatis Plus MySQL Vue这套组合不管评阅老师懂不懂前端看到技术选型都会觉得扎实。说白了这是一个“好做又好过还能学到真东西”的项目。2. 技术选型与核心原理2.1 后端为什么非Spring Boot不可现在Java后端做毕设Spring Boot基本是默认选项这里面的合理性值得展开说。Spring Boot不是一门新语言而是对Spring框架的封装和简化它的核心价值在于“自动装配”和“约定优于配置”。用一段大白话解释以前用Spring写项目你要手动配置数据源、事务管理器、包扫描路径、JSON解析器每个配置写错一个字母项目就起不来。Spring Boot把这些“样板配置”全部内置了你只需要在pom.xml里加依赖再在application.yml里写几行关键参数剩下的交给自动配置完成。开发效率的差距是数量级的尤其对还要写论文、找工作的毕设党来说省下的时间都是真金白银。另外Spring Boot自带内嵌Tomcat打包成jar直接就能跑不用在服务器上单独装Tomcat再部署war包。这对最后验收演示特别友好评审老师在自己电脑上就能启动项目不需要复杂环境。对我来说做毕设最怕的就是“在我电脑上能跑换台电脑就崩”Spring Boot大大降低了这种翻车概率。2.2 Spring Boot版本选择2.x还是一步到位3.x这个版本问题每年都有人栽跟头热词里“springboot版本太高”频繁出现真的不是段子。我的建议很明确现阶段做毕设优先选Spring Boot 2.7.x这是2.x系列最后的维护版本稳定且兼容资源最多。为什么这么说Spring Boot 3.x要求JDK 17起步底层从javax包迁移到了jakarta包很多老教程、老代码网上直接复制过来是跑不起来的。如果你遇到问题去搜答案大多还是针对2.x的如果你用3.x会发现pom依赖怎么改都报错最后心态爆炸。如果你的学校要求用新版那就认认真真用Spring Boot 3.x JDK 17并提前做好一个心理准备遇到问题优先看官方文档别依赖老博客。就拿这个毕设项目来说它对应的是Spring Boot 2.7.18 JDK 1.8这个组合是经过大量验证的稳定搭配。JDK 8虽然“老”但它的生态兼容性至今无人能比MyBatis Plus、EasyExcel、Druid这些都是适配稳定的。选择这套组合不是技术落后而是求稳毕设的核心目标是顺利落地不是追逐新版本。2.3 前端方案的搭配思路这套系统的前端有两条路可选传统服务端渲染Thymeleaf或者前后端分离Vue Element UI。我的建议是看你的时间预算和基础。如果Java基础一般开发时间也紧张用Thymeleaf完全没问题它把页面写在后端resources/templates目录里跟Controller返回的视图名对应数据通过Model传递逻辑简单直接部署也只有一个jar包学习曲线平缓。如果项目要求“前后端分离”或者你前端有一定基础那就用Vue 2 Element UI Axios搭一套管理后台Spring Boot写RESTful接口前端通过Ajax调用。这套组合视觉效果更现代答辩演示起来加分。这里提醒一句如果选VueVue的版本别搞混。Vue 2配Element UI是经典组合Vue 3配Element Plus又是另一套写法API有差异。毕设不建议盲目上Vue 3除非你已经熟练否则Vue 2的生态和教程资料是最丰富的遇到问题基本都能搜到解决方案。2.4 后端项目结构的组织方式很多毕设代码一多就乱成一锅粥Controller里面直接写SQLService层形同虚设答辩老师一看代码就摇头。一个清晰的后端分层结构应该长这样com.example.medical ├── controller // 接收前端请求参数校验返回结果 ├── service // 业务逻辑层事务控制都在这层 │ └── impl ├── mapper // MyBatis的数据访问接口 ├── entity // 数据库实体类 ├── dto // 前端传输对象避免直接把实体类暴露给前端 ├── vo // 视图返回对象比如统计报表的结果 ├── config // 全局配置拦截器、异常处理器等 ├── common // 通用工具类、统一返回结构、异常类 └── MedicalApplication.java这种分层的好处是Controller只做参数接收和结果返回Service做业务判断和事务控制Mapper只做数据库交互各层职责单一。后期如果遇到bug定位问题也能快速缩小范围。实际上这套结构本身就是参考了阿里巴巴Java开发规范的分层模型在面试和答辩中都很加分因为体现了工程化思维。3. 数据库设计进销存系统的地基3.1 物资信息与库存的分层设计数据库设计是整个系统里最不能省脑子的环节。我见过不少同学把“物资信息”和“库存”混在一张表里一个字段存当前库存数量结果订单一多数据就乱了这其实是建模思路出了问题。正确的设计是“物资档案”和“批次库存”分离。物资档案表存放的是静态信息物资编码、名称、规格、单位、生产厂家、批准文号、默认供应商ID。而库存表需要按“批次”拆开同一个物资在不同批次下是两笔独立的库存记录字段包括物资ID、批次号、生产日期、有效期至、入库数量、可用数量。这样才能做到先进先出、近效期预警和批次追溯。用生活中最容易理解的例子超市里卖同一款牛奶一批是6月1日生产的另一批是6月10日生产的你不能把两批混在一起记“总共50箱”因为它们的效期不同过期时间也不同。一旦其中一批过期你就需要知道是哪一批、剩多少、从哪批供应商进的货。批次库存表就是干这个的。3.2 单据流与流水表的设计要点进销存系统最核心的底层逻辑是“单据驱动”。不是直接在库存表里加一个数字而是通过一张张业务单据来改变库存数据。这套系统的核心表结构可以这样拆purchase_order采购单采购单号、供应商ID、采购日期、采购状态草稿/已提交/已入库、总金额、创建人。purchase_order_item采购明细采购单ID、物资ID、采购数量、采购单价、金额。stock_in入库单入库单号、关联采购单ID、入库日期、入库类型、经手人、备注。stock_in_item入库明细入库单ID、物资ID、批次号、生产日期、有效期至、入库数量、入库单价。stock_out出库单出库单号、领用科室ID、出库日期、出库类型、领用人、备注。stock_out_item出库明细出库单ID、物资ID、批次号、出库数量、出库单价。material_stock批次库存物资ID、批次号、库存数量、生产日期、有效期至。stock_flow库存流水唯一流水号、物资ID、批次号、变动类型入库/出库/报损/退库、变动前数量、变动数量、变动后数量、关联单据号、操作时间。为什么要单独设计一张库存流水表因为它记录了每一次库存变动的来龙去脉是审计追踪的基础。将来数据对不上账只需要按时间查流水就能找到是哪一笔操作出了问题。这个设计思路也是企业级系统的标准做法写进论文里是很好的亮点。3.3 预警与有效期管理的表结构医用物资的预警分两种低库存预警和近效期预警。低库存预警比较简单在物资档案表里维护一个min_stock字段查询时对比当前库存数量即可。近效期预警稍微麻烦一点它需要依赖批次库存里的expiry_date字段用一个SQL或Java定时任务扫出来。我建议的预警配置设计是在物资分类表或参数配置表里放一个“预警提前天数”比如30天。然后查询所有批次库存中有效期减去当前日期在0到30天之间的记录如果有效期小于当前日期状态标记为“已过期”要触发报损流程。这样一张stock_flow表里多一个“报损”变动类型就能把过期物资核销掉账目保持平衡。数据库表建完之后还要注意索引的建立。像库存表里的物资ID、批次号流水表里的物资ID和操作时间这些字段在查询条件里高频出现必须建索引否则数据量一旦上来查询会越来越慢。毕设的数据量虽然不大但把这个建索引的习惯培养起来对以后工作帮助很大。4. 核心功能实现与代码走读4.1 采购入库的完整链路采购入库是进销存系统的“进”整条链路是创建采购单 → 提交入库 → 增加批次库存 → 生成入库流水。我在实现的时候把“采购单”和“入库单”拆成了两步因为实际业务里采购和到货往往不是同一时间而且到货数量可能与采购数量不一致。采购单的操作相对简单就是一个主表加明细表的保存逻辑。Controller接收到前端提交的采购单对象包含明细列表后Service层开启事务先插入purchase_order再循环插入purchase_order_item。这里要注意两个点一是明细列表不能为空否则直接抛业务异常二是总金额由系统自动计算不能由前端传过来否则容易造假。入库操作的代码逻辑看起来是这样的Transactional(rollbackFor Exception.class) public Long createStockIn(StockInDTO dto) { // 1. 校验入库单关联的采购单是否存在且状态为“已提交” PurchaseOrder order purchaseOrderMapper.selectById(dto.getPurchaseOrderId()); if (order null) { throw new BusinessException(采购单不存在); } // 2. 创建入库单主记录 StockIn stockIn new StockIn(); stockIn.setStockInNo(generateStockInNo()); stockIn.setPurchaseOrderId(order.getId()); stockIn.setStockInDate(new Date()); stockInMapper.insert(stockIn); // 3. 遍历入库明细写入批次库存 流水 for (StockInItemDTO item : dto.getItems()) { MaterialStock stock materialStockMapper .findByMaterialIdAndBatch(item.getMaterialId(), item.getBatchNo()); if (stock null) { stock new MaterialStock(); stock.setMaterialId(item.getMaterialId()); stock.setBatchNo(item.getBatchNo()); stock.setQuantity(item.getQuantity()); materialStockMapper.insert(stock); } else { stock.setQuantity(stock.getQuantity() item.getQuantity()); materialStockMapper.updateById(stock); } // 写库存流水 StockFlow flow new StockFlow(); flow.setFlowNo(generateFlowNo()); flow.setMaterialId(item.getMaterialId()); flow.setBatchNo(item.getBatchNo()); flow.setChangeType(IN); flow.setChangeQuantity(item.getQuantity()); flow.setRelationBillNo(stockIn.getStockInNo()); flow.setCreateTime(new Date()); stockFlowMapper.insert(flow); } // 4. 把采购单状态改为“已入库” order.setStatus(2); purchaseOrderMapper.updateById(order); return stockIn.getId(); }这里Transactional是关键。入库涉及到采购单更新、主表插入、明细循环、库存更新、流水记录等多张表的写入任何一步失败都必须全部回滚否则就会出现“采购单状态改成已入库但库存没加上”这种脏数据。Spring事务默认只在RuntimeException出现时回滚因此要设置rollbackFor Exception.class防止检查异常吞掉事务。4.2 出库与库存扣减的并发安全出库环节是进销存系统最容易出问题的地方。因为这是一种“消耗型”操作如果多个科室同时领用同一种物资并发时可能发生“超领”问题。比如库存只剩5个两个请求同时查到库存5各自扣减5最后库存变成-5这明显是业务事故。解决这个问题有几个层次的做法。最简单的方案是在批次库存表上做乐观锁也就是加一个version字段更新时带上版本号判断UPDATE material_stock SET quantity quantity - #{outQuantity}, version version 1 WHERE id #{id} AND version #{version} AND quantity #{outQuantity}通过这条SQL我们让数据库在更新时同时校验版本号和库存量。如果两条并发请求同时来只有第一条能成功第二条更新到的行数为0再抛异常“库存不足”。这种写法在高并发程度不高的业务里完全够用而且实现成本很低。另一种更严格的方案是在数据库层面加悲观锁也就是SELECT ... FOR UPDATE把一批库存记录锁住直到事务结束再释放。这种方式可以彻底避免超卖但在Web应用里如果事务时间过长会拖慢系统性能。对于乡镇卫生所这种量级的系统乐观锁已经绰绰有余这也是我在实际项目里推荐的做法。补充一个细节出库时选择哪个批次先出标准的做法是“先进先出”。也就是按生产日期或入库日期排序优先出库最早的批次确保临近效期的物资先被用完。这个逻辑在出库单提交时的批次选择上体现实现起来不复杂就是查询库存时按expiry_date升序排序。4.3 近效期预警的实现方案近效期预警是这个项目区别于普通进销存的重点模块也是答辩时的核心亮点。我先梳理一下需求库存里的每个批次都有一个有效期系统需要在距离有效期还有指定天数时在前端给出醒目的提示比如“xx药品距失效还有10天剩余20盒”。实现方案有两种一种是写一个定时任务每天扫描一次批次库存表把预警数据存到一张预警表里另一种是每次查询库存时动态计算在SQL里直接判断。对于毕设来说动态计算的方式更简单也更直观不需要引入额外的定时任务框架减少被追问的风险。动态查询的思路是这样SELECT ms.material_id, m.material_name, ms.batch_no, ms.quantity, ms.expiry_date, DATEDIFF(ms.expiry_date, CURDATE()) AS days_to_expire FROM material_stock ms JOIN material_info m ON ms.material_id m.id WHERE ms.quantity 0 AND DATEDIFF(ms.expiry_date, CURDATE()) BETWEEN 0 AND 30 ORDER BY ms.expiry_date ASC这条SQL把“剩余有效天数”算出来之后前端仪表盘就能把近效期物资列表渲染出来。预警天数30天可以根据参数配置表动态调整不要写死在代码里方便后续修改。这就是“配置优于硬编码”的思路也是在工程实践里很容易被忽略的小细节。如果要做更完整的报表还可以在查询结果里按剩余天数区间分段统计已过期多少种、30天内到期多少种、60天内到期多少种。医疗物资管理里这个视图非常直观给卫生院库管看的时候一眼就知道哪些东西要赶紧处理。4.4 统计报表与导出Excel报表模块是卫生所系统里上级检查最常用的功能。通常需要三类报表月度入库汇总、月度出库汇总、科室领用排行。实现这类统计的核心就是SQL的GROUP BY举一个科室领用排行的例子Mapper public interface StatisticsMapper { // 统计指定时间段内各科室的领用数量 ListDeptStockOutVO selectDeptStockOutSummary(Param(startDate) String startDate, Param(endDate) String endDate); }select idselectDeptStockOutSummary resultTypecom.example.medical.vo.DeptStockOutVO SELECT d.dept_name, COUNT(so.id) AS out_count, SUM(soi.out_quantity) AS total_quantity FROM stock_out so JOIN stock_out_item soi ON so.id soi.stock_out_id JOIN dept_info d ON so.dept_id d.id WHERE so.create_time BETWEEN #{startDate} AND #{endDate} GROUP BY d.id, d.dept_name ORDER BY total_quantity DESC /select报表拿到前端之后最实用的是导出Excel。这一步我强烈推荐用EasyExcel而不是Apache POI直接手写。EasyExcel是阿里开源的库API很简洁三五行代码就能把List数据导出成Excel而且底层做了流式处理不用担心数据量大了内存溢出。这里要特别提醒一个真实踩过的坑导出Excel时如果字段值是纯数字字符串比如库存批号“001234”直接导出后Excel会自动把它变成数字1234前导零丢掉了。解决办法是在实体字段上加上ExcelProperty(value 批次号, converter StringStringConverter.class)或者把字段类型强制转为String。这类细节虽然不起眼但在实际使用中非常影响体验领导一看数据错了第一反应就是系统有问题。5. 实战中的问题与排查记录5.1 Spring Boot版本过高引发的启动异常前面我反复强调版本问题不是危言耸听。我见过太多同学在配置项目第一步就卡住了原因就是从网上复制了别人的代码但别人的Spring Boot是2.5自己建项目时IDEA默认选了3.2结果依赖全部报错。最典型的问题有两个一是pom.xml中Spring Boot 3.x对应的MyBatis Plus依赖坐标变了二是代码里引用的javax.servlet全部变成了jakarta.servlet。老代码里的import javax直接编译不过。如果你项目里所有依赖都“莫名其妙”报错先别急着怀疑代码检查一下Spring Boot版本和JDK版本是否匹配。Spring Boot 2.7.x配JDK 8或11Spring Boot 3.x必须配JDK 17。IDEA新建项目时默认的Spring Boot版本可能很高需要手动改成2.7.18。这一步定下来后面才能安心写代码。这真的是一步错、步步错改版本比重新建项目还痛苦。5.2 事务不生效的一个隐蔽错误在进销存项目里事务一旦不生效数据会乱得一塌糊涂。我见过最典型的错误是在同一个类内部一个方法调用另一个Transactional方法结果事务没有生效。比如StockService类里A方法调用了B方法B上面标了Transactional你以为B会用新事务但实际上A里调用的“this.b()”是绕过Spring代理的注解根本不会工作。正确做法是调用Admin接口// 错误写法同类内部直接调用事务不生效 this.deductStock(item); // 正确写法注入自己或使用事务代理 stockService.deductStock(item);另外还有一个很低级但很常见的坑Transactional加在了private方法上。Spring的声明式事务依赖CGLIB代理私有方法根本不会被代理所以事务同样不生效。排查这类问题确认方法必须是public而且必须从外部类调用遇到事务问题先检查这两点能省下不少排查时间。5.3 日期区间查询查不出数据做统计报表时日期查询是高频功能。我第一次做的时候踩过一个很典型的坑前端传的两个日期参数是2026-03-01和2026-03-31数据库里存的是2026-03-31 14:23:05然后SQL用BETWEEN #{startDate} AND #{endDate}去查结果3月31日的记录查不出来。原因很简单2026-03-31会被解析成2026-03-31 00:00:00而数据库里的时间是2026-03-31 14:23:05当然不在这个范围内。解决办法有两个要么把结束日期加一天再用startDate createTime AND createTime endDateNextDay的写法要么在SQL里用DATE_FORMAT(create_time, %Y-%m-%d)把时间转成日期再比较。第二种写法虽然直观但会对create_time字段做函数运算一旦数据量大索引会失效所以更推荐第一种区间写法。5.4 金额精度与BigDecimal进销存系统里跟钱打交道金额精度怎么强调都不过分。我看到很多毕设代码用double存金额这属于没过脑子的操作。浮点数在二进制表示上是有误差的0.1加0.2可能等于0.30000000000000004放在金额计算里几百万次累加误差会被放大对不上账就是事故。正确做法是所有金额字段强制使用BigDecimal数据库层面用DECIMAL(10, 2)。计算采购总金额时大忌是先转成double再乘。// 正确写法 BigDecimal total BigDecimal.ZERO; for (PurchaseOrderItemDTO item : items) { BigDecimal amount item.getPrice() .multiply(BigDecimal.valueOf(item.getQuantity())); total total.add(amount); }顺带说一句BigDecimal的构造方法也有讲究用new BigDecimal(0.1)实际上得到的不是一个精确的0.1应当使用BigDecimal.valueOf(0.1)或者new BigDecimal(0.1)这又是很多新手会踩的隐性坑。5.5 打包部署的环境问题系统开发完成后最后一步是打包部署。Spring Boot项目用Maven的package命令就能打出一个可执行jar包然后扔到服务器上跑java -jar即可。整体流程不难但环境问题经常让人头大。建议在开发阶段就固定好JDK版本并确保pom.xml中的java.version与本地JDK一致。如果你在本地是JDK 8服务器上是JDK 11接口调不通是正常现象。另外项目里的数据库连接配置不要写死成本地IP要么用环境变量要么在部署时手动修改application.yml否则换机器就起不来。如果服务器本身资源有限启动参数可以加一个-Xmx256m限制JVM最大内存占用避免小内存的云服务器直接OOM。这个参数很多人不知道但实际部署中挺管用。6. 写在最后做这套系统的真实体会这个项目做完之后我对“业务系统”的理解有了本质变化。写增删改查只是基础真正考验人的是状态流转、批次扣减、并发安全、事务边界这些看不见的部分。乡镇卫生所进销存虽然小但它把一个生产级业务系统该有的骨架都包含了做完这一遍再去看ERP、WMS之类的系统你会觉得很多概念是相通的。如果你拿到的是这个选题我最后再给三个实操层面的建议第一数据库设计阶段多花时间表关系理清楚了代码写起来几乎就是体力活第二代码规范一定要重视Controller别写业务逻辑Service层要控制好事务边界第三答辩前一定把项目跑通两遍以上特别是入库、出库、预警、报表这些核心流程现场翻车是最亏的。希望这篇文章能帮到正在为毕设奋战的同学也祝你们都能顺利通过答辩。