欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

java b2b2c多用户开源商城系统基于脚本引擎的促销架构源码分享

程序员文章站 2022-07-15 11:51:04
...

需求分析

            在分享源码之前,先将b2b2c系统中促销模块需求整理、明确,方便源码的理解。

业务需求

  • b2b2c电子商务系统中促销活动相关规则需以脚本数据的方式存放至redis缓存,在购物车与结算页面计算商品价格时从redis缓存中获取促销规则信息,实现商品价格的计算。

技术需求

  • 促销规则脚本需要使用freemarker模板引擎,需向其中设置内置变量。

  • 渲染脚本和调用脚本的方法放入工具类中,方便随时调用。

架构思路

一、脚本生成规则

1、需要生成脚本引擎的促销活动包括:满减满赠、单品立减、第二件半价、团购、限时抢购、拼团、优惠券和积分兑换。

2、根据促销活动规则的不同,生成脚本引擎的时机也不同,大致可分为四类:

第一类:满减满赠、单品立减、第二件半价和优惠券,这四种是在活动生效时生成脚本。需要设置延时任务,活动生效自动生成脚本。

第二类:拼团,由于拼团活动生效后,也可以再次添加或修改参与拼团活动的商品,并且平台可以关闭和开启拼团活动,因此与第一类稍有不同,除活动生效时需要生成脚本外,上述这些操作也要生成或更新脚本。

第三类:团购、限时抢购,这两种促销活动是平台发布商家选择商品进行参与的,参与的商品需要商家进行审核,因此是在审核通过时生成脚本。

第四类:积分兑换,积分兑换针对的是商品,因此是在商家新增和修改商品信息时,生成或更新脚本。

3、促销活动生成的脚本都需要放入缓存中,以便于减少查库操作。

4、清除缓存中无用的脚本引擎:除积分兑换外,其他促销活动都需要利用延时任务,在促销活动失效时,将缓存中的脚本数据清除掉。积分兑换在商家关闭商品的积分兑换操作时才对缓存中的脚本数据进行删除。

二、脚本生成流程图

java b2b2c多用户开源商城系统基于脚本引擎的促销架构源码分享
            
    
    博客分类: 电商架构 b2b2c系统源码分享javashop电商系统架构 

三、缓存数据结构

1、根据促销活动的不同规则,分为三种缓存数据结构,分别是:SKU级别缓存、店铺级别缓存和优惠券级别缓存。

2、结构图:

  SKU级别缓存结构和店铺级别缓存结构级别一致,如下:

java b2b2c多用户开源商城系统基于脚本引擎的促销架构源码分享
            
    
    博客分类: 电商架构 b2b2c系统源码分享javashop电商系统架构 

  而优惠券级别的缓存结构如下:

java b2b2c多用户开源商城系统基于脚本引擎的促销架构源码分享
            
    
    博客分类: 电商架构 b2b2c系统源码分享javashop电商系统架构 

3、缓存结构说明

(1)、SKU级别缓存:

  缓存key:{SKUPROMOTION} 加上SKU的ID,例如:{SKU_PROMOTION}_100。

  缓存value:是一个泛型为PromotionScriptVO的List集合。

(2)、店铺级别缓存:

  缓存key:{CARTPROMOTION} 加上店铺的ID,例如:{CART_PROMOTION}_100。

  缓存value:是一个泛型为PromotionScriptVO的List集合。

(3)、优惠券级别缓存:

  缓存key:{COUPONPROMOTION} 加上优惠券的ID,例如:{COUPON_PROMOTION}_100。

  缓存value:是一个String类型的脚本字符串。

4、促销活动存储的缓存结构区分

(1)、针对满减满赠、单品立减、第二件半价这三种促销活动,如果商家在发布活动时选择的是全部商品参与,那么则存储的是店铺级别的缓存结构,如果选择的是部分商品参与,那么则存储的是SKU级别的缓存结构。

(2)、针对拼团、团购、显示抢购和积分兑换这些促销活动,都是存储的SKU级别的缓存结构。

(3)、针对优惠券,无论是店铺优惠券还是平台优惠券,存储的都是优惠券级别的缓存结构。

四、脚本规范

1、调用脚本传入的变量规范:

变量名称 类型 说明
$currentTime int 当前时间,为了验证活动是否有效
$sku Object 详见下表
$price double 其他优惠活动优惠后总价

$sku说明:

名称 类型 说明
$price double 商品单价
$num int 商品数量
$skuId int 商品skuID
$totalPrice double 商品小计(单价*数量)

2、各个促销活动脚本中的方法说明

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

方法名 参数 返回值类型 返回值示例 说明
validTime $currentTime Boolean true/false  
countPrice $price Double 100.00  
giveGift $price Object [{"type":"freeShip","value":true},{"type":"point","value":100},{"type":"gift","value":10},{"type":"coupon","value":20}] 优惠券脚本没有此方法

单品立减、第二件半价、团购、限时抢购、团购活动脚本方法

方法名 参数 返回值类型 返回值示例
validTime $currentTime Boolean true/false
countPrice $sku Double 100.00

积分兑换活动脚本方法

方法名 参数 返回值类型 返回值示例 说明
validTime $currentTime Boolean true/false 此方法会直接返回true,积分兑换不涉及有效期,脚本中有此方法是为了脚本内容统一
countPrice $sku Double 100.00  
countPoint $sku Integer 50

源码分享

由于促销活动类型较多,此处只以团购活动为例进行相关代码的分享。

ScriptUtil

促销脚本渲染与调用工具类

 
  1 import com.enation.app.javashop.framework.logs.Logger;
  2 import com.enation.app.javashop.framework.logs.LoggerFactory;
  3 import freemarker.template.Configuration;
  4 import freemarker.template.Template;
  5 
  6 import javax.script.Invocable;
  7 import javax.script.ScriptEngine;
  8 import javax.script.ScriptEngineManager;
  9 import javax.script.ScriptException;
 10 import java.io.IOException;
 11 import java.io.StringWriter;
 12 import java.util.*;
 13 
 14 /**
 15  * 脚本生成工具类
 16  * @author duanmingyu
 17  * @version v1.0
 18  * @since v7.2.0
 19  * @date 2020-01-06
 20  */
 21 public class ScriptUtil {
 22 
 23     private static final Logger log = LoggerFactory.getLogger(ScriptUtil.class);
 24 
 25     /**
 26      * 渲染并读取脚本内容
 27      * @param name 脚本模板名称(例:test.js,test.html,test.ftl等)
 28      * @param model 渲染脚本需要的数据内容
 29      * @return
 30      */
 31     public static String renderScript(String name, Map<String, Object> model) {
 32         StringWriter stringWriter = new StringWriter();
 33 
 34         try {
 35             Configuration cfg = new Configuration(Configuration.VERSION_2_3_28);
 36 
 37             cfg.setClassLoaderForTemplateLoading(Thread.currentThread().getContextClassLoader(),"/script_tpl");
 38             cfg.setDefaultEncoding("UTF-8");
 39             cfg.setNumberFormat("#.##");
 40 
 41             Template temp = cfg.getTemplate(name);
 42 
 43             temp.process(model, stringWriter);
 44 
 45             stringWriter.flush();
 46 
 47             return stringWriter.toString();
 48 
 49         } catch (Exception e) {
 50             log.error(e.getMessage());
 51         } finally {
 52             try {
 53                 stringWriter.close();
 54             } catch (IOException ex) {
 55                 log.error(ex.getMessage());
 56             }
 57         }
 58 
 59         return null;
 60     }
 61 
 62     /**
 63      * @Description:执行script脚本
 64      * @param method script方法名
 65      * @param params 参数
 66      * @param script 脚本
 67      * @return: 返回执行结果
 68      * @Author: liuyulei
 69      * @Date: 2020/1/7
 70      */
 71     public static Object executeScript(String method,Map<String,Object> params,String script)  {
 72         if (StringUtil.isEmpty(script)){
 73             log.debug("script is " + script);
 74             return new Object();
 75         }
 76 
 77         try {
 78             ScriptEngineManager manager = new ScriptEngineManager();
 79             ScriptEngine engine = manager.getEngineByName("javascript");
 80 
 81 
 82             log.debug("脚本参数:");
 83             for (String key:params.keySet()) {
 84                 log.debug(key + "=" + params.get(key));
 85                 engine.put(key, params.get(key));
 86             }
 87 
 88             engine.eval(script);
 89             log.debug("script 脚本 :");
 90             log.debug(script);
 91 
 92             Invocable invocable = (Invocable) engine;
 93 
 94             return invocable.invokeFunction(method);
 95         } catch (ScriptException e) {
 96             log.error(e.getMessage(),e);
 97         } catch (NoSuchMethodException e) {
 98             log.error(e.getMessage(),e);
 99         }
100         return new Object();
101     }
102 }
 

groupbuy.ftl

团购活动脚本模板

 
 1 <#--
 2  验证促销活动是否在有效期内
 3  @param promotionActive 活动信息对象(内置常量)
 4         .startTime 获取开始时间
 5         .endTime 活动结束时间
 6  @param $currentTime 当前时间(变量)
 7  @returns {boolean}
 8  -->
 9 function validTime(){
10     if (${promotionActive.startTime} <= $currentTime && $currentTime <= ${promotionActive.endTime}) {
11         return true;
12     }
13     return false;
14 }
15 
16 <#--
17 活动金额计算
18 @param promotionActive 活动信息对象(内置常量)
19        .price 商品促销活动价格
20 @param $sku 商品SKU信息对象(变量)
21        .$num 商品数量
22 @returns {*}
23 -->
24 function countPrice() {
25     var resultPrice = $sku.$num * ${promotionActive.price};
26     return resultPrice < 0 ? 0 : resultPrice.toString();
27 }
 

PromotionScriptVO

促销活动脚本数据结构实体

 
  1 import com.fasterxml.jackson.databind.PropertyNamingStrategy;
  2 import com.fasterxml.jackson.databind.annotation.JsonNaming;
  3 import io.swagger.annotations.ApiModelProperty;
  4 import org.apache.commons.lang.builder.EqualsBuilder;
  5 import org.apache.commons.lang.builder.HashCodeBuilder;
  6 
  7 import java.io.Serializable;
  8 
  9 /**
 10  * @description: 促销脚本VO
 11  * @author: liuyulei
 12  * @create: 2020-01-09 09:43
 13  * @version:1.0
 14  * @since:7.1.5
 15  **/
 16 @JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class)
 17 public class PromotionScriptVO implements Serializable {
 18     private static final long serialVersionUID = 3566902764098210013L;
 19 
 20     @ApiModelProperty(value = "促销活动id")
 21     private Integer promotionId;
 22 
 23     @ApiModelProperty(value = "促销活动名称")
 24     private String promotionName;
 25 
 26     @ApiModelProperty(value = "促销活动类型")
 27     private String promotionType;
 28 
 29     @ApiModelProperty(value = "是否可以被分组")
 30     private Boolean isGrouped;
 31 
 32     @ApiModelProperty(value = "促销脚本",hidden = true)
 33     private String promotionScript;
 34 
 35     @ApiModelProperty(value = "商品skuID")
 36     private Integer skuId;
 37 
 38 
 39     public Integer getPromotionId() {
 40         return promotionId;
 41     }
 42 
 43     public void setPromotionId(Integer promotionId) {
 44         this.promotionId = promotionId;
 45     }
 46 
 47     public String getPromotionName() {
 48         return promotionName;
 49     }
 50 
 51     public void setPromotionName(String promotionName) {
 52         this.promotionName = promotionName;
 53     }
 54 
 55     public String getPromotionType() {
 56         return promotionType;
 57     }
 58 
 59     public void setPromotionType(String promotionType) {
 60         this.promotionType = promotionType;
 61     }
 62 
 63     public Boolean getIsGrouped() {
 64         return isGrouped;
 65     }
 66 
 67     public void setIsGrouped(Boolean grouped) {
 68         isGrouped = grouped;
 69     }
 70 
 71     public String getPromotionScript() {
 72         return promotionScript;
 73     }
 74 
 75     public void setPromotionScript(String promotionScript) {
 76         this.promotionScript = promotionScript;
 77     }
 78 
 79     public Integer getSkuId() {
 80         return skuId;
 81     }
 82 
 83     public void setSkuId(Integer skuId) {
 84         this.skuId = skuId;
 85     }
 86 
 87     @Override
 88     public boolean equals(Object o) {
 89         if (this == o) {
 90             return true;
 91         }
 92 
 93         if (o == null || getClass() != o.getClass()) {
 94             return false;
 95         }
 96         PromotionScriptVO that = (PromotionScriptVO) o;
 97 
 98         return new EqualsBuilder()
 99                 .append(promotionId, that.promotionId)
100                 .append(promotionName, that.promotionName)
101                 .append(promotionType, that.promotionType)
102                 .append(isGrouped, that.isGrouped)
103                 .isEquals();
104     }
105 
106     @Override
107     public int hashCode() {
108         return new HashCodeBuilder(17, 37)
109                 .append(promotionId)
110                 .append(promotionName)
111                 .append(promotionType)
112                 .append(isGrouped)
113                 .toHashCode();
114     }
115 
116     @Override
117     public String toString() {
118         return "PromotionScriptVO{" +
119                 "promotionId=" + promotionId +
120                 ", promotionName='" + promotionName + '\'' +
121                 ", promotionType='" + promotionType + '\'' +
122                 ", isGrouped=" + isGrouped +
123                 ", promotionScript='" + promotionScript + '\'' +
124                 ", skuId=" + skuId +
125                 '}';
126     }
127 }
 

GroupbuyScriptManager

团购促销活动脚本业务接口

 
 1 import com.enation.app.javashop.core.promotion.tool.model.dos.PromotionGoodsDO;
 2 
 3 import java.util.List;
 4 
 5 /**
 6  * 团购促销活动脚本业务接口
 7  * @author duanmingyu
 8  * @version v1.0
 9  * @since v7.2.0
10  * 2020-02-18
11  */
12 public interface GroupbuyScriptManager {
13 
14     /**
15      * 创建参与团购促销活动商品的脚本数据信息
16      * @param promotionId 团购促销活动ID
17      * @param goodsList 参与团购促销活动的商品集合
18      */
19     void createCacheScript(Integer promotionId, List<PromotionGoodsDO> goodsList);
20 
21     /**
22      * 删除商品存放在缓存中的团购促销活动相关的脚本数据信息
23      * @param promotionId 团购促销活动ID
24      * @param goodsList 参与团购促销活动的商品集合
25      */
26     void deleteCacheScript(Integer promotionId, List<PromotionGoodsDO> goodsList);
27 }
 

 

GroupbuyScriptManagerImpl

团购促销活动脚本业务接口实现

 
  1 import com.enation.app.javashop.core.base.CachePrefix;
  2 import com.enation.app.javashop.core.promotion.groupbuy.model.dos.GroupbuyActiveDO;
  3 import com.enation.app.javashop.core.promotion.groupbuy.service.GroupbuyActiveManager;
  4 import com.enation.app.javashop.core.promotion.groupbuy.service.GroupbuyScriptManager;
  5 import com.enation.app.javashop.core.promotion.tool.model.dos.PromotionGoodsDO;
  6 import com.enation.app.javashop.core.promotion.tool.model.enums.PromotionTypeEnum;
  7 import com.enation.app.javashop.core.promotion.tool.model.vo.PromotionScriptVO;
  8 import com.enation.app.javashop.framework.cache.Cache;
  9 import com.enation.app.javashop.framework.logs.Logger;
 10 import com.enation.app.javashop.framework.logs.LoggerFactory;
 11 import com.enation.app.javashop.framework.util.ScriptUtil;
 12 import org.springframework.beans.factory.annotation.Autowired;
 13 import org.springframework.stereotype.Service;
 14 
 15 import java.util.ArrayList;
 16 import java.util.HashMap;
 17 import java.util.List;
 18 import java.util.Map;
 19 
 20 /**
 21  * 团购促销活动脚本业务接口
 22  * @author duanmingyu
 23  * @version v1.0
 24  * @since v7.2.0
 25  * 2020-02-18
 26  */
 27 @Service
 28 public class GroupbuyScriptManagerImpl implements GroupbuyScriptManager {
 29 
 30     protected final Logger logger = LoggerFactory.getLogger(this.getClass());
 31 
 32     @Autowired
 33     private Cache cache;
 34 
 35     @Autowired
 36     private GroupbuyActiveManager groupbuyActiveManager;
 37 
 38     @Override
 39     public void createCacheScript(Integer promotionId, List<PromotionGoodsDO> goodsList) {
 40         //如果参与团购促销活动的商品集合不为空并且集合长度不为0
 41         if (goodsList != null && goodsList.size() != 0) {
 42             //获取团购活动详细信息
 43             GroupbuyActiveDO groupbuyActiveDO = this.groupbuyActiveManager.getModel(promotionId);
 44 
 45             //批量放入缓存的数据集合
 46             Map<String, List<PromotionScriptVO>> cacheMap = new HashMap<>();
 47 
 48             //循环参与团购活动的商品集合,将脚本放入缓存中
 49             for (PromotionGoodsDO goods : goodsList) {
 50 
 51                 //缓存key
 52                 String cacheKey = CachePrefix.SKU_PROMOTION.getPrefix() + goods.getSkuId();
 53 
 54                 //获取拼团活动脚本信息
 55                 PromotionScriptVO scriptVO = new PromotionScriptVO();
 56 
 57                 //渲染并读取团购促销活动脚本信息
 58                 String script = renderScript(groupbuyActiveDO.getStartTime().toString(), groupbuyActiveDO.getEndTime().toString(), goods.getPrice());
 59 
 60                 scriptVO.setPromotionScript(script);
 61                 scriptVO.setPromotionId(promotionId);
 62                 scriptVO.setPromotionType(PromotionTypeEnum.GROUPBUY.name());
 63                 scriptVO.setIsGrouped(false);
 64                 scriptVO.setPromotionName("团购");
 65                 scriptVO.setSkuId(goods.getSkuId());
 66 
 67                 //从缓存中读取脚本信息
 68                 List<PromotionScriptVO> scriptList = (List<PromotionScriptVO>) cache.get(cacheKey);
 69                 if (scriptList == null) {
 70                     scriptList = new ArrayList<>();
 71                 }
 72 
 73                 scriptList.add(scriptVO);
 74 
 75                 cacheMap.put(cacheKey, scriptList);
 76             }
 77 
 78             //将sku促销脚本数据批量放入缓存中
 79             cache.multiSet(cacheMap);
 80         }
 81     }
 82 
 83     @Override
 84     public void deleteCacheScript(Integer promotionId, List<PromotionGoodsDO> goodsList) {
 85         //如果参与团购促销活动的商品集合不为空并且集合长度不为0
 86         if (goodsList != null && goodsList.size() != 0) {
 87             //需要批量更新的缓存数据集合
 88             Map<String, List<PromotionScriptVO>> updateCacheMap = new HashMap<>();
 89 
 90             //需要批量删除的缓存key集合
 91             List<String> delKeyList = new ArrayList<>();
 92 
 93             for (PromotionGoodsDO goods : goodsList) {
 94                 //缓存key
 95                 String cacheKey = CachePrefix.SKU_PROMOTION.getPrefix() + goods.getSkuId();
 96 
 97                 //从缓存中读取促销脚本缓存
 98                 List<PromotionScriptVO> scriptCacheList = (List<PromotionScriptVO>) cache.get(cacheKey);
 99 
100                 if (scriptCacheList != null && scriptCacheList.size() != 0) {
101                     //循环促销脚本缓存数据集合
102                     for (PromotionScriptVO script : scriptCacheList) {
103                         //如果脚本数据的促销活动信息与当前修改的促销活动信息一致,那么就将此信息删除
104                         if (PromotionTypeEnum.GROUPBUY.name().equals(script.getPromotionType())
105                                 && script.getPromotionId().intValue() == promotionId.intValue()) {
106                             scriptCacheList.remove(script);
107                             break;
108                         }
109                     }
110 
111                     if (scriptCacheList.size() == 0) {
112                         delKeyList.add(cacheKey);
113                     } else {
114                         updateCacheMap.put(cacheKey, scriptCacheList);
115                     }
116                 }
117             }
118 
119             cache.multiDel(delKeyList);
120             cache.multiSet(updateCacheMap);
121         }
122     }
123 
124     /**
125      * 渲染并读取团购促销活动脚本信息
126      * @param startTime 活动开始时间
127      * @param endTime 活动结束时间
128      * @param price 限时抢购商品价格
129      * @return
130      */
131     private String renderScript(String startTime, String endTime, Double price) {
132         Map<String, Object> model = new HashMap<>();
133 
134         Map<String, Object> params = new HashMap<>();
135         params.put("startTime", startTime);
136         params.put("endTime", endTime);
137         params.put("price", price);
138 
139         model.put("promotionActive", params);
140 
141         String path = "groupbuy.ftl";
142         String script = ScriptUtil.renderScript(path, model);
143 
144         logger.debug("生成团购促销活动脚本:" + script);
145 
146         return script;
147     }
148 }
 

 

 以上是Javashop中基于脚本引擎的促销活动架构思路与部分源码分享。

 

易族智汇(javashop)原创文章