第二件半价
一、功能说明
1、第二件半价促销活动属于商家店铺可直接发布的促销活动。
2、同一个商家在同一时间段内只允许创建一个第二件半价促销活动。
3、商家在发布第二件半价活动时,可以选择店铺全部商品参与,也可以选择部分商品参与。
选择全部商品参与时,商家新创建了一个商品,那么这个商品也会自动参与到这个活动中。
4、第二件半价活动开始后,不允许修改和删除活动信息。
5、对于参与第二件半价促销活动的商品,用户购买双数即可享受优惠,且优惠可以叠加,如下:
例:参与活动的商品A单价为100元
购买数量(件) | 应支付金额(元) | 实支付金额(元) | 优惠金额(元) |
---|---|---|---|
1 | 100 | 100 | 0 |
2 | 200 | 150 | 50 |
3 | 300 | 250 | 50 |
4 | 400 | 300 | 100 |
二、数据库设计
1、表结构设计
第二件半价促销活动表—es_half_price
字段名 | 类型与长度 | 说明 |
---|---|---|
hp_id | bigint(20) | 主键ID |
start_time | bigint(20) | 活动开始时间(存放的是以秒为单位的时间戳) |
end_time | bigint(20) | 活动结束时间(存放的是以秒为单位的时间戳) |
title | varchar(50) | 活动标题(名称) |
range_type | int(1) | 商品参与活动的方式,1:全部商品,2:部分商品 |
disabled | int(1) | 活动是否停用(删除),0:否,1:是 |
description | longtext | 活动说明(描述/简介) |
seller_id | bigint(20) | 活动所属商家ID(关联商家店铺表--es_shop) |
2、表关联说明
第二件半价促销活动表(es_half_price) | 促销活动商品表(es_promotion_goods) |
---|---|
hp_id | activity_id |
无字段,促销类型值为HALF_PRICE | promotion_type |
三、缓存设计
1、商家在发布第二件半价促销活动时,在将促销活动信息入库的同时,也会将信息放入缓存中。
缓存key值为:{STOREID_HALF_PRICE_KEY}活动ID。
缓存value值为:HalfPriceDO.java这个实体对象信息。
2、第二件半价促销活动脚本引擎
脚本引擎缓存结构:
第二件半价促销活动的促销脚本引擎缓存结构有两种:
- 当发布活动时,如果选择的是全部商品参与,那么存放的是店铺级别的缓存结构。
- 当发布活动时,如果选择的是部分商品参与,那么存放的是SKU级别的缓存结构。
脚本引擎生成和删除时机:
- 生成:活动开始时生成。
- 删除:活动结束时删除。
关于促销脚本引擎缓存结构可参考《促销活动脚本引擎生成架构》这篇文档。
四、代码设计
1、接口调用流程图
2、相关代码展示
以新增第二件半价活动为例
API代码--HalfPriceSellerController
@RestController
@RequestMapping("/seller/promotion/half-prices")
@Api(description = "第二件半价相关API")
@Validated
public class HalfPriceSellerController {
@ApiOperation(value = "添加第二件半价", response = HalfPriceVO.class)
@PostMapping
public HalfPriceVO add(@Valid @RequestBody HalfPriceVO halfPrice) {
PromotionValid.paramValid(halfPrice.getStartTime(),halfPrice.getEndTime(),
halfPrice.getRangeType(),halfPrice.getGoodsList());
// 获取当前登录的店铺ID
Seller seller = UserContext.getSeller();
Long sellerId = seller.getSellerId();
halfPrice.setSellerId(sellerId);
this.halfPriceManager.add(halfPrice);
return halfPrice;
}
}新增第二件半价活动业务层代码--HalfPriceManagerImpl
@Service
public class HalfPriceManagerImpl extends AbstractPromotionRuleManagerImpl implements HalfPriceManager {
@Override
@Transactional(propagation = Propagation.REQUIRED,
rollbackFor = {RuntimeException.class, Exception.class,
ServiceException.class, NoPermissionException.class})
public HalfPriceVO add(HalfPriceVO halfPriceVO) {
//检测开始时间和结束时间
PromotionValid.paramValid(halfPriceVO.getStartTime(),
halfPriceVO.getEndTime(), 1, null);
this.verifyTime(halfPriceVO.getStartTime(),
halfPriceVO.getEndTime(), PromotionTypeEnum.HALF_PRICE, null);
//初步形成商品的DTO列表
List<PromotionGoodsDTO> goodsDTOList = new ArrayList<>();
//是否是全部商品参与
if (halfPriceVO.getRangeType() == 1) {
PromotionGoodsDTO goodsDTO = new PromotionGoodsDTO();
goodsDTO.setGoodsId(-1L);
goodsDTO.setSkuId(-1L);
goodsDTO.setGoodsName("全部商品");
goodsDTO.setThumbnail("");
goodsDTOList.add(goodsDTO);
halfPriceVO.setGoodsList(goodsDTOList);
}
this.verifyRule(halfPriceVO.getGoodsList());
HalfPriceDO halfPriceDO = new HalfPriceDO();
BeanUtils.copyProperties(halfPriceVO, halfPriceDO);
this.halfPriceMapper.insert(halfPriceDO);
Long id = halfPriceDO.getHpId();
halfPriceVO.setHpId(id);
PromotionDetailDTO detailDTO = new PromotionDetailDTO();
detailDTO.setStartTime(halfPriceVO.getStartTime());
detailDTO.setEndTime(halfPriceVO.getEndTime());
detailDTO.setActivityId(halfPriceVO.getHpId());
detailDTO.setPromotionType(PromotionTypeEnum.HALF_PRICE.name());
detailDTO.setTitle(halfPriceVO.getTitle());
//将活动商品入库
this.promotionGoodsManager.add(halfPriceVO.getGoodsList(), detailDTO);
cache.put(PromotionCacheKeys.getHalfPriceKey(
halfPriceVO.getHpId()), halfPriceDO);
//启用延时任务创建促销活动脚本信息
PromotionScriptMsg promotionScriptMsg = new PromotionScriptMsg();
promotionScriptMsg.setPromotionId(id);
promotionScriptMsg.setPromotionName(halfPriceDO.getTitle());
promotionScriptMsg.setPromotionType(PromotionTypeEnum.HALF_PRICE);
promotionScriptMsg.setOperationType(ScriptOperationTypeEnum.CREATE);
promotionScriptMsg.setEndTime(halfPriceDO.getEndTime());
String uniqueKey = "{TIME_TRIGGER_" + PromotionTypeEnum.HALF_PRICE.name()
+ "}_" + id;
timeTrigger.add(TimeExecute.SELLER_PROMOTION_SCRIPT_EXECUTER,
promotionScriptMsg, halfPriceDO.getStartTime(), uniqueKey);
return halfPriceVO;
}
}第二件半价促销活动脚本生成--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)) {
//满减满赠促销活动
//...省略...
} else if (PromotionTypeEnum.MINUS.equals(promotionType)) {
//单品立减促销活动
//...省略...
} else if (PromotionTypeEnum.HALF_PRICE.equals(promotionType)) {
//获取第二件半价促销活动信息
HalfPriceVO halfPriceVO
= this.promotionGoodsClient.getHalfPriceFromDB(promotionId);
//初始化缓存中的促销活动脚本信息
initScriptCache(halfPriceVO.getRangeType(),
halfPriceVO.getSellerId(), promotionId,
promotionType.name());
}
}
/**
* 根据促销活动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)) {
//满减满赠促销活动
//...省略...
} else if (PromotionTypeEnum.MINUS.name().equals(promotionType)) {
//单品立减促销活动
//...省略...
} else if (PromotionTypeEnum.HALF_PRICE.name().equals(promotionType)) {
//获取第二件半价促销活动信息
HalfPriceVO halfPriceVO
= this.promotionGoodsClient.getHalfPriceFromDB(promotionId);
//渲染并读取第二件半价促销活动脚本信息
String script = renderHalfPriceScript(halfPriceVO);
String tips = "第二件半价";
//构建促销脚本数据结构
PromotionScriptVO promotionScriptVO
= getScript(script, promotionId, false, promotionType, tips);
scriptVO.setSellerId(halfPriceVO.getSellerId());
scriptVO.setRangeType(halfPriceVO.getRangeType());
scriptVO.setPromotionScriptVO(promotionScriptVO);
}
//如果是部分商品参与活动,需要查询出参与促销活动的商品skuID集合
if (scriptVO.getRangeType().intValue() == 2) {
List<PromotionGoodsDO> goodsList
= this.promotionGoodsClient.getPromotionGoods(promotionId, promotionType);
scriptVO.setGoodsList(goodsList);
}
return scriptVO;
}
/**
* 渲染并读取第二件半价促销活动脚本信息
* @param halfPriceVO 第二件半价促销活动信息
* @return
*/
private String renderHalfPriceScript(HalfPriceVO halfPriceVO) {
Map<String, Object> model = new HashMap<>();
Map<String, Object> params = new HashMap<>();
params.put("startTime", halfPriceVO.getStartTime().toString());
params.put("endTime", halfPriceVO.getEndTime().toString());
model.put("promotionActive", params);
String path = "half_price.ftl";
String script = ScriptUtil.renderScript(path, model);
logger.debug("生成第二件半价促销活动脚本:" + script);
return script;
}
}第二件半价促销活动脚本引擎模板文件—half_price.ftl
<#--
验证促销活动是否在有效期内
@param promotionActive 活动信息对象(内置常量)
.startTime 获取开始时间
.endTime 活动结束时间
@param $currentTime 当前时间(变量)
@returns {boolean}
-->
function validTime(){
if (${promotionActive.startTime} <= $currentTime && $currentTime <= ${promotionActive.endTime}) {
return true;
}
return false;
}
<#--
第二件半价促销活动金额计算
@param $sku 商品SKU信息对象(变量)
.$price 商品SKU单价
.$num 商品数量
@returns {*}
-->
function countPrice() {
<#--获取实际商品总金额-->
var totalPrice = $sku.$price * $sku.$num;
<#--应该优惠的金额-->
var discountPrice = 0.00;
<#--当商品数量大于1时才计算优惠-->
if ($sku.$num > 1) {
<#--如果商品数量对2进行取余得到的结果为0-->
if ($sku.$num % 2 == 0) {
<#--优惠金额 = 商品原价的一半 * 商品数量的一半-->
discountPrice = ($sku.$price / 2) * ($sku.$num / 2);
}
<#--如果商品数量对2进行取余得到的结果为1-->
if ($sku.$num % 2 == 1) {
<#--优惠金额 = 商品原价的一半 * (商品数量 - 1)的一半-->
discountPrice = ($sku.$price / 2) * (($sku.$num - 1) / 2);
}
}
<#--最终返回优惠后的总金额 = 实际商品总金额 - 优惠金额-->
totalPrice = totalPrice - discountPrice;
return totalPrice < 0 ? 0 : totalPrice.toString();
}