跳到主要内容

优惠券

一、功能说明

1、优惠券共有两种,一种是平台优惠券,一种是商家优惠券。

平台优惠券:平台发布,优惠金额由平台、商家各自承担一部分。

商家优惠券:商家发布,优惠金额由商家承担。

2、用户可以通过两种方式获取优惠券:免费领取和活动赠送。

免费领取:可在优惠券列表或店铺首页领取。

活动赠送:商家发布满减满赠活动时选择赠送优惠券,用户购买活动商品获取。

3、使用范围:

平台优惠券:全品类、指定商品分类和指定商品。

店铺优惠券:本店铺下所有商品。

4、优惠券生效后不可以进行修改和删除操作。

5、只有在订单结算页面才可以使用优惠券,优惠券不可叠加使用,同一订单只能使用一张优惠券。

6、优惠券可以设置限领数量,如果不想做限制,设置限领数量为0即可。

7、订单申请售后,使用的优惠券不会退还。

8、优惠券使用限制:

积分商品不允许使用优惠券。

拼团商品不允许使用优惠券。

9、商品参与了其它促销活动(例如满减满折、单品立减、第二件半价、团购和秒杀),可以使用优惠券。

根据促销前的金额来判断促销优惠和优惠券是否可以同时使用,例如:

用户购买商品A,原价为2500,促销后金额2000元,用户有一张满2500减200优惠券,一张满2000减50的优惠券,两张券都可以使用。

二、数据库设计

1、表结构设计

  1. 优惠券表—es_coupon

    字段名类型与长度说明
    coupon_idbigint(20)主键ID
    titlevarchar(20)优惠券标题(名称)
    coupon_pricedecimal(20,2)优惠券金额
    coupon_threshold_pricedecimal(20,2)优惠券门槛金额(当订单满足这个金额才可使用)
    start_timebigint(20)优惠券生效时间(存放的是以秒为单位的时间戳)
    end_timebigint(20)优惠券失效时间(存放的是以秒为单位的时间戳)
    create_numint(10)优惠券发行数量
    limit_numint(10)用户限领数量
    used_numint(10)已被使用数量
    seller_idbigint(20)商家ID(关联es_shop表)
    received_numint(10)已被领取的数量
    seller_namevarchar(100)商家名称
    typevarchar(20)优惠券获取方式 FREE_GET:免费领取,ACTIVITY_GIVE:活动赠送
    use_scopevarchar(20)优惠券使用范围 ALL:全平台,CATEGORY:指定分类下的商品,SOME_GOODS:指定商品(如果是店铺优惠券此值为空)
    scope_idlongtext使用范围关联的ID(如果是店铺优惠券此值为空;如果是平台优惠券,使用范围如果是全平台,此值为0)
    shop_commissionint(5)店铺承担比例(如果是店铺优惠券此值为空)
    scope_descriptionlongtext使用范围说明
    activity_descriptionlongtext优惠券说明
  1. 会员优惠券表—es_member_coupon

    字段名类型与长度说明
    mc_idbigint(20)主键ID
    coupon_idbigint(20)优惠券ID(关联es_coupon表)
    member_idbigint(20)会员ID(关联es_member表)
    used_timebigint(11)使用时间
    create_timebigint(11)获取时间
    order_idbigint(20)订单ID(关联es_order表)
    order_snvarchar(30)订单编号(关联es_order表)
    member_namevarchar(30)会员名称
    titlevarchar(20)优惠券标题(名称)
    coupon_pricedecimal(20,2)优惠券金额
    coupon_threshold_pricedecimal(20,2)优惠券门槛金额(当订单满足这个金额才可使用)
    start_timebigint(20)优惠券生效时间(存放的是以秒为单位的时间戳)
    end_timebigint(20)优惠券失效时间(存放的是以秒为单位的时间戳)
    used_statussmallint(1)使用状态 0:未使用,1:已使用,2:已过期,3:已作废
    seller_idbigint(20)商家ID(关联es_shop表)
    seller_namevarchar(50)商家名称
    use_scopevarchar(20)优惠券使用范围 ALL:全平台,CATEGORY:指定分类下的商品,SOME_GOODS:指定商品(如果是店铺优惠券此值为空)
    scope_idlongtext使用范围关联的ID(如果是店铺优惠券此值为空;如果是平台优惠券,使用范围如果是全平台,此值为0)

2、表关联说明

  • 优惠券表与其他表之间的关联图

    image-20201104180206000

  • 优惠券表与其他表之间的关联字段对照

    优惠券表(es_coupon)会员优惠券表(es_member_coupon)
    coupon_idcoupon_id

三、缓存设计

优惠券脚本引擎

  • 脚本引擎缓存结构:优惠券级别的缓存。
  • 脚本引擎生成和删除时机:
    • 生成:优惠券生效时生成。
    • 删除:活动结束时删除。

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

四、代码设计

1、接口调用流程图

  • 平台和商家发布优惠券

    image-20201104182708888

  • 用户领取优惠券

    image-20201105095850796

  • 使用优惠券

    image-20201105095746586

2、相关代码展示

  • 发布优惠券

    @RestController
    @RequestMapping("/seller/promotion/coupons")
    @Api(description = "优惠券相关API")
    @Validated
    public class CouponSellerController {

    @ApiOperation(value = "添加优惠券", response = CouponDO.class)
    @PostMapping
    public CouponDO add(@Valid CouponDO couponDO) {

    Seller seller = UserContext.getSeller();

    couponDO.setSellerId(seller.getSellerId());
    couponDO.setSellerName(seller.getSellerName());

    this.couponManager.add(couponDO);

    return couponDO;
    }
    }
    @Service
    public class CouponManagerImpl implements CouponManager {

    @Override
    @Transactional(propagation = Propagation.REQUIRED, rollbackFor
    = {ServiceException.class, RuntimeException.class, Exception.class})
    public CouponDO add(CouponDO coupon) {
    //检测开始时间和结束时间
    PromotionValid.paramValid(coupon.getStartTime(), coupon.getEndTime(), 1, null);
    if (coupon.getLimitNum() < 0) {
    throw new ServiceException(PromotionErrorCode.E406.code(),
    "限领数量不能为负数");
    }
    //校验每人限领数是都大于发行量
    if (coupon.getLimitNum() > coupon.getCreateNum()) {
    throw new ServiceException(PromotionErrorCode.E405.code(),
    "限领数量超出发行量");
    }
    if (coupon.getCouponPrice() == null || coupon.getCouponPrice() <= 0) {
    throw new ServiceException(PromotionErrorCode.E409.code(),
    "优惠券面额必须大于0元");
    }
    if (coupon.getCouponThresholdPrice() == null ||
    coupon.getCouponThresholdPrice() <= 0) {
    throw new ServiceException(PromotionErrorCode.E409.code(),
    "优惠券门槛价格必须大于0元");
    }
    //校验优惠券面额是否小于门槛价格
    if (coupon.getCouponPrice() >= coupon.getCouponThresholdPrice()) {
    throw new ServiceException(PromotionErrorCode.E409.code(),
    "优惠券面额必须小于优惠券门槛价格");
    }

    //开始时间取前段+00:00:00 结束时间取前段+23:59:59
    String startStr = DateUtil.toString(coupon.getStartTime(), "yyyy-MM-dd");
    String endStr = DateUtil.toString(coupon.getEndTime(), "yyyy-MM-dd");

    coupon.setStartTime(DateUtil.getDateline(startStr + " 00:00:00"));
    coupon.setEndTime(DateUtil.getDateline(endStr + " 23:59:59",
    "yyyy-MM-dd hh:mm:ss"));

    this.paramValid(coupon.getStartTime(), coupon.getEndTime());

    coupon.setReceivedNum(0);
    coupon.setUsedNum(0);
    //部分商品和分类的id存储增加,,
    if (CouponUseScope.SOME_GOODS.name().equals(coupon.getUseScope()) ||
    CouponUseScope.CATEGORY.name().equals(coupon.getUseScope())) {
    coupon.setScopeId("," + coupon.getScopeId() + ",");
    }

    this.couponMapper.insert(coupon);

    //启用延时任务创建优惠券脚本信息
    PromotionScriptMsg promotionScriptMsg = new PromotionScriptMsg();
    promotionScriptMsg.setPromotionId(coupon.getCouponId());
    promotionScriptMsg.setPromotionName(coupon.getTitle());
    promotionScriptMsg.setPromotionType(PromotionTypeEnum.COUPON);
    promotionScriptMsg.setOperationType(ScriptOperationTypeEnum.CREATE);
    promotionScriptMsg.setEndTime(coupon.getEndTime());
    String uniqueKey = "{TIME_TRIGGER_" + PromotionTypeEnum.COUPON.name()
    + "}_" + coupon.getCouponId();
    timeTrigger.add(TimeExecute.COUPON_SCRIPT_EXECUTER, promotionScriptMsg,
    coupon.getStartTime(), uniqueKey);

    return coupon;
    }
    }
  • 用户领取优惠券

    @RestController
    @RequestMapping("/members/coupon")
    @Api(description = "会员优惠券相关API")
    @Validated
    public class MemberCouponBuyerController {

    @ApiOperation(value = "用户领取优惠券")
    @ApiImplicitParams({
    @ApiImplicitParam(name = "coupon_id", value = "优惠券id", required = true,
    dataType = "String", paramType = "path")
    })
    @PostMapping(value = "/{coupon_id}/receive")
    public String receiveBonus(@ApiIgnore @PathVariable("coupon_id") Long couponId) {
    //限领检测
    this.memberCouponManager.checkLimitNum(couponId);
    Buyer buyer = UserContext.getBuyer();
    this.memberCouponManager.receiveBonus(buyer.getUid(),
    buyer.getUsername(), couponId);
    return "";
    }
    }
    @Service
    public class MemberCouponManagerImpl implements MemberCouponManager {

    @Override
    @Transactional(value = "memberTransactionManager", propagation =
    Propagation.REQUIRED, rollbackFor = Exception.class)
    public void receiveBonus(Long memberId, String memberName, Long couponId) {
    //根据优惠券id获取优惠券信息
    CouponDO couponDO = this.couponClient.getModel(couponId);
    //如果会员id不为空
    if (memberId != null) {
    //添加会员优惠券表
    MemberCoupon memberCoupon = new MemberCoupon(couponDO);
    //设置优惠券创建时间
    memberCoupon.setCreateTime(DateUtil.getDateline());
    //设置会员ID
    memberCoupon.setMemberId(memberId);
    //设置会员名称
    memberCoupon.setMemberName(memberName);
    //设置优惠券使用状态 0:未使用,1:已使用,2:已过期,3:已作废
    memberCoupon.setUsedStatus(0);
    //会员优惠券入库
    memberCouponMapper.insert(memberCoupon);
    // 修改优惠券已被领取的数量
    this.couponClient.addReceivedNum(couponId);
    }
    }

    @Override
    public void checkLimitNum(Long couponId) {
    //根据优惠券ID获取优惠券信息
    CouponDO couponDO = this.couponClient.getModel(couponId);
    //获取当前登录的会员信息
    Buyer buyer = UserContext.getBuyer();
    //获取每人限领数量
    int limitNum = couponDO.getLimitNum();
    //新建查询条件包装器
    QueryWrapper<MemberCoupon> wrapper = new QueryWrapper<>();
    //以会员id为查询条件
    wrapper.eq("member_id", buyer.getUid());
    //以优惠券id为查询条件
    wrapper.eq("coupon_id", couponId);
    //查询会员优惠券数量
    int num = memberCouponMapper.selectCount(wrapper);
    //判断优惠券是否已被领完
    if (couponDO.getReceivedNum() >= couponDO.getCreateNum()) {
    throw new ServiceException(MemberErrorCode.E203.code(), "优惠券已被领完");
    }
    //判断优惠券是否已达到限领数量
    if (limitNum != 0 && num >= limitNum) {
    throw new ServiceException(MemberErrorCode.E203.code(),
    "优惠券限领" + limitNum + "个");
    }
    }
    }
  • 使用优惠券

    @Api(description = "购物车价格计算API")
    @RestController
    @RequestMapping("/trade/promotion")
    @Validated
    public class TradePromotionController {

    @ApiOperation(value = "设置优惠券", notes = "使用优惠券的时候分为三种情况:前2种情况couponId 不为0,不为空。第3种情况couponId为0," +
    "1、使用优惠券:在刚进入订单结算页,为使用任何优惠券之前。" +
    "2、切换优惠券:在1、情况之后,当用户切换优惠券的时候。" +
    "3、取消已使用的优惠券:用户不想使用优惠券的时候。")
    @ApiImplicitParams({
    @ApiImplicitParam(name = "seller_id", value = "店铺ID", required = true,
    dataType = "int", paramType = "path"),
    @ApiImplicitParam(name = "mc_id", value = "优惠券ID", required = true,
    dataType = "int", paramType = "path"),
    @ApiImplicitParam(name = "way", value = "结算方式,BUY_NOW:立即购买,CART:"
    +"购物车", required = true, dataType = "String")
    })
    @PostMapping(value = "/{seller_id}/seller/{mc_id}/coupon")
    public void setCoupon(@NotNull(message = "店铺id不能为空")
    @PathVariable("seller_id") Long sellerId,
    @NotNull(message = "优惠券id不能为空")
    @PathVariable("mc_id") Long mcId,
    @NotNull(message = "结算方式不能为空")
    @RequestParam String way) {

    CartBuilder cartBuilder =
    new DefaultCartBuilder(CartType.CART, cartSkuRenderer, null,
    cartPriceCalculator, checkDataRebderer);
    CartView cartViewold =
    cartBuilder.renderSku(CheckedWay.valueOf(way)).countPrice(false).build();
    //先判断优惠券能否使用
    MemberCoupon memberCoupon = promotionManager.detectCoupon(
    sellerId, mcId, cartViewold.getCartList());
    //查询要结算的某卖家的购物信息
    CartView cartView = cartBuilder.renderSku(CheckedWay.valueOf(way))
    .countPrice(true).build();
    //设置优惠券 goodsPrice
    promotionManager.useCoupon(sellerId, mcId, cartView.getCartList(),
    memberCoupon);
    }
    }
    @Service
    public class CartPromotionManagerImpl implements CartPromotionManager {

    @Override
    public void useCoupon(Long sellerId, Long mcId, List<CartVO> cartList,
    MemberCoupon memberCoupon) {
    if (memberCoupon != null) {
    //查询选中的促销
    SelectedPromotionVo selectedPromotionVo = getSelectedPromotion();
    CouponVO couponVO = new CouponVO(memberCoupon);
    SelectedPromotionVo selectedPromotion = getSelectedPromotion();
    selectedPromotion.putCooupon(sellerId, couponVO);
    logger.debug("使用优惠券:" + couponVO);
    logger.debug("促销信息为:" + selectedPromotionVo);
    String cacheKey = this.getOriginKey();
    cache.put(cacheKey, selectedPromotion);
    }
    }
    }
  • 创建和删除优惠券促销脚本

    @Component("couponScriptTimeTriggerExecuter")
    public class CouponScriptTimeTriggerExecuter implements TimeTriggerExecuter {

    @Override
    public void execute(Object object) {

    PromotionScriptMsg promotionScriptMsg = (PromotionScriptMsg) object;

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

    //优惠券级别缓存key
    String cacheKey = CachePrefix.COUPON_PROMOTION.getPrefix() + promotionId;

    //如果是优惠券开始生效
    if (ScriptOperationTypeEnum.CREATE.equals(
    promotionScriptMsg.getOperationType())) {

    //获取优惠券详情
    CouponDO coupon = this.couponClient.getModel(promotionId);

    //渲染并读取优惠券脚本信息
    String script = renderCouponScript(coupon);

    //将优惠券脚本信息放入缓存
    cache.put(cacheKey, script);

    //优惠券生效后,立马设置一个优惠券失效的流程
    promotionScriptMsg.setOperationType(ScriptOperationTypeEnum.DELETE);
    String uniqueKey = "{TIME_TRIGGER_"
    + promotionScriptMsg.getPromotionType().name()
    + "}_" + promotionId;
    timeTrigger.add(TimeExecute.COUPON_SCRIPT_EXECUTER, promotionScriptMsg,
    promotionScriptMsg.getEndTime(), uniqueKey);

    this.logger.debug("优惠券[" + promotionScriptMsg.getPromotionName()
    + "]开始生效,id=[" + promotionId + "]");
    } else {

    //删除缓存中的促销脚本数据
    cache.remove(cacheKey);

    this.logger.debug("优惠券[" + promotionScriptMsg.getPromotionName()
    + "]已经失效,id=[" + promotionId + "]");
    }
    }

    /**
    * 渲染并读取优惠券脚本信息
    * @param coupon 优惠券信息
    * @return
    */
    private String renderCouponScript(CouponDO coupon) {
    Map<String, Object> model = new HashMap<>();

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

    model.put("coupon", params);

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

    logger.debug("生成优惠券脚本:" + script);

    return script;
    }
    }
  • 优惠券脚本引擎模板文件—coupon.ftl

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

    <#--
    优惠券金额计算
    @param coupon 优惠券信息对象(内置常量)
    .couponPrice 优惠券面额
    @param $price 商品总价(变量,如果有商品参与了其它促销活动,为商品优惠后的总价)
    @returns {*}
    -->
    function countPrice() {
    var resultPrice = $price - ${coupon.couponPrice};
    return resultPrice < 0 ? 0 : resultPrice.toString();
    }