1. 项目概述与核心需求
这个药品管理系统本质上是一个典型的B/S架构企业级应用,采用经典的SSM(Spring+SpringMVC+MyBatis)技术栈实现。我在实际医药流通企业的信息化建设中,发现这类系统需要同时满足三个维度的需求:
- 业务合规性:必须符合GSP(药品经营质量管理规范)对进销存记录的强制性要求,包括批号追踪、效期管理、供应商资质等特殊字段
- 操作便捷性:药房工作人员往往计算机水平有限,需要极简的UI设计和防错机制
- 数据安全性:药品价格、库存等属于商业敏感数据,需要完善的权限控制和操作日志
提示:在药品管理系统中,效期预警功能建议采用"近效期先出"(FEFO)原则,这与普通商品的"先进先出"(FIFO)有本质区别,需要在数据库设计阶段就考虑。
2. 技术选型与架构设计
2.1 为什么选择SSM框架
相比Spring Boot的自动化配置,传统SSM框架在这里更具优势:
- 医院/药房IT环境往往存在老旧系统兼容需求
- 需要精细控制MyBatis的SQL优化(药品查询涉及多表关联)
- JSP便于快速开发带复杂表单的页面(如药品入库单)
技术栈版本建议:
xml复制<!-- pom.xml核心依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.18</version>
</dependency>
<dependency>
<groupId>mysql-connector-java</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
2.2 数据库设计要点
药品系统的ER模型有以下几个特殊点需要关注:
| 表名 | 关键字段 | 特殊约束 |
|---|---|---|
| drug_info | batch_no(批号), validity_date(效期) | 联合唯一索引 |
| inventory | stock_alert(库存预警值) | 触发器自动计算 |
| purchase | supplier_license(供应商许可证号) | 外键级联更新 |
sql复制-- 典型药品表DDL示例
CREATE TABLE `drug_info` (
`drug_id` varchar(20) NOT NULL COMMENT '药品编码',
`batch_no` varchar(30) NOT NULL COMMENT '生产批号',
`validity_date` date NOT NULL COMMENT '有效期至',
PRIMARY KEY (`drug_id`,`batch_no`),
KEY `idx_validity` (`validity_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3. 核心功能实现细节
3.1 进货管理的防重复提交设计
药品采购单的特殊性在于:
- 同批号药品可能分多次入库
- 需要防止操作员重复提交相同单据
解决方案:
- 前端采用Token机制+按钮禁用
- 后端校验关键字段哈希值:
java复制// PurchaseController.java
@PostMapping("/purchase")
public Result addPurchase(@RequestBody PurchaseDTO dto, HttpSession session) {
String hash = DigestUtils.md5Hex(dto.getDrugId()+dto.getBatchNo()+dto.getSupplierId());
if (redisTemplate.opsForValue().setIfAbsent("purchase:"+hash, "1", 5, TimeUnit.MINUTES)) {
// 执行业务逻辑
} else {
throw new BusinessException("相同批号的药品正在处理中");
}
}
3.2 销售出库的库存扣减策略
药品销售需要处理并发库存扣减问题,我们采用两种方案结合:
- 乐观锁实现:
java复制// InventoryMapper.xml
<update id="reduceStock">
UPDATE inventory
SET stock = stock - #{quantity}
WHERE drug_id = #{drugId} AND stock >= #{quantity}
AND version = #{version}
</update>
- 数据库事务隔离级别设置为REPEATABLE_READ,配合@Transactional注解
4. 系统安全与性能优化
4.1 审计日志实现方案
药品系统必须记录完整操作轨迹,我们采用AOP+注解方式:
java复制@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface OperationLog {
String module() default "";
String type() default "";
}
// 切面实现类
@Aspect
@Component
public class LogAspect {
@AfterReturning("@annotation(log)")
public void afterReturning(JoinPoint jp, OperationLog log) {
HttpServletRequest request = ((ServletRequestAttributes)
RequestContextHolder.getRequestAttributes()).getRequest();
// 记录操作日志到数据库
}
}
4.2 高并发查询优化
针对药品库存查询的优化措施:
- 使用Redis缓存热点药品数据(设置5分钟过期)
- MySQL查询添加覆盖索引:
sql复制ALTER TABLE inventory ADD INDEX idx_drug_stock (drug_id, stock);
- 前端采用分页加载+虚拟滚动技术
5. 部署与运维实践
5.1 生产环境配置建议
药品系统的Tomcat配置需要特殊调整:
properties复制# conf/server.xml
<Connector port="8080" maxThreads="200"
minSpareThreads="25" maxQueueSize="1000"/>
# 数据库连接池配置
spring.datasource.druid.initial-size=5
spring.datasource.druid.max-active=20
spring.datasource.druid.validation-query=SELECT 1 FROM dual
5.2 常见问题排查
-
中文乱码问题:
- 确保MySQL字符集为utf8mb4
- JSP页面添加
<%@ page contentType="text/html;charset=UTF-8"%> - Tomcat的server.xml配置URIEncoding="UTF-8"
-
事务失效场景:
- 检查方法是否为public
- 确认没有try-catch吞掉异常
- 避免同类内方法调用
在药房实际部署时,我们发现Windows服务器时间格式设置会导致效期查询异常,建议在JDBC连接串添加参数:
code复制jdbc:mysql://localhost:3306/drug_db?useSSL=false&serverTimezone=Asia/Shanghai&useLegacyDatetimeCode=false
这个系统经过三个月的实际运行,日均处理2000+笔进销存记录,最关键的教训是:药品批号校验必须放在事务最开始执行,否则并发情况下可能导致同一批号被重复入库。后来我们通过数据库唯一索引+应用层校验的双重保障解决了这个问题。
