限时抢购
一、功能说明
1、限时抢购促销活动属于平台发布的促销活动,商家选择商品参与,平台来进行审核。
2、平台在同一时间段内只允许创建一个限时抢购促销活动。
3、限时抢购活动开始后,不允许修改和删除活动信息。
4、平台添加限时抢购活动时,报名截止日期不可以大于活动日期。
活动日期为限时抢购活动持续运行的那一天,当天00:00为活动开始时间。因此,报名截止时间最晚设置到活动日期的前一天的23:59:59。
5、平台添加限时抢购活动时,抢购阶段必须设置的是0-23的整点数字。
6、商家报名参与限时抢购活动,每个抢购阶段都可以选择要参与活动的商品,可以添加多个,但是每个抢购阶段的商品不可以重复。
7、商家报名参与限时抢购活动后,需要平台进行审核,审核不通过不可再次报名。
8、限时抢购活动属于单品促销活动,用户在购物车中可以选择取消活动。
二、数据库设计
1、表结构设计
限时抢购促销活动表—es_seckill
字段名 类型与长度 说明 seckill_id bigint(20) 主键ID seckill_name varchar(50) 活动名称 start_day bigint(20) 活动日期 apply_end_time bigint(20) 活动报名截止时间 seckill_rule varchar(1000) 活动申请规则 seller_ids longtext 参与活动的商家ID,如1,2,3 seckill_status varchar(50) 活动状态 EDITING:编辑中,RELEASE:已发布,OVER:已结束 delete_status varchar(255) 删除状态 DELETED:已删除,NORMAL:正常
限时抢购促销活动时刻表—es_seckill_range
字段名 类型与长度 说明 range_id bigint(20) 主键ID seckill_id bigint(20) 限时抢购活动ID(关联es_seckill表) range_time int(10) 抢购时间的整点时刻
限时抢购促销活动商品申请表—es_seckill_apply
字段名 类型与长度 说明 apply_id bigint(20) 主键ID seckill_id bigint(20) 限时抢购促销活动ID(关联es_seckill表) time_line int(10) 限时抢购时间的整点时刻 start_day bigint(20) 活动日期 goods_id bigint(20) 商品ID(关联es_goods表) goods_name varchar(255) 商品名称 sku_id bigint(20) 商品sku(关联es_goods_sku表) seller_id bigint(20) 商家ID(关联es_shop表) shop_name varchar(50) 店铺名称 price decimal(20,2) 活动价格 sold_quantity int(10) 商品售空数量 status varchar(50) 申请状态 APPLY:申请中,PASS:已通过,FAIL:已驳回 fail_reason varchar(500) 申请驳回的原因 sales_num int(10) 已售数量 original_price decimal(20,2) 商品原价 specs longtext 商品规格信息
2、表关联说明
限时抢购相关表与表关联图
限时抢购相关表与表之间的关联字段对照
限时抢购促销活动表(es_seckill) 促销活动商品表(es_promotion_goods) seckill_id activity_id 无字段,促销类型值为SECKILL promotion_type 限时抢购促销活动表(es_seckill) 限时抢购促销活动时刻表(es_seckill_range) seckill_id seckill_id 限时抢购促销活动表(es_seckill) 限时抢购促销活动商品申请表(es_seckill_apply) seckill_id seckill_id
三、缓存设计
1、商家在选择商品参与限时抢购活动后,会将参与限时抢购活动的商品按活动时刻分组,然后存放在缓存中
- 缓存key值为:{STOREID_SECKILL_KEY}活动开始时间。时间格式为(年月日):20201215
缓存value值为:是一个类型为
Map<Integer, List<SeckillGoodsVO>>
的集合。这个Map集合的键值对为:key:活动的时刻,Integer类型,如果活动时刻为8点整,那么这个key就是8
value:是当前时刻参与活动商品的集合。
2、限时抢购促销活动脚本引擎
脚本引擎缓存结构:
限时抢购促销活动的促销脚本引擎缓存结构只有一种:SKU级别的缓存结构。
脚本引擎生成和删除时机:
- 生成:平台审核通过参与活动的商品时生成。
- 删除:活动结束时删除。
关于促销脚本引擎缓存结构可参考《促销活动脚本引擎生成架构》这篇文档。
四、代码设计
1、接口调用流程图
平台发布促销活动
商家选择商品参与限时抢购活动
平台审核商家参与活动的商品
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();
}