ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

微服务架构落地:业务域基建(下) —— 如动态定价与规则引擎,告别“改个价格发个版”

微服务架构落地:业务域基建(下) —— 如动态定价与规则引擎,告别“改个价格发个版”

大宗商品销采运储系统・六层架构连载(业务服务层)

上一篇我们通过状态机,解决了单据流转的合规性问题。单据跑到哪一步,红绿灯说了算。

但大宗商品交易中,还有一个比状态更敏感、变化更频繁的要素:价格。

与普通电商“商品标价 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 嵌套 }

痛点爆发:

  1. 频繁变更:逢年过节促销,或者上游钢厂调整了升贴水,这段代码就要改,测试完再上线。

  2. 规则冲突:销售部说必须打 9 折,财务部说大客户打折后不能低于成本线。代码里的if往往顾此失彼。

  3. 无法追溯:半年前那张单子为什么算出来是 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("价格计算异常,请联系管理员"); } } }

四、架构融合:规则引擎与公共中间件的联动

规则引擎并不孤立,它深度依赖我们前面搭建的中间件底座:

  1. 结合配置中心(Nacos):把上述的脚本pricing-rule-steel.js直接放在 Nacos 里。当价格机制改变时,业务人员在后台改完脚本,点击“发布”。Nacos 自动推送到各个微服务节点,无需重启服务。

  2. 结合 Redis 缓存:计价引擎极其高频,不可能每次开单都去查一次配置中心和编译脚本。可以在应用启动时把脚本编译为Runner缓存到 JVM 内存(本地缓存),或者针对计算结果做短时间的 Redis 缓存(如:同一个客户在 1 分钟内查询同一种商品的价格,直接走 Redis)。

  3. 结合日志与可观测:每一次计算,必须把PricingContext和最终结果存到数据库或日志系统。大宗商品客户来扯皮“为什么比昨天贵了10块”,你可以立刻把当时的计算要素和命中的规则调出来自证清白。


五、生产级避坑指南

  1. 死循环与恶意脚本:
    如果业务人员在脚本里写了while(true),会直接把服务 CPU 打满。必须使用 QLExpress 提供的超时控制机制(设置execute方法的超时时间参数)。

  2. 规则冲突(多头配置):
    同一个商品如果在不同规则里都被定义,系统必须明确优先级(如:专属客户规则 > 区域规则 > 全局默认规则),这就需要在业务层设计好“规则加载顺序”。

  3. 金额精度丢失:
    脚本引擎底层可能用Double算,涉及到钱,一定要在脚本或返回结果后,统一用BigDecimal处理,并指定四舍五入模式(如保留两位小数)。


六、本篇小结

把复杂多变的定价逻辑抽离出来,不仅救了研发的头发,更赋予了业务极大的灵活性。

记住三句话:

  1. 流程与规则分离:状态机管流转,规则引擎管判定。

  2. 规则动态下发:利用配置中心实现热更新,告别发版。

  3. 安全底线:超时控制、金额精度、执行审计,缺一不可。

下篇预告:采购业务(上):长协合同与分批到货,大宗供应链的源头设计。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进