优惠券系统设计与实现

# 优惠券系统设计与实现

# 系统概述

优惠券系统是电商平台营销活动的核心组件,通过发放各种类型的优惠券来刺激用户消费,提升平台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
);

# 总结

优惠券系统的设计要点:

  1. 高并发处理:使用Redis进行库存控制,避免超发
  2. 数据一致性:通过事务和补偿机制保证数据一致性
  3. 性能优化:合理使用缓存,优化数据库查询
  4. 扩展性设计:支持多种优惠券类型和使用规则
  5. 监控告警:完善的日志记录和异常处理机制

通过以上设计,可以构建一个高性能、高可用的优惠券系统,满足电商平台的营销需求。