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

Wesley13
• 阅读 641

需求分析

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

业务需求

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

技术需求

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

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

架构思路

一、脚本生成规则

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

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

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

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

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

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

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

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

二、脚本生成流程图

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

三、缓存数据结构

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

2、结构图:

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

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

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

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

3、缓存结构说明

(1)、SKU级别缓存:

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

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

(2)、店铺级别缓存:

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

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

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

  缓存key:{COUPON_PROMOTION}_ 加上优惠券的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)原创文章

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Wesley13 Wesley13
2年前
java b2b2c多用户开源商城系统商品模块扣减库存源码分享
需求分析在分享源码之前,先将b2b2c系统(https://www.oschina.net/action/GoToLink?urlhttp%3A%2F%2Fwww.javamall.com.cn%2F"b2b2c系统")中商品模块需求整理、明确,方便源码的理解。业务需求b2b2c电子商务系统中商品的库存存放在red
Easter79 Easter79
2年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这