背景介绍
在电商系统中,促销模块主要架构特点在于促销活动的多种多样,比如常见的优惠券、满减满送,秒杀拼团等等等,另外一个特点是促销活动的多变性,促销活动要随着运营的需要而及时演化。
基于以上特点,一个优秀的电商系统的促销模块他的架构应该做到两点:
- 促销模块和购物车订单的解耦
- 良好的扩展性
本文以Javashop电商系统中的架构为例,详细讲解如何基于脚本引擎实现一个具有松散的、良好扩展性的促销模块。
难点分析
我们先看一下如果不是基于脚本引擎的促销模块,和其他模块的耦合实例,以便我们理解应用脚本引擎后的好处。
1、领域模型的耦合
拿购物车列表来举例,为了计算、显示购物车中的促销情况,伪代码如下:
//每个店铺一个购物车,循环处理
for (Cart cart : itemList) {
List<Product> productList = cart.getProductList();
for (Sku goods : productList) {
//计算购物车商品的促销活动
countSkuPormotion(goods);
}
//计算每个购物车的优惠(如店铺级别的满减满赠等)
countCartPromotion(cart)
}
那么购物车和商品必然要和促销的领域模型耦合(如满减满赠、优惠卷等实体类):
购物车的伪代码:
public class Cart {
//店铺id
private int seller_id;
//购物车中的sku列表
private List<Sku> skuList;
//购物车中使用的优惠券
private List<Coupon> couponList;
//赠品列表
private List<FullDiscountGift> giftList;
//赠送积分
private int giftPoint;
//其他略。。一大堆的促销模块的实体类。。。。
}
同样的,购物车中的sku也要和促销领域模型进行耦合
public class Sku {
//店铺id
private int sku_id;
//商品sku名称
private String sku_name
//购物车中使用的优惠券
private List<Coupon> couponList;
//赠品列表
private List<FullDiscountGift> giftList;
//赠送积分
private int giftPoint;
//其他略。。一大堆的促销模块的实体类。。。。
}
2、促销逻辑的耦合
促销的计算必然要调用到促销模块的业务类,比如计算购物车的促销情况:
void countCartPromotion(Cart cart){
//调用优惠券业务类,计算购物车的优惠券情况
couponService.count(cart);
//调用赠品业务类,计算购物车的赠品情况
giftService.count(cart);
//其他略。。。一大堆的各种促销业务类的调用
}
其他模块也是如此
综合上述的耦合情况,如果促销领域模型发生变化,或促销业务类发生变化,其他模块如购物车、订单都需要跟随这些变化而变化,这对于开发者来讲是一个不小的灾难,促销模块和其他模块不是同一个人负责开发,开发过程中促销模块模型、参数需要频繁的沟通协调,也增加了开发难度,也很不利与促销模块的扩展。
架构思路
Javashop电商系统的架构思路是“面向规则”,用规则将促销模块和其他模块解耦,经过多年的经验,我们总结出促销的规则无非减价格或送东西(赠品、优惠券等)。首先我们建立类似如下的规则:
/**
*促销规则
*/
public class PromotionRule{
//活动的名称、标题,在界面中显示给用户用
private String title;
//促销规则:减价、送东西
private int ruleType;
//要减掉的金额
private double discount;
}
基于上述规则,在架构上促销模块和其他模块隔离开:
在javashop中这个规则的运转就是基于脚本引擎的,在详细深入之前,我们先来熟悉一下Java中的脚本引擎
核心原理
在Java6以后就已经内置了ScriptEngine,可以支持Javascript脚本的解析执行,简单示例如下:
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("javascript");
//执行脚本
String script ="funciton test() { return 'hello'; }";
//设置一些可以使用的变量参数
engine.put("somekey", "somevalue");
//解析执行脚本
engine.eval(script);
//调用test function
Invocable invocable = (Invocable) engine;
Object value = invocable.invokeFunction("test");
我们再看一下Javashop中定义的“满减满送”脚本(满减的门槛是100元),方便大家更好的理解:
//满减的function
function countPrice() {
//判断商品金额是否满足优惠条件
if (100 <= price) {
return price - 5 ;
}
return price;
}
//满送的function
function giveGift() {
// 判断商品金额是否满足优惠条件
if (100 <= price) {
//返回赠品的json
return {giftid:1,name:'一个赠品'};
}
return null;
}
以购物车为例,在Java中调用如下:
//设置价格变量
engine.put("price", cart.getPrice() );
engine.eval(script);
//尝试调起减价function
Double price = (Double)engine.invokeFunction("countPrice");
//尝试调起“送东西” function
String giftJson = (String)engine.invokeFunction("giveGift");
//形成促销规则
PromotionRule rule = new PromotionRule()
rule.setTile("某某活动");
rule.setDiscount(price);
rule.setGiftJson(giftJson);
//接下来其他模块就面向 rule 去处理自己的逻辑了
applyCartRule(cart,rule)
规则的定义和生成
细心的同学可能已经发现,上述脚本中的门槛(100元)还没有“动”起来,这就涉及到脚本的定义和生产了,其实“规则”就是脚本的算法,需要动起来的“变量”是某个活动运营人员定义的,具体的脚本只有在活动生效时才会真正生成出来,流程是这样子的:
脚本的模板可以采用很多种技术,如freemaker、thymeleaf等等,在javashop中我们采用的是freemaker,举例:
function countPrice() {
<#--判断商品金额是否满足优惠条件 -->
if (${promotionActive.fullMoney} <= $price) {
var resultPrice = $price;
if (${promotionActive.isFullMinus} == 1) {
resultPrice = $price - ${promotionActive.minusValue};
}
return resultPrice < 0 ? 0 : resultPrice.toString();
}
return $price;
}
运营人员定义了活动具体的门槛,当活动生效后,通过freemaker解析后的脚本就是具体的可执行的脚本了:
\
Promotion promotionActive = getPromotionFromDb(someID);
Map params = new HashMap();
//把当前活动压入freemarker上下文参数中,以便解析具体的值。
params.put("promotionActive",promotionActive);
Stirng script = freemarkerUtil.parse(ruleTplFile,params);
script结果如下:
//满减的function
function countPrice() {
//判断商品金额是否满足优惠条件
if (100 <= price) {
return price -5 ;
}
return price;
}
\
规范的定义
通过上述的架构思路,不难看出,我们还需要定义一套脚本的变量名称、方法名称的规范,以便其他模块调起脚本,和促销模块交互,比如定义如下规则:
1、变量规范:
\
2、方法规范
满减满赠、优惠券促销活动脚本方法
有了如上规范,其他模块就可以完成具体脚本的调起、计算了:
\
//设置规范中定义的变量
engine.put("$currentTime", getCurrentTime );
engine.put("$price", getCartPrice() );
//等等...根据不同的业务逻辑,根据规范适当的传入变量。
engine.eval(script);
//尝试调起减价function
Double price = (Double)engine.invokeFunction("countPrice");
//根据规范调起其他方法...
总结
综上所述,架构的核心是定义了一套模块之间交互的规则:
-
PromotionRule(促销规则模型)
-
脚本规范
-
脚本
本着单一职责的原则,进而抽象促销模块和其他模块的功能边界:
-
促销模块负责:定义脚本模板、生成存储脚本
-
其他模块:调起脚本、生成促销规则、应用规则。
这样就完成了促销模块和其他模块的解耦,促销模块的领域模型和业务类不再侵入其他模块的代码中,双方的交互是面向的“规则”,互相都比较灵活,比如“减多少钱,怎么减?”购物车不需要关系,只关心规则应用后如何显示给用户,而促销模块也不用关心“能不能减价?如何显示给用户”类似的问题。而且基于规则的扩展也是非常方便的,只需要写相应的脚本模板就行了。
当然实际的实现要再复杂一些,比如脚本如何存储、活动生效钩子的实现,以及sku级别、店铺级别、购物车级别的规则和脚本如何定义存储等等,因为篇幅的原因还有很多细节无法一一详细列出,在这里抛砖引玉,提供大家一种“面向规则”架构的思路,上述架构思路其实不仅仅可以应用在电商领域,希望可以给有需要的同学一点帮助、开阔思路。