ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

医药进销存系统源码解析:Java课程设计中的批号效期与库存扣减实战

医药进销存系统源码解析:Java课程设计中的批号效期与库存扣减实战 简介这是一套面向计算机专业学生与Java Web开发初学者的医药进销存管理系统完整源码适用于毕业设计、课程设计及企业级项目练手。系统围绕药品采购、入库、出库、库存查询与销售记录跟踪等核心业务展开采用Java作为主要开发语言结合MVC设计模式、Spring框架、Hibernate持久化与Servlet/JSP技术并涉及MySQL等关系型数据库操作同时包含PHP、C#、ASP.NET等标签所指向的拓展方向。压缩包共944个文件以html、css、js等前端页面资源为主辅以105个java源文件、6个sql脚本及xml、properties等配置文档整体约8.32MB目录结构清晰便于按模块研读。目前已有328人学习下载。读者可从中获取完整的业务逻辑实现、数据库建表脚本与前端交互代码深入理解企业级应用的分层设计与开发流程也可在此基础上进行二次开发或功能扩展。1. 医药进销存系统为什么成了 Java 课程设计的硬骨头医药进销存管理系统源码.zip 这个标题看着像一份普通的 Java 课程设计作业真正拆开做一遍才知道它比电商订单系统难啃得多。普通商城只要管好商品、订单、库存三张表就能跑医药行业不行同一盒阿莫西林要按批号分开记库存不同批号进价不同、有效期不同出库必须先进先出临近过期要预警处方药和非处方药销售规则还不一样。这些约束一旦落到数据库和业务代码里表结构、事务边界、并发扣减全都要重新想。我带过几届学生的课程设计十个做进销存的八个卡在库存扣减和批号追溯上。这份源码类项目真正值钱的地方不是界面多漂亮而是它把「药品」这个特殊品类拆成了通用商品 批号 效期 资质四层模型。适合谁看正在找 Java 课程设计案例源码的在校生、想用 MyBatis 练手的中级开发者、以及需要一套进销存骨架二次开发的小团队。下面我按「模型怎么设计 → 环境怎么搭 → 核心业务怎么写 → 坑在哪」的顺序把这类系统从零跑通的路径讲清楚你照着能复现出一套能演示、能答辩、能继续加功能的版本。2. 医药进销存的领域模型为什么不能照抄电商表结构2.1 药品、批号、库存三层拆分的必要性电商的库存表通常就是sku_id quantity两个字段扣减时update stock set quantity quantity - 1 where sku_id ? and quantity 1完事。医药场景直接套这个模型第一次盘库就会翻车仓库里躺着三个批号的同一药品批号 A 进价 12 元、效期 2025-06批号 B 进价 11.5 元、效期 2024-12你按哪个价出库效期近的先出还是先进先出这两个问题决定了库存必须拆成三层。第一层是药品基础信息表drug存通用名、规格、剂型、生产厂家、是否处方药这是相对静态的字典数据。第二层是批号表drug_batch一个药品对应多条批号记录每条带批号号、生产日期、有效期、进价、初始数量。第三层才是真正的库存流水stock_record记录每一次入库、出库、盘点、报损用流水累加算出当前结存而不是直接改一个数字。这样设计的好处是任何时点的库存都能追溯出了质量问题能按批号召回。-- 药品基础信息 CREATE TABLE drug ( id BIGINT PRIMARY KEY AUTO_INCREMENT, drug_code VARCHAR(32) NOT NULL UNIQUE COMMENT 药品编码, drug_name VARCHAR(128) NOT NULL COMMENT 通用名, spec VARCHAR(64) COMMENT 规格, dosage_form VARCHAR(32) COMMENT 剂型, manufacturer VARCHAR(128) COMMENT 生产厂家, is_prescription TINYINT DEFAULT 0 COMMENT 0非处方 1处方药, status TINYINT DEFAULT 1 COMMENT 1启用 0停用 ); -- 批号表一个药品多个批号 CREATE TABLE drug_batch ( id BIGINT PRIMARY KEY AUTO_INCREMENT, drug_id BIGINT NOT NULL, batch_no VARCHAR(64) NOT NULL COMMENT 生产批号, produce_date DATE, expire_date DATE NOT NULL COMMENT 有效期至, purchase_price DECIMAL(10,2) COMMENT 该批号进价, init_qty INT DEFAULT 0 COMMENT 入库数量, UNIQUE KEY uk_drug_batch (drug_id, batch_no) ); -- 库存流水只增不改 CREATE TABLE stock_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, drug_id BIGINT NOT NULL, batch_id BIGINT NOT NULL, change_type TINYINT COMMENT 1入库 2出库 3盘点 4报损, change_qty INT NOT NULL COMMENT 正数入库 负数出库, biz_no VARCHAR(64) COMMENT 关联单号, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );参数说明drug_batch上的唯一索引uk_drug_batch防止同一药品重复录入相同批号这是血泪经验没有它并发入库会插出两条一样的批号。stock_record.change_qty用正负号区分方向比单独存一个方向字段再存数量更省事统计时直接sum(change_qty)就是结存。expire_date设成 NOT NULL因为医药系统里没有有效期的药品是不允许入库的这个约束要在数据库层卡死别指望前端校验。2.2 效期预警与先进先出的实现思路有了批号表先进先出就好实现了出库时按expire_date升序取批号先扣效期近的。效期预警则是一个定时任务每天扫一遍drug_batch把expire_date在 90 天内的批号捞出来写进预警表或者直接推给采购。这里有个容易忽略的点预警阈值不能写死 90 天不同药品周转周期不一样急救药可能 30 天就要预警慢性病用药 180 天预警都来得及。稳妥做法是在drug表加一个warn_days字段按药品配置。// 按效期升序取可用批号先进先出 public ListDrugBatch listAvailableBatches(Long drugId) { return batchMapper.selectList( new LambdaQueryWrapperDrugBatch() .eq(DrugBatch::getDrugId, drugId) .gt(DrugBatch::getExpireDate, LocalDate.now()) // 排除已过期 .orderByAsc(DrugBatch::getExpireDate) // 效期近的排前面 ); }逻辑说明gt(expireDate, now)把已过期批号排除在可售范围外这一步必须在查询层做不能等到扣减时再判断否则并发下会卖出过期药。orderByAsc(expireDate)保证先进先出。真正扣减时还要再查一次该批号的实时结存因为查询和扣减之间有时间差这就是后面避坑章节要讲的并发问题。3. 把源码跑起来环境、依赖与数据库初始化3.1 JDK、Maven、MySQL 的版本选择与配置这类 Java 进销存项目主流技术栈是 Spring Boot MyBatis MySQL前端可能是 Thymeleaf 或者 Vue 分离。拿到源码压缩包后第一件事不是急着mvn spring-boot:run而是先看pom.xml里的 Spring Boot 版本再决定 JDK 用哪个。Spring Boot 2.x 配 JDK 8 或 11 最稳Spring Boot 3.x 强制要求 JDK 17 以上用错版本会报一堆Unsupported class file major version的错新手看到这个报错往往一脸懵。# 检查本机环境 java -version # 期望 1.8 / 11 / 17与 pom 里 spring-boot 版本匹配 mvn -v # Maven 3.6 mysql --version # MySQL 5.7 或 8.0 # 导入数据库假设源码里有 sql/init.sql mysql -uroot -p -e CREATE DATABASE drug_inventory DEFAULT CHARSET utf8mb4; mysql -uroot -p drug_inventory sql/init.sql # 编译并启动 mvn clean package -DskipTests java -jar target/*.jar参数说明DEFAULT CHARSET utf8mb4必须指定医药系统里药品名可能带生僻字utf8 存不下会报Incorrect string value。-DskipTests跳过测试因为课程设计类源码的测试用例经常是空的或者依赖外部环境跑测试反而卡住。启动前记得改application.yml里的数据库连接用户名密码、端口、库名三处都要对改漏一处就是Access denied或者Unknown database。3.2 数据库初始化的三个关键检查点导入 SQL 之后别急着启动先做三个检查。第一看字符集show create table drug;确认每张表都是utf8mb4有些源码的 SQL 文件里建库语句没带字符集导入后表是 latin1中文全变问号。第二看外键如果源码用了物理外键导入顺序错了会失败稳妥做法是先关外键检查SET FOREIGN_KEY_CHECKS0;再导入导完再打开。第三看初始数据管理员账号、角色权限、基础字典这些种子数据有没有没有的话登录页都进不去。检查项命令期望结果字符集show create table drug;含CHARSETutf8mb4表数量show tables;与源码文档一致通常 15-25 张种子数据select count(*) from sys_user;至少 1 条管理员记录外键状态select foreign_key_checks;导入后应为 1如果sys_user表是空的去看源码里有没有data.sql或者init-data.sql很多项目把建表和初始化数据分成两个文件只导了建表脚本就会登录失败。密码字段通常是 BCrypt 加密的别手动往表里插明文密码用源码里提供的默认账号或者自己写个main方法调new BCryptPasswordEncoder().encode(123456)生成。4. 核心业务代码入库、出库与库存扣减怎么写4.1 入库批号录入与库存流水写入入库的业务逻辑是选药品 → 填批号、生产日期、有效期、进价、数量 → 保存批号记录 → 写一条入库流水。这两步必须在同一个事务里否则批号插进去了流水没写库存就对不上。下面是一个典型的 Service 方法。Transactional(rollbackFor Exception.class) public void stockIn(StockInDTO dto) { // 1. 校验药品存在且启用 Drug drug drugMapper.selectById(dto.getDrugId()); if (drug null || drug.getStatus() 0) { throw new BizException(药品不存在或已停用); } // 2. 校验有效期必须晚于今天 if (!dto.getExpireDate().isAfter(LocalDate.now())) { throw new BizException(有效期必须晚于当前日期); } // 3. 批号已存在则累加不存在则新建 DrugBatch batch batchMapper.selectOne( new LambdaQueryWrapperDrugBatch() .eq(DrugBatch::getDrugId, dto.getDrugId()) .eq(DrugBatch::getBatchNo, dto.getBatchNo())); if (batch null) { batch new DrugBatch(); batch.setDrugId(dto.getDrugId()); batch.setBatchNo(dto.getBatchNo()); batch.setProduceDate(dto.getProduceDate()); batch.setExpireDate(dto.getExpireDate()); batch.setPurchasePrice(dto.getPurchasePrice()); batch.setInitQty(dto.getQty()); batchMapper.insert(batch); } // 4. 写库存流水正数表示入库 StockRecord record new StockRecord(); record.setDrugId(dto.getDrugId()); record.setBatchId(batch.getId()); record.setChangeType(1); record.setChangeQty(dto.getQty()); record.setBizNo(dto.getBizNo()); stockRecordMapper.insert(record); }逻辑说明Transactional(rollbackFor Exception.class)里的rollbackFor不能省默认只回滚RuntimeException如果抛的是受检异常事务不会回滚这是很多人踩过的坑。第 3 步「批号已存在则累加」要注意这里只更新了init_qty吗不是实际结存是靠流水算的init_qty只是记录首次入库量真正的可用量是sum(change_qty)。所以批号已存在时其实不需要改init_qty只写流水就行避免两处数据打架。4.2 出库先进先出与并发扣减的平衡出库是最容易出问题的地方。简单做法是查可用批号、按效期排序、逐个扣减但并发下两个收银员同时卖同一批号的药可能都查到结存 1都判断够扣结果扣成 -1。解决办法有两种悲观锁select ... for update或者乐观锁加版本号。进销存这种并发量不高的场景悲观锁更简单可靠。Transactional(rollbackFor Exception.class) public void stockOut(StockOutDTO dto) { int remain dto.getQty(); ListDrugBatch batches batchMapper.listAvailableForUpdate(dto.getDrugId()); for (DrugBatch batch : batches) { if (remain 0) break; // 加行锁查当前结存 Integer stock stockRecordMapper.sumQtyByBatch(batch.getId()); if (stock null || stock 0) continue; int deduct Math.min(stock, remain); StockRecord record new StockRecord(); record.setDrugId(dto.getDrugId()); record.setBatchId(batch.getId()); record.setChangeType(2); record.setChangeQty(-deduct); // 出库为负 record.setBizNo(dto.getBizNo()); stockRecordMapper.insert(record); remain - deduct; } if (remain 0) { throw new BizException(库存不足还差 remain); } }逻辑说明listAvailableForUpdate对应的 SQL 要带for update锁住这批批号行防止并发修改。sumQtyByBatch是select sum(change_qty) from stock_record where batch_id ?实时算结存。Math.min(stock, remain)保证不会超扣。最后remain 0说明所有批号加起来都不够抛异常触发回滚前面已经插入的出库流水全部撤销。这里的关键是「先锁批号行再算结存再写流水」顺序不能乱先算后锁等于没锁。5. 避坑与排查这类源码跑不通的五个高频原因5.1 启动报错 Table doesnt exist现象启动时控制台刷Table drug_inventory.xxx doesnt exist。原因通常是 SQL 没导入、导入了错误的库、或者表名大小写不一致。Linux 下 MySQL 默认表名区分大小写Windows 不区分源码在 Windows 写的Drug到 Linux 上就找不到drug。解决确认application.yml里的库名和实际导入的库一致Linux 环境统一用lower_case_table_names1或者在 SQL 里全用小写表名。5.2 登录一直提示密码错误现象默认账号密码输对了还是登不进去。原因多半是种子数据里的密码是 BCrypt 密文而你导入的 SQL 文件里那串密文对应的明文不是文档里写的那个。解决别猜密码直接看源码里有没有PasswordEncoder的测试类或者自己生成一个密文替换进去。另一个可能是登录接口用了username而表里字段叫user_nameMyBatis 映射没对上查一下resultMap。5.3 库存扣成负数现象盘点时发现某个批号结存是负的。原因就是 4.2 说的并发问题查询和扣减之间没有锁。解决出库方法加Transactional批号查询加for update或者用 Redis 分布式锁按drugId加锁。课程设计演示环境并发低但答辩时老师很可能问「两个人同时买怎么办」答不上来就尴尬了。5.4 中文乱码现象药品名显示成问号或者方块。原因数据库、连接串、前端编码三处有一处不是 utf8mb4。解决建库建表用 utf8mb4JDBC 连接串加useUnicodetruecharacterEncodingutf8前端页面meta charsetutf-8。三处都对了才不乱码只改一处往往没用。5.5 打包后 jar 启动找不到主类现象java -jar报no main manifest attribute。原因pom.xml里没配spring-boot-maven-plugin或者配了但没执行repackage。解决确认pom.xml的 build 节点里有这个插件然后mvn clean package重新打包看target目录下是不是有两个 jar带.original后缀的那个不能直接跑要跑没后缀的。6. 从能跑到好用效期预警定时任务与库存对账技巧把系统跑起来只是及格线答辩或者实际使用时要能体现「医药」特色效期预警是最容易加分的一个点。我一般用 Spring 的Scheduled写一个每天凌晨执行的任务扫出 90 天内到期的批号按药品配置的warn_days过滤写进预警表并在首页红字提示。这里有个细节预警任务要幂等同一天重复执行不能重复插预警记录用biz_date batch_id做唯一键或者先删当天旧记录再插。Scheduled(cron 0 0 2 * * ?) // 每天凌晨2点 public void expireWarnJob() { LocalDate today LocalDate.now(); // 查出所有未过期批号关联药品的预警天数 ListBatchWarnVO list batchMapper.listExpiringBatches(today); for (BatchWarnVO vo : list) { long days ChronoUnit.DAYS.between(today, vo.getExpireDate()); if (days vo.getWarnDays()) { warnMapper.insertIgnore(vo.getBatchId(), today, days); } } }参数说明cron 0 0 2 * * ?是每天 2 点执行秒 分 时 日 月 周别写成 6 位和 7 位混用。insertIgnore对应INSERT IGNORE靠唯一键去重比先查后插更省事也更安全。warnDays从药品表带出来不同药品不同阈值。库存对账是另一个实用技巧。系统跑一段时间后流水累加出来的结存和实际盘点数可能对不上原因可能是手工改了数据库、或者某次事务没回滚干净。我习惯写一个对账接口把每个批号的sum(change_qty)和盘点表里的实盘数比对差异超过阈值的列出来人工核查。这个功能在课程设计里是加分项在实际项目里是保命项。最后说个我自己的习惯拿到任何一份进销存源码我第一件事不是看界面而是打开数据库把stock_record表的结构看清楚看它是「只增流水」还是「直接改结存」。只增流水的追溯能力强但查询要聚合适合医药直接改结存的查询快但丢了历史适合快消。看懂这一张表整套系统的设计水平就心里有数了。希望帮到你。本文还有配套的精品资源点击获取
返回列表