java b2b2c多用戶開源商城系統基於腳本引擎的促銷架構源碼分享


需求分析

在分享源碼之前,先將b2b2c系統中促銷模塊需求整理、明確,方便源碼的理解。

業務需求

  • b2b2c電子商務系統中促銷活動相關規則需以腳本數據的方式存放至redis緩存,在購物車與結算頁面計算商品價格時從redis緩存中獲取促銷規則信息,實現商品價格的計算。

技術需求

  • 促銷規則腳本需要使用freemarker模板引擎,需向其中設置內置變量。

  • 渲染腳本和調用腳本的方法放入工具類中,方便隨時調用。

架構思路

一、腳本生成規則

1、需要生成腳本引擎的促銷活動包括:滿減滿贈、單品立減、第二件半價、團購、限時搶購、拼團、優惠券和積分兌換。

2、根據促銷活動規則的不同,生成腳本引擎的時機也不同,大致可分為四類:

第一類:滿減滿贈、單品立減、第二件半價和優惠券,這四種是在活動生效時生成腳本。需要設置延時任務,活動生效自動生成腳本。

第二類:拼團,由於拼團活動生效后,也可以再次添加或修改參與拼團活動的商品,並且平台可以關閉和開啟拼團活動,因此與第一類稍有不同,除活動生效時需要生成腳本外,上述這些操作也要生成或更新腳本。

第三類:團購、限時搶購,這兩種促銷活動是平台發布商家選擇商品進行參與的,參與的商品需要商家進行審核,因此是在審核通過時生成腳本。

第四類:積分兌換,積分兌換針對的是商品,因此是在商家新增和修改商品信息時,生成或更新腳本。

3、促銷活動生成的腳本都需要放入緩存中,以便於減少查庫操作。

4、清除緩存中無用的腳本引擎:除積分兌換外,其他促銷活動都需要利用延時任務,在促銷活動失效時,將緩存中的腳本數據清除掉。積分兌換在商家關閉商品的積分兌換操作時才對緩存中的腳本數據進行刪除。

二、腳本生成流程圖

三、緩存數據結構

1、根據促銷活動的不同規則,分為三種緩存數據結構,分別是:SKU級別緩存、店鋪級別緩存和優惠券級別緩存。

2、結構圖:

  SKU級別緩存結構和店鋪級別緩存結構級別一致,如下:

  而優惠券級別的緩存結構如下:

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中基於腳本引擎的促銷活動架構思路與部分源碼分享。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM