跳到主要内容

第二件半价

一、功能说明

1、第二件半价促销活动属于商家店铺可直接发布的促销活动。

2、同一个商家在同一时间段内只允许创建一个第二件半价促销活动。

3、商家在发布第二件半价活动时,可以选择店铺全部商品参与,也可以选择部分商品参与。

选择全部商品参与时,商家新创建了一个商品,那么这个商品也会自动参与到这个活动中。

4、第二件半价活动开始后,不允许修改和删除活动信息。

5、对于参与第二件半价促销活动的商品,用户购买双数即可享受优惠,且优惠可以叠加,如下:

例:参与活动的商品A单价为100元

购买数量(件)应支付金额(元)实支付金额(元)优惠金额(元)
11001000
220015050
330025050
4400300100

二、数据库设计

1、表结构设计

第二件半价促销活动表—es_half_price

字段名类型与长度说明
hp_idbigint(20)主键ID
start_timebigint(20)活动开始时间(存放的是以秒为单位的时间戳)
end_timebigint(20)活动结束时间(存放的是以秒为单位的时间戳)
titlevarchar(50)活动标题(名称)
range_typeint(1)商品参与活动的方式,1:全部商品,2:部分商品
disabledint(1)活动是否停用(删除),0:否,1:是
descriptionlongtext活动说明(描述/简介)
seller_idbigint(20)活动所属商家ID(关联商家店铺表--es_shop)

2、表关联说明

image-20201030092520324

第二件半价促销活动表(es_half_price)促销活动商品表(es_promotion_goods)
hp_idactivity_id
无字段,促销类型值为HALF_PRICEpromotion_type

三、缓存设计

1、商家在发布第二件半价促销活动时,在将促销活动信息入库的同时,也会将信息放入缓存中。

缓存key值为:{STOREID_HALF_PRICE_KEY}活动ID。

缓存value值为:HalfPriceDO.java这个实体对象信息。

2、第二件半价促销活动脚本引擎

  • 脚本引擎缓存结构:

    第二件半价促销活动的促销脚本引擎缓存结构有两种:

    • 当发布活动时,如果选择的是全部商品参与,那么存放的是店铺级别的缓存结构。
    • 当发布活动时,如果选择的是部分商品参与,那么存放的是SKU级别的缓存结构。
  • 脚本引擎生成和删除时机:

    • 生成:活动开始时生成。
    • 删除:活动结束时删除。

关于促销脚本引擎缓存结构可参考《促销活动脚本引擎生成架构》这篇文档。

四、代码设计

1、接口调用流程图

image-20201030100803761

2、相关代码展示

以新增第二件半价活动为例

  • API代码--HalfPriceSellerController

    @RestController
    @RequestMapping("/seller/promotion/half-prices")
    @Api(description = "第二件半价相关API")
    @Validated
    public class HalfPriceSellerController {

    @ApiOperation(value = "添加第二件半价", response = HalfPriceVO.class)
    @PostMapping
    public HalfPriceVO add(@Valid @RequestBody HalfPriceVO halfPrice) {

    PromotionValid.paramValid(halfPrice.getStartTime(),halfPrice.getEndTime(),
    halfPrice.getRangeType(),halfPrice.getGoodsList());

    // 获取当前登录的店铺ID
    Seller seller = UserContext.getSeller();
    Long sellerId = seller.getSellerId();
    halfPrice.setSellerId(sellerId);

    this.halfPriceManager.add(halfPrice);
    return halfPrice;
    }
    }
  • 新增第二件半价活动业务层代码--HalfPriceManagerImpl

    @Service
    public class HalfPriceManagerImpl extends AbstractPromotionRuleManagerImpl implements HalfPriceManager {

    @Override
    @Transactional(propagation = Propagation.REQUIRED,
    rollbackFor = {RuntimeException.class, Exception.class,
    ServiceException.class, NoPermissionException.class})
    public HalfPriceVO add(HalfPriceVO halfPriceVO) {
    //检测开始时间和结束时间
    PromotionValid.paramValid(halfPriceVO.getStartTime(),
    halfPriceVO.getEndTime(), 1, null);
    this.verifyTime(halfPriceVO.getStartTime(),
    halfPriceVO.getEndTime(), PromotionTypeEnum.HALF_PRICE, null);

    //初步形成商品的DTO列表
    List<PromotionGoodsDTO> goodsDTOList = new ArrayList<>();
    //是否是全部商品参与
    if (halfPriceVO.getRangeType() == 1) {
    PromotionGoodsDTO goodsDTO = new PromotionGoodsDTO();
    goodsDTO.setGoodsId(-1L);
    goodsDTO.setSkuId(-1L);
    goodsDTO.setGoodsName("全部商品");
    goodsDTO.setThumbnail("");
    goodsDTOList.add(goodsDTO);
    halfPriceVO.setGoodsList(goodsDTOList);
    }

    this.verifyRule(halfPriceVO.getGoodsList());

    HalfPriceDO halfPriceDO = new HalfPriceDO();
    BeanUtils.copyProperties(halfPriceVO, halfPriceDO);
    this.halfPriceMapper.insert(halfPriceDO);

    Long id = halfPriceDO.getHpId();
    halfPriceVO.setHpId(id);
    PromotionDetailDTO detailDTO = new PromotionDetailDTO();
    detailDTO.setStartTime(halfPriceVO.getStartTime());
    detailDTO.setEndTime(halfPriceVO.getEndTime());
    detailDTO.setActivityId(halfPriceVO.getHpId());
    detailDTO.setPromotionType(PromotionTypeEnum.HALF_PRICE.name());
    detailDTO.setTitle(halfPriceVO.getTitle());

    //将活动商品入库
    this.promotionGoodsManager.add(halfPriceVO.getGoodsList(), detailDTO);

    cache.put(PromotionCacheKeys.getHalfPriceKey(
    halfPriceVO.getHpId()), halfPriceDO);

    //启用延时任务创建促销活动脚本信息
    PromotionScriptMsg promotionScriptMsg = new PromotionScriptMsg();
    promotionScriptMsg.setPromotionId(id);
    promotionScriptMsg.setPromotionName(halfPriceDO.getTitle());
    promotionScriptMsg.setPromotionType(PromotionTypeEnum.HALF_PRICE);
    promotionScriptMsg.setOperationType(ScriptOperationTypeEnum.CREATE);
    promotionScriptMsg.setEndTime(halfPriceDO.getEndTime());
    String uniqueKey = "{TIME_TRIGGER_" + PromotionTypeEnum.HALF_PRICE.name()
    + "}_" + id;
    timeTrigger.add(TimeExecute.SELLER_PROMOTION_SCRIPT_EXECUTER,
    promotionScriptMsg, halfPriceDO.getStartTime(), uniqueKey);

    return halfPriceVO;
    }
    }
  • 第二件半价促销活动脚本生成--PromotionScriptTimeTriggerExecuter

    @Component("promotionScriptTimeTriggerExecuter")
    public class PromotionScriptTimeTriggerExecuter implements TimeTriggerExecuter {

    @Override
    public void execute(Object object) {

    PromotionScriptMsg promotionScriptMsg = (PromotionScriptMsg) object;

    //如果是促销活动开始
    if (ScriptOperationTypeEnum.CREATE
    .equals(promotionScriptMsg.getOperationType())) {

    //创建促销活动脚本
    this.createScript(promotionScriptMsg);

    //促销活动开始后,立马设置一个促销活动结束的流程
    promotionScriptMsg.setOperationType(ScriptOperationTypeEnum.DELETE);
    String uniqueKey = "{TIME_TRIGGER_"
    + promotionScriptMsg.getPromotionType().name() + "}_"
    + promotionScriptMsg.getPromotionId();

    timeTrigger.add(TimeExecute.SELLER_PROMOTION_SCRIPT_EXECUTER,
    promotionScriptMsg, promotionScriptMsg.getEndTime(),
    uniqueKey);

    this.logger.debug("促销活动[" + promotionScriptMsg.getPromotionName()
    + "]开始,id=[" + promotionScriptMsg.getPromotionId()
    + "]");
    } else {

    //删除缓存中的促销脚本数据
    this.deleteScript(promotionScriptMsg);

    this.logger.debug("促销活动[" + promotionScriptMsg.getPromotionName()
    + "]结束,id=[" + promotionScriptMsg.getPromotionId()
    + "]");
    }
    }

    /**
    * 创建促销活动脚本
    * @param promotionScriptMsg 促销活动脚本消息信息
    */
    private void createScript(PromotionScriptMsg promotionScriptMsg) {
    //获取促销活动类型
    PromotionTypeEnum promotionType = promotionScriptMsg.getPromotionType();

    //获取促销活动ID
    Long promotionId = promotionScriptMsg.getPromotionId();

    //获取促销脚本相关数据
    ScriptVO scriptVO = this.getPromotionScript(promotionId, promotionType.name());

    //获取商家ID
    Long sellerId = scriptVO.getSellerId();
    //购物车(店铺)级别缓存key
    String cartCacheKey = CachePrefix.CART_PROMOTION.getPrefix() + sellerId;
    //获取商品参与促销活动的方式 1:全部商品参与,2:部分商品参与
    Integer rangeType = scriptVO.getRangeType();
    //构建促销脚本数据结构
    PromotionScriptVO promotionScriptVO = scriptVO.getPromotionScriptVO();

    //如果是全部商品都参与了促销活动
    if (rangeType.intValue() == 1) {

    //构建新的促销脚本数据
    List<PromotionScriptVO> scriptList
    = getScriptList(cartCacheKey, promotionScriptVO);

    //将促销脚本数据放入缓存中
    cache.put(cartCacheKey, scriptList);

    } else {
    //获取参与促销活动的商品集合
    List<PromotionGoodsDO> goodsList = scriptVO.getGoodsList();

    //批量放入缓存的数据集合
    Map<String, List<PromotionScriptVO>> cacheMap = new HashMap<>();

    //循环skuID集合,将脚本放入缓存中
    for (PromotionGoodsDO goods : goodsList) {

    PromotionScriptVO newScript = new PromotionScriptVO();
    BeanUtil.copyProperties(promotionScriptVO, newScript);

    //缓存key
    String cacheKey = CachePrefix.SKU_PROMOTION.getPrefix()
    + goods.getSkuId();

    //脚本结构中加入商品skuID
    newScript.setSkuId(goods.getSkuId());

    //构建新的促销脚本数据
    List<PromotionScriptVO> scriptList = getScriptList(cacheKey, newScript);

    cacheMap.put(cacheKey, scriptList);
    }

    //将sku促销脚本数据批量放入缓存中
    cache.multiSet(cacheMap);

    }
    }

    /**
    * 删除促销活动脚本
    * @param promotionScriptMsg 促销活动脚本消息信息
    */
    private void deleteScript(PromotionScriptMsg promotionScriptMsg) {
    //获取促销活动类型
    PromotionTypeEnum promotionType = promotionScriptMsg.getPromotionType();

    //获取促销活动ID
    Long promotionId = promotionScriptMsg.getPromotionId();

    if (PromotionTypeEnum.FULL_DISCOUNT.equals(promotionType)) {
    //满减满赠促销活动
    //...省略...

    } else if (PromotionTypeEnum.MINUS.equals(promotionType)) {
    //单品立减促销活动
    //...省略...

    } else if (PromotionTypeEnum.HALF_PRICE.equals(promotionType)) {
    //获取第二件半价促销活动信息
    HalfPriceVO halfPriceVO
    = this.promotionGoodsClient.getHalfPriceFromDB(promotionId);
    //初始化缓存中的促销活动脚本信息
    initScriptCache(halfPriceVO.getRangeType(),
    halfPriceVO.getSellerId(), promotionId,
    promotionType.name());
    }
    }

    /**
    * 根据促销活动ID和促销活动类型获取促销脚本数据
    * @param promotionId 促销活动id
    * @param promotionType 促销活动类型
    * @return
    */
    private ScriptVO getPromotionScript(Long promotionId, String promotionType) {

    ScriptVO scriptVO = new ScriptVO();

    if (PromotionTypeEnum.FULL_DISCOUNT.name().equals(promotionType)) {
    //满减满赠促销活动
    //...省略...

    } else if (PromotionTypeEnum.MINUS.name().equals(promotionType)) {
    //单品立减促销活动
    //...省略...

    } else if (PromotionTypeEnum.HALF_PRICE.name().equals(promotionType)) {

    //获取第二件半价促销活动信息
    HalfPriceVO halfPriceVO
    = this.promotionGoodsClient.getHalfPriceFromDB(promotionId);

    //渲染并读取第二件半价促销活动脚本信息
    String script = renderHalfPriceScript(halfPriceVO);

    String tips = "第二件半价";

    //构建促销脚本数据结构
    PromotionScriptVO promotionScriptVO
    = getScript(script, promotionId, false, promotionType, tips);

    scriptVO.setSellerId(halfPriceVO.getSellerId());
    scriptVO.setRangeType(halfPriceVO.getRangeType());
    scriptVO.setPromotionScriptVO(promotionScriptVO);

    }

    //如果是部分商品参与活动,需要查询出参与促销活动的商品skuID集合
    if (scriptVO.getRangeType().intValue() == 2) {
    List<PromotionGoodsDO> goodsList
    = this.promotionGoodsClient.getPromotionGoods(promotionId, promotionType);
    scriptVO.setGoodsList(goodsList);
    }

    return scriptVO;
    }

    /**
    * 渲染并读取第二件半价促销活动脚本信息
    * @param halfPriceVO 第二件半价促销活动信息
    * @return
    */
    private String renderHalfPriceScript(HalfPriceVO halfPriceVO) {
    Map<String, Object> model = new HashMap<>();

    Map<String, Object> params = new HashMap<>();
    params.put("startTime", halfPriceVO.getStartTime().toString());
    params.put("endTime", halfPriceVO.getEndTime().toString());

    model.put("promotionActive", params);

    String path = "half_price.ftl";
    String script = ScriptUtil.renderScript(path, model);

    logger.debug("生成第二件半价促销活动脚本:" + script);

    return script;
    }
    }
  • 第二件半价促销活动脚本引擎模板文件—half_price.ftl

    <#--
    验证促销活动是否在有效期内
    @param promotionActive 活动信息对象(内置常量)
    .startTime 获取开始时间
    .endTime 活动结束时间
    @param $currentTime 当前时间(变量)
    @returns {boolean}
    -->
    function validTime(){
    if (${promotionActive.startTime} <= $currentTime && $currentTime <= ${promotionActive.endTime}) {
    return true;
    }
    return false;
    }

    <#--
    第二件半价促销活动金额计算
    @param $sku 商品SKU信息对象(变量)
    .$price 商品SKU单价
    .$num 商品数量
    @returns {*}
    -->
    function countPrice() {
    <#--获取实际商品总金额-->
    var totalPrice = $sku.$price * $sku.$num;
    <#--应该优惠的金额-->
    var discountPrice = 0.00;

    <#--当商品数量大于1时才计算优惠-->
    if ($sku.$num > 1) {
    <#--如果商品数量对2进行取余得到的结果为0-->
    if ($sku.$num % 2 == 0) {
    <#--优惠金额 = 商品原价的一半 * 商品数量的一半-->
    discountPrice = ($sku.$price / 2) * ($sku.$num / 2);
    }
    <#--如果商品数量对2进行取余得到的结果为1-->
    if ($sku.$num % 2 == 1) {
    <#--优惠金额 = 商品原价的一半 * (商品数量 - 1)的一半-->
    discountPrice = ($sku.$price / 2) * (($sku.$num - 1) / 2);
    }
    }
    <#--最终返回优惠后的总金额 = 实际商品总金额 - 优惠金额-->
    totalPrice = totalPrice - discountPrice;
    return totalPrice < 0 ? 0 : totalPrice.toString();
    }