背景介绍

在电商系统中,促销模块主要架构特点在于促销活动的多种多样,比如常见的优惠券、满减满送,秒杀拼团等等等,另外一个特点是促销活动的多变性,促销活动要随着运营的需要而及时演化。

基于以上特点,一个优秀的电商系统的促销模块他的架构应该做到两点:

  1. 促销模块和购物车订单的解耦
  2. 良好的扩展性

本文以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、变量规范:

2、方法规范

满减满赠、优惠券促销活动脚本方法

有了如上规范,其他模块就可以完成具体脚本的调起、计算了:

//设置规范中定义的变量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元。

公司官网:https://www.javamall.com.cn