
简介这是一套基于Java Web的医药管理系统源码面向计算机专业学生、课程设计者及Java初学者用于解决药品进销存与后台管理的实战练习需求。系统采用JDK1.8与MySQL开发覆盖药品添加与查询、库存查看、类别管理与统计、购买药品、销售管理、进货需求管理及系统管理等核心模块适合作为毕业设计或课程作业的参考项目。压缩包共162个文件约9.34MB以jsp页面、java源码、class编译文件、jar依赖包、xml配置及properties资源文件为主另含sql建库脚本与少量图片样式资源结构完整可直接导入运行。目前已有2058人学习下载读者可从中获取完整的项目目录结构、分层Action与Dao设计思路、数据库建表脚本以及前后端交互实现便于快速理解医药管理系统的业务逻辑与开发流程并在此基础上进行二次开发与功能扩展。1. 医药管理系统源码.zip一套能跑起来的进销存骨架长什么样拿到「医药管理系统源码.zip」这个包的人十有八九不是想研究架构而是手头有个真实场景要交差药店要管批号效期诊所要把处方和库存对上或者课程设计需要一个能演示的完整业务闭环。医药这个行业和普通商品零售最大的区别在于药是有批号、有效期、批准文号的同一盒阿莫西林进价可能因为批次不同而不同卖出去还要能追溯到具体批号。普通进销存那套「商品-库存-订单」三张表的结构放到医药场景里会立刻翻车。所以这套源码真正值钱的地方不是它用了什么框架而是它有没有把「批号效期处方供应商资质」这几件事建模进去。我见过太多号称医药管理系统的代码打开一看就是改了改商品名字的通用商城后台库存只记总数出库不扣批号这种代码拿去答辩或者上线第一次药监自查就露馅。这篇文章面向三类人想拿这套源码做二次开发的工程师、需要快速交付一个可用医药管理后台的开发者、以及想搞清楚医药进销存到底该怎么建表的学生。我会按「先看懂它该有什么 → 再动手把环境跑起来 → 然后逐个模块拆解怎么改 → 最后讲清楚哪些坑会让你白干」的顺序讲中间会给可直接抄的建表语句、批号扣减逻辑和效期预警查询。看完你应该能判断这套源码值不值得投入以及怎么把它改成能真正用的东西。2. 医药管理系统源码的核心模块拆解与建表逻辑2.1 药品主数据与批号库存为什么必须分两张表普通商品库存表长这样product_id, quantity。医药场景下这张表是错的因为同一个药品有多个批号每个批号有独立的生产日期、有效期、进价。你卖出去一盒药必须知道卖的是哪个批号否则召回的时候你连货从哪来都说不清。正确的做法是拆成两层药品主数据表管「这个药是什么」批号库存表管「这个药我手上有哪几批、各多少、什么时候过期」。这是整套源码里最该先确认的设计如果它没这么分后面所有追溯功能都是假的。-- 药品主数据一个药品一条记录管通用名、规格、批准文号 CREATE TABLE drug_info ( drug_id BIGINT PRIMARY KEY AUTO_INCREMENT, drug_code VARCHAR(32) NOT NULL COMMENT 内部编码, generic_name VARCHAR(128) NOT NULL COMMENT 通用名如阿莫西林胶囊, trade_name VARCHAR(128) COMMENT 商品名, spec VARCHAR(64) NOT NULL COMMENT 规格如0.25g*24粒, approval_no VARCHAR(64) NOT NULL COMMENT 国药准字唯一追溯依据, manufacturer VARCHAR(128) NOT NULL COMMENT 生产厂家, is_rx TINYINT DEFAULT 0 COMMENT 是否处方药 0否1是, UNIQUE KEY uk_approval_spec (approval_no, spec) ); -- 批号库存一个药品多个批号各自独立效期和数量 CREATE TABLE drug_batch ( batch_id BIGINT PRIMARY KEY AUTO_INCREMENT, drug_id BIGINT NOT NULL, batch_no VARCHAR(64) NOT NULL COMMENT 生产批号, produce_date DATE NOT NULL, expire_date DATE NOT NULL COMMENT 有效期至, purchase_price DECIMAL(10,2) NOT NULL COMMENT 该批进价, quantity INT NOT NULL DEFAULT 0 COMMENT 当前可用数量, warehouse_id BIGINT NOT NULL, UNIQUE KEY uk_drug_batch (drug_id, batch_no, warehouse_id), KEY idx_expire (expire_date) );逻辑说明drug_info用approval_no spec做唯一键是因为同一个批准文号可能有不同规格这是医药数据的真实约束。drug_batch的uk_drug_batch保证同一药品同一批号在同一仓库只有一条记录避免重复入库把数量算重。idx_expire这个索引是给效期预警查询用的没有它几万条批号数据下按效期筛选会全表扫描。参数上要注意expire_date存的是「有效期至」而不是「生产日期保质期」因为不同药品保质期规则不一样直接存到期日最省事也最不容易算错。purchase_price放在批号表而不是药品表是因为同一药品不同批次进价会变出库成本核算必须按批号走。2.2 出库时按批号扣减的先进先出实现库存扣减是医药系统最容易出 bug 的地方。常见错误是直接UPDATE drug_batch SET quantity quantity - 1 WHERE drug_id ?这样会随机扣一个批号导致先过期的药压在仓库里最后只能报损。正确做法是按效期先进先出优先扣最快过期的批号。def deduct_stock(conn, drug_id, warehouse_id, need_qty): 按效期先进先出扣减库存返回实际扣减的批号明细 cur conn.cursor() # 锁定待扣批号按效期升序FOR UPDATE 防止并发超卖 cur.execute( SELECT batch_id, quantity FROM drug_batch WHERE drug_id %s AND warehouse_id %s AND quantity 0 ORDER BY expire_date ASC FOR UPDATE , (drug_id, warehouse_id)) batches cur.fetchall() total sum(b[1] for b in batches) if total need_qty: raise Exception(f库存不足可用{total}需要{need_qty}) deducted [] remain need_qty for batch_id, qty in batches: if remain 0: break take min(qty, remain) cur.execute( UPDATE drug_batch SET quantity quantity - %s WHERE batch_id %s, (take, batch_id) ) deducted.append({batch_id: batch_id, qty: take}) remain - take return deducted逻辑说明ORDER BY expire_date ASC是先进先出的核心保证先扣快过期的。FOR UPDATE是并发安全的关键两个收银台同时卖同一批药时没有行锁会出现超卖——库存显示还有 1 盒结果两个订单都扣成功变成 -1。返回deducted明细是为了写销售出库明细表将来追溯「这盒药卖给了谁」时能对上批号。参数上warehouse_id不能省多仓库场景下同一药品在不同仓库有独立批号库存不传仓库会跨仓扣减。need_qty建议在调用前就校验为正整数负数会导致库存反向增加这种输入校验放在 service 层比放在这里更合适。2.3 效期预警与处方药销售拦截医药系统有两个普通进销存没有的强制功能效期预警和处方药管控。效期预警是每天定时扫一遍批号表把 90 天内到期的批号标出来提醒采购和店员。处方药管控是销售时如果药品is_rx 1必须关联一张有效处方才能出库。-- 效期预警查90天内到期且还有库存的批号 SELECT d.generic_name, d.spec, b.batch_no, b.expire_date, b.quantity, DATEDIFF(b.expire_date, CURDATE()) AS days_left FROM drug_batch b JOIN drug_info d ON b.drug_id d.drug_id WHERE b.quantity 0 AND b.expire_date DATE_ADD(CURDATE(), INTERVAL 90 DAY) ORDER BY b.expire_date ASC;逻辑说明DATEDIFF算出剩余天数前端可以按 30/60/90 分档标红。quantity 0过滤掉已经卖完的批号否则预警列表里全是历史数据没人看。这个查询走idx_expire索引几万条数据毫秒级返回。处方拦截的逻辑放在销售下单前查drug_info.is_rx如果是 1校验订单是否绑定了prescription_id且处方状态是「已审核」。这里有个坑处方是有有效期的一般 3 天过期处方不能再用校验时要带上prescription.expire_time NOW()。很多源码只判断了处方存在没判断有效期这是典型的合规漏洞。3. 把医药管理系统源码在本地跑起来的最小步骤3.1 环境依赖确认与数据库初始化拿到 zip 之后别急着改代码先把环境跑通确认这套源码本身是能跑的。医药管理系统源码主流是 Java SpringBoot MySQL 或者 Python Django MySQL 两种少数是 PHP。先看根目录有没有pom.xml、requirements.txt或composer.json这决定了你要装什么。以最常见的 SpringBoot MySQL 为例需要 JDK 8 或 11、Maven 3.6、MySQL 5.7 或 8.0。版本别乱升我见过 JDK 17 跑老 SpringBoot 2.1 直接启动失败的报Unsupported class file major version这种就是版本不匹配换 JDK 8 就好。# 1. 建库字符集必须 utf8mb4医药名称有生僻字 mysql -uroot -p -e CREATE DATABASE med_db DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci; # 2. 导入初始化脚本一般在 sql/ 或 doc/ 目录下 mysql -uroot -p med_db sql/init.sql # 3. 确认表都建好了 mysql -uroot -p med_db -e SHOW TABLES;逻辑说明utf8mb4不是可选项药品通用名里有「䓬」「羟」这类字用utf8三字节存会报Incorrect string value。导入脚本前先确认init.sql里有没有CREATE DATABASE语句如果有先手动建库再导入会冲突把脚本里的建库语句注释掉再导。参数上 MySQL 8.0 默认认证插件是caching_sha2_password老版本 JDBC 驱动连不上报Unable to load authentication plugin。解决办法要么升级驱动要么改用户认证方式ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码;。3.2 配置文件修改与启动验证数据库通了之后改配置文件。SpringBoot 看application.yml或application.properties重点改三处数据库连接、端口、文件上传路径。医药系统一般有药品图片和资质证照上传上传路径写相对路径容易在部署时找不到文件。spring: datasource: url: jdbc:mysql://localhost:3306/med_db?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver server: port: 8080 file: upload-path: /data/med/upload/ # 用绝对路径别用相对路径逻辑说明serverTimezoneAsia/Shanghai必须加不加的话 MySQL 8 驱动会报时区错误或者存进去的时间差 8 小时效期计算全错。upload-path用绝对路径是因为相对路径依赖启动时的工作目录用java -jar启动和用 IDE 启动的工作目录不一样相对路径会导致上传成功但访问 404。启动命令mvn spring-boot:run或者打包后java -jar target/xxx.jar。启动日志里看到Started Application in x seconds就是成功了。然后浏览器访问http://localhost:8080默认账号密码一般在init.sql的sys_user表里或者 README 里写着常见的是admin/123456。验证是否真的跑通不要只看首页能不能打开要实际走一遍新增一个药品 → 入库一个批号 → 销售出库 → 查库存是否扣减。这四步走通说明核心链路是好的可以开始改。4. 医药管理系统源码二次开发避坑与常见问题排查4.1 批号库存对不上并发扣减没加锁现象压测或者多人同时下单时库存出现负数或者两个订单扣了同一批药导致总数对不上。原因扣减逻辑用了SELECT查库存再UPDATE中间没有事务和行锁两个请求同时读到库存 1都判断够都扣变成 -1。解决把查询和更新放进同一个事务查询时加FOR UPDATE如 2.2 的代码所示。如果用的是 MyBatis注意Transactional注解要加在 service 方法上加在 controller 上不生效。4.2 效期预警查不出数据日期字段类型用错现象明明有快过期的批号预警列表是空的。原因expire_date建表时用了VARCHAR存日期字符串DATEDIFF和DATE_ADD比较时隐式转换失败或者格式不统一有的存2024-01-01有的存2024/01/01。解决日期字段一律用DATE或DATETIME类型不要用字符串。已经用了字符串的先UPDATE统一格式再ALTER TABLE改类型。4.3 中文药品名乱码连接串和表字符集不一致现象页面显示药品名是问号或者乱码。原因三个地方字符集不统一——数据库连接串、表、字段。常见是连接串写了characterEncodingutf8但表是utf8mb4或者反过来。解决三处全部统一成utf8mb4。连接串加characterEncodingutf8mb4建表和字段用DEFAULT CHARSETutf8mb4。改完重启已乱码的数据需要重新录入。4.4 处方药能直接卖拦截逻辑漏了有效期校验现象处方药没关联处方也能下单或者关联了一张过期处方照样通过。原因拦截只判断了is_rx和处方是否存在没判断处方状态和有效期。解决在销售下单的 service 里加完整校验——处方存在、状态为已审核、expire_time NOW()、处方里的药品和下单药品一致。最后一条最容易被忽略一张治感冒的处方拿来买抗生素系统不拦就是合规事故。4.5 部署后上传的图片访问 404路径没映射现象本地开发图片能显示打包部署后上传成功但访问 404。原因上传路径用了相对路径或者没有配置静态资源映射。解决上传路径用绝对路径同时在配置里加资源映射把上传目录映射到 URL 路径。SpringBoot 里加一个WebMvcConfigurer实现把/upload/**映射到file.upload-path指向的物理目录。5. 让医药管理系统源码真正可用的三个进阶改造5.1 用定时任务把效期预警做成主动推送2.3 的效期查询是被动的要人点进去才看得到。真正可用的系统应该每天早上自动跑一次把 90 天内到期的批号汇总成一条消息推给采购。用 Spring 的Scheduled就能做不用引额外框架。Scheduled(cron 0 0 8 * * ?) // 每天早上8点执行 public void expireWarningJob() { ListExpireWarning list batchMapper.selectExpiringSoon(90); if (list.isEmpty()) return; // 按剩余天数分档30天内标红加急 String summary list.stream() .collect(Collectors.groupingBy( w - w.getDaysLeft() 30 ? 紧急 : 关注, Collectors.counting())) .toString(); messageService.sendToPurchase(summary, list); }逻辑说明cron 0 0 8 * * ?是秒 分 时 日 月 周早上 8 点执行。分档是为了让采购一眼看出哪些要立刻处理。selectExpiringSoon就是 2.3 那条 SQL 封装成 mapper 方法。注意定时任务在多实例部署时会重复执行要么加分布式锁要么单独部署一个调度节点这是上线前必须处理的。5.2 批号追溯从销售单反查全链路医药系统最硬的需求是追溯。药监来查给你一个批号你要能说出这批药从哪个供应商进的、入库多少、卖了多少、卖给谁、还剩多少。这要求销售出库明细表必须记batch_id不能只记drug_id。-- 输入批号反查全链路 SELECT 入库 AS type, p.purchase_no AS doc_no, p.create_time, b.quantity AS qty, s.supplier_name AS party FROM drug_batch b JOIN purchase_order p ON b.batch_id p.batch_id JOIN supplier s ON p.supplier_id s.supplier_id WHERE b.batch_no ? UNION ALL SELECT 出库, so.order_no, so.create_time, si.qty, c.customer_name FROM sale_item si JOIN sale_order so ON si.order_id so.order_id JOIN customer c ON so.customer_id c.customer_id WHERE si.batch_id (SELECT batch_id FROM drug_batch WHERE batch_no ? LIMIT 1);逻辑说明UNION ALL把入库和出库两条链路拼成一张时间线按批号串起来。LIMIT 1是因为同一批号可能在不同仓库有记录实际追溯要带上仓库条件。这条查询的前提是sale_item表有batch_id字段如果源码里没有这是你二次开发第一个要加的字段不加追溯功能无从谈起。5.3 我踩过的坑和给你的建议说个血泪经验。我最早做医药系统时觉得批号管理麻烦想着「先按药品总数管批号以后再加」结果上线三个月后要做效期管理发现历史销售数据全没记批号追溯功能等于从零开始只能把老数据全部作废重新录。这个后悔药没得吃。所以如果你现在拿到这套源码第一件事不是改界面是确认sale_item有没有batch_id、drug_batch有没有expire_date索引、扣减逻辑有没有加锁。这三个点决定了这套代码是能用的骨架还是只能演示的玩具。界面丑可以慢慢改数据模型错了要推倒重来。另一个习惯每次改完库存相关逻辑手动造一条并发测试。开两个终端同时调扣减接口看库存会不会变负。这个测试花五分钟能帮你省掉上线后对账对到半夜的痛苦。医药系统的库存和钱一样错一分都要查半天。希望帮到你。本文还有配套的精品资源点击获取