李浩
微盟 到店业务组架构师
现在的电商系统中,优惠活动种类繁多,比如会员价、满减满折、现金券、折扣券、第二件半价或者SKU级别的优惠等等。还涉及会员权益的抵扣、积分、会员余额等等。优惠项目两两之间可能还有不能同享关系。例如使用了满减就不能享受使用折扣券,使用了余额就不能再用现金券等。
如何设计和实现一套优雅的可以得到默认最优支付方案的代码,是所有电商服务端研发人员都会面临的挑战。
设计目标
活动及优惠类型的可添加扩展
在众多的支付方案中,选出实付最少,优惠最多的
运行效率高
复用性高
功能稳定
概要设计
优惠类目有固定的顺序,可以通过系统配置和用户现有权益,生成此次批价的所有可行支付方案。
同享关系对可行支付路径的影响。
用户指定必须使用的优惠节点和不能使用的优惠节点,可以在生成支付路径后通过用户指定的信息,对可行路径进行过滤。即可行路径中必须包含用户指定的节点,并去掉含有用户关闭节点的路径。
先介绍一下Reduce Node、Iteration Node、Enum Node、Apply First Node和Preferential Node这五个节点。
Reduce Node
其所有子结点返回一个或者多个支付方案,结点本身对上级返回最优支付方案。上级传入的支付方案,分别传入每一个子结点。传入子结点的方案是上级传入方案的深度克隆。
Iteration Node
每个IterationNode代表一个可支付路径,把上级给入的支付方案,串行传入每一个子结点。就是把第一个子结点返回的支付方案传入第二个子结点。
Enum Node
子结点的处理及支付方案的传递和Reduce Node类似,不同的是其不对支付方案进行选择,因为支付方案的处理并未完成,其返回的支付方案的集合,会进入后续结点,继续处理。
Apply First Node
此结点也是把传入的方案,依次传递给子结点运行。每次子结点返回方案后,检测方案的实付是否与传入方案实付有变动,如果有变动,立即向上返回改方案,不再用后续子结点对方案进行处理。
Preferential Node
PreferentialNode 为批价树的叶子结点,其他四个结点,均不可为叶子结点。
其主要功能有:
根据结点上配置对输入的sku集合进行分组。例如传入了10个不同的sku,其中2个sku可以参与一个满减活动,则将这2个sku进行包装,与另外8个sku组成一个不同于传入方案的新支付方案。
判断优惠触发条件。有部分优惠有触发条件,比如,10元抵扣券,满100元可用。能否适用当前结点的优惠 由其所持有的一个条件树结合输入方案中的信息和context中的信息 返回boolean值给出。上图中的and,or,及其子结点,均为条件树结点。条件树有无比优良的复用和扩展特性,其深入描述在之后的文章中会和大家详细解释。
在支付方案中产生优惠。此结点上的优惠适用传入的方案后,便会调用结点上持有的PreferentialGenerator与适用的sku或者sku分组一起,生成一个Preferential对象(包含优惠的金额和优惠信息),此对象与适用的单个sku或者sku分组直接关联。
有了以上的不同功能结点,我们可以根据需要随意组合此类结点来构建批价树,在根结点上调用一个方法,同时传入输入sku集合,得到最优的可行支付方案。
在可行路径上每一个独立的优惠项目(Iteration Node的直接子结点),开启缓存,其完成计算后的结果会缓存到当前的context中,路径更远或者有分叉的其他方案,可以复用中间结果,直接使用中间结果,往后传递,避免从头算起。例如:未优惠的支付方案经过A结点后,一定得到一个固定的优惠方案ResultA,其他的可行方案AB和AC,可以分别把ResultA传入B和C得到。
支付方案结构
3个PreferentialNode成功对一个批价方案处理前后对比。
PricingContext 引擎输入参数
PricingSolution 引擎返回结果
PreferentialGenerator 三个子类分别是:打折、每满减(如: 每满50元减5元)、立减,而且可以扩展满送、包邮等优惠
树的构建步骤
1. 读取请求批价策略。根据批价策略构建可行路径集合,为每条可行优惠路径生成一个Iteration Node, 所有的Iteration Node 作为子节点添加到根节点 Reduce Node 下面。
2. 读取商户的活动和请求用户的券,积分和余额. 每一种创建一个Preferential Node 对象. 根据各个活动的参与条件, 构建一颗条件树. 设置到Preferential Node上, 同时也将价格变动规则, 生成PreferentialGenerator 设置到Prefertntial Node上. 然后将Preferential Node 作为子节点, 关联到各个Iteration Node 下面。
部分代码示例
条件树的使用与优惠的生成
private PreferentialPricingItem applyPreferential(PreferentialPricingItem pricingItemSubSet) {
if(getConditionFilter() == null || getConditionFilter().filterWithProcessInfo(pricingItemSubSet)) {
//条件树存在, 并且条件满足, 根据当前实际支付金额,生成优惠对象
Preferential currentPreferential = this.preferentialGenerator.genPreferrntial(pricingItemSubSet.getActualPay());
if (currentPreferential != null) {
currentPreferential.setPreferentialGenerator(this.getPreferentialGenerator());
pricingItemSubSet.setActualPay(pricingItemSubSet.getActualPay().subtract(currentPreferential.getPriceCut()));
if (CollectionUtils.isEmpty(pricingItemSubSet.getPreferentialList())) {
pricingItemSubSet.setPreferentialList(new ArrayList<Preferential>());
}
//添加优惠对象到当前优惠集合 pricingItemSubSet.getPreferentialList().add(currentPreferential);
}
}
return pricingItemSubSet;
}
注:上述代码可左右滑动进行查看
上述批价引擎可以根据需要构建出一棵树,对根节点做一次调用,即可得到最优的支付方案,方案选择节点和金额计算节点可以根据需要扩展 ,来满足未来的变化,各节点功能单一明确,稳定性和复用性好,维护起来简单,完成了我们既定的设计目标。
实际效果
实际迭代过程中单sku项目优惠,子商品集合优惠等后续需求,都能在可控的代码添加和改动下,得到支持。目前改服务线上性能稳定,支持多种产品,多种场景下的订单批价,未出现过事故,接口平均耗时在30ms以内。
End
希望今天的文章能对正在阅读的你有所启发,也欢迎您在留言区留言和我们交流!