YAOTU INSIGHTS

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

微服务架构落地:业务域基建(下) —— 如动态定价与规则引擎,告别“改个价格发个版”
大宗商品销采运储系统・六层架构连载业务服务层上一篇我们通过状态机解决了单据流转的合规性问题。单据跑到哪一步红绿灯说了算。但大宗商品交易中还有一个比状态更敏感、变化更频繁的要素价格。与普通电商“商品标价 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. 准备执行参数 DefaultContextString, 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处理并指定四舍五入模式如保留两位小数。六、本篇小结把复杂多变的定价逻辑抽离出来不仅救了研发的头发更赋予了业务极大的灵活性。记住三句话流程与规则分离状态机管流转规则引擎管判定。规则动态下发利用配置中心实现热更新告别发版。安全底线超时控制、金额精度、执行审计缺一不可。下篇预告采购业务上长协合同与分批到货大宗供应链的源头设计。