满减满赠
一、功能说明
1、满减满赠促销活动属于商家店铺可直接发布的促销活动。
2、满减满赠促销活动属于组合促销活动,其中包含减现金、打折、送积分、免邮费、送优惠券、送赠品。
其中减现金和打折只能选择一项。
送积分这项只有自营店铺才可选择。
3、同一个商家在同一时间段内只允许创建一个满减满赠促销活动。
4、商家在发布满减满赠活动时,可以选择店铺全部商品参与,也可以选择部分商品参与。
选择全部商品参与时,商家新创建了一个商品,那么这个商品也会自动参与到这个活动中。
5、优惠券和赠品只能选择一个来参与活动。
6、满减满赠活动开始后,不允许修改和删除活动信息。
7、现阶段满减满赠活动赠送的积分和优惠券,在订单申请售后时,是不进行退还的。
二、数据库设计
1、表结构展示
满减满赠促销活动表--es_full_discount
字段名 类型与长度 说明 fd_id bigint(20) 主键ID full_money decimal(20,2) 优惠门槛金额(订单金额必须达到这个数值才可享受优惠) minus_value decimal(20,2) 减免的现金金额 point_value int(10) 赠送的积分数量 is_full_minus int(1) 是否选择了减现金这种优惠方式,0:否,1:是 is_free_ship int(1) 是否选择了免邮费这种优惠方式,0:否,1:是 is_send_point int(1) 是否选择了送积分这种优惠方式,0:否,1:是 is_send_gift int(1) 是否选择了送赠品这种优惠方式,0:否,1:是 is_send_bonus int(1) 是否选择了送优惠券这种优惠方式,0:否,1:是 gift_id bigint(20) 赠送的赠品ID(关联赠品表--es_full_discount_gift) bonus_id bigint(20) 赠送的优惠券ID(关联优惠券表--es_coupon) is_discount int(1) 是否选择了打折这种优惠方式,0:否,1:是 discount_value decimal(20,2) 打多少折 start_time bigint(20) 活动开始时间(存放的是以秒为单位的时间戳) end_time bigint(20) 活动结束时间(存放的是以秒为单位的时间戳) title varchar(255) 活动标题(活动名称) range_type int(1) 商品参与活动的方式,1:全部商品,2:部分商品 disabled int(1) 活动是否停用(删除),0:否,1:是 description longtext 活动说明(描述/简介) seller_id bigint(20) 活动所属商家ID(关联商家店铺表--es_shop)
赠品表--es_full_discount_gift
字段名 类型与长度 说明 gift_id bigint(20) 主键ID gift_name varchar(255) 赠品名称 gift_price decimal(20,2) 赠品价格 gift_img varchar(255) 赠品图片 gift_type int(1) 赠品类型(预留扩展字段,现阶段暂时无用) actual_store int(10) 赠品实际库存 enable_store int(10) 赠品可用库存 create_time bigint(20) 创建时间 goods_id bigint(20) 商品ID(预留扩展字段,现阶段暂时无用) disabled int(1) 是否禁用(删除),0:否,1:是 seller_id bigint(20) 活动所属商家ID(关联商家店铺表--es_shop) 满减满赠选择赠送赠品时,现阶段只支持赠送一种赠品并且赠送的数量暂时只支持一个。
赠品不是商品,不可以上架售卖,只允许在满减满赠活动中选择赠送。
赠品可以设置库存,库存也分为实际库存和可用库存,当可用库存为0时不再赠送。
赠品会和订单中的商品一起发货。
关于赠品库存的扣减与恢复:
扣减:下单扣减赠品的可用库存,发货扣减赠品的实际库存。
恢复:下单未付款时,用户取消订单,恢复赠品的可用库存;订单申请售后并且售后服务完成时,同时恢复赠品的可用库存与实际库存。
2、表关联说明
满减满赠活动表与其他表之间的关联图
满减满赠活动表与其他表之间的关联字段对照
满减满赠活动表(es_full_discount) 赠品表(es_full_discount_gift) gift_id gift_id 满减满赠活动表(es_full_discount) 优惠券表(es_coupon) bonus_id coupon_id 满减满赠活动表(es_full_discount) 促销活动商品表(es_promotion_goods) fd_id activity_id 无字段,促销类型值为FULL_DISCOUNT promotion_type
三、缓存设计
1、商家在发布满减满赠促销活动时,在将促销活动信息入库的同时,也会将信息放入缓存中。
缓存key值为:{STOREID_FULL_DISCOUNT_KEY}活动ID。
缓存value值为:FullDiscountDO.java这个实体对象信息。
2、满减满赠促销活动脚本引擎
脚本引擎缓存结构:
满减满赠促销活动的促销脚本引擎缓存结构有两种:
- 当发布活动时,如果选择的是全部商品参与,那么存放的是店铺级别的缓存结构。
- 当发布活动时,如果选择的是部分商品参与,那么存放的是SKU级别的缓存结构。
脚本引擎生成和删除时机:
- 生成:活动开始时生成。
- 删除:活动结束时删除。
关于促销脚本引擎缓存结构可参考《促销活动脚本引擎生成架构》这篇文档。
四、代码展示
1、接口调用流程图
2、相关代码展示
以新增满减满赠活动为例
API代码--FullDiscountSellerController
@RestController
@RequestMapping("/seller/promotion/full-discounts")
@Api(description = "满优惠活动相关API")
@Validated
public class FullDiscountSellerController {
@ApiOperation(value = "添加满优惠活动", response = FullDiscountVO.class)
@PostMapping
public FullDiscountVO add(@Valid @RequestBody FullDiscountVO fullDiscountVO) {
this.verifyFullDiscountParam(fullDiscountVO);
// 获取当前登录的店铺ID
Seller seller = UserContext.getSeller();
Long sellerId = seller.getSellerId();
fullDiscountVO.setSellerId(sellerId);
this.fullDiscountManager.add(fullDiscountVO);
return fullDiscountVO;
}
}新增满减满赠活动业务层代码--FullDiscountManagerImpl
@Override
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = {RuntimeException.class, Exception.class, ServiceException.class, NoPermissionException.class})
public FullDiscountVO add(FullDiscountVO fullDiscountVO) {
//检测开始时间和结束时间
PromotionValid.paramValid(fullDiscountVO.getStartTime(),
fullDiscountVO.getEndTime(),1,null);
this.verifyTime(fullDiscountVO.getStartTime(),
fullDiscountVO.getEndTime(), PromotionTypeEnum.FULL_DISCOUNT, null);
List<PromotionGoodsDTO> goodsDTOList = new ArrayList<>();
//是否是全部商品参与
if (fullDiscountVO.getRangeType() == 1) {
PromotionGoodsDTO goodsDTO = new PromotionGoodsDTO();
goodsDTO.setGoodsId(-1L);
goodsDTO.setSkuId(-1L);
goodsDTO.setGoodsName("全部商品");
goodsDTO.setThumbnail("");
goodsDTOList.add(goodsDTO);
fullDiscountVO.setGoodsList(goodsDTOList);
}
this.verifyRule(fullDiscountVO.getGoodsList());
// 查看是否赠送优惠券,是则判断优惠券的使用时间是否大于活动结束时间
if (fullDiscountVO.getIsSendBonus() != null
&& fullDiscountVO.getIsSendBonus() == 1) {
Long couponId = fullDiscountVO.getBonusId();
couponManager.getModel(couponId);
CouponDO coupon = couponManager.getModel(couponId);
if(coupon.getEndTime()<fullDiscountVO.getEndTime()){
throw new ServiceException(PromotionErrorCode.E401.code(),
"赠送的优惠券有效时间必须大于活动时间");
}
}
//新建满减满赠活动对象
FullDiscountDO fullDiscountDO = new FullDiscountDO();
//从VO中复制相关属性值
BeanUtils.copyProperties(fullDiscountVO, fullDiscountDO);
//满减满赠活动信息入库
fullDiscountMapper.insert(fullDiscountDO);
//获取活动Id
Long id = fullDiscountDO.getFdId();
fullDiscountVO.setFdId(id);
//设置促销信息
PromotionDetailDTO detailDTO = new PromotionDetailDTO();
detailDTO.setStartTime(fullDiscountVO.getStartTime());
detailDTO.setEndTime(fullDiscountVO.getEndTime());
detailDTO.setActivityId(fullDiscountVO.getFdId());
detailDTO.setPromotionType(PromotionTypeEnum.FULL_DISCOUNT.name());
detailDTO.setTitle(fullDiscountVO.getTitle());
//将活动商品入库
this.promotionGoodsManager.add(fullDiscountVO.getGoodsList(), detailDTO);
cache.put(PromotionCacheKeys.getFullDiscountKey(id), fullDiscountDO);
//启用延时任务创建促销活动脚本信息
PromotionScriptMsg promotionScriptMsg = new PromotionScriptMsg();
promotionScriptMsg.setPromotionId(id);
promotionScriptMsg.setPromotionName(fullDiscountDO.getTitle());
promotionScriptMsg.setPromotionType(PromotionTypeEnum.FULL_DISCOUNT);
promotionScriptMsg.setOperationType(ScriptOperationTypeEnum.CREATE);
promotionScriptMsg.setEndTime(fullDiscountDO.getEndTime());
String uniqueKey = "{TIME_TRIGGER_" + PromotionTypeEnum.FULL_DISCOUNT.name()
+ "}_" + id;
timeTrigger.add(TimeExecute.SELLER_PROMOTION_SCRIPT_EXECUTER,
promotionScriptMsg, fullDiscountDO.getStartTime(), uniqueKey);
return fullDiscountVO;
}由此方法可以看出:
首先是满减满赠促销活动主体信息入库:
//满减满赠活动信息入库
fullDiscountMapper.insert(fullDiscountDO);其次将参与活动的商品信息入库:
//将活动商品入库
this.promotionGoodsManager.add(fullDiscountVO.getGoodsList(), detailDTO);然后把活动主体信息放入缓存中:
cache.put(PromotionCacheKeys.getFullDiscountKey(id), fullDiscountDO);
最后启用延时任务创建促销活动脚本信息:
//启用延时任务创建促销活动脚本信息
PromotionScriptMsg promotionScriptMsg = new PromotionScriptMsg();
promotionScriptMsg.setPromotionId(id);
promotionScriptMsg.setPromotionName(fullDiscountDO.getTitle());
promotionScriptMsg.setPromotionType(PromotionTypeEnum.FULL_DISCOUNT);
promotionScriptMsg.setOperationType(ScriptOperationTypeEnum.CREATE);
promotionScriptMsg.setEndTime(fullDiscountDO.getEndTime());
String uniqueKey = "{TIME_TRIGGER_" + PromotionTypeEnum.FULL_DISCOUNT.name()
+ "}_" + id;
timeTrigger.add(TimeExecute.SELLER_PROMOTION_SCRIPT_EXECUTER,
promotionScriptMsg, fullDiscountDO.getStartTime(), uniqueKey);
满减满赠促销活动脚本生成--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)) {
//获取满减满赠促销活动详细信息
FullDiscountVO fullDiscountVO =
this.promotionGoodsClient.getFullDiscountModel(promotionId);
//初始化缓存中的促销活动脚本信息
initScriptCache(fullDiscountVO.getRangeType(),
fullDiscountVO.getSellerId(), promotionId,
promotionType.name());
} else if (PromotionTypeEnum.MINUS.equals(promotionType)) {
//单品立减促销活动
//...省略...
} else if (PromotionTypeEnum.HALF_PRICE.equals(promotionType)) {
//第二件半价促销活动
//...省略...
}
}
/**
* 根据促销活动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)) {
//获取满减满赠促销活动详细信息
FullDiscountVO fullDiscountVO =
this.promotionGoodsClient.getFullDiscountModel(promotionId);
//渲染并读取满减满赠促销活动脚本信息
String script = renderFullDiscountScript(fullDiscountVO);
//构建促销脚本数据结构
PromotionScriptVO promotionScriptVO =
getScript(script, promotionId, true, promotionType,
getFullDiscountTips(fullDiscountVO));
scriptVO.setSellerId(fullDiscountVO.getSellerId());
scriptVO.setRangeType(fullDiscountVO.getRangeType());
scriptVO.setPromotionScriptVO(promotionScriptVO);
} else if (PromotionTypeEnum.MINUS.name().equals(promotionType)) {
//单品立减促销活动
//...省略...
} else if (PromotionTypeEnum.HALF_PRICE.name().equals(promotionType)) {
//第二件半价促销活动
//...省略...
}
//如果是部分商品参与活动,需要查询出参与促销活动的商品skuID集合
if (scriptVO.getRangeType().intValue() == 2) {
List<PromotionGoodsDO> goodsList =
this.promotionGoodsClient.getPromotionGoods(promotionId, promotionType);
scriptVO.setGoodsList(goodsList);
}
return scriptVO;
}
/**
* 渲染并读取满减满赠促销活动脚本信息
* @param fullDiscountVO 满减满赠促销活动信息
* @return
*/
private String renderFullDiscountScript(FullDiscountVO fullDiscountVO) {
Map<String, Object> model = new HashMap<>();
Map<String, Object> params = new HashMap<>();
params.put("startTime", fullDiscountVO.getStartTime().toString());
params.put("endTime", fullDiscountVO.getEndTime().toString());
params.put("isFullMinus", fullDiscountVO.getIsFullMinus());
params.put("isDiscount", fullDiscountVO.getIsDiscount());
params.put("fullMoney", fullDiscountVO.getFullMoney());
params.put("minusValue", fullDiscountVO.getMinusValue()
== null ? 0.00 : fullDiscountVO.getMinusValue());
params.put("discountValue", fullDiscountVO.getDiscountValue()
== null ? 0.00 : CurrencyUtil.div(fullDiscountVO.getDiscountValue(),
10, 2));
List<Map<String, Object>> giftList = new ArrayList<>();
//判断是否免邮费
if (fullDiscountVO.getIsFreeShip().intValue() == 1) {
Map<String, Object> free = new HashMap<>();
free.put("type", "freeShip");
free.put("value", true);
giftList.add(free);
}
//判断是否送积分
if (fullDiscountVO.getIsSendPoint().intValue() == 1) {
Map<String, Object> point = new HashMap<>();
point.put("type", "point");
point.put("value", fullDiscountVO.getPointValue());
giftList.add(point);
}
//判断是否送赠品
if (fullDiscountVO.getIsSendGift().intValue() == 1 ) {
Map<String, Object> gift = new HashMap<>();
gift.put("type", "gift");
gift.put("value", fullDiscountVO.getGiftId().toString());
giftList.add(gift);
}
//判断是否送优惠券
if (fullDiscountVO.getIsSendBonus().intValue() == 1) {
Map<String, Object> coupon = new HashMap<>();
coupon.put("type", "coupon");
coupon.put("value", fullDiscountVO.getBonusId().toString());
giftList.add(coupon);
}
params.put("gift", JsonUtil.objectToJson(giftList));
model.put("promotionActive", params);
String path = "full_discount.ftl";
String script = ScriptUtil.renderScript(path, model);
logger.debug("生成满减满赠促销活动脚本:" + script);
return script;
}
}满减满赠促销活动脚本引擎模板文件--full_discount.ftl
<#--
验证促销活动是否在有效期内
@param promotionActive 活动日期对象(内置常量)
.startTime 获取开始时间
.endTime 活动结束时间
@param $currentTime 当前时间(变量)
@returns {boolean}
-->
function validTime(){
if (${promotionActive.startTime} <= $currentTime && $currentTime <= ${promotionActive.endTime}) {
return true;
}
return false;
}
<#--
减现金或打折活动价格计算
@param promotionActive 促销活动信息对象(内置常量)
.fullMoney 优惠门槛金额
.isFullMinus 是否立减现金 0:否,1:是
.minusValue 立减金额
.isDiscount 是否打折 0:否,1:是
.discountValue 打多少折,例:打8折,值为0.8
@param $price 参与活动的商品总金额(变量)
@returns {*}
-->
function countPrice() {
<#--判断商品金额是否满足优惠条件 -->
if (${promotionActive.fullMoney} <= $price) {
var resultPrice = $price;
if (${promotionActive.isFullMinus} == 1) {
resultPrice = $price - ${promotionActive.minusValue};
}
if (${promotionActive.isDiscount} == 1) {
resultPrice = $price * ${promotionActive.discountValue};
}
return resultPrice < 0 ? 0 : resultPrice.toString();
}
return $price;
}
<#--
赠送赠品
@param promotionActive 促销活动信息对象(内置常量)
.fullMoney 优惠门槛金额
.gift 赠品信息对象json
@param $price 参与活动的商品总金额(变量)
@returns {*}
-->
function giveGift() {
<#-- 判断商品金额是否满足优惠条件 -->
if (${promotionActive.fullMoney} <= $price) {
return JSON.stringify(${promotionActive.gift});
}
return null;
}