跳到主要内容

满减满赠

一、功能说明

1、满减满赠促销活动属于商家店铺可直接发布的促销活动。

2、满减满赠促销活动属于组合促销活动,其中包含减现金、打折、送积分、免邮费、送优惠券、送赠品。

其中减现金和打折只能选择一项。

送积分这项只有自营店铺才可选择。

3、同一个商家在同一时间段内只允许创建一个满减满赠促销活动。

4、商家在发布满减满赠活动时,可以选择店铺全部商品参与,也可以选择部分商品参与。

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

5、优惠券和赠品只能选择一个来参与活动。

6、满减满赠活动开始后,不允许修改和删除活动信息。

7、现阶段满减满赠活动赠送的积分和优惠券,在订单申请售后时,是不进行退还的。

二、数据库设计

1、表结构展示

  • 满减满赠促销活动表--es_full_discount

    字段名类型与长度说明
    fd_idbigint(20)主键ID
    full_moneydecimal(20,2)优惠门槛金额(订单金额必须达到这个数值才可享受优惠)
    minus_valuedecimal(20,2)减免的现金金额
    point_valueint(10)赠送的积分数量
    is_full_minusint(1)是否选择了减现金这种优惠方式,0:否,1:是
    is_free_shipint(1)是否选择了免邮费这种优惠方式,0:否,1:是
    is_send_pointint(1)是否选择了送积分这种优惠方式,0:否,1:是
    is_send_giftint(1)是否选择了送赠品这种优惠方式,0:否,1:是
    is_send_bonusint(1)是否选择了送优惠券这种优惠方式,0:否,1:是
    gift_idbigint(20)赠送的赠品ID(关联赠品表--es_full_discount_gift)
    bonus_idbigint(20)赠送的优惠券ID(关联优惠券表--es_coupon)
    is_discountint(1)是否选择了打折这种优惠方式,0:否,1:是
    discount_valuedecimal(20,2)打多少折
    start_timebigint(20)活动开始时间(存放的是以秒为单位的时间戳)
    end_timebigint(20)活动结束时间(存放的是以秒为单位的时间戳)
    titlevarchar(255)活动标题(活动名称)
    range_typeint(1)商品参与活动的方式,1:全部商品,2:部分商品
    disabledint(1)活动是否停用(删除),0:否,1:是
    descriptionlongtext活动说明(描述/简介)
    seller_idbigint(20)活动所属商家ID(关联商家店铺表--es_shop)
  • 赠品表--es_full_discount_gift

    字段名类型与长度说明
    gift_idbigint(20)主键ID
    gift_namevarchar(255)赠品名称
    gift_pricedecimal(20,2)赠品价格
    gift_imgvarchar(255)赠品图片
    gift_typeint(1)赠品类型(预留扩展字段,现阶段暂时无用)
    actual_storeint(10)赠品实际库存
    enable_storeint(10)赠品可用库存
    create_timebigint(20)创建时间
    goods_idbigint(20)商品ID(预留扩展字段,现阶段暂时无用)
    disabledint(1)是否禁用(删除),0:否,1:是
    seller_idbigint(20)活动所属商家ID(关联商家店铺表--es_shop)

    满减满赠选择赠送赠品时,现阶段只支持赠送一种赠品并且赠送的数量暂时只支持一个。

    赠品不是商品,不可以上架售卖,只允许在满减满赠活动中选择赠送。

    赠品可以设置库存,库存也分为实际库存和可用库存,当可用库存为0时不再赠送。

    赠品会和订单中的商品一起发货。

    关于赠品库存的扣减与恢复:

    扣减:下单扣减赠品的可用库存,发货扣减赠品的实际库存。

    恢复:下单未付款时,用户取消订单,恢复赠品的可用库存;订单申请售后并且售后服务完成时,同时恢复赠品的可用库存与实际库存。

2、表关联说明

  • 满减满赠活动表与其他表之间的关联图

    image-20201029153719971

  • 满减满赠活动表与其他表之间的关联字段对照

    满减满赠活动表(es_full_discount)赠品表(es_full_discount_gift)
    gift_idgift_id
    满减满赠活动表(es_full_discount)优惠券表(es_coupon)
    bonus_idcoupon_id
    满减满赠活动表(es_full_discount)促销活动商品表(es_promotion_goods)
    fd_idactivity_id
    无字段,促销类型值为FULL_DISCOUNTpromotion_type

三、缓存设计

1、商家在发布满减满赠促销活动时,在将促销活动信息入库的同时,也会将信息放入缓存中。

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

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

2、满减满赠促销活动脚本引擎

  • 脚本引擎缓存结构:

    满减满赠促销活动的促销脚本引擎缓存结构有两种:

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

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

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

四、代码展示

1、接口调用流程图

满减满赠接口调用关系图

2、相关代码展示

以新增满减满赠活动为例

  • API代码--FullDiscountSellerController

    @RestController
    @RequestMapping("/seller/promotion/full-discounts")
    @Api(description = "满优惠活动相关API")
    @Validated
    public class FullDiscountSellerController {

    @ApiOperation(value = "添加满优惠活动", response = FullDiscountVO.class)
    @PostMapping
    public FullDiscountVO add(@Valid @RequestBody FullDiscountVO fullDiscountVO) {

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

    return fullDiscountVO;
    }

    }
  • 新增满减满赠活动业务层代码--FullDiscountManagerImpl

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

    List<PromotionGoodsDTO> goodsDTOList = new ArrayList<>();
    //是否是全部商品参与
    if (fullDiscountVO.getRangeType() == 1) {
    PromotionGoodsDTO goodsDTO = new PromotionGoodsDTO();
    goodsDTO.setGoodsId(-1L);
    goodsDTO.setSkuId(-1L);
    goodsDTO.setGoodsName("全部商品");
    goodsDTO.setThumbnail("");
    goodsDTOList.add(goodsDTO);
    fullDiscountVO.setGoodsList(goodsDTOList);
    }

    this.verifyRule(fullDiscountVO.getGoodsList());

    // 查看是否赠送优惠券,是则判断优惠券的使用时间是否大于活动结束时间
    if (fullDiscountVO.getIsSendBonus() != null
    && fullDiscountVO.getIsSendBonus() == 1) {
    Long couponId = fullDiscountVO.getBonusId();
    couponManager.getModel(couponId);
    CouponDO coupon = couponManager.getModel(couponId);
    if(coupon.getEndTime()<fullDiscountVO.getEndTime()){
    throw new ServiceException(PromotionErrorCode.E401.code(),
    "赠送的优惠券有效时间必须大于活动时间");
    }
    }

    //新建满减满赠活动对象
    FullDiscountDO fullDiscountDO = new FullDiscountDO();
    //从VO中复制相关属性值
    BeanUtils.copyProperties(fullDiscountVO, fullDiscountDO);
    //满减满赠活动信息入库
    fullDiscountMapper.insert(fullDiscountDO);

    //获取活动Id
    Long id = fullDiscountDO.getFdId();
    fullDiscountVO.setFdId(id);

    //设置促销信息
    PromotionDetailDTO detailDTO = new PromotionDetailDTO();
    detailDTO.setStartTime(fullDiscountVO.getStartTime());
    detailDTO.setEndTime(fullDiscountVO.getEndTime());
    detailDTO.setActivityId(fullDiscountVO.getFdId());
    detailDTO.setPromotionType(PromotionTypeEnum.FULL_DISCOUNT.name());
    detailDTO.setTitle(fullDiscountVO.getTitle());

    //将活动商品入库
    this.promotionGoodsManager.add(fullDiscountVO.getGoodsList(), detailDTO);
    cache.put(PromotionCacheKeys.getFullDiscountKey(id), fullDiscountDO);

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

    return fullDiscountVO;
    }

    由此方法可以看出:

    • 首先是满减满赠促销活动主体信息入库:

      //满减满赠活动信息入库
      fullDiscountMapper.insert(fullDiscountDO);
    • 其次将参与活动的商品信息入库:

      //将活动商品入库
      this.promotionGoodsManager.add(fullDiscountVO.getGoodsList(), detailDTO);
    • 然后把活动主体信息放入缓存中:

      cache.put(PromotionCacheKeys.getFullDiscountKey(id), fullDiscountDO);
    • 最后启用延时任务创建促销活动脚本信息:

      //启用延时任务创建促销活动脚本信息
      PromotionScriptMsg promotionScriptMsg = new PromotionScriptMsg();
      promotionScriptMsg.setPromotionId(id);
      promotionScriptMsg.setPromotionName(fullDiscountDO.getTitle());
      promotionScriptMsg.setPromotionType(PromotionTypeEnum.FULL_DISCOUNT);
      promotionScriptMsg.setOperationType(ScriptOperationTypeEnum.CREATE);
      promotionScriptMsg.setEndTime(fullDiscountDO.getEndTime());
      String uniqueKey = "{TIME_TRIGGER_" + PromotionTypeEnum.FULL_DISCOUNT.name()
      + "}_" + id;
      timeTrigger.add(TimeExecute.SELLER_PROMOTION_SCRIPT_EXECUTER,
      promotionScriptMsg, fullDiscountDO.getStartTime(), uniqueKey);
  • 满减满赠促销活动脚本生成--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)) {
    //获取满减满赠促销活动详细信息
    FullDiscountVO fullDiscountVO =
    this.promotionGoodsClient.getFullDiscountModel(promotionId);
    //初始化缓存中的促销活动脚本信息
    initScriptCache(fullDiscountVO.getRangeType(),
    fullDiscountVO.getSellerId(), promotionId,
    promotionType.name());

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

    } else if (PromotionTypeEnum.HALF_PRICE.equals(promotionType)) {
    //第二件半价促销活动
    //...省略...
    }
    }

    /**
    * 根据促销活动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)) {
    //获取满减满赠促销活动详细信息
    FullDiscountVO fullDiscountVO =
    this.promotionGoodsClient.getFullDiscountModel(promotionId);

    //渲染并读取满减满赠促销活动脚本信息
    String script = renderFullDiscountScript(fullDiscountVO);

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

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

    } else if (PromotionTypeEnum.MINUS.name().equals(promotionType)) {
    //单品立减促销活动
    //...省略...
    } else if (PromotionTypeEnum.HALF_PRICE.name().equals(promotionType)) {
    //第二件半价促销活动
    //...省略...
    }

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

    /**
    * 渲染并读取满减满赠促销活动脚本信息
    * @param fullDiscountVO 满减满赠促销活动信息
    * @return
    */
    private String renderFullDiscountScript(FullDiscountVO fullDiscountVO) {
    Map<String, Object> model = new HashMap<>();

    Map<String, Object> params = new HashMap<>();
    params.put("startTime", fullDiscountVO.getStartTime().toString());
    params.put("endTime", fullDiscountVO.getEndTime().toString());
    params.put("isFullMinus", fullDiscountVO.getIsFullMinus());
    params.put("isDiscount", fullDiscountVO.getIsDiscount());
    params.put("fullMoney", fullDiscountVO.getFullMoney());
    params.put("minusValue", fullDiscountVO.getMinusValue()
    == null ? 0.00 : fullDiscountVO.getMinusValue());
    params.put("discountValue", fullDiscountVO.getDiscountValue()
    == null ? 0.00 : CurrencyUtil.div(fullDiscountVO.getDiscountValue(),
    10, 2));

    List<Map<String, Object>> giftList = new ArrayList<>();

    //判断是否免邮费
    if (fullDiscountVO.getIsFreeShip().intValue() == 1) {
    Map<String, Object> free = new HashMap<>();
    free.put("type", "freeShip");
    free.put("value", true);
    giftList.add(free);
    }

    //判断是否送积分
    if (fullDiscountVO.getIsSendPoint().intValue() == 1) {
    Map<String, Object> point = new HashMap<>();
    point.put("type", "point");
    point.put("value", fullDiscountVO.getPointValue());
    giftList.add(point);
    }

    //判断是否送赠品
    if (fullDiscountVO.getIsSendGift().intValue() == 1 ) {
    Map<String, Object> gift = new HashMap<>();
    gift.put("type", "gift");
    gift.put("value", fullDiscountVO.getGiftId().toString());
    giftList.add(gift);
    }

    //判断是否送优惠券
    if (fullDiscountVO.getIsSendBonus().intValue() == 1) {
    Map<String, Object> coupon = new HashMap<>();
    coupon.put("type", "coupon");
    coupon.put("value", fullDiscountVO.getBonusId().toString());
    giftList.add(coupon);
    }

    params.put("gift", JsonUtil.objectToJson(giftList));

    model.put("promotionActive", params);

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

    logger.debug("生成满减满赠促销活动脚本:" + script);

    return script;
    }
    }
  • 满减满赠促销活动脚本引擎模板文件--full_discount.ftl

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

    <#--
    减现金或打折活动价格计算
    @param promotionActive 促销活动信息对象(内置常量)
    .fullMoney 优惠门槛金额
    .isFullMinus 是否立减现金 0:否,1:是
    .minusValue 立减金额
    .isDiscount 是否打折 0:否,1:是
    .discountValue 打多少折,例:打8折,值为0.8
    @param $price 参与活动的商品总金额(变量)
    @returns {*}
    -->
    function countPrice() {
    <#--判断商品金额是否满足优惠条件 -->
    if (${promotionActive.fullMoney} <= $price) {
    var resultPrice = $price;
    if (${promotionActive.isFullMinus} == 1) {
    resultPrice = $price - ${promotionActive.minusValue};
    }
    if (${promotionActive.isDiscount} == 1) {
    resultPrice = $price * ${promotionActive.discountValue};
    }
    return resultPrice < 0 ? 0 : resultPrice.toString();
    }
    return $price;
    }

    <#--
    赠送赠品
    @param promotionActive 促销活动信息对象(内置常量)
    .fullMoney 优惠门槛金额
    .gift 赠品信息对象json
    @param $price 参与活动的商品总金额(变量)
    @returns {*}
    -->
    function giveGift() {
    <#-- 判断商品金额是否满足优惠条件 -->
    if (${promotionActive.fullMoney} <= $price) {
    return JSON.stringify(${promotionActive.gift});
    }
    return null;
    }