下午茶拼单系统—1. 规则树驱动的试算链路
前言
拼单系统最核心的入口之一是商品试算。用户还没有真正下单,只是在页面上看一杯奶茶能不能参团、优惠后多少钱、是否命中人群限制。这个接口一旦写成一串 if else,后面加活动开关、渠道限制、人群标签、库存校验时会越来越难维护。
所以我把试算链路设计成规则树。每个节点只处理一个判断或计算,然后把结果写入动态上下文,再由节点决定下一步走向。这个设计和责任链类似,但规则树更强调分支:成功继续、失败进入错误节点、命中标签后进入结束节点。
为什么用规则树
营销规则的特点是变化频繁,并且经常有组合关系。比如活动必须处于有效期内,商品要绑定活动,用户要命中标签,优惠策略要存在,最后才能返回试算结果。如果写在一个方法里,很难知道某个规则失败后应该返回什么,也很难给不同活动插拔节点。
规则树把流程拆成 RootNode、SwitchNode、MarketNode、TagNode、EndNode、ErrorNode。RootNode 做入口校验,SwitchNode 判断活动开关,MarketNode 加载活动和商品并计算优惠,TagNode 判断可见性和参与资格,EndNode 组装结果,ErrorNode 兜底异常返回。
@Service
public class DefaultActivityStrategyFactory {
private final RootNode rootNode;
public DefaultActivityStrategyFactory(RootNode rootNode) {
this.rootNode = rootNode;
}
public StrategyHandler<MarketProductEntity, DynamicContext, TrialBalanceEntity> strategyHandler() {
return rootNode;
}
}
工厂只暴露根节点,外部调用方不需要知道树里有几个节点。以后要新增一个库存节点,只需要改树的路由关系,而不是把调用方一起拖进来。
动态上下文
规则树里最重要的是 DynamicContext。它不是随便放数据的 Map,而是一次试算过程的业务快照。活动配置、商品信息、优惠后价格、可见性、可参与状态都在这里流转。这样节点之间不需要互相调用,数据通过上下文传递。
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class DynamicContext {
private GroupBuyActivityDiscountVO groupBuyActivityDiscountVO;
private SkuVO skuVO;
private BigDecimal deductionPrice;
private boolean visible;
private boolean enable;
}
这个上下文也方便做日志。线上排查试算异常时,我不只看入口参数,还会看每个节点写入了什么字段。比如 groupBuyActivityDiscountVO 为空,说明活动配置没有查到;deductionPrice 为空,说明优惠策略没有成功计算;visible 为 false,说明人群或活动限制没有通过。
节点如何流转
MarketNode 是试算链路里最重的节点,它负责加载活动配置和 SKU,并调用不同优惠策略。这里有两个关键设计:第一,活动和商品并行查询,避免串行等待;第二,优惠策略通过 Spring Map 注入,避免硬编码策略选择。
IDiscountCalculateService discountCalculateService =
discountCalculateServiceMap.get(groupBuyDiscount.getMarketPlan());
if (null == discountCalculateService) {
throw new AppException(ResponseCode.E0001.getCode(), ResponseCode.E0001.getInfo());
}
BigDecimal deductionPrice = discountCalculateService.calculate(
requestParameter.getUserId(),
skuVO.getOriginalPrice(),
groupBuyDiscount);
dynamicContext.setDeductionPrice(deductionPrice);
return router(requestParameter, dynamicContext);
这里的 marketPlan 可以是 MJ、ZK、N、ZJ 等策略标识。策略 Bean 使用 @Service("MJ") 这样的名称注册,Map 的 key 就自然成为策略编码。这样新增一种优惠,只需要新增一个实现类并约定编码。
异常节点和收口
规则树一定要有失败出口。活动不存在、商品不存在、策略不存在、用户不在可见范围内,都不能让接口返回空指针。ErrorNode 的意义是把不可试算的情况变成可解释结果,EndNode 则把正常结果组装成前端可使用的 TrialBalanceEntity。
我喜欢把错误节点看成业务兜底,而不是异常吞掉。比如活动没配置,前端可以展示原价和不可参团;用户没命中标签,可以展示普通价;系统异常才返回错误码。营销系统不能因为一个活动配错就拖垮整个商品页。
小结
规则树的价值不是把代码写复杂,而是让复杂规则有地方放。试算链路天然会越来越长,如果一开始就把节点、上下文、路由和异常收口设计好,后面加 DCC、标签、库存和渠道限制都会更从容。





