☰
Spring Boot 乡镇卫生所医用物资进销存系统设计:批次建模与效期预警实践
2026/10/6 19:10:26 网站建设 项目流程

前阵子帮一个乡镇卫生所改造库存管理流程,我到现在还记得库管员那张办公桌——三本厚册子,一本入库、一本出库、一本报废登记,全部手工记账,每月底对账要对到天黑。最让他们发怵的不是数量对不上,而是一批医用耗材躺在角落里,到了效期才发现已经不能用了。这种场景在基层网点和中小医疗机构里其实非常普遍:医用物资的种类不算特别多,但管理颗粒度一点都不低,批号、效期、注册证号、急救物资保管这些事绕不开。

我当时就决定用 Spring Boot 做一套轻量的医用物资进销存系统,前后端最终打成一个 jar,扔到一台普通电脑或小服务器上就能跑。整个过程做下来,踩了不少坑,也积累了一些对这个垂直场景的判断。这篇就完整分享一下乡镇卫生所医用物资进销存系统的设计与实现思路,包含需求定位、技术选型、库存建模、出入库流程、效期预警、权限和部署,以及我最想说的那些容易翻车的细节。正在做 Spring Boot 毕设的同学,或者准备给基层做信息化改造的人,都可以直接参考这套方案。

1. 为什么乡镇卫生所的物资账,比普通仓库难管得多

很多人觉得进销存系统到处都是,做个 CRUD 套个模板就行。但医用物资和普通商品有个根本差异:普通仓库管的是“数量对不对”,医疗场景还要管“效期到没到”“批号追不追得到”“采购资质全不全”。乡镇卫生所又是医疗场景里比较特殊的一种,我把它拆成三层来看。

1.1 医用物资管理的三个特殊基因

第一,品种杂,且每一类都有“身份信息”。从一次性注射器、纱布、棉签、口罩、手套这类低值耗材,到缝合线、留置针、骨科耗材这类相对高值的材料,再到急救药品、消毒液,它们的档案字段完全不一样。有的要登记注册证号,有的要记录生产批号和有效期,有的还要在意冷链和保存条件。如果数据库里只存“名称 + 库存数量”,后面批号追溯和效期管控根本没法定。

第二,效期是硬约束,过期不是“商品贬值”而是“安全性事故”。普通超市卖一罐过期罐头顶多下架,医用物资过期如果还用,直接是人命关天的事。这意味着系统里每个批次都要单独记录生产日期和有效期,不仅出库要按效期优先出,还要在过期前提前预警,让库管员有时间处理。

第三,乡镇卫生所通常没有专职信息岗。库管员可能同时还管收费、管医保结算,工作人员对电脑操作的熟悉程度参差不齐。系统如果做得太重、菜单太绕、动不动报错,最后一定会被弃用,打回手工记账。所以设计目标里“简单稳定”排在“功能丰富”前面。

1.2 手工账和 Excel 管理撑到极限的四个表现

我调研时问过几家乡镇卫生所,去翻了一下他们之前的账本和 Excel 表格,问题高度集中在四件事上。

  • 效期失控:手工登记时批号、效期经常漏填,只能在纸面上记一个“2024年买的一批纱布”,具体几月到期完全查不到。即便是 Excel 表,很多人也只会登记总数,不会按批次去拆。
  • 账实不符:领用的时候不登记、借调给隔壁卫生所不写单,月底一盘点,账上 100 卷纱布,柜子里只有 60 卷,谁都说不清楚中间去哪了。
  • 应急物资断档:急救药品和抢救耗材平时用得少,经常等到真要用的时候才发现过期了或库存不足。这种物资恰恰是最不该断的。
  • 审计说不清:医用耗材采购、入库、领用、报损必须留痕。手工本子虽然勉强能看,但纸质单据和记账之间经常对不上,遇到检查或者内部审计就很被动。

这些问题不是 Excel 加几列就能解决的。关键是“批次”和“效期”这两个维度必须成为系统的一等公民,所有业务都围绕它们流转。这也是我后来整个数据库建模的核心出发点。

1.3 这套系统要解决到什么程度

基于上面的痛点,我把项目目标收敛成一句话:让一个不懂技术的库管员,每天只需要录入“进了什么、出了什么”,系统自动维护批次库存、效期预警、低库存提醒和全部台账记录。

对应的功能模块我列下来是这样的:

  • 基础数据:物资档案、供应商管理、科室管理、物资分类。
  • 入库管理:采购入库单、入库退货单、入库历史。
  • 出库管理:科室领用单、报损单、借用归还单。
  • 库存管理:实时总库存、批次库存、近效期预警、低库存预警、过期锁定。
  • 盘点管理:盘点单生成、盘点差异处理、盘盈盘亏自动出入库。
  • 报表统计:入库统计、出库统计、库存周转、科室领用排行。
  • 系统管理:用户、角色、菜单、日志管理。

这套模块清单对 Spring Boot 毕设来说也非常友好,既有 CRUD、又有业务规则、还有定时任务和报表,技术点和业务点都够说。接下来聊聊技术选型,这部分我想先给你泼一盆冷水。

2. 技术选型:我为什么用单体的 Spring Boot 而不是微服务干这件事

每年都能看到不少毕设和项目把 Spring Cloud 那一套塞进一个十几张表的管理系统里,服务拆了七八个,每个服务就两三个接口,最后部署还要写半天 Docker 编排。对这种体量的系统来说,属于过度设计。

2.1 先算算这个系统的真实规模

乡镇卫生所的典型规模大概是这样:一到三个服务网点,几十个科室和卫生室,可管理的物资 SKU 在几百到一千出头,同时在线操作的用户可能就三五个到十来个人。这种访问压力下,一台 2 核 4G 内存的服务器甚至一台普通的台式电脑就能稳稳扛住。并发量不会超过几十,数据量一年也就几万条出入库记录。

用微服务去拆分这种规模的系统,除了招人围观,解决不了任何真实问题。Spring Boot 单体应用把业务模块分好包,代码一样清晰,部署还特别简单——一个 jar 启动就完了。对乡镇卫生所这种没有专职运维的场景,“出问题一个人能搞定”就是最大的优点。

2.2 我最终采用的技术栈和选择理由

我在这套系统里用的是前后端分离方案,最终打成单 jar 部署,具体选型如下表。

层次选型关键理由
后端框架Spring Boot 2.7.18稳定、生态兼容最好,不盲目追新
JDKJava 8乡镇所和老服务器兼容性最好,毕设环境也常见
ORMMyBatis-Plus 3.5.3.1单表 CRUD 省事,分页和条件构造器好用
数据库MySQL 8.0免费、生态成熟,初始化脚本好维护
缓存Caffeine(本地缓存)数据量不大,没必要上 Redis
认证授权Sa-Token 1.37 + JWT 模式比手写 Spring Security 配置简单太多
前端Vue 3 + Element Plus + ECharts表单、表格、报表可视化都有现成组件
文件上传本地目录存储 + 预览Excel 导入用,不做对象存储
部署单 jar + 内置 Tomcat前端 dist 打入 static,一个进程全部搞定

这里最想强调的是版本匹配问题。二〇二三年以后 Spring Boot 3.x 已经很普及,JDK 17 也成了默认选项,但如果你的项目和毕设用的是 MyBatis-Plus 老版本、Druid 连接池,换到 Spring Boot 3 经常会遇到 javax 和 jakarta 包名冲突、自动配置不生效、拦截器失效这些问题。对这套业务来说,技术新旧的收益微乎其微,踩兼容性坑的代价却很大。所以我最后坚定选了 Spring Boot 2.7.18 + JDK 8,这是所有组件兼容性最稳的组合。

2.3 前端两种路线对比:一套是前后端分离,一套是服务端渲染

如果你做的是毕设,我建议你认真想想前端方案,因为答辩时老师主要看的是“完整性和合理性”。

方案一:Vue 3 做前端,开发时用 vite 代理到后端 8080 端口,写完npm run build把生成的dist目录拷进src/main/resources/static。这样最终部署就是一个 springboot jar,不用额外起 Nginx,也不用管跨域。这也是热搜词里“vue打包放进springboot中”那个问题的标准答案,后面部署章节我会把具体步骤和坑都写出来。

方案二:后端用 Thymeleaf 模板加 Bootstrap 或 Layui,后端小姐姐直接返回页面。这个方案开发速度快,但页面交互和报表展示的美观度要差不少。如果只是为了快速跑通业务流程,可以选;但想拿它当完整作品展示,我还是推荐方案一。

2.4 别被“Spring Boot 整合 Flink / ActiveMQ”这类关键词带偏

我在检索资料时发现很多人搜“springboot 整合 flink”“springboot 整合 activemq”,我不建议在这个项目里碰这些东西。乡镇卫生所没有大流量日志流要处理,也没有复杂的异步消息需要解耦。硬上 Flink 和 MQ,除了把部署复杂度拉高,业务价值基本为零。

如果后面想把预警通知做成异步,用 Spring 自带的@Async事件监听就够了;如果后续要多卫生所接入,优先考虑在数据库模型里加org_id做多租户,而不是引入服务拆分。技术选型永远跟着业务规模走,这个判断比任何技术本身都重要。

3. 库存建模:物资表、批次表、库存表怎么拆,才不出烂账

这块是整个系统能不能真正能用的命门。我先给你看一个反例,这是很多进销存系统做烂的原因。

3.1 为什么不能只用一张“物资-数量”表

假设现在有一张material表,字段是id, name, category, total_qty。纱布一共 5000 卷,就记一个总数 5000。问题来了:这 5000 卷是分三批进的,一批生产日期是今年 1 月、效期到明年 1 月,另一批是今年 9 月进的、效期到后年 9 月。你光看总数,根本不知道哪批快过期。等到医生领用时想先用效期短的,系统也没法区分,只能靠库管员去货架前翻。

更麻烦的是追溯。某一批纱布被消费者投诉或者质检不过关要召回,你只知道“进了 5000 卷、卖了 3000 卷”,但不知道这 3000 卷分别出给哪个科室、哪一天出的。没有批次概念,召回和追溯就是空中楼阁。所以医用物资进销存的第一步,就是把“批次”立起来。

3.2 核心表结构:物资档案、批次库存、总库存三层

我最终的表结构围绕三层展开。

第一层是物资档案表material,存的是“这件物资是什么”,不关心有多少。关键字段包括:

CREATE TABLE `material` ( `id` bigint NOT NULL AUTO_INCREMENT, `material_code` varchar(50) NOT NULL COMMENT '物资编码', `material_name` varchar(100) NOT NULL COMMENT '物资名称', `category_id` bigint DEFAULT NULL COMMENT '分类ID', `specification` varchar(100) DEFAULT NULL COMMENT '规格型号', `unit` varchar(20) NOT NULL COMMENT '单位', `manufacturer` varchar(100) DEFAULT NULL COMMENT '生产厂家', `registration_no` varchar(50) DEFAULT NULL COMMENT '注册证号/备案号', `min_stock` int DEFAULT 0 COMMENT '最低库存预警线', `expiry_warn_days` int DEFAULT 90 COMMENT '近效期预警天数', `status` tinyint DEFAULT 1 COMMENT '1正常 0停用', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_material_code` (`material_code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

第二层是批次库存表material_batch_stock,这是整套系统的心脏。同一件医用物资,进来几批就要拆成几条记录,每条记录自带批号、生产日期、有效期和剩余数量。

CREATE TABLE `material_batch_stock` ( `id` bigint NOT NULL AUTO_INCREMENT, `material_id` bigint NOT NULL, `batch_no` varchar(50) NOT NULL COMMENT '生产批号', `production_date` date DEFAULT NULL COMMENT '生产日期', `expiry_date` date NOT NULL COMMENT '有效期至', `quantity` int NOT NULL DEFAULT 0 COMMENT '剩余数量', `purchase_price` decimal(10,2) DEFAULT NULL COMMENT '入库单价', `supplier_id` bigint DEFAULT NULL COMMENT '供应商ID', `status` tinyint DEFAULT 1 COMMENT '1正常 2过期锁定 3已清空', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_material_batch` (`material_id`, `batch_no`), KEY `idx_expiry` (`expiry_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

第三层是总库存表material_stock,存“这件物资目前一共还剩多少”,只做快速查询和预警用,不承担批次逻辑。所有数量变化必须同时更新总库存和批次库存。

这样拆完,物资基础的“身份证信息”在档案表,一批具体的“货”在批次表,实时总余量在库存表,各管一摊。你在页面列表上看到的“纱布总数 3500 卷”,来自总库存表;点进详情,按有效期排序看到的具体批次明细,来自批次表;把鼠标移到某一批上,能查它所有出入库历史,靠的是后面的流水表。

3.3 批次数量、总库存、流水表三者如何协作

我见过一些方案是直接用批次表汇总算总库存,不单独建总库存表。这种做法逻辑上没毛病,但每次列表页展示库存都要SUM(quantity) GROUP BY material_id,几十个物料还好,几百上千个物料再加上筛选条件,查询会明显变慢,而且每次都要算。

我更推荐冗余一个总库存表,出库入库时在同一事务里同时更新。查询时直查总库存表,加索引后秒开。需要注意,总库存字段的更新不能靠应用层“先查出来加加减减再写回去”,要用原子 SQL,后面讲防超卖时还会再提。

最后,每一笔入库、出库、盘点调整,都要往库存流水表stock_record里插一条记录,记录变更类型、变更数量、变更后数量、关联单号、操作人。这张流水表是审计和追溯的关键,也方便你在出问题时排查到底是哪笔单子把库存搞乱的。

3.4 批次查询必须默认按有效期排序

因为核心业务是效期管理,所以“查询某物资的可用批次”这个操作,在任何地方都要默认按expiry_date ASC排序。哪怕你没做 FEFO 出库,列表展示也要让效期最近的排最上面,让人一眼看到危险。

SELECT id, batch_no, expiry_date, quantity FROM material_batch_stock WHERE material_id = #{materialId} AND quantity > 0 AND status = 1 ORDER BY expiry_date ASC;

低库存预警的 SQL 也很简单,直接关联两张表查低于最低库存线的物资:

SELECT m.id, m.material_name, s.total_quantity, m.min_stock FROM material m JOIN material_stock s ON s.material_id = m.id WHERE s.total_quantity < m.min_stock;

4. 出入库流程的核心逻辑:批次流转、事务边界、防超卖

数据库建模定下来以后,真正的业务逻辑就在出入库流程里。这部分我建议你先把“单据”这个概念想清楚:不要直接在库存表上做加减,而是要先生成入库单、出库单,再通过单据去影响库存。

4.1 入库流程:单头、明细、批次,三层嵌套

入库不是简单“点一下数量 +1”,而是要留全程凭证。我设计的入库单结构是:一张入库单头,对应多条入库明细,一条明细可能对应一个或多个批次。

单头字段:入库单号、供应商、入库日期、操作员、单据状态(草稿/已入库/已作废)、备注。 明细字段:物资、入库数量、单价、生产批号、生产日期、有效期。 批次展开:如果同一种物资一批有多个批号,就在保存时拆成多条批次库存记录。

入库保存的核心逻辑是:在一个@Transactional方法里,先插入单头和明细,再根据明细的批号批量插入或累加material_batch_stock,最后累加material_stock.total_quantity,并写库存流水。中间任何一步失败,整个事务回滚,绝不出现“单子保存了但库存没加”的情况。

这里有个实操细节,供应商送货单上的批号,同一个物资可能重复出现。如果系统里已经存在同批号记录,不要新建批次,而是在原批次上quantity = quantity + 采购数量。用代码判断时,先SELECT id FROM material_batch_stock WHERE material_id=? AND batch_no=?查一下,有就更新,没有就插入。

4.2 出库流程:默认按效期优先,也就是 FEFO 策略

出库是科室领用的核心场景。库管员在页面上选一个科室,再选“纱布 500 卷”,提交后系统不是简单地找总库存减 500,而是自动分解到批次上:优先扣除效期最近的批次。这就是 FEFO,First Expire First Out,先到期先出库。

这个策略很好理解:常规仓储讲究先进先出 FIFO,按入库时间先出;但医用物资的效期和生产日期往往不对应,可能一批先进来的物资效期反而比后进来的更长。按入库时间出库,极可能把效期长的先出完,把效期短的压在库里直到过期。所以这里必须牺牲“入库时间顺序”,严格以expiry_date排序。

具体实现的思路是这样:

@Transactional(rollbackFor = Exception.class) public void doOutbound(OutboundBill bill) { // 1. 保存出库单头、出库明细(先保存,状态为待出库) // 2. 逐条处理出库明细 for (OutboundItem item : bill.getItems()) { int remainQty = item.getQty(); // 查出所有可用批次,按效期升序 List<BatchStock> batches = batchStockMapper.selectAvailableBatch(item.getMaterialId()); for (BatchStock batch : batches) { if (remainQty <= 0) break; int deductQty = Math.min(remainQty, batch.getQuantity()); // 用行锁/条件更新扣减批次库存 int rows = batchStockMapper.deductQtyWithLock(batch.getId(), deductQty); if (rows == 0) { // 说明这一批已经被并发掏空,重新刷列表继续找下一批 continue; } // 同步扣减总库存,写库存流水 stockService.deductTotalStock(item.getMaterialId(), deductQty); stockRecordService.record(item.getMaterialId(), batch.getId(), "OUT", deductQty, bill.getBillNo()); remainQty -= deductQty; } if (remainQty > 0) { throw new RuntimeException("物资[" + item.getMaterialName() + "]库存不足,剩余未出库:" + remainQty); } } // 3. 更新出库单状态为已出库 }

这个代码示例省略了不少细节,但核心骨架就是这样:遍历批次、逐批扣减、不足就抛异常回滚。跨批次出库后,出库明细里可以记录“实际扣减了哪几个批次、各扣多少”,这样以后追溯“这 500 卷纱布给哪个科室了”就能直接按批次查到。这也是卫生所审计时最喜欢看到的效果。

4.3 防超卖:条件更新是底线,不能用“先查后改”

很多新手写扣库存,习惯先查一下剩余数量,Java 里判断够不够,再 UPDATE。这在单用户测试时没问题,一旦两个人同时领用,就会出大问题。

举个例子:纱布总库存还剩 100 卷。A 要领 80,B 要领 50。两个请求同时读到剩余 100,A 判断够,写回去 20;B 判断也够,写回去 50。最终库存变成 50,而实际出了 130 卷,库存变成负数,账全乱了。

解决办法只有一个:把“判断 + 扣减”合并成一条原子 SQL。

UPDATE material_batch_stock SET quantity = quantity - #{deductQty} WHERE id = #{batchId} AND quantity >= #{deductQty} AND status = 1;

这条 SQL 的执行是数据库行锁级别的原子操作。如果quantity不够扣,影响行数是 0,代码就知道这个批次不能用了,接着去试下一个批次。总库存表同理:

UPDATE material_stock SET total_quantity = total_quantity - #{deductQty} WHERE material_id = #{materialId} AND total_quantity >= #{deductQty};

这两个操作必须放在同一个事务里。我早期版本就是因为只看批次扣减、忘了同步扣总库存,导致列表显示总数不变,排查了整整一个下午才发现少了第二步。

4.4 盘点与报损:把“盘盈盘亏”变成标准的出入库单据

月底盘点是卫生所避不开的工作。我的实现方式是:盘点开始时生成一张盘点单,系统自动把当前账存数量冻结到盘点明细里;库管员拿着明细表去点数,把实盘数量填回来;提交后系统自动计算差异,生成盘盈单或盘亏单——盘亏走类似出库流程扣减库存,盘盈走类似入库流程增加库存。

这里要注意,盘点期间如果有人领用物资,会导致差异算不准。简单做法是盘点单保存时记录账存快照,差异 = 实盘数量 - 快照数量,后续领用不影响这个差异计算,但盘点完成后需要重新同步总库存。如果卫生所规模小、操作人不多,也可以约定盘点期间暂停领用,系统里给一张盘点单加个“锁定”状态,被锁定的物资在盘点期间禁止出库,逻辑更干净。

报损单则单独处理:记录物资、批次号、报损数量、报损原因(过期/破损/污染/质检不合格)、经手人、处理方式(销毁/退货),并关联到某个批次做扣减。过期的批次必须走报损流程,不能静默删掉,这样才能在报表里看到“这个卫生所这个季度报损了多少物资、主要是什么原因”。

4.5 事务边界的三条纪律

出入库逻辑基本成型后,我特别总结了三条事务纪律,写代码时一直守着。

  • 不要把文件上传、消息通知、邮件发送放进大事务里。比如提交入库单后要通知供应商,发邮件失败不应该导致入库回滚。做法是先提交事务,再用事件监听异步去做通知。
  • Excel 批量导入不要在一个事务里insert几千条。MyBatis-Plus 的saveBatch底层虽然分段批量插入,但大事务里还是容易锁表和超时。我的做法是分批读取、分批校验、分批保存,每批 200 条,出错的记录单独返回给前端。
  • 跨方法调用事务时注意this自调用问题,@Transactional不会生效。要通过注入的代理对象调用,或者把事务逻辑拆到一个独立的 Service 类里。这一点很多人笔试都会答,但真写代码时经常忘。

5. 近效期预警与低库存提醒:卫生所的“监工”怎么写进定时任务

进销存系统做到这里,业务闭环已经完整。但真正让乡镇卫生所觉得“这系统终于救了我”的,其实是预警功能——它把原先靠人工翻箱子的活儿变成了每天早上自动检查。

5.1 预警规则设计:三条规则三个档位

我把预警拆成三条规则,尽量简单直接,不搞太多花哨参数。

  • 近效期预警:物资有效期减去当天,小于等于该物资设定的expiry_warn_days,默认给 90 天,分类里可以单独覆盖。比如急救药品可能要求 180 天就开始预警,一次性手套 30 天预警就够。
  • 低库存预警:material_stock.total_quantity < material.min_stock,并且物资状态正常。这个最适合作出采购建议。
  • 过期锁定:每天扫描批次表,把expiry_date < CURDATE()且状态为正常的批次,自动置为status = 2(过期锁定),这样一来过期的物资在出库里就选不到,从系统层面杜绝“误用过期物资”。

在页面展示上,我习惯做三个红黄绿色块:红色是 30 天内过期,黄色是 30~90 天内过期,绿色是安全。库管员一上班打开首页,扫一眼色块就知道今天要先处理哪批货。

5.2 定时任务如何实现:凌晨扫一遍,加锁防止重复跑

Spring Boot 做定时任务很简单,@EnableScheduling打开,然后在 Service 方法上加@Scheduled(cron = "0 0 2 * * ?"),表示每天凌晨两点执行一次。

核心逻辑有三步:

@Component public class StockWarnTask { @Scheduled(cron = "0 0 2 * * ?") public void scanExpiryAndStock() { // 1. 近效期批次扫描:查询 expiry_date <= 今天+预警天数 的批次 // 2. 低库存扫描:查询 total_quantity < min_stock 的物资 // 3. 过期批次锁定:UPDATE batch SET status=2 WHERE expiry_date < CURDATE() AND status=1 // 4. 生成预警记录,写站内消息 } }

这里有个多实例部署才会踩到的坑:如果你以后用两台服务器跑同一个 jar,两台机器会在同一个时间点同时跑定时任务,预警消息会重复生成。解决方式有几种:简单点只部署一台实例;复杂点用 Redis 分布式锁,在一个任务开始时setnx一个 key,设置了过期时间,其他实例发现 key 存在就跳过本次执行。

乡镇卫生所的场景一台服务器就够了,但代码里加个@SchedulerLock或者简单 Redis 锁也不算多大事,毕设里写上这个设计点还是个加分项。

5.3 预警之后怎么通知:站内消息优先,邮件和企业微信按需加

预警信息生成后,必须让库管员能“看见”。我在系统里建了一张warn_record表,字段包括:物资、批次、预警类型、预警天数、当前数量、预警时间、处理状态、处理人、处理时间。同时往站内消息表插一条记录,用户登录后右上角有红点提示。

邮件通知不是必须的。如果确实要配,Spring Boot 集成spring-boot-starter-mail,YAML 里配置 SMTP 服务器地址、账号、密码就行。企业微信机器人则更轻量,只要一个 Webhook 地址,用RestTemplatePOST 一段 JSON 就能推送消息到群里。这个对乡镇卫生所不一定用得上,但医院集团或者连锁网点会喜欢。

5.4 避免重复预警:同一条记录“未处理”时不重复生成

这里有一个非常影响体验的细节:如果今天生成了一条“纱布近效期预警”,明天扫描时又查出来,再生成一条一模一样的,站内消息会变成刷屏。我的做法是生成前先查warn_record,判断“同一物资、同一批次、同一规则、处理状态为未处理”的记录是否存在,存在就跳过,不存在才插入新记录。

等到库管员点了“确认处理”,比如这批纱布已经用完了、或者已经报损出库了,这条预警状态变成已处理。下次扫描时如果还有别的批次继续预警,就不会再骚扰他。

6. 权限、前端打包与部署:让系统在乡镇卫生所真正跑起来

开发环境跑得好不算本事,让一个不懂技术的库管员在一台老电脑上稳定用三个月才算。这里分享几个部署和落地阶段的硬经验。

6.1 角色和权限怎么设计才不麻烦

不需要很复杂的 RBAC 模型,三类角色就够:

  • 管理员:用户管理、系统配置、数据字典、全部菜单可见。
  • 库管员:出入库、盘点、预警处理、报表查看。
  • 普通用户/护士:只能申请领用、查看库存和预警,不能直接改库存。

权限注解挂在 Controller 方法上就行。Sa-Token 的用法比较简洁——登录后拿到 token,拦截器里校验登录,按钮和菜单按权限码控制。我用的方式是在后端角色表里维护权限码集合,前端登录后返回菜单和按钮权限。

@SaCheckPermission("stock:outbound:add") @PostMapping("/outbound") public R<Void> createOutbound(@RequestBody OutboundBill bill) { outboundService.doOutbound(bill); return R.ok(); }

值得提醒的是,权限不是给你自己看的,是给卫生所的责任划分看的。谁入了库、谁出了库、谁调整了盘点,日志表里都要有记录,出了问题能找到人。所以我在所有写操作上还加了一个 AOP 日志注解,自动记录操作人、IP、时间、请求参数和方法描述,统一写入sys_log表。

6.2 Vue 项目打包放进 Spring Boot:三个关键点

前端npm run build之后,把dist目录下的文件复制到src/main/resources/static,重新打包后端 jar,前端页面就能从同一个端口访问。这一步很简单,但有三个人人都会踩的坑。

第一,Vue Router 如果用了createWebHistory模式,刷新某个子路由页面时会 404,因为后端 Tomcat 不知道这个路径。最简单的解决方式是改用createWebHashHistory,URL 变成/#/stock/list,刷新时不会发给后端。如果一定要 history 模式,就得在后端加一个转发 Controller,把非/api开头的路径统一转发到index.html。

第二,开发时前端会跨域。在vite.config.js里配置 proxy 转发,把/api代理到http://localhost:8080,这样调试前端不用每次改后端@CrossOrigin。

第三,打包后要检查前端资源路径是相对路径还是绝对路径。有些项目构建出来的 JS 和 CSS 路径是/assets/xxx.js,在根路径部署没问题,如果后面要放到子路径下就会白屏。这类系统一般直接部署在根路径,问题不大,但建议提前确认。

6.3 部署时最容易忽略的配置项

以下这几个地方是我实际部署时踩过或帮别人排查过的,提出来你可以直接抄。

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/hospital_stock?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: xxxxxx servlet: multipart: max-file-size: 50MB max-request-size: 50MB sa-token: token-name: satoken timeout: 86400
  • 数据库连接串里serverTimezone=Asia/Shanghai必须加,否则 MySQL 驱动会找默认时区,可能和你系统时间差 8 个小时,导致登录时间、单据时间全部错位。
  • 文件上传上限:默认 Tomcat 的max-file-size只有 1MB,做 Excel 导入时随便一个带图片的表格就超过,不调大会直接报 500。改成 50MB 后省很多事。
  • jar 包启动内存:一台 2G 内存的服务器,启动时提示OutOfMemory或者很卡,用java -Xms256m -Xmx512m -jar stock-system.jar限定内存即可。
  • 如果用了 Redis 做缓存,记得确认卫生所的服务器安装了 Redis;如果不想额外装,就把代码里的缓存换成 Caffeine 本地缓存,少一个外部依赖。

7. 我踩过的配置坑,和对这套系统的扩展想法

最后这部分没有章法,就是我在真实开发里磕出来的几个教训,加上一点对未来扩展的判断。写出来是希望你不必重走这些弯路。

7.1 版本选择真的能卡死人

我做这个项目时最早图新鲜,用了 Spring Boot 3.2 + JDK 21,结果 MyBatis-Plus 老版本里的分页插件不兼容,Druid 连接池初始化报错,Redis 序列化方式也变了。折腾一整天后我把整个项目回退到 Spring Boot 2.7.18 + JDK 8,所有问题当场消失。

如果你的毕设已经用了 Spring Boot 3,倒也不用推翻,注意把 MyBatis-Plus 升到 3.5.5 以上、JDK 用 17、Druid 用 1.2.20 以上。但我建议大部分读者直接复制我的组合:2.7.18 + JDK 8,少流一滴泪。

7.2 时间差 8 小时和日期类型选择

第一次联调时发现,数据库里存的时间比页面显示时间多了 8 小时。根因就是连接串没加serverTimezone=Asia/Shanghai,而 MySQL 驱动默认用服务器 UTC 时区。后来我除了改连接串,还把代码里所有的java.util.Date统一换成LocalDateTime,并配置了 Jackson 序列化格式,避免前端显示“2025-11-12T10:30:00”这种带 T 的字串。

7.3 库存变成负数,问题出在“先查后改”

这应该是我在整个项目里遇到的最严重 bug。A 科室和 B 科室同时领用同一种纱布,我在用户少的情况下先查库存判断够不够,再执行扣减更新,结果两个请求都判断“够”,最后库存直接变成负数。修正方式就是前面写的条件更新 SQL,一行解决问题。从那以后,团队里只要有人写库存更新,我都会盯着他用原子操作。

7.4 后续扩展:多卫生所、对接 HIS、消息异步化

这套系统如果要推广到多个卫生所,我建议在核心表里增加org_id字段,做成逻辑多租户;登录时根据用户所属机构自动过滤数据。系统层面不需要太大改动,但要注意报表统计时要带上机构维度。

如果后续要对接 HIS 系统或者医保平台,一般不是通过页面操作,而是提供 REST API 接口,让 HIS 侧调用入库、出库、库存查询。这个时候你可以在系统里增加一个独立的api_client表,维护调用凭据和签名认证,避免外部系统直接用用户名密码登录。如果消息通知压力变大,再考虑用 MQ 解耦,现阶段事件监听完全够用。

说实话,这种系统做完你会发现:真正难的不是 CRUD,而是业务规则建模和事务边界控制。只要你把批号和效期这两件事想清楚,把库存更新做成原子操作,乡镇卫生所医用物资进销存系统的核心价值就已经立住了。剩下的报表、权限和页面优化,都是在这个可靠地基上慢慢添砖加瓦的事。

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

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

立即咨询