1. 项目概述与行业背景
中药材行业作为我国传统医药产业的重要组成部分,长期以来面临着管理粗放、信息化程度低的痛点。药材从采购到销售的全流程中,手工记录、纸质单据仍是主流方式,导致数据孤岛严重、库存周转率低、损耗难以控制等问题。这套基于SpringBoot的中药材进存销管理系统,正是针对这些行业痛点设计的全流程数字化解决方案。
我在实际开发中发现,中药材管理相比普通商品有着显著特殊性:首先,药材存在严格的批次管理和效期控制需求;其次,同种药材可能因产地、采收季节不同而存在质量差异;再者,药材的计量单位复杂(如公斤、克、甚至传统计量单位"钱")。这些特性都必须在系统设计中予以充分考虑。
系统采用B/S架构,前端使用Vue.js实现响应式布局,后端基于SpringBoot框架搭建RESTful API,数据库选用MySQL 5.7。这种技术栈的选择主要基于三点考量:一是Java生态在中大型企业系统中的稳定性和成熟度;二是SpringBoot的自动配置特性可大幅减少XML配置;三是MySQL在事务处理和并发控制上的可靠性,完全能满足年交易量百万级的中小型药材企业需求。
2. 系统架构设计与技术选型
2.1 整体技术架构解析
系统采用经典的三层架构设计,但针对药材行业特性做了特殊优化:
code复制表示层(Vue.js) → 业务逻辑层(SpringBoot) → 数据访问层(MyBatis)
↑ ↑ ↑
移动端适配 药材业务规则引擎 药材特性数据模型
在表示层,我们使用Vue CLI 4.x搭建前端工程,通过Axios与后端交互。特别开发了药材专用表单控件,如支持"克-公斤-钱"单位自动换算的输入组件。
业务逻辑层采用SpringBoot 2.3.4,其核心优势在于:
- 内嵌Tomcat简化部署
- Starter依赖自动管理JAR包版本
- Actuator提供完善的系统监控
- 与MyBatis的无缝集成
数据层使用MySQL 5.7,关键配置参数:
ini复制innodb_buffer_pool_size = 4G # 缓冲池设为物理内存70%
transaction-isolation = READ-COMMITTED
innodb_lock_wait_timeout = 120
2.2 数据库设计要点
药材管理的E-R模型需要特别关注几个核心实体:
- 药材基础信息表(herb_base)
sql复制CREATE TABLE `herb_base` (
`herb_id` varchar(20) NOT NULL COMMENT '药材编码',
`herb_name` varchar(50) NOT NULL COMMENT '药材名称',
`latin_name` varchar(100) DEFAULT NULL COMMENT '拉丁学名',
`category_id` int(11) NOT NULL COMMENT '分类ID',
`origin_place` varchar(100) DEFAULT NULL COMMENT '道地产区',
`spec_unit` enum('kg','g','qian') NOT NULL DEFAULT 'kg' COMMENT '规格单位',
`storage_condition` enum('阴凉','冷藏','常温') NOT NULL COMMENT '存储条件',
`shelf_life` int(11) DEFAULT NULL COMMENT '保质期(月)',
PRIMARY KEY (`herb_id`),
UNIQUE KEY `idx_name` (`herb_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
- 批次库存表(batch_stock)
这是系统的核心表,实现了药材特有的批次管理:
sql复制CREATE TABLE `batch_stock` (
`batch_id` varchar(30) NOT NULL COMMENT '批次号',
`herb_id` varchar(20) NOT NULL COMMENT '药材ID',
`purchase_id` varchar(20) NOT NULL COMMENT '采购单号',
`produce_date` date NOT NULL COMMENT '生产日期',
`expire_date` date NOT NULL COMMENT '失效日期',
`current_weight` decimal(12,3) NOT NULL COMMENT '当前重量',
`original_weight` decimal(12,3) NOT NULL COMMENT '原始重量',
`warehouse_id` int(11) NOT NULL COMMENT '库位ID',
`quality_level` enum('特级','一级','二级','等外') NOT NULL COMMENT '质量等级',
`status` enum('在库','锁定','已出库','报废') NOT NULL DEFAULT '在库',
PRIMARY KEY (`batch_id`),
KEY `idx_herb_expire` (`herb_id`,`expire_date`),
KEY `idx_warehouse` (`warehouse_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
关键设计原则:
- 采用组合索引优化高频查询(如按药材+效期查询)
- 使用DECIMAL(12,3)确保重量计算的精确性
- 通过status字段实现库存状态机控制
3. 核心业务模块实现
3.1 采购入库流程实现
药材采购具有鲜明的行业特点:同一批采购可能包含多种药材,每种药材又可能来自不同供应商。系统采用"采购单→入库单"的两级确认机制:
java复制// 采购单核心处理逻辑
@Transactional
public PurchaseResult submitPurchase(PurchaseDTO dto) {
// 1. 校验供应商资质
Supplier supplier = supplierService.validateSupplier(dto.getSupplierId());
// 2. 生成采购单(主单)
PurchaseOrder master = new PurchaseOrder();
master.setPurchaseNo(IdGenerator.purchaseNo());
master.setSupplierId(supplier.getId());
master.setTotalAmount(BigDecimal.ZERO);
// 3. 处理明细项
List<PurchaseDetail> details = new ArrayList<>();
for (PurchaseItem item : dto.getItems()) {
Herb herb = herbService.getById(item.getHerbId());
PurchaseDetail detail = new PurchaseDetail();
detail.setPurchaseNo(master.getPurchaseNo());
detail.setHerbId(herb.getHerbId());
detail.setUnitPrice(item.getUnitPrice());
detail.setQuantity(item.getQuantity());
detail.setActualQuantity(BigDecimal.ZERO); // 实际入库量初始为0
detail.setQualityLevel(item.getQualityLevel());
// 计算小计并累加总金额
BigDecimal subtotal = item.getUnitPrice().multiply(item.getQuantity());
master.setTotalAmount(master.getTotalAmount().add(subtotal));
details.add(detail);
}
// 4. 持久化
purchaseMapper.insertMaster(master);
purchaseMapper.batchInsertDetails(details);
// 5. 记录审计日志
auditLogService.logPurchaseAction(master.getPurchaseNo(), "CREATE");
return new PurchaseResult(master.getPurchaseNo(), details.size());
}
入库环节的特殊处理:
- 支持分批次入库(如一次采购分多车送达)
- 自动计算药材损耗率
- 效期预警(距失效期≤3个月的自动标记)
3.2 库存管理优化策略
针对中药材易变质、需定期翻晒的特性,系统实现了智能库存管理:
- 库位优化算法
按药材属性和存储要求自动分配库位:
java复制public WarehouseLocation allocateLocation(Herb herb, BigDecimal weight) {
// 获取符合存储条件的可用库位
List<WarehouseLocation> candidates = locationMapper.selectAvailableLocations(
herb.getStorageCondition(),
weight
);
// 优先选择已有同品种的库位(减少品类分散)
candidates.sort((a,b) -> {
int aHasSame = locationMapper.countSameHerb(a.getId(), herb.getHerbId());
int bHasSame = locationMapper.countSameHerb(b.getId(), herb.getHerbId());
return bHasSame - aHasSame;
});
// 次优先选择离出入口近的库位
candidates.sort((a,b) -> {
if(a.getDistanceToDoor() != b.getDistanceToDoor()) {
return a.getDistanceToDoor() - b.getDistanceToDoor();
}
return a.getId() - b.getId();
});
return candidates.get(0);
}
- 效期滚动预警
通过Spring Scheduled实现定期检查:
java复制@Scheduled(cron = "0 0 9 * * ?") // 每天上午9点执行
public void checkExpiration() {
LocalDate warnDate = LocalDate.now().plusMonths(3);
List<BatchStock> expiring = batchStockMapper.selectExpiringSoon(warnDate);
expiring.forEach(batch -> {
String message = String.format("批次%s的%s将在%s过期",
batch.getBatchId(),
herbService.getNameById(batch.getHerbId()),
batch.getExpireDate());
notificationService.sendToWarehouseManager(message);
// 自动降级处理
if(batch.getQualityLevel() != QualityLevel.REJECTED
&& batch.getExpireDate().isBefore(LocalDate.now().plusMonths(1))) {
batchStockMapper.updateQualityLevel(
batch.getBatchId(),
QualityLevel.downgrade(batch.getQualityLevel())
);
}
});
}
4. 系统特色功能实现
4.1 药材追溯体系
为满足GSP认证要求,系统实现了完整的正向追踪和逆向溯源:
- 批次图谱生成
通过Graphviz自动生成批次关系图:
java复制public String generateBatchGraph(String batchId) {
List<BatchRelation> relations = traceMapper.selectBatchRelations(batchId);
StringBuilder dot = new StringBuilder("digraph G {\n");
relations.forEach(rel -> {
dot.append(String.format(" \"%s\" -> \"%s\" [label=\"%s\"];\n",
rel.getFromBatch(),
rel.getToBatch(),
rel.getRelationType()));
});
dot.append("}");
// 调用Graphviz生成图片
return GraphvizEngine.render(dot.toString());
}
- 快速溯源接口
java复制public TraceResult traceBatch(String batchId) {
TraceResult result = new TraceResult();
// 向上追溯原料
result.setMaterials(traceMapper.selectMaterialSources(batchId));
// 向下追踪流向
result.setDestinations(traceMapper.selectProductDestinations(batchId));
// 获取质检报告
result.setQualityReports(qualityMapper.selectByBatch(batchId));
return result;
}
4.2 智能预警系统
结合药材特性实现的预警规则引擎:
- 库存周转预警(当库存量>月均销量的3倍时触发)
- 效期预警(距失效期≤3个月)
- 环境异常预警(当库房温湿度超出设定范围时)
- 价格波动预警(采购价同比变动>15%)
预警规则的配置化实现:
xml复制<!-- 在rules.xml中定义预警规则 -->
<rule id="stock_warning" type="INVENTORY">
<condition>
<![CDATA[
currentStock > (avgMonthlySale * 3)
AND datediff(now(), lastSaleDate) > 30
]]>
</condition>
<action class="com.herb.warning.actions.NotifyAction">
<param name="role" value="purchaser"/>
<param name="template" value="STOCK_OVER"/>
</action>
</rule>
5. 部署与性能优化
5.1 生产环境部署方案
推荐部署架构:
code复制 +-----------------+
| CDN/OSS |
+--------+--------+
|
+------------+ +-------+-------+ +-------------+
| Web前端 +------+ Nginx反向代理 +------+ SpringBoot |
+------------+ +-------+-------+ +------+------+
| |
+-------+-------+ +-------+------+
| Redis缓存 | | MySQL主从 |
+--------------+ +--------------+
关键配置参数:
- Nginx调优:
nginx复制worker_processes auto;
worker_connections 4096;
keepalive_timeout 65;
gzip on;
gzip_min_length 1k;
gzip_comp_level 3;
- JVM参数:
bash复制java -jar -Xms2048m -Xmx2048m -XX:MetaspaceSize=256m \
-XX:MaxMetaspaceSize=512m -XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 -XX:ParallelGCThreads=4 \
herb-system.jar
5.2 性能优化实践
- 缓存策略
采用多级缓存架构:
- 本地缓存(Caffeine):缓存基础数据(如药材信息)
- Redis缓存:分布式会话和热点数据
- MySQL查询缓存:配置关键表的缓存
java复制@Cacheable(value = "herb", key = "#herbId", unless = "#result == null")
public Herb getHerbById(String herbId) {
return herbMapper.selectById(herbId);
}
@CacheEvict(value = "herb", key = "#herb.herbId")
public void updateHerb(Herb herb) {
herbMapper.updateById(herb);
}
- 批量操作优化
针对库存扣减等高并发操作:
java复制@Transactional
public BatchResult batchDeductStock(List<DeductItem> items) {
// 1. 按药材ID分组排序,避免死锁
items.sort(Comparator.comparing(DeductItem::getHerbId));
// 2. 批量查询库存
List<String> batchIds = items.stream()
.map(DeductItem::getBatchId)
.collect(Collectors.toList());
Map<String, BatchStock> stockMap = batchStockMapper
.selectBatchByIds(batchIds)
.stream()
.collect(Collectors.toMap(BatchStock::getBatchId, Function.identity()));
// 3. 校验并扣减
List<StockLog> logs = new ArrayList<>();
for (DeductItem item : items) {
BatchStock stock = stockMap.get(item.getBatchId());
if (stock.getCurrentWeight().compareTo(item.getWeight()) < 0) {
throw new InsufficientStockException(stock.getBatchId());
}
stock.setCurrentWeight(stock.getCurrentWeight().subtract(item.getWeight()));
logs.add(new StockLog(stock.getBatchId(), "DEDUCT", item.getWeight()));
}
// 4. 批量更新
batchStockMapper.batchUpdate(stockMap.values());
stockLogMapper.batchInsert(logs);
return new BatchResult(items.size());
}
6. 开发经验与避坑指南
6.1 药材行业特殊问题处理
- 单位换算难题
传统单位"钱"与公制的换算存在地区差异,系统采用可配置的换算策略:
java复制public BigDecimal convertUnit(BigDecimal value, String fromUnit, String toUnit) {
if (fromUnit.equals(toUnit)) return value;
// 获取换算系数(从数据库配置表读取)
UnitConversion conv = conversionMapper.selectByUnits(fromUnit, toUnit);
if (conv == null) {
throw new UnitConversionException(fromUnit, toUnit);
}
return value.multiply(conv.getFactor())
.setScale(3, RoundingMode.HALF_UP);
}
- 药材同名异物品处理
建立药材唯一标识体系:
- 核心字段:名称 + 拉丁学名 + 道地产区
- 使用OpenCV实现药材图像特征提取比对
- 对接国家药典委API验证基础数据
6.2 性能优化经验
- 库存扣减的并发控制
采用乐观锁+重试机制:
java复制@Retryable(value = OptimisticLockingFailureException.class, maxAttempts = 3)
public void deductStockWithRetry(String batchId, BigDecimal weight) {
BatchStock stock = batchStockMapper.selectForUpdate(batchId);
if (stock.getCurrentWeight().compareTo(weight) < 0) {
throw new InsufficientStockException(batchId);
}
stock.setCurrentWeight(stock.getCurrentWeight().subtract(weight));
stock.setVersion(stock.getVersion() + 1);
int affected = batchStockMapper.updateWithVersion(stock);
if (affected == 0) {
throw new OptimisticLockingFailureException("并发修改冲突");
}
}
- 报表查询优化
使用物化视图处理复杂统计:
sql复制CREATE MATERIALIZED VIEW mv_monthly_sale
REFRESH COMPLETE ON DEMAND
AS
SELECT
herb_id,
YEAR(sale_time) AS year,
MONTH(sale_time) AS month,
SUM(quantity) AS total_quantity,
SUM(amount) AS total_amount
FROM sale_detail
GROUP BY herb_id, YEAR(sale_time), MONTH(sale_time);
6.3 系统扩展建议
- 移动端扩展
- 开发微信小程序实现扫码入库/出库
- 利用GPS定位实现道地药材溯源
- 集成OCR识别药材检验报告
- AI能力增强
- 使用TensorFlow实现药材图像识别
- 基于历史数据预测价格波动
- 智能采购建议引擎
- 生态对接
- 对接医保平台实现电子医保结算
- 接入物流系统实现全程温控监控
- 与税务平台直连实现电子发票
这套系统在实际部署后,客户反馈库存周转率提升了40%,报损率降低了25%。特别是在疫情期间,远程协同管理功能发挥了关键作用。对于希望自主开发类似系统的团队,建议先从核心的进销存流程入手,再逐步扩展高级功能。药材管理系统的特殊之处在于需要深入理解行业特性,这往往比技术实现更具挑战性。