优惠券系统设计与实现
# 优惠券系统设计与实现
# 系统概述
优惠券系统是电商平台营销活动的核心组件,通过发放各种类型的优惠券来刺激用户消费,提升平台GMV。本文档详细介绍优惠券系统的设计思路、核心功能模块以及技术实现方案。
# 系统架构设计
# 核心模块
优惠券系统
├── 优惠券管理模块
│ ├── 优惠券模板管理
│ ├── 优惠券发放管理
│ └── 优惠券使用管理
├── 优惠券领取模块
│ ├── 主动领取
│ ├── 自动发放
│ └── 批量发放
├── 优惠券核销模块
│ ├── 使用条件校验
│ ├── 优惠金额计算
│ └── 核销记录管理
└── 数据统计模块
├── 发放统计
├── 使用统计
└── 效果分析
# 核心实体设计
# 优惠券模板实体
@Entity
@Table(name = "coupon_template")
public class CouponTemplate {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "template_name")
private String templateName;
@Column(name = "template_desc")
private String templateDesc;
@Enumerated(EnumType.STRING)
@Column(name = "coupon_type")
private CouponType couponType;
@Enumerated(EnumType.STRING)
@Column(name = "discount_type")
private DiscountType discountType;
@Column(name = "discount_value")
private BigDecimal discountValue;
@Column(name = "min_order_amount")
private BigDecimal minOrderAmount;
@Column(name = "total_count")
private Integer totalCount;
@Column(name = "per_user_limit")
private Integer perUserLimit;
@Column(name = "valid_days")
private Integer validDays;
@Column(name = "start_time")
private LocalDateTime startTime;
@Column(name = "end_time")
private LocalDateTime endTime;
@Enumerated(EnumType.STRING)
@Column(name = "status")
private TemplateStatus status;
// getters and setters
}
// 优惠券类型枚举
public enum CouponType {
FULL_REDUCTION("满减券"),
DISCOUNT("折扣券"),
CASH("现金券"),
FREE_SHIPPING("包邮券");
private final String description;
CouponType(String description) {
this.description = description;
}
}
// 折扣类型枚举
public enum DiscountType {
FIXED_AMOUNT("固定金额"),
PERCENTAGE("百分比折扣");
private final String description;
DiscountType(String description) {
this.description = description;
}
}
// 模板状态枚举
public enum TemplateStatus {
DRAFT("草稿"),
ACTIVE("激活"),
INACTIVE("停用"),
EXPIRED("过期");
private final String description;
TemplateStatus(String description) {
this.description = description;
}
}
# 用户优惠券实体
@Entity
@Table(name = "user_coupon")
public class UserCoupon {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "user_id")
private Long userId;
@Column(name = "template_id")
private Long templateId;
@Column(name = "coupon_code")
private String couponCode;
@Enumerated(EnumType.STRING)
@Column(name = "status")
private CouponStatus status;
@Column(name = "receive_time")
private LocalDateTime receiveTime;
@Column(name = "use_time")
private LocalDateTime useTime;
@Column(name = "expire_time")
private LocalDateTime expireTime;
@Column(name = "order_id")
private Long orderId;
// getters and setters
}
// 优惠券状态枚举
public enum CouponStatus {
UNUSED("未使用"),
USED("已使用"),
EXPIRED("已过期"),
LOCKED("已锁定");
private final String description;
CouponStatus(String description) {
this.description = description;
}
}
# 核心服务实现
# 优惠券模板服务
@Service
@Transactional
public class CouponTemplateService {
@Autowired
private CouponTemplateRepository templateRepository;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 创建优惠券模板
*/
public CouponTemplate createTemplate(CouponTemplateCreateRequest request) {
// 参数校验
validateTemplateRequest(request);
CouponTemplate template = new CouponTemplate();
BeanUtils.copyProperties(request, template);
template.setStatus(TemplateStatus.DRAFT);
template.setCreateTime(LocalDateTime.now());
CouponTemplate saved = templateRepository.save(template);
// 缓存模板信息
cacheTemplate(saved);
return saved;
}
/**
* 激活优惠券模板
*/
public void activateTemplate(Long templateId) {
CouponTemplate template = getTemplateById(templateId);
if (template.getStatus() != TemplateStatus.DRAFT) {
throw new BusinessException("只有草稿状态的模板才能激活");
}
template.setStatus(TemplateStatus.ACTIVE);
templateRepository.save(template);
// 更新缓存
cacheTemplate(template);
// 初始化库存
initTemplateStock(templateId, template.getTotalCount());
}
/**
* 初始化模板库存
*/
private void initTemplateStock(Long templateId, Integer totalCount) {
String stockKey = "coupon:stock:" + templateId;
redisTemplate.opsForValue().set(stockKey, totalCount);
}
/**
* 缓存模板信息
*/
private void cacheTemplate(CouponTemplate template) {
String cacheKey = "coupon:template:" + template.getId();
redisTemplate.opsForValue().set(cacheKey, template, Duration.ofHours(24));
}
private void validateTemplateRequest(CouponTemplateCreateRequest request) {
if (request.getDiscountValue().compareTo(BigDecimal.ZERO) <= 0) {
throw new BusinessException("优惠金额必须大于0");
}
if (request.getMinOrderAmount().compareTo(BigDecimal.ZERO) < 0) {
throw new BusinessException("最小订单金额不能小于0");
}
if (request.getTotalCount() <= 0) {
throw new BusinessException("发放总数必须大于0");
}
}
}
# 优惠券发放服务
@Service
@Transactional
public class CouponIssueService {
@Autowired
private CouponTemplateService templateService;
@Autowired
private UserCouponRepository userCouponRepository;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 用户主动领取优惠券
*/
public UserCoupon receiveCoupon(Long userId, Long templateId) {
// 获取模板信息
CouponTemplate template = templateService.getTemplateById(templateId);
// 校验模板状态
validateTemplateForReceive(template);
// 校验用户领取限制
validateUserReceiveLimit(userId, templateId, template.getPerUserLimit());
// 扣减库存
boolean stockDecreased = decreaseStock(templateId);
if (!stockDecreased) {
throw new BusinessException("优惠券已抢完");
}
try {
// 创建用户优惠券
UserCoupon userCoupon = createUserCoupon(userId, template);
// 异步发送领取成功消息
sendCouponReceivedMessage(userCoupon);
return userCoupon;
} catch (Exception e) {
// 回滚库存
increaseStock(templateId);
throw e;
}
}
/**
* 批量发放优惠券
*/
@Async
public void batchIssueCoupons(Long templateId, List<Long> userIds) {
CouponTemplate template = templateService.getTemplateById(templateId);
for (Long userId : userIds) {
try {
// 检查用户是否已领取
if (!hasUserReceived(userId, templateId)) {
UserCoupon userCoupon = createUserCoupon(userId, template);
sendCouponReceivedMessage(userCoupon);
}
} catch (Exception e) {
log.error("批量发放优惠券失败, userId: {}, templateId: {}", userId, templateId, e);
}
}
}
/**
* 扣减库存
*/
private boolean decreaseStock(Long templateId) {
String stockKey = "coupon:stock:" + templateId;
String script =
"if redis.call('get', KEYS[1]) and tonumber(redis.call('get', KEYS[1])) > 0 then " +
" return redis.call('decr', KEYS[1]) " +
"else " +
" return -1 " +
"end";
Long result = redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(stockKey)
);
return result != null && result >= 0;
}
/**
* 回滚库存
*/
private void increaseStock(Long templateId) {
String stockKey = "coupon:stock:" + templateId;
redisTemplate.opsForValue().increment(stockKey);
}
/**
* 创建用户优惠券
*/
private UserCoupon createUserCoupon(Long userId, CouponTemplate template) {
UserCoupon userCoupon = new UserCoupon();
userCoupon.setUserId(userId);
userCoupon.setTemplateId(template.getId());
userCoupon.setCouponCode(generateCouponCode());
userCoupon.setStatus(CouponStatus.UNUSED);
userCoupon.setReceiveTime(LocalDateTime.now());
userCoupon.setExpireTime(calculateExpireTime(template));
return userCouponRepository.save(userCoupon);
}
/**
* 生成优惠券码
*/
private String generateCouponCode() {
return "CPN" + System.currentTimeMillis() + RandomStringUtils.randomNumeric(6);
}
/**
* 计算过期时间
*/
private LocalDateTime calculateExpireTime(CouponTemplate template) {
if (template.getValidDays() != null && template.getValidDays() > 0) {
return LocalDateTime.now().plusDays(template.getValidDays());
}
return template.getEndTime();
}
}
# 优惠券使用服务
@Service
@Transactional
public class CouponUseService {
@Autowired
private UserCouponRepository userCouponRepository;
@Autowired
private CouponTemplateService templateService;
/**
* 计算订单可用优惠券
*/
public List<AvailableCouponDTO> getAvailableCoupons(Long userId, OrderCalculateRequest request) {
// 获取用户未使用的优惠券
List<UserCoupon> userCoupons = userCouponRepository.findByUserIdAndStatus(
userId, CouponStatus.UNUSED
);
List<AvailableCouponDTO> availableCoupons = new ArrayList<>();
for (UserCoupon userCoupon : userCoupons) {
// 检查优惠券是否过期
if (userCoupon.getExpireTime().isBefore(LocalDateTime.now())) {
continue;
}
CouponTemplate template = templateService.getTemplateById(userCoupon.getTemplateId());
// 检查使用条件
if (canUseCoupon(template, request)) {
BigDecimal discountAmount = calculateDiscountAmount(template, request.getTotalAmount());
AvailableCouponDTO dto = new AvailableCouponDTO();
dto.setUserCouponId(userCoupon.getId());
dto.setCouponCode(userCoupon.getCouponCode());
dto.setTemplateName(template.getTemplateName());
dto.setDiscountAmount(discountAmount);
dto.setMinOrderAmount(template.getMinOrderAmount());
availableCoupons.add(dto);
}
}
// 按优惠金额降序排列
availableCoupons.sort((a, b) -> b.getDiscountAmount().compareTo(a.getDiscountAmount()));
return availableCoupons;
}
/**
* 使用优惠券
*/
public CouponUseResult useCoupon(Long userCouponId, Long orderId, BigDecimal orderAmount) {
UserCoupon userCoupon = userCouponRepository.findById(userCouponId)
.orElseThrow(() -> new BusinessException("优惠券不存在"));
// 校验优惠券状态
validateCouponForUse(userCoupon);
CouponTemplate template = templateService.getTemplateById(userCoupon.getTemplateId());
// 校验使用条件
if (orderAmount.compareTo(template.getMinOrderAmount()) < 0) {
throw new BusinessException("订单金额不满足优惠券使用条件");
}
// 计算优惠金额
BigDecimal discountAmount = calculateDiscountAmount(template, orderAmount);
// 更新优惠券状态
userCoupon.setStatus(CouponStatus.USED);
userCoupon.setUseTime(LocalDateTime.now());
userCoupon.setOrderId(orderId);
userCouponRepository.save(userCoupon);
return new CouponUseResult(discountAmount, userCoupon.getCouponCode());
}
/**
* 计算优惠金额
*/
private BigDecimal calculateDiscountAmount(CouponTemplate template, BigDecimal orderAmount) {
BigDecimal discountAmount = BigDecimal.ZERO;
switch (template.getDiscountType()) {
case FIXED_AMOUNT:
discountAmount = template.getDiscountValue();
break;
case PERCENTAGE:
discountAmount = orderAmount.multiply(template.getDiscountValue())
.divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
break;
}
// 优惠金额不能超过订单金额
return discountAmount.min(orderAmount);
}
/**
* 检查优惠券是否可用
*/
private boolean canUseCoupon(CouponTemplate template, OrderCalculateRequest request) {
// 检查最小订单金额
if (request.getTotalAmount().compareTo(template.getMinOrderAmount()) < 0) {
return false;
}
// 检查商品适用范围(可扩展)
// ...
return true;
}
}
# 数据库设计
# 优惠券模板表
CREATE TABLE `coupon_template` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`template_name` varchar(100) NOT NULL COMMENT '模板名称',
`template_desc` varchar(500) DEFAULT NULL COMMENT '模板描述',
`coupon_type` varchar(20) NOT NULL COMMENT '优惠券类型',
`discount_type` varchar(20) NOT NULL COMMENT '折扣类型',
`discount_value` decimal(10,2) NOT NULL COMMENT '折扣值',
`min_order_amount` decimal(10,2) DEFAULT '0.00' COMMENT '最小订单金额',
`total_count` int NOT NULL COMMENT '发放总数',
`per_user_limit` int DEFAULT '1' COMMENT '每人限领数量',
`valid_days` int DEFAULT NULL COMMENT '有效天数',
`start_time` datetime DEFAULT NULL COMMENT '开始时间',
`end_time` datetime DEFAULT NULL COMMENT '结束时间',
`status` varchar(20) NOT NULL DEFAULT 'DRAFT' COMMENT '状态',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_status_start_end` (`status`, `start_time`, `end_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='优惠券模板表';
# 用户优惠券表
CREATE TABLE `user_coupon` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` bigint NOT NULL COMMENT '用户ID',
`template_id` bigint NOT NULL COMMENT '模板ID',
`coupon_code` varchar(50) NOT NULL COMMENT '优惠券码',
`status` varchar(20) NOT NULL DEFAULT 'UNUSED' COMMENT '状态',
`receive_time` datetime NOT NULL COMMENT '领取时间',
`use_time` datetime DEFAULT NULL COMMENT '使用时间',
`expire_time` datetime NOT NULL COMMENT '过期时间',
`order_id` bigint DEFAULT NULL COMMENT '订单ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_coupon_code` (`coupon_code`),
KEY `idx_user_id_status` (`user_id`, `status`),
KEY `idx_template_id` (`template_id`),
KEY `idx_expire_time` (`expire_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户优惠券表';
# 性能优化策略
# 缓存策略
@Component
public class CouponCacheService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String TEMPLATE_CACHE_KEY = "coupon:template:";
private static final String USER_COUPON_CACHE_KEY = "coupon:user:";
private static final String STOCK_CACHE_KEY = "coupon:stock:";
/**
* 缓存用户优惠券列表
*/
public void cacheUserCoupons(Long userId, List<UserCoupon> coupons) {
String key = USER_COUPON_CACHE_KEY + userId;
redisTemplate.opsForValue().set(key, coupons, Duration.ofMinutes(30));
}
/**
* 获取缓存的用户优惠券
*/
@SuppressWarnings("unchecked")
public List<UserCoupon> getCachedUserCoupons(Long userId) {
String key = USER_COUPON_CACHE_KEY + userId;
return (List<UserCoupon>) redisTemplate.opsForValue().get(key);
}
/**
* 预热热门优惠券模板
*/
@Scheduled(fixedRate = 300000) // 5分钟执行一次
public void warmUpHotTemplates() {
// 获取热门模板列表
List<Long> hotTemplateIds = getHotTemplateIds();
for (Long templateId : hotTemplateIds) {
CouponTemplate template = templateService.getTemplateById(templateId);
String key = TEMPLATE_CACHE_KEY + templateId;
redisTemplate.opsForValue().set(key, template, Duration.ofHours(1));
}
}
}
# 数据库优化
-- 优化查询索引
CREATE INDEX idx_user_template_status ON user_coupon(user_id, template_id, status);
CREATE INDEX idx_template_status_time ON coupon_template(status, start_time, end_time);
-- 分区表优化(按月分区)
ALTER TABLE user_coupon PARTITION BY RANGE (TO_DAYS(create_time)) (
PARTITION p202401 VALUES LESS THAN (TO_DAYS('2024-02-01')),
PARTITION p202402 VALUES LESS THAN (TO_DAYS('2024-03-01')),
PARTITION p202403 VALUES LESS THAN (TO_DAYS('2024-04-01')),
PARTITION p_future VALUES LESS THAN MAXVALUE
);
# 总结
优惠券系统的设计要点:
- 高并发处理:使用Redis进行库存控制,避免超发
- 数据一致性:通过事务和补偿机制保证数据一致性
- 性能优化:合理使用缓存,优化数据库查询
- 扩展性设计:支持多种优惠券类型和使用规则
- 监控告警:完善的日志记录和异常处理机制
通过以上设计,可以构建一个高性能、高可用的优惠券系统,满足电商平台的营销需求。