大宗商品销采运储系统・六层架构连载(业务服务层)
上一篇我们通过状态机,解决了单据流转的合规性问题。单据跑到哪一步,红绿灯说了算。
但大宗商品交易中,还有一个比状态更敏感、变化更频繁的要素:价格。
与普通电商“商品标价 100 元,直接下单”不同,大宗商品的定价极度复杂:基准价(期货/指数) + 运输方式+ 地区运费 + 质量加减价(如水分超标扣款) - 付款方式折扣(如现款优惠)。不仅如此,不同客户(大客户/散户)、不同仓库、不同采购量级,计算公式全都不一样。
如果把定价逻辑写死在 Java 代码里,结局只有一个:业务规则每变一次,研发就要改代码、走测试、重新发版。业务跑得越快,研发越像个“无情的发版机器”。
一句话定调:
业务流程靠状态机管,业务规则靠规则引擎管。让懂业务的人去配规则,让研发回归架构本身。
一、痛点:为什么大宗商品的定价不能写死代码?
我们先看一个大宗商品结算价计算公式示例:
最终结算价 = (期货主力合约结算价 + 品牌 - 运费) × (1 - 水分扣杂率) × 付款方式系数
如果用代码写:
public BigDecimal calculatePrice(OrderDTO order) { BigDecimal basePrice = getFuturesPrice(order.getDate()); BigDecimal premium = 0; if ("X钢".equals(order.getBrand())) { premium = new BigDecimal("50"); } else if ("Y钢".equals(order.getBrand())) { premium = new BigDecimal("30"); } BigDecimal freight = 0; if (order.getWarehouse().equals("Z海")) { freight = new BigDecimal("100"); } else if (...) if ("现款".equals(order.getPayType())) { // 现款打折 } // ... 几百行 if-else 嵌套 }痛点爆发:
频繁变更:逢年过节促销,或者上游钢厂调整了升贴水,这段代码就要改,测试完再上线。
规则冲突:销售部说必须打 9 折,财务部说大客户打折后不能低于成本线。代码里的
if往往顾此失彼。无法追溯:半年前那张单子为什么算出来是 3800 块?代码早改了,根本无法复盘当时的计算逻辑。
二、技术选型:为什么选择轻量级规则引擎?
为了解决这种“多条件、易变化”的判定逻辑,我们需要把“规则”从“代码”里抽离出来。业界有几种方案:
| 方案 | 代表技术 | 优点 | 缺点 | 项目适配 |
|---|---|---|---|---|
| 重量级规则引擎 | Drools | 功能极强,支持复杂 RETE 算法 | 学习成本极高,太重,性能有损耗 | ❌ 杀鸡用牛刀 |
| 轻量级表达式引擎 | AviatorScript | 性能极高(编译为字节码),语法简单 | 逻辑编排能力较弱 | ✅ 适合简单的公式计算 |
| 动态脚本引擎 | QLExpress (阿里开源) | 轻量、灵活,支持宏定义和自定义操作符 | 复杂脚本有安全风险 | ✅本项目首选,适合计价与校验 |
本项目选型结论:
引入QLExpress或Aviator作为动态定价引擎,并将规则脚本存放在配置中心(Nacos)中,实现热更新。
三、实战落地:基于规则引擎的定价中心
我们把计价能力抽成一个独立的通用服务(或业务域内的PricingEngine)。
1. 定义定价上下文(Context)
传给引擎的数据必须标准化,包含计算所需的全部要素。
@Data public class PricingContext { private BigDecimal basePrice; // 基准价 private String brand; // 品牌 private String warehouse; // 仓库 private BigDecimal weight; // 重量(吨) private String payType; // 付款方式 private Integer customerLevel; // 客户等级 }2. 规则脚本示例(存在配置中心)
业务人员或实施人员可以在后台配置这段脚本,无需写 Java。
// 计算品牌升水 if (brand == "X钢") { premium = 50.0; } else if (brand == "Y钢") { premium = 30.0; } else { premium = 0.0; } // 地区运费 if (warehouse == "Z海") { freight = 100.0; } else if (warehouse == "M安") { freight = 150.0; } // 客户等级折扣(V1 大客户 98折,现款再减 10块) priceBeforePay = (basePrice + premium - freight); if (customerLevel == 1) { priceBeforePay = priceBeforePay * 0.98; } if (payType == "现款") { finalPrice = priceBeforePay - 10.0; } else { finalPrice = priceBeforePay; } return finalPrice;3. 核心代码:用 QLExpress 执行脚本
@Service @Slf4j public class PricingEngineService { @Autowired private NacosConfigManager configManager; // 动态拉取规则 public BigDecimal calculate(PricingContext context) { // 1. 从配置中心获取当前生效的计价脚本 (支持按业务线/租户隔离) String script = configManager.getConfig("pricing-rule-steel.js"); if (StringUtils.isBlank(script)) { throw new BusinessException("未找到计价规则"); } // 2. 初始化 QLExpress 引擎 ExpressRunner runner = new ExpressRunner(); // 3. 准备执行参数 DefaultContext<String, Object> expressContext = new DefaultContext<>(); expressContext.put("basePrice", context.getBasePrice()); expressContext.put("brand", context.getBrand()); expressContext.put("warehouse", context.getWarehouse()); expressContext.put("customerLevel", context.getCustomerLevel()); expressContext.put("payType", context.getPayType()); try { // 4. 执行脚本 Object result = runner.execute(script, expressContext, null, true, false); return new BigDecimal(result.toString()); } catch (Exception e) { log.error("动态计价执行失败, 上下文: {}", context, e); // 兜底策略:降级到系统默认的最保守价格,或抛出异常 throw new BusinessException("价格计算异常,请联系管理员"); } } }四、架构融合:规则引擎与公共中间件的联动
规则引擎并不孤立,它深度依赖我们前面搭建的中间件底座:
结合配置中心(Nacos):把上述的脚本
pricing-rule-steel.js直接放在 Nacos 里。当价格机制改变时,业务人员在后台改完脚本,点击“发布”。Nacos 自动推送到各个微服务节点,无需重启服务。结合 Redis 缓存:计价引擎极其高频,不可能每次开单都去查一次配置中心和编译脚本。可以在应用启动时把脚本编译为
Runner缓存到 JVM 内存(本地缓存),或者针对计算结果做短时间的 Redis 缓存(如:同一个客户在 1 分钟内查询同一种商品的价格,直接走 Redis)。结合日志与可观测:每一次计算,必须把
PricingContext和最终结果存到数据库或日志系统。大宗商品客户来扯皮“为什么比昨天贵了10块”,你可以立刻把当时的计算要素和命中的规则调出来自证清白。
五、生产级避坑指南
死循环与恶意脚本:
如果业务人员在脚本里写了while(true),会直接把服务 CPU 打满。必须使用 QLExpress 提供的超时控制机制(设置execute方法的超时时间参数)。规则冲突(多头配置):
同一个商品如果在不同规则里都被定义,系统必须明确优先级(如:专属客户规则 > 区域规则 > 全局默认规则),这就需要在业务层设计好“规则加载顺序”。金额精度丢失:
脚本引擎底层可能用Double算,涉及到钱,一定要在脚本或返回结果后,统一用BigDecimal处理,并指定四舍五入模式(如保留两位小数)。
六、本篇小结
把复杂多变的定价逻辑抽离出来,不仅救了研发的头发,更赋予了业务极大的灵活性。
记住三句话:
流程与规则分离:状态机管流转,规则引擎管判定。
规则动态下发:利用配置中心实现热更新,告别发版。
安全底线:超时控制、金额精度、执行审计,缺一不可。
下篇预告:采购业务(上):长协合同与分批到货,大宗供应链的源头设计。