跳到主要内容

限时抢购

一、功能说明

1、限时抢购促销活动属于平台发布的促销活动,商家选择商品参与,平台来进行审核。

2、平台在同一时间段内只允许创建一个限时抢购促销活动。

3、限时抢购活动开始后,不允许修改和删除活动信息。

4、平台添加限时抢购活动时,报名截止日期不可以大于活动日期。

活动日期为限时抢购活动持续运行的那一天,当天00:00为活动开始时间。因此,报名截止时间最晚设置到活动日期的前一天的23:59:59。

5、平台添加限时抢购活动时,抢购阶段必须设置的是0-23的整点数字。

6、商家报名参与限时抢购活动,每个抢购阶段都可以选择要参与活动的商品,可以添加多个,但是每个抢购阶段的商品不可以重复。

7、商家报名参与限时抢购活动后,需要平台进行审核,审核不通过不可再次报名。

8、限时抢购活动属于单品促销活动,用户在购物车中可以选择取消活动。

二、数据库设计

1、表结构设计

  1. 限时抢购促销活动表—es_seckill

    字段名类型与长度说明
    seckill_idbigint(20)主键ID
    seckill_namevarchar(50)活动名称
    start_daybigint(20)活动日期
    apply_end_timebigint(20)活动报名截止时间
    seckill_rulevarchar(1000)活动申请规则
    seller_idslongtext参与活动的商家ID,如1,2,3
    seckill_statusvarchar(50)活动状态 EDITING:编辑中,RELEASE:已发布,OVER:已结束
    delete_statusvarchar(255)删除状态 DELETED:已删除,NORMAL:正常
  1. 限时抢购促销活动时刻表—es_seckill_range

    字段名类型与长度说明
    range_idbigint(20)主键ID
    seckill_idbigint(20)限时抢购活动ID(关联es_seckill表)
    range_timeint(10)抢购时间的整点时刻
  1. 限时抢购促销活动商品申请表—es_seckill_apply

    字段名类型与长度说明
    apply_idbigint(20)主键ID
    seckill_idbigint(20)限时抢购促销活动ID(关联es_seckill表)
    time_lineint(10)限时抢购时间的整点时刻
    start_daybigint(20)活动日期
    goods_idbigint(20)商品ID(关联es_goods表)
    goods_namevarchar(255)商品名称
    sku_idbigint(20)商品sku(关联es_goods_sku表)
    seller_idbigint(20)商家ID(关联es_shop表)
    shop_namevarchar(50)店铺名称
    pricedecimal(20,2)活动价格
    sold_quantityint(10)商品售空数量
    statusvarchar(50)申请状态 APPLY:申请中,PASS:已通过,FAIL:已驳回
    fail_reasonvarchar(500)申请驳回的原因
    sales_numint(10)已售数量
    original_pricedecimal(20,2)商品原价
    specslongtext商品规格信息

2、表关联说明

  • 限时抢购相关表与表关联图

    image-20201102171849270

  • 限时抢购相关表与表之间的关联字段对照

    限时抢购促销活动表(es_seckill)促销活动商品表(es_promotion_goods)
    seckill_idactivity_id
    无字段,促销类型值为SECKILLpromotion_type
    限时抢购促销活动表(es_seckill)限时抢购促销活动时刻表(es_seckill_range)
    seckill_idseckill_id
    限时抢购促销活动表(es_seckill)限时抢购促销活动商品申请表(es_seckill_apply)
    seckill_idseckill_id

三、缓存设计

1、商家在选择商品参与限时抢购活动后,会将参与限时抢购活动的商品按活动时刻分组,然后存放在缓存中

  • 缓存key值为:{STOREID_SECKILL_KEY}活动开始时间。时间格式为(年月日):20201215
  • 缓存value值为:是一个类型为Map<Integer, List<SeckillGoodsVO>>的集合。这个Map集合的键值对为:

    key:活动的时刻,Integer类型,如果活动时刻为8点整,那么这个key就是8

    value:是当前时刻参与活动商品的集合。

2、限时抢购促销活动脚本引擎

  • 脚本引擎缓存结构:

    限时抢购促销活动的促销脚本引擎缓存结构只有一种:SKU级别的缓存结构。

  • 脚本引擎生成和删除时机:

    • 生成:平台审核通过参与活动的商品时生成。
    • 删除:活动结束时删除。

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

四、代码设计

1、接口调用流程图

  • 平台发布促销活动

    image-20201103170300022

  • 商家选择商品参与限时抢购活动

    image-20201103174303093

  • 平台审核商家参与活动的商品

    image-20201103174726333

2、相关代码展示

  • 新增和修改限时抢购活动

    @RestController
    @RequestMapping("/admin/promotion/seckills")
    @Api(description = "限时抢购活动相关API")
    @Validated
    public class SeckillManagerController {

    @ApiOperation(value = "添加限时抢购入库", response = SeckillVO.class)
    @PostMapping
    public SeckillVO add(@Valid @RequestBody SeckillVO seckill) {
    this.verifyParam(seckill);
    seckill.setSeckillStatus(SeckillStatusEnum.EDITING.name());
    this.seckillManager.add(seckill);
    return seckill;
    }

    @PutMapping(value = "/{id}")
    @ApiOperation(value = "修改限时抢购入库", response = SeckillDO.class)
    @ApiImplicitParams({
    @ApiImplicitParam(name = "id", value = "主键",
    required = true, dataType = "int", paramType = "path")
    })
    public SeckillVO edit(@Valid @RequestBody SeckillVO seckill,
    @PathVariable @NotNull(message = "限时抢购ID参数错误") Long id) {
    this.verifyParam(seckill);
    this.seckillManager.edit(seckill, id);
    return seckill;
    }
    }
    @Service
    public class SeckillManagerImpl extends AbstractPromotionRuleManagerImpl implements SeckillManager {

    @Override
    @Transactional(propagation = Propagation.REQUIRED,
    rollbackFor = {ServiceException.class, RuntimeException.class})
    public SeckillVO add(SeckillVO seckill) {

    //验证活动名称是否为空
    this.checkName(seckill.getSeckillName(), null);

    String date = DateUtil.toString(seckill.getStartDay(), "yyyy-MM-dd");
    long startTime = DateUtil.getDateline(date + " 00:00:00",
    "yyyy-MM-dd HH:mm:ss");
    long endTime = DateUtil.getDateline(date + " 23:59:59", "yyyy-MM-dd HH:mm:ss");
    this.verifyTime(startTime, endTime, PromotionTypeEnum.SECKILL, null);

    SeckillDO seckillDO = new SeckillDO();
    seckillDO.setDeleteStatus(DeleteStatusEnum.NORMAL.value());
    BeanUtils.copyProperties(seckill, seckillDO);
    seckillMapper.insert(seckillDO);

    Long id = seckillDO.getSeckillId();

    this.seckillRangeManager.addList(seckill.getRangeList(), id);

    //开启延时任务执行器
    openTimeExecuter(seckill, endTime, id);

    return seckill;
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRED,
    rollbackFor = {ServiceException.class})
    public SeckillVO edit(SeckillVO seckill, Long id) {

    //验证活动名称是否为空
    this.checkName(seckill.getSeckillName(), id);

    String date = DateUtil.toString(seckill.getStartDay(), "yyyy-MM-dd");
    long startTime = DateUtil.getDateline(date + " 00:00:00",
    "yyyy-MM-dd HH:mm:ss");
    long endTime = DateUtil.getDateline(date + " 23:59:59", "yyyy-MM-dd HH:mm:ss");
    PromotionValid.paramValid(startTime, endTime, 1, null);
    this.verifyTime(startTime, endTime, PromotionTypeEnum.SECKILL, id);

    SeckillDO seckillDO = new SeckillDO();
    BeanUtils.copyProperties(seckill, seckillDO);

    seckillDO.setSeckillId(id);
    seckillMapper.updateById(seckillDO);

    this.seckillRangeManager.addList(seckill.getRangeList(), id);

    //开启延时任务执行器
    openTimeExecuter(seckill, endTime, id);

    return seckill;
    }
    }
  • 发布限时抢购活动

    @RestController
    @RequestMapping("/admin/promotion/seckills")
    @Api(description = "限时抢购活动相关API")
    @Validated
    public class SeckillManagerController {

    @ApiOperation(value = "发布限时抢购活动")
    @PostMapping("/{seckill_id}/release")
    @ApiImplicitParams({
    @ApiImplicitParam(name = "seckill_id", value = "要查询的限时抢购入库主键",
    required = true, dataType = "int", paramType = "path")
    })
    public SeckillVO publish(@Valid @RequestBody SeckillVO seckill,
    @ApiIgnore @PathVariable("seckill_id") Long seckillId) {

    this.verifyParam(seckill);
    //发布状态
    seckill.setSeckillStatus(SeckillStatusEnum.RELEASE.name());
    if (seckillId == null || seckillId == 0) {
    seckillManager.add(seckill);
    } else {
    seckillManager.edit(seckill, seckillId);
    }

    return seckill;
    }
    }
  • 平台审核商家参与限时抢购活动的商品

    @RestController
    @RequestMapping("/admin/promotion/seckills")
    @Api(description = "限时抢购活动相关API")
    @Validated
    public class SeckillManagerController {

    @ApiOperation(value = "批量审核商品")
    @PostMapping(value = "/batch/audit")
    public String batchAudit(@Valid @RequestBody SeckillAuditParam param) {

    this.seckillManager.batchAuditGoods(param);
    return "";
    }
    }
    @Service
    public class SeckillManagerImpl extends AbstractPromotionRuleManagerImpl implements SeckillManager {

    @Override
    @Transactional(propagation = Propagation.REQUIRED,
    rollbackFor = {ServiceException.class, RuntimeException.class})
    public void batchAuditGoods(SeckillAuditParam param) {
    if (param.getApplyIds() == null || param.getApplyIds().length == 0) {
    throw new ServiceException(GoodsErrorCode.E301.code(), "请选择要审核的商品");
    }

    if (StringUtil.isEmpty(param.getStatus())) {
    throw new ServiceException(PromotionErrorCode.E400.code(), "审核状态值不正确");
    }

    //状态值不正确
    SeckillGoodsApplyStatusEnum applyStatusEnum =
    SeckillGoodsApplyStatusEnum.valueOf(param.getStatus());

    //驳回,原因必填
    if (applyStatusEnum.equals(SeckillGoodsApplyStatusEnum.FAIL)) {
    if (StringUtil.isEmpty(param.getFailReason())) {
    throw new ServiceException(PromotionErrorCode.E400.code(),
    "驳回原因必填");
    }

    if (param.getFailReason().length() > 500) {
    throw new ServiceException(PromotionErrorCode.E400.code(),
    "驳回原因长度不能超过500个字符");
    }
    }

    //审核状态 true:审核通过,false:审核未通过
    boolean auditStatus = SeckillGoodsApplyStatusEnum.PASS.equals(applyStatusEnum);
    //参与限时抢购促销活动并且已被平台审核通过的商品集合
    List<SeckillApplyDO> goodsList = new ArrayList<>();
    //审核通过的限时抢购商品集合
    List<PromotionGoodsDO> promotionGoodsDOS = new ArrayList<>();
    Long actId = 0L;
    //批量审核
    for (Long applyId : param.getApplyIds()) {

    SeckillApplyDO apply = new QueryChainWrapper<>(seckillApplyMapper)
    //拼接活动id查询条件
    .eq("apply_id", applyId)
    //拼接秒杀活动商品的申请状态查询条件
    .eq("status", SeckillGoodsApplyStatusEnum.APPLY.name())
    //查询单个对象
    .one();
    //申请不存在
    if (apply == null) {
    throw new ServiceException(PromotionErrorCode.E400.code(),
    "商品不是可以审核的状态");
    }
    if (actId == 0) {
    actId = apply.getSeckillId();
    }

    apply.setStatus(applyStatusEnum.name());
    apply.setFailReason(param.getFailReason());

    new UpdateChainWrapper<>(seckillApplyMapper)
    //按活动id修改
    .eq("apply_id", applyId)
    //提交修改
    .update(apply);

    //查询商品
    CacheGoods goods = goodsClient.getFromCache(apply.getGoodsId());
    //将审核通过的商品,存储到活动商品表和缓存中
    if (auditStatus) {
    //将审核通过的商品放入集合中
    goodsList.add(apply);

    //促销商品表
    PromotionGoodsDO promotion = new PromotionGoodsDO();
    promotion.setTitle("限时抢购");
    promotion.setGoodsId(apply.getGoodsId());
    promotion.setSkuId(apply.getSkuId());
    promotion.setPromotionType(PromotionTypeEnum.SECKILL.name());
    promotion.setActivityId(apply.getSeckillId());
    promotion.setNum(apply.getSoldQuantity());
    promotion.setPrice(apply.getPrice());
    promotion.setSellerId(goods.getSellerId());
    //商品活动的开始时间为当前商品的参加时间段
    int timeLine = apply.getTimeLine();
    String date = DateUtil.toString(apply.getStartDay(), "yyyy-MM-dd");
    long startTime = DateUtil.getDateline(
    date + " " + timeLine + ":00:00", "yyyy-MM-dd HH:mm:ss");
    long endTime = DateUtil.getDateline(
    date + " 23:59:59", "yyyy-MM-dd HH:mm:ss");

    promotion.setStartTime(startTime);
    promotion.setEndTime(endTime);
    promotionGoodsDOS.add(promotion);


    //从缓存读取限时抢购的活动的商品
    String redisKey = getRedisKey(apply.getStartDay());
    Map<Integer, List<SeckillGoodsVO>> map = this.cache.getHash(redisKey);
    //如果redis中有当前审核商品参与的限时抢购活动商品信息,就删除掉
    if (map != null && !map.isEmpty()) {
    this.cache.remove(redisKey);
    }

    //设置延迟加载任务,到活动开始时间后将搜索引擎中的优惠价格设置为0
    PromotionPriceDTO promotionPriceDTO = new PromotionPriceDTO();
    promotionPriceDTO.setGoodsId(apply.getGoodsId());
    promotionPriceDTO.setPrice(apply.getPrice());
    timeTrigger.add(TimeExecute.PROMOTION_EXECUTER,
    promotionPriceDTO, startTime, null);
    //此活动结束后将索引的优惠价格重置为0
    promotionPriceDTO.setPrice(0.0);
    timeTrigger.add(TimeExecute.PROMOTION_EXECUTER,
    promotionPriceDTO, endTime, null);
    }
    }

    //如果当前是商品审核通过的操作
    if (auditStatus) {
    SeckillVO seckillVO = this.getModel(actId);
    if (seckillVO != null) {
    String date = DateUtil.toString(seckillVO.getStartDay(), "yyyy-MM-dd");
    long startTime = DateUtil.getDateline(
    date + " 00:00:00", "yyyy-MM-dd HH:mm:ss");
    long endTime = DateUtil.getDateline(
    date + " 23:59:59", "yyyy-MM-dd HH:mm:ss");
    //活动信息DTO
    PromotionDetailDTO detailDTO = new PromotionDetailDTO();
    detailDTO.setActivityId(seckillVO.getSeckillId());
    detailDTO.setStartTime(startTime);
    detailDTO.setEndTime(endTime);
    detailDTO.setPromotionType(PromotionTypeEnum.SECKILL.name());
    detailDTO.setTitle("限时抢购");
    this.promotionGoodsManager.addAndCheck(promotionGoodsDOS, detailDTO);
    }

    //创建审核通过的商品限时抢购促销活动脚本信息
    this.seckillScriptManager.createCacheScript(
    goodsList.get(0).getSeckillId(), goodsList);
    }
    }
    }
  • 创建限时抢购促销活动脚本引擎

    @Service
    public class SeckillScriptManagerImpl implements SeckillScriptManager {

    @Override
    public void createCacheScript(Long promotionId, List<SeckillApplyDO> goodsList) {
    //如果参与促销活动的商品集合不为空并且集合长度不等于0
    if (goodsList != null && goodsList.size() != 0) {
    //批量放入缓存的数据集合
    Map<String, List<PromotionScriptVO>> cacheMap = new HashMap<>();

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

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

    //获取商品开始的时刻
    String[] time = this.getTime(goods.getTimeLine(), goods.getStartDay());

    //获取拼团活动脚本信息
    PromotionScriptVO scriptVO = new PromotionScriptVO();

    //渲染并读取限时抢购促销活动脚本信息
    String script = renderScript(time[0], time[1], goods.getPrice());

    scriptVO.setPromotionScript(script);
    scriptVO.setPromotionId(promotionId);
    scriptVO.setPromotionType(PromotionTypeEnum.SECKILL.name());
    scriptVO.setIsGrouped(false);
    scriptVO.setPromotionName("限时抢购");
    scriptVO.setSkuId(goods.getSkuId());

    //从缓存中读取脚本信息
    List<PromotionScriptVO> scriptList = (List<PromotionScriptVO>)
    cache.get(cacheKey);
    if (scriptList == null) {
    scriptList = new ArrayList<>();
    }

    scriptList.add(scriptVO);

    cacheMap.put(cacheKey, scriptList);
    }

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

    /**
    * 获取限时抢购开始和结束时间
    * @param timeLine 开始时刻
    * @param startDay 活动开始当天的起始时间
    * @return
    */
    private String[] getTime(Integer timeLine, Long startDay) {
    String date = DateUtil.toString(startDay, "yyyy-MM-dd");
    Long startTime = DateUtil.getDateline(date + " " + timeLine + ":00:00",
    "yyyy-MM-dd HH:mm:ss");
    Long endTime = DateUtil.getDateline(date + " 23:59:59", "yyyy-MM-dd HH:mm:ss");
    String[] time = new String[2];
    time[0] = startTime.toString();
    time[1] = endTime.toString();
    return time;
    }

    /**
    * 渲染并读取限时抢购促销活动脚本信息
    * @param startTime 活动开始时间
    * @param endTime 活动结束时间
    * @param price 限时抢购商品价格
    * @return
    */
    private String renderScript(String startTime, String endTime, Double price) {
    Map<String, Object> model = new HashMap<>();

    Map<String, Object> params = new HashMap<>();
    params.put("startTime", startTime);
    params.put("endTime", endTime);
    params.put("price", price);

    model.put("promotionActive", params);

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

    logger.debug("生成限时抢购促销活动脚本:" + script);

    return script;
    }
    }
  • 删除限时抢购促销活动脚本引擎

    @Component("seckillScriptTimeTriggerExecuter")
    public class SeckillScriptTimeTriggerExecuter implements TimeTriggerExecuter {

    protected final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private PromotionScriptClient promotionScriptClient;

    @Autowired
    private PromotionGoodsClient promotionGoodsClient;

    @Override
    public void execute(Object object) {

    PromotionScriptMsg promotionScriptMsg = (PromotionScriptMsg) object;

    //如果是限时抢购促销活动结束
    if (ScriptOperationTypeEnum.DELETE.equals(
    promotionScriptMsg.getOperationType())) {

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

    //获取所有参与活动审核通过的商品集合
    List<SeckillApplyDO> goodsList =
    this.promotionGoodsClient.getSeckillGoodsList(
    promotionId, SeckillGoodsApplyStatusEnum.PASS.value());

    //清除促销活动脚本信息
    this.promotionScriptClient.deleteCacheScript(promotionId, goodsList);

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

    }
    }
    @Service
    public class SeckillScriptManagerImpl implements SeckillScriptManager {

    @Override
    public void deleteCacheScript(Long promotionId, List<SeckillApplyDO> goodsList) {
    //如果参与促销活动的商品集合不为空并且集合长度不等于0
    if (goodsList != null && goodsList.size() != 0) {
    //需要批量更新的缓存数据集合
    Map<String, List<PromotionScriptVO>> updateCacheMap = new HashMap<>();

    //需要批量删除的缓存key集合
    List<String> delKeyList = new ArrayList<>();

    for (SeckillApplyDO goods : goodsList) {
    //缓存key
    String cacheKey = CachePrefix.SKU_PROMOTION.getPrefix()
    + goods.getSkuId();

    //从缓存中读取促销脚本缓存
    List<PromotionScriptVO> scriptCacheList =
    (List<PromotionScriptVO>) cache.get(cacheKey);

    if (scriptCacheList != null && scriptCacheList.size() != 0) {
    //循环促销脚本缓存数据集合
    for (PromotionScriptVO script : scriptCacheList) {
    //如果脚本数据的促销活动信息与当前修改的促销活动信息一致,那么就将此信息删除
    if (PromotionTypeEnum.SECKILL.name().
    equals(script.getPromotionType())
    && script.getPromotionId().intValue()
    == promotionId.intValue()) {
    scriptCacheList.remove(script);
    break;
    }
    }

    if (scriptCacheList.size() == 0) {
    delKeyList.add(cacheKey);
    } else {
    updateCacheMap.put(cacheKey, scriptCacheList);
    }
    }
    }

    cache.multiDel(delKeyList);
    cache.multiSet(updateCacheMap);
    }
    }
    }
  • 限时抢购促销活动脚本引擎模板文件—single_promotion.ftl

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

    <#--
    活动金额计算
    @param promotionActive 活动信息对象(内置常量)
    .price 商品促销活动价格
    @param $sku 商品SKU信息对象(变量)
    .$num 商品数量
    @returns {*}
    -->
    function countPrice() {
    var resultPrice = $sku.$num * ${promotionActive.price};
    return resultPrice < 0 ? 0 : resultPrice.toString();
    }