
简介本资源是一套基于Java Web与MySQL开发的医药销售管理课程设计系统面向计算机专业本科生及Java初学者解决中小型医药企业员工与经理在会员管理、供应商协作、药品库存查询、采购销售及仓库盘点等核心业务中的信息化需求。压缩包共26个文件含19个JSP页面覆盖登录、首页、会员/员工/供应商增删查、药品销售与退货、财务统计等完整业务模块、1个SQL建库脚本、1个MySQL驱动JAR包、1个系统说明README.md及配套图片与许可证文件整体仅1.06MB轻量易部署。已有264人学习下载适合课程设计参考、Web开发实训或SSM框架入门拓展。读者可直接导入Eclipse/IDEA运行获得结构清晰的MVC分层实现、角色权限控制逻辑员工/经理双角色差异化功能、完整数据库设计及典型医药行业业务流程闭环是理解Java Web企业级应用开发的优质实践案例。1. 医药销售管理系统为什么不能只靠ExcelJavaMySQL组合如何扛住日均500单据、30角色权限和GMP合规留痕需求某高校实验室在帮本地连锁药房做数字化升级时最初用Excel模板管理进销存结果第三个月就崩了采购员填错一格批号导致整批药品追溯链断裂店长导出的销售报表和财务对不上因为库存实时扣减逻辑分散在6个Sheet里更麻烦的是药监飞检要求所有操作必须可回溯——谁在什么时间修改了哪条记录、改前改后值是什么Excel根本没法满足。后来他们转向「基于JavaMySQL实现的医药销售管理系统」核心不是换了个技术栈而是用事务一致性锁住数据流、用角色-资源-操作三级权限模型管住人、用带时间戳和操作人ID的审计日志满足合规底线。这个系统不是给程序员炫技的是给药店老板、库管员、执业药师、财务人员每人配一套「不翻车」的操作界面库管扫个码自动扣库存药师开处方时系统弹窗提醒该药品是否近效期财务点一下生成符合《药品经营质量管理规范》GMP要求的月度盘点报告。如果你正被Excel拖垮、被纸质单据淹没、被药监检查反复卡住这篇就是你从「手忙脚乱」走向「稳态运转」的实操路径。2. 用Spring Boot MyBatis-Plus快速搭起医药系统骨架从数据库建模到CRUD接口自动生成2.1 先画清医药领域最关键的5张表为什么药品主数据必须拆成drug_infodrug_batch两张表医药系统的数据建模第一道坎是「药品」怎么存。新手常犯的错是把所有信息塞进一张drug表品名、规格、厂家、批准文号、生产日期、有效期至、库存量……看着省事但上线三天就出问题——同一款阿莫西林胶囊0.25g×24粒不同批次的生产日期、有效期、供应商、采购价全不一样库存还得按批次单独管理。合规要求也明确药品追溯必须精确到最小销售单元的批号。所以必须拆-- 药品基础信息不变项 CREATE TABLE drug_info ( id BIGINT PRIMARY KEY AUTO_INCREMENT, drug_code VARCHAR(20) NOT NULL COMMENT 国药准字编号如国药准字H11020001, name VARCHAR(100) NOT NULL COMMENT 通用名, spec VARCHAR(50) COMMENT 规格如0.25g×24粒, manufacturer VARCHAR(100) COMMENT 生产厂家, approval_no VARCHAR(50) COMMENT 批准文号, category ENUM(处方药,非处方药,中药饮片) NOT NULL, created_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 药品批次信息变动项 CREATE TABLE drug_batch ( id BIGINT PRIMARY KEY AUTO_INCREMENT, drug_id BIGINT NOT NULL COMMENT 关联drug_info.id, batch_no VARCHAR(30) NOT NULL COMMENT 批号如20230801A, production_date DATE NOT NULL COMMENT 生产日期, expiry_date DATE NOT NULL COMMENT 有效期至, stock_quantity INT DEFAULT 0 COMMENT 当前库存, purchase_price DECIMAL(10,2) COMMENT 采购单价, sale_price DECIMAL(10,2) COMMENT 零售单价, supplier VARCHAR(100) COMMENT 供应商, status ENUM(在库,已售,报损,召回) DEFAULT 在库, created_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_drug_batch (drug_id, batch_no) );提示drug_info存药品的“身份证”drug_batch存每一批次的“健康档案”。查询某药品总库存时用SUM(drug_batch.stock_quantity)查某批号详细信息时两表JOIN。这种设计让GMP要求的“同品不同批分开管理”天然落地避免后期为补追溯逻辑推倒重来。2.2 用MyBatis-Plus代码生成器一键生成实体类、Mapper、Service层手动写POJO、XML映射、Service方法在医药系统这种字段多、校验严、增删改查高频的场景下纯手工等于给自己埋雷。MyBatis-Plus的AutoGenerator能根据数据库表结构5分钟生成90%的CRUD代码。关键配置如下CodeGenerator.java// 指定数据库连接和表名 DataSourceConfig dsc new DataSourceConfig(); dsc.setUrl(jdbc:mysql://localhost:3306/pharma_system?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/Shanghai); dsc.setDriverName(com.mysql.cj.jdbc.Driver); dsc.setUsername(root); dsc.setPassword(123456); // 生成策略只生成drug_info和drug_batch两张表 StrategyConfig strategy new StrategyConfig(); strategy.setInclude(drug_info, drug_batch, supplier, employee, sales_order); // 明确指定表名 strategy.setNaming(NamingStrategy.underline_to_camel); // 下划线转驼峰 strategy.setColumnNaming(NamingStrategy.underline_to_camel); // 包路径和作者 PackageConfig pc new PackageConfig(); pc.setParent(com.pharma); pc.setEntity(entity); pc.setMapper(mapper); pc.setService(service); pc.setController(controller); // 执行生成 new AutoGenerator() .setGlobalConfig(gc) .setDataSource(dsc) .setPackageInfo(pc) .setStrategy(strategy) .execute();生成后你会得到DrugInfo.java含TableField(value approval_no)注解字段名与数据库严格对应DrugInfoMapper.java继承BaseMapperDrugInfo自带selectList()、insert()等方法DrugInfoServiceImpl.java实现IServiceDrugInfo封装了常用业务逻辑入口。参数说明serverTimezoneAsia/Shanghai必须显式设置否则MySQL 8.0会因时区问题报错include数组必须写全表名避免生成无关表污染项目NamingStrategy.underline_to_camel确保batch_no生成为batchNo符合Java命名习惯。这步省下的不是代码行数是后续排查“字段名大小写不一致导致空指针”的3小时。3. 权限控制不能只靠if-else用Shiro实现药店多角色动态授权店长/药师/库管/收银员3.1 为什么RBAC模型必须扩展为ABAC——当“执业药师”身份遇上“处方药销售”动作药店系统里角色权限远比普通OA复杂店长能审核所有单据但不能修改药品基础信息执业药师能开处方、销售处方药但无权查看财务报表库管能出入库但销售出库时必须关联销售单号不能直接扣减收银员只能开销售单且每单必须绑定执业药师工号。如果只用传统RBAC角色-权限会陷入“权限爆炸”为满足所有组合得建20个角色。而ABAC属性-基于属性的访问控制用动态规则解耦主体Subject当前登录用户含角色、所属门店、执业资格状态资源Resource要访问的API或数据如/api/sales/create、drug_batch表动作ActionPOST、UPDATE、DELETE环境Environment当前时间、是否在营业时间、药品是否近效期。Shiro通过RequiresPermissions(sales:create)注解声明静态权限再用自定义AuthorizingRealm注入动态规则// 自定义Realm中重写doGetAuthorizationInfo protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String username (String) principals.getPrimaryPrincipal(); SimpleAuthorizationInfo info new SimpleAuthorizationInfo(); // 1. 加载用户静态权限从数据库查 ListString permissions permissionService.findPermissionsByUser(username); info.addStringPermissions(permissions); // 如[sales:create, inventory:adjust] // 2. 注入动态规则执业药师才能销售处方药 if (user.getQualification().equals(执业药师)) { info.addStringPermission(prescription:sale); } // 3. 环境规则库管操作库存时检查是否在营业时间 if (user.getRole().equals(库管员)) { info.addStringPermission(inventory:time-bound); } return info; }然后在Controller中用RequiresPermissions和自定义注解结合PostMapping(/create) RequiresPermissions(sales:create) public Result createSalesOrder(RequestBody SalesOrder order) { // 动态校验当前用户是否为执业药师用于处方药 if (order.containsPrescriptionDrug() !currentUser.isLicensedPharmacist()) { return Result.fail(销售处方药需执业药师操作); } // 动态校验库存是否充足查drug_batch.stock_quantity if (!inventoryService.hasEnoughStock(order.getItems())) { return Result.fail(库存不足请检查批次); } salesService.save(order); return Result.success(); }关键逻辑Shiro的RequiresPermissions只做粗粒度拦截有无权限真正的业务规则如“执业药师才能卖处方药”“近效期药品禁止销售”必须在Service层用Java代码硬校验。这是医药系统的生命线——权限框架可以帮你挡掉非法请求但挡不住合法用户误操作。把规则写死在代码里比依赖配置文件更可控。3.2 三张权限表设计为什么role_permission中间表必须加data_scope字段单纯的角色-权限关联不够医药系统必须支持“数据级权限”A门店店长只能看A门店的销售数据总部管理员能看到所有门店但不能修改单店库存财务只看汇总报表不看明细批次。因此在标准RBAC三张表sys_role,sys_permission,role_permission基础上扩展data_scope字段-- 角色-权限关联表增强版 CREATE TABLE role_permission ( id BIGINT PRIMARY KEY AUTO_INCREMENT, role_id BIGINT NOT NULL, permission_id BIGINT NOT NULL, data_scope ENUM(all, own_store, own_dept, custom) DEFAULT all COMMENT 数据范围, custom_sql TEXT COMMENT 自定义SQL条件如store_id ?, created_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_role_perm (role_id, permission_id) );当店长访问销售报表时系统自动拼接WHERE store_id ?条件总部管理员则跳过此限制。custom_sql字段支持写store_id IN (SELECT store_id FROM user_store WHERE user_id ?)实现复杂的数据归属关系。这比在每个DAO方法里硬写if (user.getRole().equals(店长)) queryWrapper.eq(store_id, user.getStoreId())更统一、更易维护。4. 避坑医药系统上线前必须跨过的5个血泪深坑4.1 现象库存扣减后出现负数但日志显示“扣减成功”原因MySQL默认隔离级别REPEATABLE READ下并发下单时两个事务同时读取stock_quantity10各自减1后都写入9实际应为8。这不是代码bug是数据库并发控制缺失。解决对库存操作使用SELECT ... FOR UPDATE加行锁或改用UPDATE drug_batch SET stock_quantity stock_quantity - ? WHERE id ? AND stock_quantity ?用AND stock_quantity ?做乐观锁校验。后者更轻量失败时捕获SQLException提示“库存不足请刷新后重试”。4.2 现象药品效期预警不准系统显示“距到期30天”实际已过期原因数据库字段expiry_date存的是DATE类型但Java中用new Date()获取当前时间再计算差值忽略了时区转换——MySQL服务器时区是UTC应用服务器是CST导致日期计算偏差1天。解决统一用LocalDate.now(ZoneId.of(Asia/Shanghai))获取本地日期数据库查询用CURDATE()函数而非NOW()效期校验逻辑全部下沉到SQL层WHERE expiry_date DATE_ADD(CURDATE(), INTERVAL 30 DAY)。4.3 现象导出的销售报表Excel打开报“文件损坏”但用WPS能打开原因Apache POI生成Excel时未设置WorkbookFactory.create(new FileInputStream(file))的兼容模式Excel 2007格式.xlsx与旧版Excel Viewer解析不一致。解决强制指定XSSFWorkbook并设置setForceFormulaRecalculation(true)导出前用FileOutputStream写入时确保response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charsetutf-8)且Content-Disposition头包含filename*UTF-8xxx.xlsx。4.4 现象执业药师签名电子化后药监系统不认可PDF签章原因前端用Canvas手写签名后转PNG再用iText合并到PDF但未嵌入数字证书和时间戳不符合《电子签名法》对可靠电子签名的要求。解决集成国家授时中心时间戳服务TSA用Bouncy Castle库对签名图片哈希值进行SM2国密算法签名PDF生成时调用PdfSigner.signDetached()嵌入CA颁发的数字证书链。4.5 现象MySQL慢查询日志显示SELECT * FROM drug_batch WHERE status 在库耗时2s原因status字段未建索引且在库是中文枚举值MySQL无法高效走索引。解决将status改为TINYINT类型0在库1已售2报损并建立联合索引INDEX idx_status_stock (status, stock_quantity)查询时用WHERE status 0避免字符比较开销。5. GMP合规留痕如何用MySQL触发器Java审计日志双保险满足药监飞检的“操作可追溯”硬要求5.1 数据库层用BEFORE UPDATE触发器捕获每一次关键字段变更药监检查最常问“这条销售记录是谁在什么时候修改的改前值是多少”光靠Java层日志不可信——万一应用崩溃日志没刷盘就丢了。必须在数据库层做兜底。以drug_batch表为例创建触发器记录每次库存变更DELIMITER $$ CREATE TRIGGER tr_drug_batch_update_audit BEFORE UPDATE ON drug_batch FOR EACH ROW BEGIN DECLARE v_user VARCHAR(50) DEFAULT USER(); DECLARE v_host VARCHAR(50) DEFAULT SUBSTRING_INDEX(USER(), , -1); -- 只记录库存、状态变更 IF OLD.stock_quantity ! NEW.stock_quantity OR OLD.status ! NEW.status THEN INSERT INTO audit_log ( table_name, record_id, operation, old_value, new_value, operator, ip_address, created_time ) VALUES ( drug_batch, NEW.id, UPDATE, CONCAT(stock:, OLD.stock_quantity, ;status:, OLD.status), CONCAT(stock:, NEW.stock_quantity, ;status:, NEW.status), v_user, v_host, NOW() ); END IF; END$$ DELIMITER ;注意USER()函数返回app_user192.168.1.100SUBSTRING_INDEX(..., , -1)提取IP确保操作人定位到具体机器old_value和new_value用CONCAT拼成JSON-like字符串便于后续解析触发器只对stock_quantity和status生效避免日志爆炸。这是药监飞检时最硬的证据——哪怕Java应用重启10次数据库里每条变更都刻在硬盘上。5.2 应用层用Spring AOP切面统一收集操作上下文补全“谁在什么业务场景下操作”触发器知道“谁改了数据”但不知道“为什么改”。比如库存扣减可能是销售出库、报损、盘点调整业务含义天壤之别。这时需要Java层补充上下文。用Around切面拦截Service方法Aspect Component public class AuditLogAspect { Around(annotation(org.springframework.transaction.annotation.Transactional) execution(* com.pharma.service..*.*(..))) public Object logOperation(ProceedingJoinPoint joinPoint) throws Throwable { long start System.currentTimeMillis(); Object result joinPoint.proceed(); long cost System.currentTimeMillis() - start; // 提取关键信息 String methodName joinPoint.getSignature().getName(); Object[] args joinPoint.getArgs(); String businessContext extractBusinessContext(methodName, args); // 如销售单号SO202308001 // 写入审计日志表异步避免阻塞主流程 auditLogService.asyncSave( SecurityUtils.getCurrentUser(), // 当前登录用户 methodName, businessContext, JSON.toJSONString(args), // 入参快照 cost, result instanceof Boolean (Boolean) result ? SUCCESS : FAILED ); return result; } private String extractBusinessContext(String method, Object[] args) { if (createSalesOrder.equals(method) args.length 0 args[0] instanceof SalesOrder) { return 销售单: ((SalesOrder) args[0]).getOrderNo(); } if (adjustInventory.equals(method) args.length 0 args[0] instanceof InventoryAdjustment) { return 盘点调整: ((InventoryAdjustment) args[0]).getAdjustNo(); } return 未知场景; } }关键设计切点用Transactional注解确保只记录真正影响数据库的操作asyncSave用线程池异步写日志避免日志IO拖慢主业务businessContext提取业务单号让审计日志能和具体单据关联记录cost耗时飞检时可证明系统响应在合理范围内如销售单创建1.5s。5.3 合规验证飞检前必须跑通的3个审计日志查询SQL药监人员不会看你代码只会现场执行SQL验证。以下3条是必查项必须100%返回预期结果检查项SQL语句预期结果说明操作完整性SELECT COUNT(*) FROM audit_log WHERE DATE(created_time) CURDATE();≥当日业务单据总数×0.95日志丢失率不能超5%否则视为系统不可靠关键操作可追溯SELECT * FROM audit_log WHERE business_context LIKE 销售单:% ORDER BY created_time DESC LIMIT 10;每条记录含operator、business_context、old_value、new_value必须能定位到具体销售单、操作人、变更前后值敏感操作强管控SELECT * FROM audit_log WHERE operation UPDATE AND table_name drug_batch AND new_value LIKE %status:%报损% LIMIT 5;每条记录operator字段必须是库管员或店长报损操作必须由授权角色执行我带过的三个医药系统项目飞检过关的关键不是功能多炫而是这三张表audit_log、drug_batch、sales_order能用一条SQL串起来SELECT a.operator, a.business_context, a.old_value, a.new_value, s.total_amount, s.payment_method FROM audit_log a JOIN sales_order s ON a.business_context LIKE CONCAT(销售单:%, s.order_no) WHERE a.created_time BETWEEN 2023-08-01 00:00:00 AND 2023-08-01 23:59:59 ORDER BY a.created_time;——药监老师盯着屏幕看10秒点头说“行留痕完整”你就过了。这背后是触发器切面SQL验证三位一体的设计不是堆功能是守底线。希望帮到你。本文还有配套的精品资源点击获取