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

需求分析

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

业务需求

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

技术需求

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

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

架构思路

一、脚本生成规则

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

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

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

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

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

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

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

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

二、脚本生成流程图

三、缓存数据结构

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

2、结构图:

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

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

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

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

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

groupbuy.ftl

团购活动脚本模板

1 1 <#-- 2 2 验证促销活动是否在有效期内 3 3 @param promotionActive 活动信息对象(内置常量) 4 4 .startTime 获取开始时间 5 5 .endTime 活动结束时间 6 6 @param $currentTime 当前时间(变量) 7 7 @returns {boolean} 8 8 --> 9 9 function validTime(){ 1010 if (${promotionActive.startTime} <= $currentTime && $currentTime <= ${promotionActive.endTime}) { 1111 return true; 1212 } 1313 return false; 1414 } 1515 1616 <#-- 1717 活动金额计算 1818 @param promotionActive 活动信息对象(内置常量) 1919 .price 商品促销活动价格 2020 @param $sku 商品SKU信息对象(变量) 2121 .$num 商品数量 2222 @returns {*} 2323 --> 2424 function countPrice() { 2525 var resultPrice = $sku.$num * ${promotionActive.price}; 2626 return resultPrice < 0 ? 0 : resultPrice.toString(); 2727 }

PromotionScriptVO

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

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

GroupbuyScriptManager

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

11 import com.enation.app.javashop.core.promotion.tool.model.dos.PromotionGoodsDO; 2 2 3 3 import java.util.List; 4 4 5 5 /** 6 6 * 团购促销活动脚本业务接口 7 7 * @author duanmingyu 8 8 * @version v1.0 9 9 * @since v7.2.0 1010 * 2020-02-18 1111 */ 1212 public interface GroupbuyScriptManager { 1313 1414 /** 1515 * 创建参与团购促销活动商品的脚本数据信息 1616 * @param promotionId 团购促销活动ID 1717 * @param goodsList 参与团购促销活动的商品集合 1818 */ 1919 void createCacheScript(Integer promotionId, List<PromotionGoodsDO> goodsList); 2020 2121 /** 2222 * 删除商品存放在缓存中的团购促销活动相关的脚本数据信息 2323 * @param promotionId 团购促销活动ID 2424 * @param goodsList 参与团购促销活动的商品集合 2525 */ 2626 void deleteCacheScript(Integer promotionId, List<PromotionGoodsDO> goodsList); 2727 }

GroupbuyScriptManagerImpl

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

1 1 import com.enation.app.javashop.core.base.CachePrefix; 2 2 import com.enation.app.javashop.core.promotion.groupbuy.model.dos.GroupbuyActiveDO; 3 3 import com.enation.app.javashop.core.promotion.groupbuy.service.GroupbuyActiveManager; 4 4 import com.enation.app.javashop.core.promotion.groupbuy.service.GroupbuyScriptManager; 5 5 import com.enation.app.javashop.core.promotion.tool.model.dos.PromotionGoodsDO; 6 6 import com.enation.app.javashop.core.promotion.tool.model.enums.PromotionTypeEnum; 7 7 import com.enation.app.javashop.core.promotion.tool.model.vo.PromotionScriptVO; 8 8 import com.enation.app.javashop.framework.cache.Cache; 9 9 import com.enation.app.javashop.framework.logs.Logger; 10 10 import com.enation.app.javashop.framework.logs.LoggerFactory; 11 11 import com.enation.app.javashop.framework.util.ScriptUtil; 12 12 import org.springframework.beans.factory.annotation.Autowired; 13 13 import org.springframework.stereotype.Service; 14 14 15 15 import java.util.ArrayList; 16 16 import java.util.HashMap; 17 17 import java.util.List; 18 18 import java.util.Map; 19 19 20 20 /** 21 21 * 团购促销活动脚本业务接口 22 22 * @author duanmingyu 23 23 * @version v1.0 24 24 * @since v7.2.0 25 25 * 2020-02-18 26 26 */ 27 27 @Service 28 28 public class GroupbuyScriptManagerImpl implements GroupbuyScriptManager { 29 29 30 30 protected final Logger logger = LoggerFactory.getLogger(this.getClass()); 31 31 32 32 @Autowired 33 33 private Cache cache; 34 34 35 35 @Autowired 36 36 private GroupbuyActiveManager groupbuyActiveManager; 37 37 38 38 @Override 39 39 public void createCacheScript(Integer promotionId, List<PromotionGoodsDO> goodsList) { 40 40 //如果参与团购促销活动的商品集合不为空并且集合长度不为0 41 41 if (goodsList != null && goodsList.size() != 0) { 42 42 //获取团购活动详细信息 43 43 GroupbuyActiveDO groupbuyActiveDO = this.groupbuyActiveManager.getModel(promotionId); 44 44 45 45 //批量放入缓存的数据集合 46 46 Map<String, List<PromotionScriptVO>> cacheMap = new HashMap<>(); 47 47 48 48 //循环参与团购活动的商品集合,将脚本放入缓存中 49 49 for (PromotionGoodsDO goods : goodsList) { 50 50 51 51 //缓存key 52 52 String cacheKey = CachePrefix.SKU_PROMOTION.getPrefix() + goods.getSkuId(); 53 53 54 54 //获取拼团活动脚本信息 55 55 PromotionScriptVO scriptVO = new PromotionScriptVO(); 56 56 57 57 //渲染并读取团购促销活动脚本信息 58 58 String script = renderScript(groupbuyActiveDO.getStartTime().toString(), groupbuyActiveDO.getEndTime().toString(), goods.getPrice()); 59 59 60 60 scriptVO.setPromotionScript(script); 61 61 scriptVO.setPromotionId(promotionId); 62 62 scriptVO.setPromotionType(PromotionTypeEnum.GROUPBUY.name()); 63 63 scriptVO.setIsGrouped(false); 64 64 scriptVO.setPromotionName("团购"); 65 65 scriptVO.setSkuId(goods.getSkuId()); 66 66 67 67 //从缓存中读取脚本信息 68 68 List<PromotionScriptVO> scriptList = (List<PromotionScriptVO>) cache.get(cacheKey); 69 69 if (scriptList == null) { 70 70 scriptList = new ArrayList<>(); 71 71 } 72 72 73 73 scriptList.add(scriptVO); 74 74 75 75 cacheMap.put(cacheKey, scriptList); 76 76 } 77 77 78 78 //将sku促销脚本数据批量放入缓存中 79 79 cache.multiSet(cacheMap); 80 80 } 81 81 } 82 82 83 83 @Override 84 84 public void deleteCacheScript(Integer promotionId, List<PromotionGoodsDO> goodsList) { 85 85 //如果参与团购促销活动的商品集合不为空并且集合长度不为0 86 86 if (goodsList != null && goodsList.size() != 0) { 87 87 //需要批量更新的缓存数据集合 88 88 Map<String, List<PromotionScriptVO>> updateCacheMap = new HashMap<>(); 89 89 90 90 //需要批量删除的缓存key集合 91 91 List<String> delKeyList = new ArrayList<>(); 92 92 93 93 for (PromotionGoodsDO goods : goodsList) { 94 94 //缓存key 95 95 String cacheKey = CachePrefix.SKU_PROMOTION.getPrefix() + goods.getSkuId(); 96 96 97 97 //从缓存中读取促销脚本缓存 98 98 List<PromotionScriptVO> scriptCacheList = (List<PromotionScriptVO>) cache.get(cacheKey); 99 99 100100 if (scriptCacheList != null && scriptCacheList.size() != 0) { 101101 //循环促销脚本缓存数据集合 102102 for (PromotionScriptVO script : scriptCacheList) { 103103 //如果脚本数据的促销活动信息与当前修改的促销活动信息一致,那么就将此信息删除 104104 if (PromotionTypeEnum.GROUPBUY.name().equals(script.getPromotionType()) 105105 && script.getPromotionId().intValue() == promotionId.intValue()) { 106106 scriptCacheList.remove(script); 107107 break; 108108 } 109109 } 110110 111111 if (scriptCacheList.size() == 0) { 112112 delKeyList.add(cacheKey); 113113 } else { 114114 updateCacheMap.put(cacheKey, scriptCacheList); 115115 } 116116 } 117117 } 118118 119119 cache.multiDel(delKeyList); 120120 cache.multiSet(updateCacheMap); 121121 } 122122 } 123123 124124 /** 125125 * 渲染并读取团购促销活动脚本信息 126126 * @param startTime 活动开始时间 127127 * @param endTime 活动结束时间 128128 * @param price 限时抢购商品价格 129129 * @return 130130 */ 131131 private String renderScript(String startTime, String endTime, Double price) { 132132 Map<String, Object> model = new HashMap<>(); 133133 134134 Map<String, Object> params = new HashMap<>(); 135135 params.put("startTime", startTime); 136136 params.put("endTime", endTime); 137137 params.put("price", price); 138138 139139 model.put("promotionActive", params); 140140 141141 String path = "groupbuy.ftl"; 142142 String script = ScriptUtil.renderScript(path, model); 143143 144144 logger.debug("生成团购促销活动脚本:" + script); 145145 146146 return script; 147147 } 148148 }

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

易族智汇(javashop)原创文章

点赞
收藏

评论区

加载中...

相关推荐

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

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

java b2b2c多用户开源商城系统商品模块扣减库存源码分享

需求分析在分享源码之前,先将b2b2c系统(https://www.oschina.net/action/GoToLink?urlhttp%3A%2F%2Fwww.javamall.com.cn%2F"b2b2c系统")中商品模块需求整理、明确,方便源码的理解。业务需求b2b2c电子商务系统中商品的库存存放在red