参考文档
优惠券表设计:https://blog.csdn.net/t_332741160/article/details/86591243
电商平台-优惠券设计与架构:https://blog.csdn.net/NotBugger/article/details/80942762
优惠券详解:优惠券组成、分类、使用及案例:https://blog.csdn.net/k7Jz78GeJJ/article/details/79493305
关于优惠券后台设计思考:https://blog.csdn.net/LoveATM911/article/details/77931470
数据表设计
适合整体为一个商铺的网站体系
优惠券配置表
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()); } }