大宗商品销采运储系统・六层架构连载(业务服务层)
上一篇我们完成了长协合同与执行子单的拆分,供应商的货终于送到了场站。
如果是普通电商,货车到了直接扫码入库。但在大宗商品(煤炭、矿石、粮食等)领域,货车到了只是第一步。
大宗商品的痛点是:账面重量 ≠ 实际物理重量。
供应商发货单上写的是 100 吨,但货车过磅可能只有 98 吨;更严重的是,经过化验,发现这批货水分超标 2%。这就意味着,你花钱买的一部分是水。
这就是大宗商品极其核心的“按质论价、过磅扣杂”环节。如果这一步系统算错了,企业会直接面临几十上百万的真金白银损失。
一句话定调:
大宗采购入库,不是简单的数量加法,而是一次由“硬件采集 + 质检化验 + 规则计算”共同完成的物理账校准。
一、业务痛点:为什么大宗入库极其复杂?
-
过磅防作弊难:大货车司机可能会“压磅”、冲磅,甚至两辆车同时上磅,导致地磅数据虚高。
-
质检盲检要求高:为了避免化验员和供应商勾结,化验必须是“盲检”(化验员不知道这车货是谁的)。
-
扣杂公式极度复杂:水分超标扣重、硫分超标罚款、灰分超标拒收。这些规则随时在变。
二、过磅防作弊:IoT 硬件联动
过磅不是人工敲键盘录入(人工录入存在不可信),必须引入物联网硬件。
防作弊链路:
-
车辆上磅,红外对射仪感应车辆完全在磅上(防止压磅)。
-
车牌识别摄像头抓拍车牌,与系统预约提货单比对。
-
地磅仪表通过串口/网口实时读取重量。
-
如果重量稳定 3 秒,且红外无遮挡,系统自动锁定重量,抓拍车厢照片留存证据。
这套硬件交互通常封装在履约域(场站服务),采集完成后,生成一条“过磅流水记录”,通过 MQ 发给采购域和质检域。

三、质检化验与“扣重防损”计算(结合规则引擎)
货过完磅了,化验室拿走了样品。化验结果出来后,真正的计算才开始。
1. 复用规则引擎算“折标重量”
在前面篇章中,我们用规则引擎算价格。在这里,我们同样用规则引擎来算扣重。
不同品种(如焦煤、铁矿石)的扣杂公式完全不同,用规则引擎动态配置,极其灵活。
质检扣杂脚本示例(假设结果)(存在配置中心):
// 假设化验结果为:化验水分 water,化验灰分 ash// 合同标准水分 standardWater = 8%,超标 1% 扣重 1.5%if (water > standardWater) {// 计算水分超标比例overRate = water - standardWater;// 计算扣重百分比deductRate = overRate * 1.5;} else {deductRate = 0;}// 如果硫分严重超标,直接拒收或重罚if (sulfur > 1.5) {deductRate = deductRate + 5.0; // 直接多扣5%的重量}// 最终折标重量 = 毛重 * (1 - 扣重百分比)finalWeight = grossWeight * (1 - deductRate / 100.0);return finalWeight;
2. 盲检机制设计
化验单里绝对不包含供应商 ID。系统只接收“过磅流水号”和“化验指标”。
只有等化验单审核通过,系统才通过内部映射表,把化验结果和供应商对应起来,防止人为篡改。

四、状态机驱动:生成最终的采购入库单
当折标重量算出来后,终于可以入库了。这里必须由状态机(前面内容)来严格管控。
状态流转:
-
待过磅:车辆入场。 -
已过磅待化验:过磅完成,锁定毛重、皮重。 -
化验完成待入库:扣杂计算完成,有了净重。 -
已入库:库存域真正增加了可用库存。
核心落库伪代码
@Service
public class PurchaseInboundService {
@Autowired
private StateMachine<InboundState, InboundEvent, InboundContext> inboundStateMachine;
@Autowired
private RuleEngineService ruleEngineService;
// 处理质检完成事件,计算重量并流转状态
@Transactional
public void processQualityInspection(String recordNo, QualityResult qualityResult) {
// 1. 获取过磅记录
ArrivalRecord record = arrivalRecordMapper.selectByNo(recordNo);
if (record.getStatus() != InboundState.PENDING_INSPECTION) {
throw new BusinessException("当前单据状态不允许质检入库");
}
// 2. 组装规则引擎上下文
DeductContext ctx = new DeductContext();
ctx.setGrossWeight(record.getGrossWeight());
ctx.setWater(qualityResult.getWater());
ctx.setSulfur(qualityResult.getSulfur());
// 3. 调用规则引擎,计算最终折标净重
BigDecimal finalNetWeight = ruleEngineService.calculateDeduct(ctx);
// 4. 触发状态机流转(化验完成 -> 待入库)
InboundContext stateContext = new InboundContext();
stateContext.setRecordNo(recordNo);
stateContext.setNetWeight(finalNetWeight);
InboundState nextState = inboundStateMachine.fireEvent(
record.getStatus(), InboundEvent.INSPECTION_PASSED, stateContext);
// 5. 更新本地记录
record.setNetWeight(finalNetWeight);
record.setStatus(nextState);
record.setQualityResult(JSON.toJSONString(qualityResult));
arrivalRecordMapper.updateById(record);
// 6. 发送领域事件,通知库存域增加库存
rocketMQTemplate.convertAndSend("topic_inventory_inbound",
new InboundEventMsg(recordNo, record.getGoodsCode(), finalNetWeight));
}
}
五、跨域联动:库存域如何安全地“加库存”?
采购域发出了 MQ 消息,库存域(前面划分的核心域)接收到消息后,必须完成实际的库存落库。
库存域落库的防重与并发控制:
-
幂等控制:监听 MQ 时,根据
recordNo(过磅流水号)校验,防止 MQ 重发导致库存多加。 -
合并库存:如果该仓库下已经有相同批次/品牌的库存,直接执行
UPDATE stock SET qty = qty + ? WHERE ...。如果是新批次,则INSERT。 -
库存流水:增加多少库存,必须生成一条对应的“库存流入流水”,供后续对账和审计使用。
@Service
public class PurchaseInboundService {
@Autowired
private StateMachine<InboundState, InboundEvent, InboundContext> inboundStateMachine;
@Autowired
private RuleEngineService ruleEngineService;
// 处理质检完成事件,计算重量并流转状态
@Transactional
public void processQualityInspection(String recordNo, QualityResult qualityResult) {
// 1. 获取过磅记录
ArrivalRecord record = arrivalRecordMapper.selectByNo(recordNo);
if (record.getStatus() != InboundState.PENDING_INSPECTION) {
throw new BusinessException("当前单据状态不允许质检入库");
}
// 2. 组装规则引擎上下文
DeductContext ctx = new DeductContext();
ctx.setGrossWeight(record.getGrossWeight());
ctx.setWater(qualityResult.getWater());
ctx.setSulfur(qualityResult.getSulfur());
// 3. 调用规则引擎,计算最终折标净重
BigDecimal finalNetWeight = ruleEngineService.calculateDeduct(ctx);
// 4. 触发状态机流转(化验完成 -> 待入库)
InboundContext stateContext = new InboundContext();
stateContext.setRecordNo(recordNo);
stateContext.setNetWeight(finalNetWeight);
InboundState nextState = inboundStateMachine.fireEvent(
record.getStatus(), InboundEvent.INSPECTION_PASSED, stateContext);
// 5. 更新本地记录
record.setNetWeight(finalNetWeight);
record.setStatus(nextState);
record.setQualityResult(JSON.toJSONString(qualityResult));
arrivalRecordMapper.updateById(record);
// 6. 发送领域事件,通知库存域增加库存
rocketMQTemplate.convertAndSend("topic_inventory_inbound",
new InboundEventMsg(recordNo, record.getGoodsCode(), finalNetWeight));
}
}
六、本篇小结
大宗采购的入库,是一场从物理世界(硬件)到数字世界(规则计算)的精准映射。
记住三句话:
-
过磅必须联动硬件防作弊,红线对射加车牌识别是标配。
-
质检盲检保公平,扣杂计算靠规则引擎动态配置。
-
入库动作由状态机严格把控,通过 MQ 解耦库存域的最终落库。
下篇预告:销售业务(上):现货挂牌与高并发抢单,Redis 防超卖的终极实战。
481



被折叠的 条评论
为什么被折叠?



