在医药流通领域,高效精准的药品管理是保障业务顺畅运行与合规性的基石。传统依赖手工记账的管理方式不仅效率低下,且极易因人为疏漏导致库存数据失真、药品过期或供应断档,给企业带来直接的经济损失与运营风险。针对这一痛点,我们设计并实现了一套基于SSM(Spring + Spring MVC + MyBatis)技术栈的医药流通智能管理平台,旨在为中小型药店、诊所及医药公司提供全流程、数字化的解决方案。
该系统深度融合了业务实践与技术架构,实现了从药品采购、入库、库存管理、销售到财务核算的全链路闭环管理。其核心价值在于将零散的纸质信息转化为结构化的动态数据,通过实时库存监控、智能预警机制与多维度数据分析,赋能管理者进行科学决策,显著提升运营效率与风险控制能力。
技术架构选型与设计
平台采用经典的三层架构模式,确保系统的高内聚、低耦合与可维护性。
表现层 使用JSP(JavaServer Pages)结合jQuery与Bootstrap框架构建用户界面。JSP负责动态页面的渲染,而jQuery则处理前端的异步交互(AJAX)、表单验证与动态内容加载。Bootstrap提供了响应式布局与统一的UI组件,确保系统在不同终端设备上均有良好的用户体验。
控制层 由Spring MVC框架担当。通过@Controller注解定义控制器类,将HTTP请求映射到相应的业务处理方法。Spring MVC支持RESTful风格的API设计,使得前后端数据交互更加清晰规范。例如,药品信息的查询、新增、修改等操作都被映射到不同的URL路径上。
业务层 的核心是Spring框架的IoC(控制反转)容器。它负责管理所有业务逻辑组件(如DrugService, PurchaseService)的生命周期和依赖关系。通过@Service和@Autowired等注解,实现了依赖的自动注入。此外,Spring的声明式事务管理(@Transactional)是关键特性,它确保了如“采购入库同时更新库存”这类多步骤操作的原子性,避免数据不一致。
持久层 选用MyBatis作为ORM框架。与Hibernate的全自动化不同,MyBatis允许开发者编写细粒度的SQL语句,通过XML映射文件或注解方式将Java对象与数据库记录进行映射。这种半自动化的方式在复杂查询和性能优化方面提供了更大的灵活性。MyBatis的动态SQL功能(如<if>, <foreach>标签)能够轻松构建多条件组合查询。
数据层 采用MySQL关系型数据库,利用其事务ACID特性保障数据的完整性与一致性。
整个项目通过Maven进行依赖管理和构建,确保了第三方库版本的一致性和项目结构的标准化。
核心数据库表结构设计解析
一个健壮的管理系统离不开精心设计的数据库模型。以下是几个核心表的设计亮点:
1. 药品信息表 (drug_information)
此表是系统的核心数据基础,记录了药品的静态属性。其设计不仅包含了基本信息,还充分考虑了业务规则。
CREATE TABLE `drug_information` (
`drug_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '药品ID',
`drug_name` varchar(100) NOT NULL COMMENT '药品名称',
`drug_spec` varchar(50) DEFAULT NULL COMMENT '药品规格',
`manufacturer` varchar(100) DEFAULT NULL COMMENT '生产厂家',
`drug_type` varchar(50) DEFAULT NULL COMMENT '药品类型(如:处方药、非处方药)',
`prescription_flag` tinyint(1) DEFAULT '0' COMMENT '处方药标志(0:否,1:是)',
`stock_quantity` int(11) NOT NULL DEFAULT '0' COMMENT '当前库存数量',
`min_stock` int(11) DEFAULT '0' COMMENT '最低库存预警线',
`max_stock` int(11) DEFAULT '0' COMMENT '最高库存预警线',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`drug_id`),
UNIQUE KEY `uk_drug_name_spec` (`drug_name`, `drug_spec`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='药品信息表';
设计亮点分析:
- 唯一性约束 (
uk_drug_name_spec):通过drug_name和drug_spec的组合唯一键,有效防止了同一规格的药品被重复录入,确保了数据的唯一性。 - 库存预警机制字段:
min_stock(最低库存)和max_stock(最高库存)字段是实现智能预警的核心。业务逻辑层可以定期扫描或通过触发器实时检查stock_quantity是否超出此范围,从而向管理员发出补货或暂停采购的提醒。 - 审计字段:
create_time和update_time自动记录数据的创建和修改时间,便于追踪和审计。 - 处方药标志 (
prescription_flag):这是一个重要的业务规则字段。在销售模块,系统可以根据此标志强制要求关联处方单,确保合规销售。
2. 采购入库单主表 (purchase_order) 与明细表 (purchase_item)
采购业务涉及头-明细结构,这种设计是ERP系统中的经典模式。
CREATE TABLE `purchase_order` (
`order_id` varchar(32) NOT NULL COMMENT '入库单号(业务主键)',
`supplier_id` int(11) NOT NULL COMMENT '供应商ID',
`total_amount` decimal(12,2) NOT NULL COMMENT '订单总金额',
`order_status` tinyint(1) DEFAULT '1' COMMENT '订单状态(1:待入库,2:已入库,3:已取消)',
`operator` varchar(50) DEFAULT NULL COMMENT '操作员',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`storage_time` datetime DEFAULT NULL COMMENT '实际入库时间',
PRIMARY KEY (`order_id`),
KEY `idx_supplier_id` (`supplier_id`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='采购入库单主表';
CREATE TABLE `purchase_item` (
`item_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '明细项ID',
`order_id` varchar(32) NOT NULL COMMENT '所属入库单号',
`drug_id` int(11) NOT NULL COMMENT '药品ID',
`purchase_price` decimal(8,2) NOT NULL COMMENT '采购单价',
`quantity` int(11) NOT NULL COMMENT '采购数量',
`batch_number` varchar(100) DEFAULT NULL COMMENT '生产批号',
`production_date` date DEFAULT NULL COMMENT '生产日期',
`expiration_date` date DEFAULT NULL COMMENT '有效期至',
PRIMARY KEY (`item_id`),
KEY `idx_order_id` (`order_id`),
KEY `idx_drug_id` (`drug_id`),
CONSTRAINT `fk_item_order` FOREIGN KEY (`order_id`) REFERENCES `purchase_order` (`order_id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='采购入库明细表';
设计亮点分析:
- 关系与数据一致性:明细表通过
order_id外键关联主表,并设置了ON DELETE CASCADE级联删除。这意味着删除一个主订单,其所有明细项会自动删除,维护了数据的参照完整性。 - 批次管理:明细表中
batch_number(批号)、production_date(生产日期)和expiration_date(有效期至)是实现药品批次和效期管理的关键。系统可以基于这些数据进行先进先出(FIFO)的出库策略和近效期预警。 - 状态流转:主表的
order_status字段清晰地定义了采购单的生命周期(待入库->已入库/取消),业务逻辑围绕状态的变化展开,例如只有“已入库”的订单才会触发库存更新。
3. 角色权限表 (role) 与用户表 (user)
为了保证系统数据安全,设计了基于角色的访问控制(RBAC)模型。
CREATE TABLE `role` (
`role_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
`role_name` varchar(50) NOT NULL COMMENT '角色名称(如:管理员、库管、收银员)',
`role_desc` varchar(200) DEFAULT NULL COMMENT '角色描述',
`permissions` text COMMENT '权限列表(JSON格式或权限点字符串)',
PRIMARY KEY (`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色表';
CREATE TABLE `user` (
`user_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`username` varchar(50) NOT NULL COMMENT '用户名',
`password` varchar(100) NOT NULL COMMENT '密码(加密存储)',
`real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名',
`role_id` int(11) NOT NULL COMMENT '所属角色ID',
`is_enabled` tinyint(1) DEFAULT '1' COMMENT '账号是否启用',
`last_login_time` datetime DEFAULT NULL COMMENT '最后登录时间',
PRIMARY KEY (`user_id`),
UNIQUE KEY `uk_username` (`username`),
KEY `idx_role_id` (`role_id`),
CONSTRAINT `fk_user_role` FOREIGN KEY (`role_id`) REFERENCES `role` (`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户表';
设计亮点分析:
- 密码安全:
password字段存储的是经过加密(如BCrypt)的密文,而非明文,极大增强了安全性。 - 权限控制:
role表中的permissions字段(可以是JSON字符串)定义了该角色可以访问的菜单和操作权限。在用户登录后,系统根据其角色加载对应的权限集,从而在界面和API层面进行动态控制。例如,收银员角色可能只有销售开单和查询库存的权限,而库管员则拥有采购入库和库存管理的权限。
核心功能模块深度解析
1. 药品信息管理与库存预警
药品信息管理是系统的基础。管理员可以在此模块维护所有药品的基本信息、价格和库存上下限。

后端Controller代码示例(查询与分页):
@Controller
@RequestMapping("/drug")
public class DrugController {
@Autowired
private DrugService drugService;
@RequestMapping(value = "/list", method = RequestMethod.GET)
@ResponseBody
public PageResult<Drug> getDrugList(
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "limit", defaultValue = "10") Integer limit,
@RequestParam(value = "drugName", required = false) String drugName,
@RequestParam(value = "drugType", required = false) String drugType) {
// 构建查询条件
DrugQuery query = new DrugQuery();
query.setDrugName(drugName);
query.setDrugType(drugType);
query.setPage((page - 1) * limit);
query.setLimit(limit);
// 调用Service层获取数据
List<Drug> drugs = drugService.getDrugListByPage(query);
long count = drugService.getDrugCount(query);
// 返回统一的分页结果对象
return new PageResult<>(0, "success", count, drugs);
}
}
Service层库存检查逻辑:
@Service
@Transactional
public class DrugServiceImpl implements DrugService {
@Override
public void checkStockWarning() {
List<Drug> drugList = drugMapper.selectAll();
List<String> warningMessages = new ArrayList<>();
for (Drug drug : drugList) {
if (drug.getStockQuantity() < drug.getMinStock()) {
warningMessages.add(String.format("药品【%s】库存不足预警!当前库存:%d,最低库存:%d",
drug.getDrugName(), drug.getStockQuantity(), drug.getMinStock()));
} else if (drug.getStockQuantity() > drug.getMaxStock()) {
warningMessages.add(String.format("药品【%s】库存积压预警!当前库存:%d,最高库存:%d",
drug.getDrugName(), drug.getStockQuantity(), drug.getMaxStock()));
}
}
// 将warningMessages发送给管理员(如站内信、邮件、短信)
if (!warningMessages.isEmpty()) {
notificationService.sendStockWarning(warningMessages);
}
}
}
2. 采购入库与库存更新联动
采购管理模块实现了从创建订单到库存增加的全流程。创建采购单时,状态为“待入库”,此时库存不变。当实物药品验收入库后,执行“入库”操作,系统状态变为“已入库”,并自动更新对应药品的库存数量。

入库操作的核心Service方法(展示事务管理):
@Service
public class PurchaseServiceImpl implements PurchaseService {
@Autowired
private PurchaseOrderMapper orderMapper;
@Autowired
private PurchaseItemMapper itemMapper;
@Autowired
private DrugMapper drugMapper;
@Override
@Transactional(rollbackFor = Exception.class) // 声明式事务,异常时回滚
public boolean confirmStorage(String orderId) {
// 1. 查询采购单及其明细
PurchaseOrder order = orderMapper.selectByPrimaryKey(orderId);
if (order == null || !order.getOrderStatus().equals(OrderStatus.PENDING_STORAGE)) {
throw new BusinessException("采购单不存在或状态不正确,无法入库");
}
List<PurchaseItem> items = itemMapper.selectByOrderId(orderId);
// 2. 遍历明细项,更新药品库存
for (PurchaseItem item : items) {
Drug drug = drugMapper.selectByPrimaryKey(item.getDrugId());
if (drug == null) {
throw new BusinessException("药品不存在,入库失败");
}
// 更新库存(当前库存 + 采购数量)
int newQuantity = drug.getStockQuantity() + item.getQuantity();
drug.setStockQuantity(newQuantity);
// 同时可以更新最近采购价等信息
drug.setLastPurchasePrice(item.getPurchasePrice());
int updateCount = drugMapper.updateStockQuantity(drug);
if (updateCount != 1) {
throw new BusinessException("更新药品库存失败,可能数据已被修改");
}
}
// 3. 更新采购单状态和入库时间
order.setOrderStatus(OrderStatus.STORAGED);
order.setStorageTime(new Date());
orderMapper.updateStatus(order);
return true;
}
}
3. 销售出库与处方药校验
销售模块是直接产生效益的环节,其核心是快速开单、准确扣减库存并确保合规性。

销售创建的Controller和Service代码:
@RestController
@RequestMapping("/sale")
public class SaleController {
@PostMapping("/create")
public ResponseEntity<ApiResponse> createSale(@RequestBody @Valid SaleOrderDTO saleOrderDTO) {
// DTO中包含销售单头信息(如客户信息)和明细列表(药品ID,数量等)
saleService.createSaleOrder(saleOrderDTO);
return ResponseEntity.ok(ApiResponse.success("销售单创建成功"));
}
}
@Service
@Transactional
public class SaleServiceImpl implements SaleService {
@Override
public void createSaleOrder(SaleOrderDTO saleOrderDTO) {
// 1. 基本校验
if (saleOrderDTO.getItems() == null || saleOrderDTO.getItems().isEmpty()) {
throw new BusinessException("销售明细不能为空");
}
// 2. 生成销售单号
String saleNo = generateSaleNo();
// 3. 遍历销售明细
for (SaleItemDTO itemDTO : saleOrderDTO.getItems()) {
Drug drug = drugMapper.selectByPrimaryKey(itemDTO.getDrugId());
// 3.1 库存校验
if (drug.getStockQuantity() < itemDTO.getQuantity()) {
throw new BusinessException("药品【" + drug.getDrugName() + "】库存不足");
}
// 3.2 处方药校验
if (drug.getPrescriptionFlag() == 1) {
// 检查DTO中是否关联了有效的处方单ID
if (itemDTO.getPrescriptionId() == null) {
throw new BusinessException("药品【" + drug.getDrugName() + "】为处方药,必须关联处方单");
}
// 进一步校验处方单的有效性、医生签名、患者信息等(需调用处方单服务)
// ...
}
// 4. 扣减库存(悲观锁或乐观锁方式,防止超卖)
int updateRows = drugMapper.decreaseStock(itemDTO.getDrugId(), itemDTO.getQuantity());
if (updateRows == 0) {
throw new BusinessException("扣减药品【" + drug.getDrugName() + "】库存失败,请重试");
}
// 5. 保存销售明细记录
SaleItem item = new SaleItem();
// ... 属性拷贝
saleItemMapper.insert(item);
}
// 6. 保存销售单主记录
SaleOrder order = new SaleOrder();
// ... 属性设置
saleOrderMapper.insert(order);
}
}
4. 近效期与过期药品处理
药品效期管理是医药行业特有的刚性需求,直接关系到用药安全。

定时任务检查近效期药品:
@Component
public class DrugExpirationTask {
@Autowired
private DrugExpirationMapper expirationMapper;
/**
* 每天凌晨1点执行,检查近效期(如60天内过期)和已过期药品
*/
@Scheduled(cron = "0 0 1 * * ?")
public void checkExpiringDrugs() {
Date now = new Date();
Calendar calendar = Calendar.getInstance();
calendar.setTime(now);
calendar.add(Calendar.DAY_OF_YEAR, 60); // 计算