一、功能说明
1、团购促销活动属于平台发布的促销活动,商家选择商品参与,平台来进行审核。
2、平台在同一时间段内只允许创建一个团购促销活动。
3、团购活动开始后,不允许修改和删除活动信息。
4、平台添加团购活动时,报名截止日期不可以大于活动开始日期。
5、平台可以添加团购分类,发布团购活动时,要选择团购分类,用户可以在团购商品页面按照团购分类筛选商品。
团购分类不等于商品分类,是团购活动商品独立的分类。
团购分类只支持一级分类,平台可以设置分类的排序,排序值越大位置越靠后显示。
6、商家在选择商品参与团购活动时,不可以批量添加,只能一个一个的添加。
7、商家选择商品参与团购活动后,需要平台进行审核。
审核通过可正常售卖;审核不通过的,商家可以编辑商品信息后,再次申请审核。
如果团购活动开始了,商家不允许再次编辑或删除参与团购活动的商品。
8、团购活动属于单品促销活动,用户在购物车中可以选择取消活动,取消后价格恢复为原商品价格。
二、数据库设计
1、表结构设计
团购促销活动表—es_groupbuy_active
字段名 类型与长度 说明 act_id bigint(20) 主键ID act_name varchar(255) 活动名称 start_time bigint(20) 活动开始时间(存放的是以秒为单位的时间戳) end_time bigint(20) 活动结束时间(存放的是以秒为单位的时间戳) join_end_time bigint(20) 活动报名截止时间(存放的是以秒为单位的时间戳) add_time bigint(20) 活动创建时间 act_tag_id int(10) 团购活动标签ID(预留字段,暂时无用) goods_num int(10) 参与团购活动的商品数量 delete_reason varchar(255) 团购活动删除原因 delete_time bigint(20) 团购活动删除时间 delete_name varchar(50) 团购活动删除人 delete_status varchar(20) 是否删除 DELETED:已删除,NORMAL:正常
团购商品分类表—es_groupbuy_cat
字段名 类型与长度 说明 cat_id bigint(20) 主键ID parent_id bigint(20) 父分类ID cat_name varchar(255) 分类名称 cat_path varchar(255) 分类结构目录(预留字段,暂时无用) cat_order int(8) 排序,数值越小排序越靠前
团购商品表—es_groupbuy_goods
字段名 类型与长度 说明 gb_id bigint(20) 主键ID sku_id bigint(20) 商品SKU(关联es_goods_sku表) act_id bigint(20) 团购活动ID(关联es_groupbuy_active表) cat_id bigint(20) 团购分类ID(关联es_groupbuy_cat表) area_id int(11) 已废弃字段 gb_name varchar(1000) 团购名称 gb_title varchar(1000) 团购标题 goods_name varchar(255) 商品名称 goods_id bigint(20) 商品ID(关联es_goods表) original_price decimal(20,2) 商品原价 price decimal(20,2) 商品团购价 img_url varchar(255) 团购商品图片 goods_num int(11) 商品总数 visual_num int(11) 虚拟购买数量 limit_num int(11) 限购数量(预留字段,暂时无用) buy_num int(11) 已团购数量 view_num int(11) 浏览数量(预留字段,暂时无用) remark longtext 团购介绍 gb_status int(1) 团购商品状态 0:待审核,1:通过审核,2:未通过审核 add_time bigint(20) 商品参与团购活动时间 wap_thumbnail varchar(255) WAP端商品缩略图(预留字段,暂时无用) thumbnail varchar(255) PC端商品缩略图(预留字段,暂时无用) seller_id bigint(20) 商品所属商家(关联es_shop表) seller_name varchar(20) 商家名称 specs longtext 商品规格信息
团购商品库存日志表—es_groupbuy_quantity_log
字段名 类型与长度 说明 log_id bigint(20) 主键ID order_sn varchar(20) 订单编号(关联es_order表) goods_id bigint(20) 商品ID(关联es_goods表) quantity int(10) 团购商品售空数量 op_time bigint(20) 操作时间 log_type varchar(20) 日志类型 BUY:售出,CANCEL:取消 reason varchar(200) 操作原因 gb_id bigint(20) 团购活动ID
2、表关联说明
团购相关表与表关联图
团购相关表与表之间的关联字段对照
团购促销活动表(es_groupbuy_active) 促销活动商品表(es_promotion_goods) act_id activity_id 无字段,促销类型值为GROUPBUY promotion_type 团购促销活动表(es_groupbuy_active) 团购商品表(es_groupbuy_goods) act_id act_id 团购商品表(es_groupbuy_goods) 团购分类表(es_groupbuy_cat) cat_id cat_id 团购商品表(es_groupbuy_goods) 团购商品库存日志表(es_groupbuy_quantity_log) act_id gb_id goods_id goods_id
三、缓存设计
1、平台在发布团购促销活动时,在将促销活动信息入库的同时,也会将信息放入缓存中。
缓存key值为:{STOREID_GROUP_BUY_KEY}活动ID。
缓存value值为:GroupbuyActiveVO.java这个实体对象信息。
2、团购促销活动脚本引擎
脚本引擎缓存结构:
团购促销活动的促销脚本引擎缓存结构只有一种:SKU级别的缓存结构。
脚本引擎生成与删除时机:
生成:团购活动开始时生成。
如果活动开始后,还有没被平台审核的商品,那么审核商品通过后也会重新生成脚本。
删除:团购活动结束时删除。
关于促销脚本引擎缓存结构可参考《促销活动脚本引擎生成架构》这篇文档。
四、代码设计
1、接口调用流程图
平台发布促销活动
商家选择商品参与团购活动
平台审核商家参与活动的商品
2、相关代码展示
新增团购活动
@RestController
@RequestMapping("/admin/promotion/group-buy-actives")
@Api(description = "团购活动表相关API")
@Validated
public class GroupbuyActiveManagerController {
@ApiOperation(value = "添加团购活动表", response = GroupbuyActiveDO.class)
@ApiImplicitParam(name = "activeDO", value = "团购信息", required = true,
dataType = "GroupbuyActiveDO", paramType = "body")
@PostMapping
public GroupbuyActiveDO add(@ApiIgnore @Valid @RequestBody GroupbuyActiveDO activeDO) {
this.verifyParam(activeDO.getStartTime(), activeDO.getEndTime(),
activeDO.getJoinEndTime());
this.groupbuyActiveManager.add(activeDO);
return activeDO;
}
}@Service
public class GroupbuyActiveManagerImpl extends AbstractPromotionRuleManagerImpl implements GroupbuyActiveManager {
@Override
@Transactional(propagation = Propagation.REQUIRED,
rollbackFor = {RuntimeException.class})
public GroupbuyActiveDO add(GroupbuyActiveDO groupbuyActive) {
//参数验证 1、活动起始时间必须大于当前时间 2、活动开始时间是否大于活动结束时间
PromotionValid.paramValid(groupbuyActive.getStartTime(),
groupbuyActive.getEndTime(), 1, null);
//验证活动时间 同一时间只能有一个活动生效
this.verifyTime(groupbuyActive.getStartTime(),
groupbuyActive.getEndTime(), PromotionTypeEnum.GROUPBUY, null);
//验证活动名称是否重复
this.verifyName(groupbuyActive.getActName(), false, 0L);
//设置添加时间为当前时间
groupbuyActive.setAddTime(DateUtil.getDateline());
//设置删除状态为正常未删除
groupbuyActive.setDeleteStatus(DeleteStatusEnum.NORMAL.value());
//团购活动入库操作
groupbuyActiveMapper.insert(groupbuyActive);
//获取团购活动主键ID
Long id = groupbuyActive.getActId();
//启用延时任务创建促销活动脚本信息
PromotionScriptMsg promotionScriptMsg = new PromotionScriptMsg();
//设置活动ID
promotionScriptMsg.setPromotionId(id);
//设置活动名称
promotionScriptMsg.setPromotionName(groupbuyActive.getActName());
//设置活动类型
promotionScriptMsg.setPromotionType(PromotionTypeEnum.GROUPBUY);
//设置脚本操作类型为创建脚本
promotionScriptMsg.setOperationType(ScriptOperationTypeEnum.CREATE);
//设置活动结束时间
promotionScriptMsg.setEndTime(groupbuyActive.getEndTime());
//创建延时任务key值
String uniqueKey = "{TIME_TRIGGER_" + PromotionTypeEnum.GROUPBUY.name()
+ "}_" + id;
//发送消息创建延时任务
timeTrigger.add(TimeExecute.GROUPBUY_SCRIPT_EXECUTER,
promotionScriptMsg, groupbuyActive.getStartTime(), uniqueKey);
return groupbuyActive;
}
}商家选择商品参与团购活动
@RestController
@RequestMapping("/seller/promotion/group-buy-goods")
@Api(description = "团购商品相关API")
@Validated
public class GroupbuyGoodsSellerController {
@ApiOperation(value = "添加团购商品", response = GroupbuyGoodsDO.class)
@ApiImplicitParam(name = "groupbuyGoods", value = "团购商品信息",
required = true, dataType = "GroupbuyGoodsDO", paramType = "body")
@PostMapping
public GroupbuyGoodsDO add(@Valid @RequestBody GroupbuyGoodsDO groupbuyGoods) {
groupbuyGoods.setAddTime(DateUtil.getDateline());
this.verifyParam(groupbuyGoods);
Seller seller = UserContext.getSeller();
groupbuyGoods.setSellerId(seller.getSellerId());
groupbuyGoods.setSellerName(seller.getSellerName());
groupbuyGoods.setBuyNum(0);
groupbuyGoods.setViewNum(0);
this.groupbuyGoodsManager.add(groupbuyGoods);
return groupbuyGoods;
}
}@Service
public class GroupbuyGoodsManagerImpl extends AbstractPromotionRuleManagerImpl implements GroupbuyGoodsManager {
@Override
@Transactional(propagation = Propagation.REQUIRED,
rollbackFor = {Exception.class, RuntimeException.class})
public GroupbuyGoodsDO add(GroupbuyGoodsDO goodsDO) {
//获取团购活动信息并校验
GroupbuyActiveDO activeDO = groupbuyActiveManager.getModel(goodsDO.getActId());
if (activeDO == null) {
throw new ServiceException(PromotionErrorCode.E400.code(), "参与的活动不存在");
}
//如果团购价格 大于等于商品原价,则抛出异常
if (goodsDO.getPrice() >= goodsDO.getOriginalPrice()) {
throw new ServiceException(PromotionErrorCode.E400.code(),
"参与活动的商品促销价格不得大于或等于商品原价");
}
//校验限购数量是否超过商品总数
if (goodsDO.getLimitNum() > goodsDO.getGoodsNum()) {
throw new ServiceException(PromotionErrorCode.E400.code(),
"商品限购数量不能大于商品总数");
}
//新建查询条件包装器
QueryWrapper<PromotionGoodsDO> wrapper = new QueryWrapper<>();
//以促销活动类型为限时抢购为条件查询
wrapper.eq("promotion_type", PromotionTypeEnum.SECKILL.name());
//以商品SKU为查询条件
wrapper.eq("sku_id", goodsDO.getSkuId());
//按(活动开始时间小于等于当前活动开始时间 并且 活动结束时间大于等于当前活动开始时间)或者(活动开始时间小于等于当前活动结束时间 并且 活动结束时间大于等于当前活动结束时间)
//或者 (活动开始时间小于等于当前活动开始时间 并且 活动结束时间大于等于当前活动结束时间)或者 活动开始时间大于等于当前活动开始时间 并且 活动结束时间小于等于当前活动结束时间)
wrapper.and(i -> i.le("start_time", activeDO.getStartTime())
.ge("end_time", activeDO.getStartTime())
.or().le("start_time", activeDO.getEndTime())
.ge("end_time", activeDO.getEndTime())
.or().le("start_time", activeDO.getStartTime())
.ge("end_time", activeDO.getEndTime())
.or().ge("start_time", activeDO.getStartTime())
.le("end_time", activeDO.getEndTime()));
//获取当前商品参与限时抢购活动的数量
int count = promotionGoodsMapper.selectCount(wrapper);
if (count > 0) {
throw new ServiceException(PromotionErrorCode.E400.code(), "该商品已经在重叠的时间段参加了限时抢购活动,不能参加团购活动");
}
//新建促销商品集合
List<PromotionGoodsDTO> goodsDTOList = new ArrayList<>();
//新建促销商品对象DTO
PromotionGoodsDTO goodsDTO = new PromotionGoodsDTO();
//设置商品ID
goodsDTO.setGoodsId(goodsDO.getGoodsId());
//设置商品图片
goodsDTO.setThumbnail(goodsDO.getThumbnail());
//设置商品名称
goodsDTO.setGoodsName(goodsDO.getGoodsName());
//将商品放入集合中
goodsDTOList.add(goodsDTO);
//检测活动商品规则
this.verifyRule(goodsDTOList);
//设置团购商品审核状态为待审核
goodsDO.setGbStatus(GroupBuyGoodsStatusEnum.PENDING.status());
//获取商品sku
Long skuId = goodsDO.getSkuId();
//查询存储相关的规格
GoodsSkuVO sku = goodsClient.getSkuFromCache(skuId);
//设置团购商品规格信息
goodsDO.setSpecs(sku.getSpecs());
//团购商品信息入库
groupbuyGoodsMapper.insert(goodsDO);
return goodsDO;
}
}平台审核参与团购活动的商品
@RestController
@RequestMapping("/admin/promotion/group-buy-actives")
@Api(description = "团购活动表相关API")
@Validated
public class GroupbuyActiveManagerController {
@ApiOperation(value = "批量审核商品")
@PostMapping(value = "/batch/audit")
public void batchAudit(@Valid @RequestBody GroupbuyAuditParam param) {
this.groupbuyActiveManager.batchAuditGoods(param);
}
}@Service
public class GroupbuyActiveManagerImpl extends AbstractPromotionRuleManagerImpl implements GroupbuyActiveManager {
@Override
@Transactional(propagation = Propagation.REQUIRED,
rollbackFor = {RuntimeException.class, Exception.class})
public void batchAuditGoods(GroupbuyAuditParam param) {
//校验活动id
if (param.getActId() == null || param.getActId() == 0) {
throw new ServiceException(PromotionErrorCode.E400.code(), "参数活动ID不正确");
}
//校验活动商品
if (param.getGbIds() == null || param.getGbIds().length == 0) {
throw new ServiceException(GoodsErrorCode.E301.code(), "请选择要审核的商品");
}
//校验审核状态
if (param.getStatus() == null) {
throw new ServiceException(PromotionErrorCode.E400.code(),
"参数审核状态值不能为空");
}
//校验参数审核状态值是否服务规范
if (param.getStatus().intValue() != 1 && param.getStatus().intValue() != 2) {
throw new ServiceException(PromotionErrorCode.E400.code(),
"参数审核状态值不正确");
}
//获取团购活动信息
GroupbuyActiveDO activeDO = this.getModel(param.getActId());
//审核状态 true:审核通过,false:审核未通过
boolean auditStatus = param.getStatus().intValue()
== GroupBuyGoodsStatusEnum.APPROVED.status();
//参与团购促销活动并且已被平台审核通过的商品集合
List<PromotionGoodsDO> goodsList = new ArrayList<>();
//循环团购商品ID集合
for (Long gbId : param.getGbIds()) {
//读取团购商品信息并校验
GroupbuyGoodsDO goodsDO = this.groupbuyGoodsManager.getModel(gbId);
if (goodsDO == null) {
throw new ServiceException(PromotionErrorCode.E400.code(),
"商品【" + goodsDO.getGoodsName() + "】不存在");
}
if (goodsDO.getGbStatus().intValue() != 0) {
throw new ServiceException(PromotionErrorCode.E400.code(),
"商品【" + goodsDO.getGoodsName()
+ "】不是可以审核的状态");
}
//修改状态
this.groupbuyGoodsManager.updateStatus(gbId, param.getStatus());
//如果通过审核
if (auditStatus) {
//新建修改条件包装器
UpdateWrapper<GroupbuyActiveDO> wrapper = new UpdateWrapper<>();
//以团购活动ID为修改条件,修改参与团购商品数量+1
wrapper.setSql("goods_num=goods_num+1").eq("act_id", param.getActId());
//修改已参与团购活动的商品数量
groupbuyActiveMapper.update(new GroupbuyActiveDO(), wrapper);
//商品集合
List<PromotionGoodsDO> goodsDTOList = new ArrayList<>();
//初始化促销活动商品数据
PromotionGoodsDO promotion = new PromotionGoodsDO();
//设置活动名称
promotion.setTitle(activeDO.getActName());
//设置商品ID
promotion.setGoodsId(goodsDO.getGoodsId());
//设置商品sku
promotion.setSkuId(goodsDO.getSkuId());
//设置促销活动类型为团购活动
promotion.setPromotionType(PromotionTypeEnum.GROUPBUY.name());
//设置活动ID
promotion.setActivityId(activeDO.getActId());
//设置参与促销活动的商品数量
promotion.setNum(activeDO.getGoodsNum());
//设置商品价格
promotion.setPrice(goodsDO.getPrice());
//设置商家ID
promotion.setSellerId(goodsDO.getSellerId());
//设置活动开始时间
promotion.setStartTime(activeDO.getStartTime());
//设置活动结束时间
promotion.setEndTime(activeDO.getEndTime());
goodsDTOList.add(promotion);
//初始化活动信息DTO
PromotionDetailDTO detailDTO = new PromotionDetailDTO();
//设置活动ID
detailDTO.setActivityId(param.getActId());
//设置活动开始时间
detailDTO.setStartTime(activeDO.getStartTime());
//设置活动结束时间
detailDTO.setEndTime(activeDO.getEndTime());
//设置促销活动类型为团购活动
detailDTO.setPromotionType(PromotionTypeEnum.GROUPBUY.name());
//设置活动名称
detailDTO.setTitle(activeDO.getActName());
//入库到活动商品对照表
this.promotionGoodsManager.addAndCheck(goodsDTOList, detailDTO);
//将此商品加入延迟加载队列,到指定的时间将索引价格变成最新的优惠价格
PromotionPriceDTO promotionPriceDTO = new PromotionPriceDTO();
promotionPriceDTO.setGoodsId(goodsDO.getGoodsId());
promotionPriceDTO.setPrice(goodsDO.getPrice());
//检测开始时间和结束时间
PromotionValid.paramValid(detailDTO.getStartTime(),
detailDTO.getEndTime(), 1, null);
timeTrigger.add(TimeExecute.PROMOTION_EXECUTER,
promotionPriceDTO, activeDO.getStartTime(), null);
//此活动结束后将索引的优惠价格重置为0
promotionPriceDTO.setPrice(0.0);
timeTrigger.add(TimeExecute.PROMOTION_EXECUTER,
promotionPriceDTO, activeDO.getEndTime(), null);
//构建要生成脚本的促销商品数据集合
PromotionGoodsDO promotionGoodsDO = new PromotionGoodsDO();
promotionGoodsDO.setSkuId(promotion.getSkuId());
promotionGoodsDO.setPrice(goodsDO.getPrice());
goodsList.add(promotionGoodsDO);
}
}
//获取当前时间
long currTime = DateUtil.getDateline();
//活动是否在进行中
boolean activeStatus = activeDO.getStartTime().longValue()
<= currTime && activeDO.getEndTime().longValue() >= currTime;
//如果当前团购活动正在进行中并且是商品审核通过的操作
if (activeStatus && auditStatus) {
//先删除已存在的脚本信息
this.groupbuyScriptManager.deleteCacheScript(param.getActId(), goodsList);
//创建审核通过的商品团购促销活动脚本信息
this.groupbuyScriptManager.createCacheScript(param.getActId(), goodsList);
}
}
}团购促销活动脚本引擎生成
@Component("groupBuyScriptTimeTriggerExecuter")
public class GroupBuyScriptTimeTriggerExecuter implements TimeTriggerExecuter {
@Override
public void execute(Object object) {
PromotionScriptMsg promotionScriptMsg = (PromotionScriptMsg) object;
//获取促销活动ID
Long promotionId = promotionScriptMsg.getPromotionId();
//获取参与团购活动的所有商品信息集合
List<PromotionGoodsDO> goodsList =
this.promotionGoodsClient.getPromotionGoods(
promotionId, PromotionTypeEnum.GROUPBUY.name());
//如果是团购促销活动开始生效
if (ScriptOperationTypeEnum.CREATE.equals(
promotionScriptMsg.getOperationType())) {
//创建脚本信息
this.promotionScriptClient.createGroupBuyCacheScript(
promotionId, goodsList);
//开启活动后,立马设置一个关闭的流程
promotionScriptMsg.setOperationType(ScriptOperationTypeEnum.DELETE);
String uniqueKey = "{TIME_TRIGGER_"
+ promotionScriptMsg.getPromotionType().name() + "}_" + promotionId;
timeTrigger.add(TimeExecute.GROUPBUY_SCRIPT_EXECUTER, promotionScriptMsg,
promotionScriptMsg.getEndTime(), uniqueKey);
this.logger.debug("促销活动[" + promotionScriptMsg.getPromotionName()
+ "]开始,id=[" + promotionId + "]");
} else {
//删除脚本信息
this.promotionScriptClient.deleteGroupBuyCacheScript(
promotionId, goodsList);
this.logger.debug("促销活动[" + promotionScriptMsg.getPromotionName()
+ "]结束,id=[" + promotionId + "]");
}
}
}@Service
public class GroupbuyScriptManagerImpl implements GroupbuyScriptManager {
@Override
public void createCacheScript(Long promotionId, List<PromotionGoodsDO> goodsList) {
//如果参与团购促销活动的商品集合不为空并且集合长度不为0
if (goodsList != null && goodsList.size() != 0) {
//获取团购活动详细信息
GroupbuyActiveDO groupbuyActiveDO
= this.groupbuyActiveManager.getModel(promotionId);
//批量放入缓存的数据集合
Map<String, List<PromotionScriptVO>> cacheMap = new HashMap<>();
//循环参与团购活动的商品集合,将脚本放入缓存中
for (PromotionGoodsDO goods : goodsList) {
//缓存key
String cacheKey = CachePrefix.SKU_PROMOTION.getPrefix()
+ goods.getSkuId();
//获取拼团活动脚本信息
PromotionScriptVO scriptVO = new PromotionScriptVO();
//渲染并读取团购促销活动脚本信息
String script = renderScript(groupbuyActiveDO.getStartTime().toString(),
groupbuyActiveDO.getEndTime().toString(),
goods.getPrice());
scriptVO.setPromotionScript(script);
scriptVO.setPromotionId(promotionId);
scriptVO.setPromotionType(PromotionTypeEnum.GROUPBUY.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 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;
}
}团购促销活动脚本引擎模板文件—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();
}