需求分析
在分享源码之前,先将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中基于脚本引擎的促销活动架构思路与部分源码分享。
相关推荐
本设计源码提供了一个基于Java的小象电商B2B2C小程序电商商城开源系统。项目包含153个文件,主要使用JavaScript和微信小程序编程语言。文件类型包括37个JavaScript脚本文件、30个WXSS样式文件、29个JSON配置文件、28...
ShopNC B2B2C是一款专业级别的多用户商城系统,...总的来说,ShopNC B2B2C多用户商城系统源码提供了强大的电子商务平台基础,通过理解和定制源码,企业能够构建出具有竞争力的在线商城,同时满足B2B和B2C的业务场景。
【标题】"基于PHP的bymall B2B2C多用户开源商城系统 php版.zip" 描述了一个基于PHP编程语言开发的开源电子商务平台。B2B2C(Business-to-Business-to-Consumer)模式是一种电子商务类型,它允许商家与商家(B2B)...
3. **MVC(Model-View-Controller)架构**:这是B2B2C商城源码可能采用的设计模式,它将业务逻辑、数据和用户界面分离,便于管理和维护。 4. **前端技术**:HTML、CSS和JavaScript是构建用户界面的关键,可能还需要...
"PHP实例开发源码—bymall B2B2C多用户开源商城系统 php版.zip" 这个标题表明我们正在处理一个基于PHP编程语言的开源商城系统,名为“bymall”。B2B2C模式是指Business-to-Business-to-Consumer的电子商务模式,它...
DSMALL开源商城B2B2C源码V6.1.9版是一款基于PHP语言开发的电商系统,适用于构建B2B2C类型的在线交易平台。这个版本的源码提供了全面的功能,允许商家入驻、商品销售、消费者购物,以及第三方开发者进行定制和扩展。...
综上所述,DSmall多商户B2B2C开源商城基于ThinkPHP框架,提供了完整的源码,具备良好的安全性和可扩展性。通过Composer管理依赖,采用Git进行版本控制,遵循一定的自动化构建和测试流程。开发者可以利用这些资源快速...
【标题】"基于PHP的bymallB2B2C多用户开源商城系统php版源码.zip" 提供的是一个采用PHP编程语言开发的多用户电子商务平台,B2B2C模式代表Business-to-Business-to-Consumer,即商家对商家对消费者的商业模式。...
标题中的“JAVA商城 PHP商城系统 分销商城 多用户商城 SaaS O2O商城 B2B2C S2B2C 小程序直播 商城源码”表明这是一个关于电子商务平台开发的项目,涵盖多种技术栈和模式。让我们逐一解析这些关键词,并深入探讨相关...
【标题】"ThinkPHP开源商城多店铺商城B2B2C源码DSMall商城" 涉及的核心技术及知识点主要包括以下几个方面: 1. **ThinkPHP框架**:这是一个广泛使用的PHP开发框架,以简洁、高效的特性著称。ThinkPHP提供了丰富的...
飞蛙B2B2C(FeiWa B2B2C)商城电商系统是一款基于PHP开发的电子商务平台,旨在提供全面、灵活且可扩展的在线购物解决方案。B2B2C模式代表Business-to-Business-to-Consumer,即企业对商家对消费者,允许零售商或中间...
"niushop_b2b2c_release_v1.03"可能是这个多用户商城系统的源码包名称,表明这是一个名为“牛铺”的B2B2C解决方案的v1.03版本。该版本可能包含了系统的主要功能模块、数据库脚本、配置文件等,供开发者部署和二次...
【标题】"基于PHP的DSmall多商户B2B2C开源商城源码"是一个针对电商领域的项目,它采用PHP编程语言实现,旨在提供一个支持多商家入驻的电子商务平台。在B2B2C(Business-to-Business-to-Consumer)模式下,该商城允许...
《基于PHP的Thinkphp5.1内核综合电子商务系统多用户B2B2C商城源码解析》 在当今互联网时代,电子商务系统已经成为商业运营的重要组成部分。本篇将深入探讨一款基于PHP语言、采用Thinkphp5.1框架构建的多用户B2B2C...
【标题】"PHP实例开发源码—DSmall多商户B2B2C开源商城源码.zip" 提供的是一个基于PHP编程语言实现的电子商务平台源代码,特别关注于多商户B2B2C(Business-to-Business-to-Consumer)模式。这种模式允许多个商家在...
本实例——“Thinkphp5.1内核综合电子商务系统多用户B2B2C商城源码”提供了一个全面的、基于PHP的电商解决方案,旨在帮助开发者深入了解PHP在电商平台建设中的应用。 首先,我们来了解Thinkphp5.1框架。Thinkphp是...
飞蛙B2B2C(FeiWaB2B2C)商城电商系统是一款基于PHP编程语言开发的电子商务平台,其源码的提供为开发者和企业提供了构建在线购物网站的强大工具。这款系统的设计旨在满足多用户、多商家的业务需求,涵盖了B2B...
【标题】"基于PHP的shangfan(商范商城)B2B2C商城系统源码.zip" 提供的是一个采用PHP编程语言开发的电子商务平台,名为“商范商城”。这个系统是B2B2C模式,即Business-to-Business-to-Consumer,允许商家与商家以及...