电商系统促销模块脚本引擎的应用

556 阅读5分钟

背景介绍

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

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

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

本文以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级别、店铺级别、购物车级别的规则和脚本如何定义存储等等,因为篇幅的原因还有很多细节无法一一详细列出,在这里抛砖引玉,提供大家一种“面向规则”架构的思路,上述架构思路其实不仅仅可以应用在电商领域,希望可以给有需要的同学一点帮助、开阔思路。