优惠券
一、功能说明
1、优惠券共有两种,一种是平台优惠券,一种是商家优惠券。
平台优惠券:平台发布,优惠金额由平台、商家各自承担一部分。
商家优惠券:商家发布,优惠金额由商家承担。
2、用户可以通过两种方式获取优惠券:免费领取和活动赠送。
免费领取:可在优惠券列表或店铺首页领取。
活动赠送:商家发布满减满赠活动时选择赠送优惠券,用户购买活动商品获取。
3、使用范围:
平台优惠券:全品类、指定商品分类和指定商品。
店铺优惠券:本店铺下所有商品。
4、优惠券生效后不可以进行修改和删除操作。
5、只有在订单结算页面才可以使用优惠券,优惠券不可叠加使用,同一订单只能使用一张优惠券。
6、优惠券可以设置限领数量,如果不想做限制,设置限领数量为0即可。
7、订单申请售后,使用的优惠券不会退还。
8、优惠券使用限制:
积分商品不允许使用优惠券。
拼团商品不允许使用优惠券。
9、商品参与了其它促销活动(例如满减满折、单品立减、第二件半价、团购和秒杀),可以使用优惠券。
根据促销前的金额来判断促销优惠和优惠券是否可以同时使用,例如:
用户购买商品A,原价为2500,促销后金额2000元,用户有一张满2500减200优惠券,一张满2000减50的优惠券,两张券都可以使用。
二、数据库设计
1、表结构设计
优惠券表—es_coupon
字段名 类型与长度 说明 coupon_id bigint(20) 主键ID title varchar(20) 优惠券标题(名称) coupon_price decimal(20,2) 优惠券金额 coupon_threshold_price decimal(20,2) 优惠券门槛金额(当订单满足这个金额才可使用) start_time bigint(20) 优惠券生效时间(存放的是以秒为单位的时间戳) end_time bigint(20) 优惠券失效时间(存放的是以秒为单位的时间戳) create_num int(10) 优惠券发行数量 limit_num int(10) 用户限领数量 used_num int(10) 已被使用数量 seller_id bigint(20) 商家ID(关联es_shop表) received_num int(10) 已被领取的数量 seller_name varchar(100) 商家名称 type varchar(20) 优惠券获取方式 FREE_GET:免费领取,ACTIVITY_GIVE:活动赠送 use_scope varchar(20) 优惠券使用范围 ALL:全平台,CATEGORY:指定分类下的商品,SOME_GOODS:指定商品(如果是店铺优惠券此值为空) scope_id longtext 使用范围关联的ID(如果是店铺优惠券此值为空;如果是平台优惠券,使用范围如果是全平台,此值为0) shop_commission int(5) 店铺承担比例(如果是店铺优惠券此值为空) scope_description longtext 使用范围说明 activity_description longtext 优惠券说明
会员优惠券表—es_member_coupon
字段名 类型与长度 说明 mc_id bigint(20) 主键ID coupon_id bigint(20) 优惠券ID(关联es_coupon表) member_id bigint(20) 会员ID(关联es_member表) used_time bigint(11) 使用时间 create_time bigint(11) 获取时间 order_id bigint(20) 订单ID(关联es_order表) order_sn varchar(30) 订单编号(关联es_order表) member_name varchar(30) 会员名称 title varchar(20) 优惠券标题(名称) coupon_price decimal(20,2) 优惠券金额 coupon_threshold_price decimal(20,2) 优惠券门槛金额(当订单满足这个金额才可使用) start_time bigint(20) 优惠券生效时间(存放的是以秒为单位的时间戳) end_time bigint(20) 优惠券失效时间(存放的是以秒为单位的时间戳) used_status smallint(1) 使用状态 0:未使用,1:已使用,2:已过期,3:已作废 seller_id bigint(20) 商家ID(关联es_shop表) seller_name varchar(50) 商家名称 use_scope varchar(20) 优惠券使用范围 ALL:全平台,CATEGORY:指定分类下的商品,SOME_GOODS:指定商品(如果是店铺优惠券此值为空) scope_id longtext 使用范围关联的ID(如果是店铺优惠券此值为空;如果是平台优惠券,使用范围如果是全平台,此值为0)
2、表关联说明
优惠券表与其他表之间的关联图
优惠券表与其他表之间的关联字段对照
优惠券表(es_coupon) 会员优惠券表(es_member_coupon) coupon_id coupon_id
三、缓存设计
优惠券脚本引擎
- 脚本引擎缓存结构:优惠券级别的缓存。
- 脚本引擎生成和删除时机:
- 生成:优惠券生效时生成。
- 删除:活动结束时删除。
关于促销脚本引擎缓存结构可参考《促销活动脚本引擎生成架构》这篇文档。
四、代码设计
1、接口调用流程图
平台和商家发布优惠券
用户领取优惠券
使用优惠券
2、相关代码展示
发布优惠券
@RestController
@RequestMapping("/seller/promotion/coupons")
@Api(description = "优惠券相关API")
@Validated
public class CouponSellerController {
@ApiOperation(value = "添加优惠券", response = CouponDO.class)
@PostMapping
public CouponDO add(@Valid CouponDO couponDO) {
Seller seller = UserContext.getSeller();
couponDO.setSellerId(seller.getSellerId());
couponDO.setSellerName(seller.getSellerName());
this.couponManager.add(couponDO);
return couponDO;
}
}@Service
public class CouponManagerImpl implements CouponManager {
@Override
@Transactional(propagation = Propagation.REQUIRED, rollbackFor
= {ServiceException.class, RuntimeException.class, Exception.class})
public CouponDO add(CouponDO coupon) {
//检测开始时间和结束时间
PromotionValid.paramValid(coupon.getStartTime(), coupon.getEndTime(), 1, null);
if (coupon.getLimitNum() < 0) {
throw new ServiceException(PromotionErrorCode.E406.code(),
"限领数量不能为负数");
}
//校验每人限领数是都大于发行量
if (coupon.getLimitNum() > coupon.getCreateNum()) {
throw new ServiceException(PromotionErrorCode.E405.code(),
"限领数量超出发行量");
}
if (coupon.getCouponPrice() == null || coupon.getCouponPrice() <= 0) {
throw new ServiceException(PromotionErrorCode.E409.code(),
"优惠券面额必须大于0元");
}
if (coupon.getCouponThresholdPrice() == null ||
coupon.getCouponThresholdPrice() <= 0) {
throw new ServiceException(PromotionErrorCode.E409.code(),
"优惠券门槛价格必须大于0元");
}
//校验优惠券面额是否小于门槛价格
if (coupon.getCouponPrice() >= coupon.getCouponThresholdPrice()) {
throw new ServiceException(PromotionErrorCode.E409.code(),
"优惠券面额必须小于优惠券门槛价格");
}
//开始时间取前段+00:00:00 结束时间取前段+23:59:59
String startStr = DateUtil.toString(coupon.getStartTime(), "yyyy-MM-dd");
String endStr = DateUtil.toString(coupon.getEndTime(), "yyyy-MM-dd");
coupon.setStartTime(DateUtil.getDateline(startStr + " 00:00:00"));
coupon.setEndTime(DateUtil.getDateline(endStr + " 23:59:59",
"yyyy-MM-dd hh:mm:ss"));
this.paramValid(coupon.getStartTime(), coupon.getEndTime());
coupon.setReceivedNum(0);
coupon.setUsedNum(0);
//部分商品和分类的id存储增加,,
if (CouponUseScope.SOME_GOODS.name().equals(coupon.getUseScope()) ||
CouponUseScope.CATEGORY.name().equals(coupon.getUseScope())) {
coupon.setScopeId("," + coupon.getScopeId() + ",");
}
this.couponMapper.insert(coupon);
//启用延时任务创建优惠券脚本信息
PromotionScriptMsg promotionScriptMsg = new PromotionScriptMsg();
promotionScriptMsg.setPromotionId(coupon.getCouponId());
promotionScriptMsg.setPromotionName(coupon.getTitle());
promotionScriptMsg.setPromotionType(PromotionTypeEnum.COUPON);
promotionScriptMsg.setOperationType(ScriptOperationTypeEnum.CREATE);
promotionScriptMsg.setEndTime(coupon.getEndTime());
String uniqueKey = "{TIME_TRIGGER_" + PromotionTypeEnum.COUPON.name()
+ "}_" + coupon.getCouponId();
timeTrigger.add(TimeExecute.COUPON_SCRIPT_EXECUTER, promotionScriptMsg,
coupon.getStartTime(), uniqueKey);
return coupon;
}
}用户领取优惠券
@RestController
@RequestMapping("/members/coupon")
@Api(description = "会员优惠券相关API")
@Validated
public class MemberCouponBuyerController {
@ApiOperation(value = "用户领取优惠券")
@ApiImplicitParams({
@ApiImplicitParam(name = "coupon_id", value = "优惠券id", required = true,
dataType = "String", paramType = "path")
})
@PostMapping(value = "/{coupon_id}/receive")
public String receiveBonus(@ApiIgnore @PathVariable("coupon_id") Long couponId) {
//限领检测
this.memberCouponManager.checkLimitNum(couponId);
Buyer buyer = UserContext.getBuyer();
this.memberCouponManager.receiveBonus(buyer.getUid(),
buyer.getUsername(), couponId);
return "";
}
}@Service
public class MemberCouponManagerImpl implements MemberCouponManager {
@Override
@Transactional(value = "memberTransactionManager", propagation =
Propagation.REQUIRED, rollbackFor = Exception.class)
public void receiveBonus(Long memberId, String memberName, Long couponId) {
//根据优惠券id获取优惠券信息
CouponDO couponDO = this.couponClient.getModel(couponId);
//如果会员id不为空
if (memberId != null) {
//添加会员优惠券表
MemberCoupon memberCoupon = new MemberCoupon(couponDO);
//设置优惠券创建时间
memberCoupon.setCreateTime(DateUtil.getDateline());
//设置会员ID
memberCoupon.setMemberId(memberId);
//设置会员名称
memberCoupon.setMemberName(memberName);
//设置优惠券使用状态 0:未使用,1:已使用,2:已过期,3:已作废
memberCoupon.setUsedStatus(0);
//会员优惠券入库
memberCouponMapper.insert(memberCoupon);
// 修改优惠券已被领取的数量
this.couponClient.addReceivedNum(couponId);
}
}
@Override
public void checkLimitNum(Long couponId) {
//根据优惠券ID获取优惠券信息
CouponDO couponDO = this.couponClient.getModel(couponId);
//获取当前登录的会员信息
Buyer buyer = UserContext.getBuyer();
//获取每人限领数量
int limitNum = couponDO.getLimitNum();
//新建查询条件包装器
QueryWrapper<MemberCoupon> wrapper = new QueryWrapper<>();
//以会员id为查询条件
wrapper.eq("member_id", buyer.getUid());
//以优惠券id为查询条件
wrapper.eq("coupon_id", couponId);
//查询会员优惠券数量
int num = memberCouponMapper.selectCount(wrapper);
//判断优惠券是否已被领完
if (couponDO.getReceivedNum() >= couponDO.getCreateNum()) {
throw new ServiceException(MemberErrorCode.E203.code(), "优惠券已被领完");
}
//判断优惠券是否已达到限领数量
if (limitNum != 0 && num >= limitNum) {
throw new ServiceException(MemberErrorCode.E203.code(),
"优惠券限领" + limitNum + "个");
}
}
}使用优惠券
@Api(description = "购物车价格计算API")
@RestController
@RequestMapping("/trade/promotion")
@Validated
public class TradePromotionController {
@ApiOperation(value = "设置优惠券", notes = "使用优惠券的时候分为三种情况:前2种情况couponId 不为0,不为空。第3种情况couponId为0," +
"1、使用优惠券:在刚进入订单结算页,为使用任何优惠券之前。" +
"2、切换优惠券:在1、情况之后,当用户切换优惠券的时候。" +
"3、取消已使用的优惠券:用户不想使用优惠券的时候。")
@ApiImplicitParams({
@ApiImplicitParam(name = "seller_id", value = "店铺ID", required = true,
dataType = "int", paramType = "path"),
@ApiImplicitParam(name = "mc_id", value = "优惠券ID", required = true,
dataType = "int", paramType = "path"),
@ApiImplicitParam(name = "way", value = "结算方式,BUY_NOW:立即购买,CART:"
+"购物车", required = true, dataType = "String")
})
@PostMapping(value = "/{seller_id}/seller/{mc_id}/coupon")
public void setCoupon(@NotNull(message = "店铺id不能为空")
@PathVariable("seller_id") Long sellerId,
@NotNull(message = "优惠券id不能为空")
@PathVariable("mc_id") Long mcId,
@NotNull(message = "结算方式不能为空")
@RequestParam String way) {
CartBuilder cartBuilder =
new DefaultCartBuilder(CartType.CART, cartSkuRenderer, null,
cartPriceCalculator, checkDataRebderer);
CartView cartViewold =
cartBuilder.renderSku(CheckedWay.valueOf(way)).countPrice(false).build();
//先判断优惠券能否使用
MemberCoupon memberCoupon = promotionManager.detectCoupon(
sellerId, mcId, cartViewold.getCartList());
//查询要结算的某卖家的购物信息
CartView cartView = cartBuilder.renderSku(CheckedWay.valueOf(way))
.countPrice(true).build();
//设置优惠券 goodsPrice
promotionManager.useCoupon(sellerId, mcId, cartView.getCartList(),
memberCoupon);
}
}@Service
public class CartPromotionManagerImpl implements CartPromotionManager {
@Override
public void useCoupon(Long sellerId, Long mcId, List<CartVO> cartList,
MemberCoupon memberCoupon) {
if (memberCoupon != null) {
//查询选中的促销
SelectedPromotionVo selectedPromotionVo = getSelectedPromotion();
CouponVO couponVO = new CouponVO(memberCoupon);
SelectedPromotionVo selectedPromotion = getSelectedPromotion();
selectedPromotion.putCooupon(sellerId, couponVO);
logger.debug("使用优惠券:" + couponVO);
logger.debug("促销信息为:" + selectedPromotionVo);
String cacheKey = this.getOriginKey();
cache.put(cacheKey, selectedPromotion);
}
}
}创建和删除优惠券促销脚本
@Component("couponScriptTimeTriggerExecuter")
public class CouponScriptTimeTriggerExecuter implements TimeTriggerExecuter {
@Override
public void execute(Object object) {
PromotionScriptMsg promotionScriptMsg = (PromotionScriptMsg) object;
//获取促销活动ID
Long promotionId = promotionScriptMsg.getPromotionId();
//优惠券级别缓存key
String cacheKey = CachePrefix.COUPON_PROMOTION.getPrefix() + promotionId;
//如果是优惠券开始生效
if (ScriptOperationTypeEnum.CREATE.equals(
promotionScriptMsg.getOperationType())) {
//获取优惠券详情
CouponDO coupon = this.couponClient.getModel(promotionId);
//渲染并读取优惠券脚本信息
String script = renderCouponScript(coupon);
//将优惠券脚本信息放入缓存
cache.put(cacheKey, script);
//优惠券生效后,立马设置一个优惠券失效的流程
promotionScriptMsg.setOperationType(ScriptOperationTypeEnum.DELETE);
String uniqueKey = "{TIME_TRIGGER_"
+ promotionScriptMsg.getPromotionType().name()
+ "}_" + promotionId;
timeTrigger.add(TimeExecute.COUPON_SCRIPT_EXECUTER, promotionScriptMsg,
promotionScriptMsg.getEndTime(), uniqueKey);
this.logger.debug("优惠券[" + promotionScriptMsg.getPromotionName()
+ "]开始生效,id=[" + promotionId + "]");
} else {
//删除缓存中的促销脚本数据
cache.remove(cacheKey);
this.logger.debug("优惠券[" + promotionScriptMsg.getPromotionName()
+ "]已经失效,id=[" + promotionId + "]");
}
}
/**
* 渲染并读取优惠券脚本信息
* @param coupon 优惠券信息
* @return
*/
private String renderCouponScript(CouponDO coupon) {
Map<String, Object> model = new HashMap<>();
Map<String, Object> params = new HashMap<>();
params.put("startTime", coupon.getStartTime().toString());
params.put("endTime", coupon.getEndTime().toString());
params.put("couponPrice", coupon.getCouponPrice());
model.put("coupon", params);
String path = "coupon.ftl";
String script = ScriptUtil.renderScript(path, model);
logger.debug("生成优惠券脚本:" + script);
return script;
}
}优惠券脚本引擎模板文件—coupon.ftl
<#--
验证优惠券是否在有效期内
@param coupon 优惠券信息对象(内置常量)
.startTime 有效期开始时间
.endTime 有效期结束时间
@param $currentTime 当前时间(变量)
@return {boolean}
-->
function validTime(){
if (${coupon.startTime} <= $currentTime && $currentTime <= ${coupon.endTime}) {
return true;
}
return false;
}
<#--
优惠券金额计算
@param coupon 优惠券信息对象(内置常量)
.couponPrice 优惠券面额
@param $price 商品总价(变量,如果有商品参与了其它促销活动,为商品优惠后的总价)
@returns {*}
-->
function countPrice() {
var resultPrice = $price - ${coupon.couponPrice};
return resultPrice < 0 ? 0 : resultPrice.toString();
}