一、功能概述
美团买菜系统的优惠券通用功能需要支持多种类型的优惠券(满减券、折扣券、无门槛券等),能够在不同业务场景(商品购买、配送费减免等)下通用,并具备灵活的规则配置能力。
二、系统架构设计
1. 数据库设计
```sql
-- 优惠券表
CREATE TABLE `coupon` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL COMMENT 优惠券名称,
`type` tinyint NOT NULL COMMENT 类型:1-满减券,2-折扣券,3-无门槛券,
`amount` decimal(10,2) NOT NULL COMMENT 金额/折扣率,
`min_order_amount` decimal(10,2) DEFAULT NULL COMMENT 最低订单金额,
`valid_from` datetime NOT NULL COMMENT 有效期开始,
`valid_to` datetime NOT NULL COMMENT 有效期结束,
`scope` tinyint NOT NULL COMMENT 使用范围:1-全品类,2-指定品类,3-指定商品,
`user_limit` int DEFAULT NULL COMMENT 每人限领数量,
`total_count` int NOT NULL COMMENT 发放总量,
`remaining_count` int NOT NULL COMMENT 剩余数量,
`status` tinyint NOT NULL COMMENT 状态:0-未开始,1-进行中,2-已结束,
`create_time` datetime NOT NULL,
`update_time` datetime NOT NULL,
PRIMARY KEY (`id`)
);
-- 优惠券范围关联表(当scope=2或3时使用)
CREATE TABLE `coupon_scope` (
`id` bigint NOT NULL AUTO_INCREMENT,
`coupon_id` bigint NOT NULL,
`scope_type` tinyint NOT NULL COMMENT 范围类型:1-品类,2-商品,
`scope_id` bigint NOT NULL COMMENT 品类ID或商品ID,
PRIMARY KEY (`id`),
KEY `idx_coupon_id` (`coupon_id`)
);
-- 用户优惠券表
CREATE TABLE `user_coupon` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL,
`coupon_id` bigint NOT NULL,
`status` tinyint NOT NULL COMMENT 状态:0-未使用,1-已使用,2-已过期,
`get_time` datetime NOT NULL,
`use_time` datetime DEFAULT NULL,
`order_id` bigint DEFAULT NULL COMMENT 使用的订单ID,
PRIMARY KEY (`id`),
KEY `idx_user_coupon` (`user_id`,`coupon_id`)
);
```
2. 核心服务模块
1. 优惠券模板服务:管理优惠券的创建、修改、查询等
2. 优惠券发放服务:处理优惠券的领取逻辑
3. 优惠券使用服务:验证优惠券是否可用并计算优惠
4. 优惠券规则引擎:执行复杂的优惠规则判断
三、核心功能实现
1. 优惠券创建与配置
```java
public class CouponTemplate {
private Long id;
private String name;
private CouponType type; // 枚举:满减、折扣、无门槛
private BigDecimal amount;
private BigDecimal minOrderAmount;
private Date validFrom;
private Date validTo;
private CouponScope scope; // 枚举:全品类、指定品类、指定商品
private List scopeIds; // 范围ID列表
private Integer userLimit;
private Integer totalCount;
// getters and setters
}
public interface CouponService {
// 创建优惠券模板
CouponTemplate createCoupon(CouponTemplate template);
// 更新优惠券模板
boolean updateCoupon(CouponTemplate template);
// 查询可用优惠券
List queryAvailableCoupons(Long userId, BigDecimal orderAmount);
}
```
2. 优惠券领取逻辑
```java
@Transactional
public boolean领取优惠券(Long userId, Long couponId) {
CouponTemplate coupon = couponRepository.findById(couponId)
.orElseThrow(() -> new RuntimeException("优惠券不存在"));
// 检查优惠券状态
if (coupon.getStatus() != CouponStatus.ONGOING) {
throw new RuntimeException("优惠券不可用");
}
// 检查用户领取限制
long userReceivedCount = userCouponRepository.countByUserIdAndCouponId(userId, couponId);
if (userReceivedCount >= coupon.getUserLimit()) {
throw new RuntimeException("已达到领取上限");
}
// 检查总量限制
if (coupon.getRemainingCount() <= 0) {
throw new RuntimeException("优惠券已领完");
}
// 创建用户优惠券记录
UserCoupon userCoupon = new UserCoupon();
userCoupon.setUserId(userId);
userCoupon.setCouponId(couponId);
userCoupon.setStatus(CouponStatus.UNUSED);
userCoupon.setGetTime(new Date());
userCouponRepository.save(userCoupon);
// 更新优惠券剩余数量
coupon.setRemainingCount(coupon.getRemainingCount() - 1);
couponRepository.save(coupon);
return true;
}
```
3. 优惠券使用验证
```java
public CouponValidationResult validateCoupon(Long userId, Long couponId, BigDecimal orderAmount, List productIds) {
UserCoupon userCoupon = userCouponRepository.findByUserIdAndCouponIdAndStatus(userId, couponId, CouponStatus.UNUSED);
if (userCoupon == null) {
return CouponValidationResult.fail("优惠券不可用");
}
CouponTemplate coupon = couponRepository.findById(couponId)
.orElseThrow(() -> new RuntimeException("优惠券不存在"));
// 检查有效期
Date now = new Date();
if (now.before(coupon.getValidFrom()) || now.after(coupon.getValidTo())) {
return CouponValidationResult.fail("优惠券已过期");
}
// 检查使用范围
if (coupon.getScope() == CouponScope.SPECIFIC_PRODUCTS) {
List validProductIds = couponScopeRepository.findProductIdsByCouponId(couponId);
boolean allProductsValid = productIds.stream()
.allMatch(validProductIds::contains);
if (!allProductsValid) {
return CouponValidationResult.fail("部分商品不适用此优惠券");
}
}
// 检查最低消费
if (coupon.getMinOrderAmount() != null && orderAmount.compareTo(coupon.getMinOrderAmount()) < 0) {
return CouponValidationResult.fail("订单金额未达到使用条件");
}
// 验证通过
return CouponValidationResult.success(calculateDiscount(coupon, orderAmount));
}
private DiscountResult calculateDiscount(CouponTemplate coupon, BigDecimal orderAmount) {
switch (coupon.getType()) {
case FULL_REDUCTION:
return new DiscountResult(
coupon.getAmount(),
orderAmount.subtract(coupon.getAmount())
);
case DISCOUNT:
BigDecimal discountAmount = orderAmount.multiply(BigDecimal.valueOf(1 - coupon.getAmount().doubleValue()));
return new DiscountResult(
orderAmount.subtract(discountAmount),
discountAmount
);
case NO_THRESHOLD:
return new DiscountResult(
coupon.getAmount(),
orderAmount.subtract(coupon.getAmount())
);
default:
throw new IllegalStateException("未知优惠券类型");
}
}
```
4. 订单结算中使用优惠券
```java
public Order结算(OrderCreateRequest request) {
// 获取用户选择的优惠券
Long couponId = request.getCouponId();
BigDecimal originalAmount = calculateOriginalAmount(request.getProducts());
CouponValidationResult validation = validateCoupon(
request.getUserId(),
couponId,
originalAmount,
request.getProductIds()
);
if (!validation.isSuccess()) {
throw new RuntimeException(validation.getMessage());
}
// 应用优惠券
DiscountResult discount = validation.getDiscount();
BigDecimal finalAmount = discount.getFinalAmount();
// 创建订单
Order order = new Order();
order.setUserId(request.getUserId());
order.setOriginalAmount(originalAmount);
order.setDiscountAmount(discount.getDiscountAmount());
order.setFinalAmount(finalAmount);
order.setCouponId(couponId);
// 其他订单字段设置...
// 更新优惠券状态为已使用
userCouponRepository.updateStatusToUsed(request.getUserId(), couponId);
return orderRepository.save(order);
}
```
四、关键业务规则实现
1. 优惠券互斥规则:
```java
public boolean checkCouponExclusion(Long userId, Long couponId, Set excludedCouponTypes) {
List usedCoupons = userCouponRepository.findUsedCouponsByUser(userId);
return usedCoupons.stream()
.anyMatch(uc -> excludedCouponTypes.contains(getCouponType(uc.getCouponId())));
}
```
2. 优惠券叠加使用限制:
```java
public boolean canStackCoupons(List couponIds) {
// 查询这些优惠券是否允许叠加
Map coupons = couponRepository.findByIds(couponIds);
return coupons.values().stream()
.allMatch(c -> c.getAllowStacking());
}
```
3. 品类/商品范围验证:
```java
public boolean isProductValidForCoupon(Long productId, Long couponId) {
CouponTemplate coupon = couponRepository.findById(couponId).orElse(null);
if (coupon == null || coupon.getScope() == CouponScope.ALL) {
return true;
}
List validProductIds = couponScopeRepository.findProductIdsByCouponId(couponId);
return validProductIds.contains(productId);
}
```
五、性能优化考虑
1. 缓存策略:
- 缓存常用优惠券模板(Redis)
- 缓存用户已领取优惠券列表
2. 数据库优化:
- 优惠券表按状态分区
- 用户优惠券表按用户ID分片
3. 异步处理:
- 优惠券发放采用消息队列异步处理
- 优惠券使用记录异步归档
六、测试用例设计
1. 正常流程测试:
- 用户领取有效期内通用优惠券
- 在满足条件下使用优惠券结算
2. 异常流程测试:
- 使用过期优惠券
- 使用不满足最低金额的优惠券
- 在不适用商品上使用限定品类优惠券
3. 并发测试:
- 多个用户同时领取限量优惠券
- 同一用户同时使用多张优惠券
4. 边界条件测试:
- 刚好满足最低金额的订单
- 优惠券有效期最后时刻使用
七、部署与监控
1. 监控指标:
- 优惠券领取成功率
- 优惠券使用率
- 规则验证耗时
2. 告警规则:
- 优惠券领取失败率突增
- 规则验证超时
3. 日志收集:
- 优惠券领取记录
- 优惠券使用记录
- 规则验证失败原因
通过以上设计,美团买菜系统可以实现一个灵活、可扩展的优惠券通用功能,支持各种业务场景和复杂的优惠规则。