背景介绍
在电商系统中,促销模块主要架构特点在于促销活动的多种多样,比如常见的优惠券、满减满送、秒杀拼团等等等,另外一个特点是促销活动的多变性,促销活动要随着运营的需要而及时演化。
基于以上特点,一个优秀的电商系统的促销模块他的架构应该做到两点:
促销模块和购物车订单的解耦
良好的扩展性
本文以Javashop电商系统中的架构为例,详细讲解如何基于脚本引擎实现一个具有松散的、良好扩展性的促销模块。
难点分析
我们先看一下如果不是基于脚本引擎的促销模块,和其他模块的耦合实例,以便我们理解应用脚本引擎后的好处。
1、领域模型的耦合
拿购物车列表来举例,为了计算、显示购物车中的促销情况,伪代码如下:
//每个店铺一个购物车,循环处理for (Cart cart : itemList) { List productList = cart.getProductList(); for (Sku goods : productList) { //计算购物车商品的促销活动 countSkuPormotion(goods); } //计算每个购物车的优惠(如店铺级别的满减满赠等) countCartPromotion(cart)}
那么购物车和商品必然要和促销的领域模型耦合(如满减满赠、优惠卷等实体类):
购物车的伪代码:
public class Cart { //店铺id private int seller_id; //购物车中的sku列表 private List skuList; //购物车中使用的优惠券 private List couponList; //赠品列表 private List giftList; //赠送积分 private int giftPoint; //其他略。。一大堆的促销模块的实体类。。。。}
同样的,购物车中的sku也要和促销领域模型进行耦合
public class Sku { //店铺id private int sku_id; //商品sku名称 private String sku_name //购物车中使用的优惠券 private List couponList; //赠品列表 private List 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 functionInvocable invocable = (Invocable) engine;Object value = invocable.invokeFunction("test");
我们再看一下Javashop中定义的“满减满送”脚本(满减的门槛是100元),方便大家更好的理解:
//满减的functionfunction countPrice() { //判断商品金额是否满足优惠条件 if (100 <= price) { return price - 5 ; } return price;}//满送的functionfunction giveGift() { // 判断商品金额是否满足优惠条件 if (100 <= price) { //返回赠品的json return {giftid:1,name:'一个赠品'}; } return null;}
以购物车为例,在Java中调用如下:
//设置价格变量engine.put("price", cart.getPrice() );engine.eval(script);//尝试调起减价functionDouble price = (Double)engine.invokeFunction("countPrice");//尝试调起“送东西” functionString 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结果如下:
//满减的functionfunction countPrice() { //判断商品金额是否满足优惠条件 if (100 <= price) { return price -5 ; } return price;}
规范的定义
通过上述的架构思路,不难看出,我们还需要定义一套脚本的变量名称、方法名称的规范,以便其他模块调起脚本,和促销模块交互,比如定义如下规则:
1、变量规范:
变量名称 | 类型 | 说明 |
$currentTime | int | 当前时间,为了验证活动是否有效 |
$sku | Object | 商品的sku |
$price | double | 其他优惠活动优惠后总价 |
2、方法规范
满减满赠、优惠券促销活动脚本方法
方法名 | 返回值类型 | 返回值说明 |
validTime | Boolean | 是否在有效期 |
countPrice | Double | 计算减价金额 |
giveGift | Object | 计算赠送物品 |
有了如上规范,其他模块就可以完成具体脚本的调起、计算了:
//设置规范中定义的变量engine.put("$currentTime", getCurrentTime );engine.put("$price", getCartPrice() );//等等...根据不同的业务逻辑,根据规范适当的传入变量。engine.eval(script);//尝试调起减价functionDouble price = (Double)engine.invokeFunction("countPrice");//根据规范调起其他方法...
总结
综上所述,架构的核心是定义了一套模块之间交互的规则:
PromotionRule(促销规则模型)
脚本规范
脚本
本着单一职责的原则,进而抽象促销模块和其他模块的功能边界:
促销模块负责:定义脚本模板、生成存储脚本
其他模块:调起脚本、生成促销规则、应用规则。
这样就完成了促销模块和其他模块的解耦,促销模块的领域模型和业务类不再侵入其他模块的代码中,双方的交互是面向的“规则”,互相都比较灵活,比如“减多少钱,怎么减?”购物车不需要关系,只关心规则应用后如何显示给用户,而促销模块也不用关心“能不能减价?如何显示给用户”类似的问题。而且基于规则的扩展也是非常方便的,只需要写相应的脚本模板就行了。
当然实际的实现要再复杂一些,比如脚本如何存储、活动生效钩子的实现,以及sku级别、店铺级别、购物车级别的规则和脚本如何定义存储等等,因为篇幅的原因还有很多细节无法一一详细列出,在这里抛砖引玉,提供大家一种“面向规则”架构的思路,上述架构思路其实不仅仅可以应用在电商领域,希望可以给有需要的同学一点帮助、开阔思路。
在最后给自己的产品打个广告:Javashop电商系统是易族智汇10多年打磨积累的的一套电商系统,前后端分离,有spring boot基础架构的版本也有微服务架构的版本。
我们的优势:
功能丰富、架构优良、代码精致、文档齐全、易于二次开发
提供的服务:
部署实施、全面架构培训、二次开发培训、二开一对一问题解答
我们的产品涵盖:
模式上:b2b2c/b2c/o2o/b2b/saas,终端上:PC/WAP/小程序/APP
我们的案例:
吉利集团、新奥集团、安琪酵母集团、山东京博集团、波司登....
如果您公司有相关电商的业务,基于Javashop来二次开发会大大提升效率、降低开发成本,如果向公司推荐成功采购,凭渠道码给您个人返现5000元。
渠道优惠码:【JAVASHOP-MP-NWTVWO39】
也欢迎您关注我们的公众号,上面会经常分享一些架构思路。