参考文档

优惠券表设计:

电商平台-优惠券设计与架构:

优惠券详解:优惠券组成、分类、使用及案例:

关于优惠券后台设计思考:

数据表设计

适合整体为一个商铺的网站体系

优惠券配置表

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());
    }

}