参考文档
优惠券表设计:
电商平台-优惠券设计与架构:
优惠券详解:优惠券组成、分类、使用及案例:
关于优惠券后台设计思考:
数据表设计
适合整体为一个商铺的网站体系
优惠券配置表
CREATE TABLE `order_coupon_config` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键id',
`title` varchar(128) NOT NULL DEFAULT '' COMMENT '优惠券标题',
`sub_title` varchar(64) NOT NULL DEFAULT '' COMMENT '副标题',
`context` varchar(256) NOT NULL DEFAULT '' COMMENT '优惠券内容',
`icon` varchar(128) NOT NULL DEFAULT '' COMMENT '图片',
`business_type` tinyint(1) unsigned NOT NULL DEFAULT '1' COMMENT '业务类型 1 通用 2 包车 3 接送机',
`coupon_type` tinyint(1) unsigned NOT NULL DEFAULT '1' COMMENT '优惠券类型 1 用户注册',
`full_money` decimal(10,2) unsigned NOT NULL DEFAULT '0.00' COMMENT '满额使用条件',
`coupon_money` decimal(10,2) unsigned NOT NULL DEFAULT '0.00' COMMENT '优惠券钱',
`total_quota_num` int(10) unsigned NOT NULL COMMENT '优惠券总配额数量',
`no_use_invitation_dispatched_num` int(10) unsigned NOT NULL COMMENT '不使用邀请码配额:发券数量',
`use_invitation_dispatched_num` int(10) unsigned NOT NULL COMMENT '使用邀请码配额:发券数量',
`take_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '已发放的优惠券数量',
`used_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '已使用的优惠券数量',
`start_time` datetime NOT NULL COMMENT '发放开始时间',
`end_time` datetime NOT NULL COMMENT '发放结束时间',
`valid_type` tinyint(1) unsigned NOT NULL DEFAULT '1' COMMENT '时效:1绝对时效(领取后2019-11-30 12:00:00-2019-12-30 12:00:00时间段有效)2相对时效(领取后N天有效)',
`valid_start_time` datetime NOT NULL COMMENT '使用开始时间',
`valid_end_time` datetime NOT NULL COMMENT '使用结束时间',
`valid_days` int(1) unsigned NOT NULL DEFAULT '7' COMMENT '自领取之日起有效天数',
`status` tinyint(1) unsigned NOT NULL DEFAULT '1' COMMENT '1 可使用 2 不使用',
`create_user_id` bigint(20) unsigned NOT NULL COMMENT '创建人的userId',
`update_user_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '更新人的userId',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COMMENT='优惠券配置表'
用户优惠券表
CREATE TABLE `order_coupon_user` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键id',
`user_id` bigint(20) DEFAULT NULL COMMENT '使用者id',
`order_no` varchar(64) DEFAULT '' COMMENT '订单号',
`coupon_id` bigint(20) DEFAULT NULL COMMENT '优惠券编号',
`coupon_money` decimal(10,2) DEFAULT '0.00' COMMENT '优惠券钱',
`full_money` decimal(10,2) DEFAULT '0.00' COMMENT '满额使用条件',
`status` tinyint(1) DEFAULT '1' COMMENT '状态,1未使用 2已使用',
`start_time` datetime DEFAULT NULL COMMENT '开始时间',
`end_time` datetime DEFAULT NULL COMMENT '结束时间',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户优惠券表'
配置案例
# 新人优惠券(绝对时效)
insert into `coupon_config` (`id`, `title`, `icon`, `used_type`, `coupon_type`, `with_amount`, `used_amount`, `total_quota_num`,
`no_use_invitation_dispatched_num`,`use_invitation_dispatched_num`, `take_count`, `used_count`, `start_time`, `end_time`, `valid_type`,
`valid_start_time`, `valid_end_time`, `valid_days`, `status`,`create_user_id`, `create_time`, `update_user_id`, `update_time`)
values('1','新人优惠券(绝对时效)','http://img','1','1','200.00','30.00','1000','2','3','0','0','2019-11-01 14:35:14','2019-12-01 14:35:21',
'1','2019-11-01 14:44:30','2019-12-01 14:51:51','0','1','1','2019-11-01 14:36:42','0','2019-11-01 14:52:44');
# 新人优惠券(相对时效)
insert into `coupon_config` (`id`, `title`, `icon`, `used_type`, `coupon_type`, `with_amount`, `used_amount`, `total_quota_num`,
`no_use_invitation_dispatched_num`, `use_invitation_dispatched_num`, `take_count`, `used_count`, `start_time`, `end_time`, `valid_type`,
`valid_start_time`, `valid_end_time`, `valid_days`, `status`, `create_user_id`, `create_time`, `update_user_id`, `update_time`)
values('2','新人优惠券(相对时效)','http://img','1','1','300.00','20.00','1000','2','3','0','0','2019-11-01 14:53:29','2019-12-01 14:53:33',
'2','0000-00-00 00:00:00','0000-00-00 00:00:00','7','1','1','2019-11-01 14:54:08','0','2019-11-01 14:54:13');
接口设计
优惠券配置模块
couponConfig/add(增)
couponConfig/del(删)
couponConfig/update(改)
couponConfig/get(查询单个)
couponConfig/list(分页查询),按照修改时间排序
用户优惠券模块
userCoupon/addWithRegister(注册时发放优惠券),注意使用邀请码时和不使用邀请码的发放数量不一样,注意更新配置表里面的take_count字段
userCoupon/use(使用优惠券),在某个订单上使用该优惠券,注意更新配置表里面的used_count字段
userCoupon/list(分页查询),按照可使用状态排序,优惠券的失效状态通过开始和结束时间的判断
代码
优惠券配置表
import lombok.Data;
import tk.mybatis.mapper.annotation.KeySql;
import javax.persistence.Column;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Datapublic class OrderCouponConfig implements Serializable {
private static final long serialVersionUID = 1033517251653865204L;
/**
* 主键id
*/
@Id
@Column(name = "`id`")
@KeySql(useGeneratedKeys = true)
private Long id;
/**
* 优惠券标题
*/
private String title;
/**
* 副标题
*/
private String subTitle;
/**
* 优惠券内容
*/
private String context;
/**
* 图片
*/
private String icon;
/**
* 1 通用 2 包车 3 接送机
*/
private Integer businessType;
/**
* 优惠券类型 1 用户注册
*/
private Integer couponType;
/**
* 满额使用条件
*/
private BigDecimal fullMoney;
/**
* 优惠券钱
*/
private BigDecimal couponMoney;
/**
* 优惠券总配额数量
*/
private Long totalQuotaNum;
/**
* 不使用邀请码配额:发券数量
*/
private Integer noUseInvitationDispatchedNum;
/**
* 使用邀请码配额:发券数量
*/
private Integer useInvitationDispatchedNum;
/**
* 已发放的优惠券数量
*/
private Long takeCount;
/**
* 已使用的优惠券数量
*/
private Long usedCount;
/**
* 发放开始时间
*/
private LocalDateTime startTime;
/**
* 发放结束时间
*/
private LocalDateTime endTime;
/**
* 时效:1绝对时效(领取后2019-11-30 12:00:00-2019-12-30 12:00:00时间段有效)2相对时效(领取后N天有效)
*/
private Integer validType;
/**
* 使用开始时间
*/
private LocalDateTime validStartTime;
/**
* 使用结束时间
*/
private LocalDateTime validEndTime;
/**
* 自领取之日起有效天数
*/
private Integer validDays;
/**
* 1生效 2失效 3已结束
*/
private Integer status;
/**
* 创建人的userId
*/
private Long createUserId;
/**
* 更新人的userId
*/
private Long updateUserId;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 修改时间
*/
private LocalDateTime updateTime;
}
用户优惠券
import lombok.Data;
import tk.mybatis.mapper.annotation.KeySql;
import javax.persistence.Column;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* @author shuaige
*/
@Datapublic class OrderCouponUser implements Serializable {
private static final long serialVersionUID = -245769809879792977L;
/**
* 主键id
*/
@Id
@Column(name = "`id`")
@KeySql(useGeneratedKeys = true)
private Long id;
/**
* 用户优惠券编号
*/
private String userCouponNo;
/**
* 使用者id
*/
private Long userId;
/**
* 订单号
*/
private String orderNo;
/**
* 优惠券编号
*/
private Long couponId;
/**
* 优惠券钱
*/
private BigDecimal couponMoney;
/**
* 满额使用条件
*/
private BigDecimal fullMoney;
/**
* 状态,1未使用 2已使用
*
* @See OrderCouponUserEnum.Status
*/
private Integer status;
/**
* 开始时间
*/
private LocalDateTime startTime;
/**
* 结束时间
*/
private LocalDateTime endTime;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 修改时间
*/
private LocalDateTime updateTime;
}
优惠券列表返回类
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* @author: shuaige
* @description: 优惠券列表
*/
@Data
public class UserCouponListResponse implements Serializable {
private static final long serialVersionUID = 5927015985585134260L;
@ApiModelProperty("优惠券编号")
private String couponNo;
@ApiModelProperty("货币符号")
private String currency;
@ApiModelProperty("优惠券金额")
private String couponMoney;
@ApiModelProperty("优惠券标题")
private String title;
@ApiModelProperty("优惠券副标题")
private String subTitle;
@ApiModelProperty("使用时间")
private String useTime;
}
优惠券接口
import com.anchi.car.coresystem.consumer.dao.entity.OrderCouponUser;
import com.anchi.car.coresystem.consumer.model.response.pay.UserCouponListResponse;
import java.util.List;
public interface UserCouponService {
/**
* 注册时发放优惠券
*
* @param userId 用户id
* @param isUseInvitationCode 是否使用了邀请码
*/
boolean addWithRegister(Long userId, boolean isUseInvitationCode);
/**
* 校验优惠券
*
* @param userCouponNo 用户的优惠券编号
* @param userId 用户id
*/
OrderCouponUser validCoupon(String userCouponNo, Long userId);
/**
* 使用优惠券
*
* @param userCouponNo 用户的优惠券编号
* @param orderNo 订单号
*/
boolean use(String userCouponNo, String orderNo, Long userId);
/**
* 分页查询
*
* @param userId 用户id
*/
List<UserCouponListResponse> list(Long userId);
}
优惠券实现
import com.anchi.car.coresystem.consumer.common.BigDecimalUtil;
import com.anchi.car.coresystem.consumer.common.DateUtil;
import com.anchi.car.coresystem.consumer.common.NumberUtils;
import com.anchi.car.coresystem.consumer.common.RandomUtil;
import com.anchi.car.coresystem.consumer.constants.dao.OrderCouponConfigEnum;
import com.anchi.car.coresystem.consumer.constants.dao.OrderCouponUserEnum;
import com.anchi.car.coresystem.consumer.dao.entity.OrderCouponConfigDO;
import com.anchi.car.coresystem.consumer.dao.entity.OrderCouponUserDO;
import com.anchi.car.coresystem.consumer.dao.mapper.OrderCouponConfigMapper;
import com.anchi.car.coresystem.consumer.dao.mapper.OrderCouponUserMapper;
import com.anchi.car.coresystem.consumer.exception.BaseBusinessException;
import com.anchi.car.coresystem.consumer.model.response.pay.UserCouponListResponse;
import com.anchi.car.coresystem.consumer.service.usercoupon.UserCouponService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
@Service
@Slf4j
public class UserCouponServiceImpl implements UserCouponService {
@Resource
private OrderCouponConfigMapper orderCouponConfigMapper;
@Resource
private OrderCouponUserMapper orderCouponUserMapper;
@Resource
private BigDecimalUtil bigDecimalUtil;
@Resource
private DateUtil dateUtil;
@Resource
private RandomUtil randomUtil;
/**
* 读取优惠券的配置 order_coupon_config
* 过滤掉不合格的优惠券
* 发放优惠券 order_coupon_user
* 更新优惠券的配置take_count字段 order_coupon_config
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean addWithRegister(Long userId, boolean isUseInvitationCode) {
// 读取优惠券的配置
OrderCouponConfigDO orderCouponConfigList = new OrderCouponConfigDO();
orderCouponConfigList.setBusinessType(OrderCouponConfigEnum.BusinessType.COMMON.getValue());
orderCouponConfigList.setCouponType(OrderCouponConfigEnum.CouponType.USER_REGISTER.getValue());
orderCouponConfigList.setStatus(OrderCouponConfigEnum.Status.ENABLE.getValue());
// 获取北京时间
LocalDateTime beijingTime = dateUtil.getNowLocalDateTime("beijing");
ArrayList<OrderCouponConfigDO> list = new ArrayList<>();
orderCouponConfigMapper.select(orderCouponConfigList)
.stream()
// 过滤掉不合格的优惠券
.filter(orderCouponConfig -> {
return validOrderCouponConfig(beijingTime, orderCouponConfig);
})
.forEach(orderCouponConfig -> {
list.add(orderCouponConfig);
int dispatchNum;
// 获取优惠券派发的个数
if (isUseInvitationCode) {
dispatchNum = orderCouponConfig.getUseInvitationDispatchedNum();
} else {
dispatchNum = orderCouponConfig.getNoUseInvitationDispatchedNum();
}
// 根据是否使用了邀请码派发
for (int i = 0; i < dispatchNum; i++) {
// 根据天数
dispatcheOrder(userId, beijingTime, orderCouponConfig);
// 更新优惠券的配置take_count字段 order_coupon_config
updateOrderCouponConfigInfo(orderCouponConfig);
}
});
return !list.isEmpty();
}
/**
* 更新优惠券信息
*
* @param orderCouponConfig 优惠券配置信息
*/
private void updateOrderCouponConfigInfo(OrderCouponConfigDO orderCouponConfig) {
OrderCouponConfigDO orderCouponConfigUpdate = new OrderCouponConfigDO();
orderCouponConfigUpdate.setId(orderCouponConfig.getId());
orderCouponConfigUpdate.setTakeCount(orderCouponConfig.getTakeCount() + 1);
orderCouponConfigMapper.updateByPrimaryKeySelective(orderCouponConfigUpdate);
}
/**
* 发放优惠券
*
* @param userId 用户id
* @param beijingTime 北京时间
* @param orderCouponConfig 优惠券配置信息
*/
private void dispatcheOrder(Long userId, LocalDateTime beijingTime, OrderCouponConfigDO orderCouponConfig) {
// 添加数据
OrderCouponUserDO orderCouponUser = new OrderCouponUserDO();
orderCouponUser.setUserId(userId);
orderCouponUser.setCouponId(orderCouponConfig.getId());
orderCouponUser.setCouponMoney(orderCouponConfig.getCouponMoney());
orderCouponUser.setFullMoney(orderCouponConfig.getFullMoney());
orderCouponUser.setUserCouponNo(randomUtil.createCouponCode());
// 获取时效
if (OrderCouponConfigEnum.ValidType.RELATIVELY_TIME.getValue().equals(orderCouponConfig.getValidType())) {
Integer validDays = orderCouponConfig.getValidDays();
// 相对时效
orderCouponUser.setStartTime(beijingTime);
// 相对时效延伸到截止日的24点
LocalDateTime beijingTimePlusDays = beijingTime.plusDays(validDays + 1);
orderCouponUser.setEndTime(beijingTimePlusDays.withHour(0).withMinute(0).withSecond(0).withNano(0));
} else {
// 绝对时效
orderCouponUser.setStartTime(orderCouponConfig.getStartTime());
orderCouponUser.setEndTime(orderCouponConfig.getEndTime());
}
orderCouponUserMapper.insertSelective(orderCouponUser);
}
/**
* 校验优惠券配置
*
* @param beijingTime 北京时间
* @param orderCouponConfig 优惠券配置
*/
private boolean validOrderCouponConfig(LocalDateTime beijingTime, OrderCouponConfigDO orderCouponConfig) {
Long totalQuotaNum = orderCouponConfig.getTotalQuotaNum();
// 校验优惠券金额
if (bigDecimalUtil.isNullOrLessOrEqZero(orderCouponConfig.getCouponMoney())) {
return false;
}
// 校验优惠券的数量
if (NumberUtils.isNullOrEqualOrLessZero(totalQuotaNum)) {
return false;
}
// 校验已发放的优惠券数量
Long takeCount = orderCouponConfig.getTakeCount();
// 防止数据放入错误
if (takeCount == null) {
return false;
}
// 校验已发放优惠券数量不能超过总数量
if (totalQuotaNum.compareTo(takeCount) <= 0) {
return false;
}
// 校验是否在发放时间
LocalDateTime startTime = orderCouponConfig.getStartTime();
LocalDateTime endTime = orderCouponConfig.getEndTime();
return beijingTime.isAfter(startTime) && beijingTime.isBefore(endTime);
}
/**
* 获取优惠券信息
* 校验优惠券,日期
* 校验优惠券的所属
*/
@Override
public OrderCouponUserDO validCoupon(String userCouponNo, Long userId) {
OrderCouponUserDO orderCouponUserSelect = new OrderCouponUserDO();
orderCouponUserSelect.setUserCouponNo(userCouponNo);
// 查询用户优惠券信息
OrderCouponUserDO orderCouponUser = Optional.ofNullable(orderCouponUserMapper.selectOne(orderCouponUserSelect))
.orElseThrow(() -> {
return new BaseBusinessException("no user coupon info");
});
// 校验用户优惠券是否已被使用
if (OrderCouponUserEnum.Status.USED.getValue().equals(orderCouponUser.getStatus())) {
throw new BaseBusinessException("coupon is used");
}
// 校验优惠券的所属
if (!Objects.equals(userId, orderCouponUser.getUserId())) {
throw new BaseBusinessException("coupon is not yours");
}
return orderCouponUser;
}
/**
* 查询用户优惠券信息
* 校验用户的优惠券
* 查询优惠券配置信息
* 更新用户的优惠券信息的status字段为已使用 order_coupon_user
* 更新优惠券的配置表的used_count字段+1 order_coupon_config
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean use(String userCouponNo, String orderNo, Long userId) {
OrderCouponUserDO orderCouponUser = validCoupon(userCouponNo, userId);
// 查询优惠券配置信息
OrderCouponConfigDO orderCouponConfig = Optional.ofNullable(orderCouponConfigMapper.selectByPrimaryKey(orderCouponUser.getCouponId()))
.orElseThrow(() -> {
return new BaseBusinessException("no coupon config info");
});
// 更新用户的优惠券信息 order_coupon_user
OrderCouponUserDO orderCouponUserUpdate = new OrderCouponUserDO();
orderCouponUserUpdate.setId(orderCouponUser.getId());
orderCouponUserUpdate.setOrderNo(orderNo);
orderCouponUserUpdate.setStatus(OrderCouponUserEnum.Status.USED.getValue());
if (orderCouponUserMapper.updateByPrimaryKeySelective(orderCouponUserUpdate) <= 0) {
throw new BaseBusinessException("update coupon info error");
}
OrderCouponConfigDO orderCouponConfigUpdate = new OrderCouponConfigDO();
orderCouponConfigUpdate.setId(orderCouponConfig.getId());
orderCouponConfigUpdate.setUsedCount(orderCouponConfig.getUsedCount() + 1);
if (orderCouponConfigMapper.updateByPrimaryKeySelective(orderCouponConfigUpdate) <= 0) {
throw new BaseBusinessException("update coupon config info error");
}
return true;
}
/**
* 根据用户id和状态查询 order_coupon_user
*/
@Override
public List<UserCouponListResponse> list(Long userId) {
HashMap<String, Object> condition = new HashMap<>(1);
condition.put("userId", userId);
return orderCouponUserMapper.list(condition)
.stream()
.map(userCouponList -> {
UserCouponListResponse userCouponListResponse = new UserCouponListResponse();
userCouponListResponse.setCouponNo(userCouponList.getUserCouponNo());
userCouponListResponse.setCurrency("HKD");
userCouponListResponse.setCouponMoney(userCouponList.getCouponMoney().toString());
userCouponListResponse.setTitle(userCouponList.getTitle());
userCouponListResponse.setSubTitle(userCouponList.getSubTitle());
String startTime = userCouponList.getStartTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
String endTime = userCouponList.getEndTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
userCouponListResponse.setUseTime(startTime + "-" + endTime);
return userCouponListResponse;
})
.collect(Collectors.toList());
}
}