一、功能概述
美团买菜系统的优惠券通用功能需要支持多种类型的优惠券(满减券、折扣券、无门槛券等)在买菜业务场景下的灵活使用,包括商品级、品类级、全平台级的优惠券应用。
二、系统架构设计
1. 优惠券服务模块
```
优惠券服务
├── 优惠券模板管理
├── 优惠券实例管理
├── 优惠券发放服务
├── 优惠券核销服务
└── 优惠券计算引擎
```
2. 数据库设计
优惠券模板表(coupon_template)
```sql
CREATE TABLE coupon_template (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
type TINYINT NOT NULL COMMENT 1-满减券 2-折扣券 3-无门槛券,
discount_type TINYINT COMMENT 1-固定金额 2-百分比,
discount_value DECIMAL(10,2) NOT NULL,
min_order_amount DECIMAL(10,2) DEFAULT 0,
valid_days INT COMMENT 有效期天数,
expire_time DATETIME,
range_type TINYINT NOT NULL COMMENT 1-全平台 2-指定品类 3-指定商品,
range_ids VARCHAR(1000) COMMENT 适用范围ID集合,
total_count INT DEFAULT 0 COMMENT 总发放量,
remaining_count INT DEFAULT 0 COMMENT 剩余量,
status TINYINT DEFAULT 1 COMMENT 1-有效 0-无效,
create_time DATETIME,
update_time DATETIME
);
```
用户优惠券表(user_coupon)
```sql
CREATE TABLE user_coupon (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
template_id BIGINT NOT NULL,
coupon_no VARCHAR(32) NOT NULL UNIQUE,
status TINYINT DEFAULT 0 COMMENT 0-未使用 1-已使用 2-已过期,
order_id BIGINT COMMENT 使用订单ID,
get_time DATETIME,
use_time DATETIME,
expire_time DATETIME,
FOREIGN KEY (template_id) REFERENCES coupon_template(id)
);
```
三、核心功能实现
1. 优惠券发放
```java
public class CouponService {
// 发放优惠券给用户
public UserCoupon grantCoupon(Long userId, Long templateId) {
CouponTemplate template = couponTemplateRepository.findById(templateId)
.orElseThrow(() -> new RuntimeException("优惠券模板不存在"));
if (template.getRemainingCount() <= 0) {
throw new RuntimeException("优惠券已发放完毕");
}
// 创建用户优惠券实例
UserCoupon userCoupon = new UserCoupon();
userCoupon.setUserId(userId);
userCoupon.setTemplateId(templateId);
userCoupon.setCouponNo(generateCouponNo());
userCoupon.setStatus(0); // 未使用
userCoupon.setExpireTime(calculateExpireTime(template));
// 更新模板剩余数量
couponTemplateRepository.decreaseRemainingCount(templateId);
return userCouponRepository.save(userCoupon);
}
private String generateCouponNo() {
// 生成唯一优惠券码
return UUID.randomUUID().toString().replace("-", "").substring(0, 16);
}
private Date calculateExpireTime(CouponTemplate template) {
if (template.getExpireTime() != null) {
return template.getExpireTime();
}
// 按有效期天数计算
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DAY_OF_YEAR, template.getValidDays());
return calendar.getTime();
}
}
```
2. 优惠券适用范围检查
```java
public class CouponRangeChecker {
public boolean isCouponApplicable(UserCoupon userCoupon, List cartItems) {
CouponTemplate template = getCouponTemplate(userCoupon.getTemplateId());
// 检查是否过期或已使用
if (userCoupon.getStatus() != 0 || new Date().after(userCoupon.getExpireTime())) {
return false;
}
// 全平台通用券
if (template.getRangeType() == 1) {
return true;
}
// 检查购物车商品是否在适用范围内
Set rangeIds = parseRangeIds(template.getRangeIds());
return cartItems.stream().anyMatch(item -> {
if (template.getRangeType() == 2) { // 品类适用
return rangeIds.contains(item.getCategoryId());
} else if (template.getRangeType() == 3) { // 商品适用
return rangeIds.contains(item.getProductId());
}
return false;
});
}
private Set parseRangeIds(String rangeIdsStr) {
if (StringUtils.isBlank(rangeIdsStr)) {
return Collections.emptySet();
}
return Arrays.stream(rangeIdsStr.split(","))
.map(Long::parseLong)
.collect(Collectors.toSet());
}
}
```
3. 优惠券计算引擎
```java
public class CouponCalculator {
public OrderDiscount calculateDiscount(UserCoupon userCoupon, List cartItems) {
CouponTemplate template = getCouponTemplate(userCoupon.getTemplateId());
// 检查适用性
if (!new CouponRangeChecker().isCouponApplicable(userCoupon, cartItems)) {
throw new RuntimeException("优惠券不适用当前商品");
}
OrderDiscount discount = new OrderDiscount();
// 计算总金额(满足最小订单金额)
BigDecimal totalAmount = calculateTotalAmount(cartItems);
if (template.getMinOrderAmount() > 0 &&
totalAmount.compareTo(template.getMinOrderAmount()) < 0) {
throw new RuntimeException("订单金额不满足优惠券使用条件");
}
// 根据优惠券类型计算折扣
switch (template.getType()) {
case 1: // 满减券
discount.setDiscountType(1);
discount.setDiscountAmount(template.getDiscountValue());
break;
case 2: // 折扣券
discount.setDiscountType(2);
if (template.getDiscountType() == 1) { // 固定金额折扣
discount.setDiscountAmount(template.getDiscountValue());
} else { // 百分比折扣
BigDecimal discountPercent = BigDecimal.valueOf(template.getDiscountValue())
.divide(BigDecimal.valueOf(100));
BigDecimal discountAmount = totalAmount.multiply(discountPercent)
.setScale(2, RoundingMode.HALF_UP);
discount.setDiscountAmount(discountAmount);
}
break;
case 3: // 无门槛券
discount.setDiscountType(3);
discount.setDiscountAmount(template.getDiscountValue());
break;
}
return discount;
}
private BigDecimal calculateTotalAmount(List cartItems) {
return cartItems.stream()
.map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
```
4. 优惠券核销
```java
public class CouponRedeemService {
@Transactional
public Order redeemCoupon(Long userId, Long orderId, String couponNo) {
// 1. 验证优惠券
UserCoupon userCoupon = userCouponRepository.findByCouponNoAndUserId(couponNo, userId)
.orElseThrow(() -> new RuntimeException("优惠券不存在或不属于当前用户"));
if (userCoupon.getStatus() != 0) {
throw new RuntimeException("优惠券不可用");
}
if (new Date().after(userCoupon.getExpireTime())) {
throw new RuntimeException("优惠券已过期");
}
// 2. 获取订单信息
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new RuntimeException("订单不存在"));
if (!order.getUserId().equals(userId)) {
throw new RuntimeException("订单不属于当前用户");
}
// 3. 计算优惠券折扣
List cartItems = order.getCartItems();
OrderDiscount discount = new CouponCalculator().calculateDiscount(userCoupon, cartItems);
// 4. 更新订单金额
BigDecimal originalAmount = order.getTotalAmount();
BigDecimal discountedAmount = originalAmount.subtract(discount.getDiscountAmount());
order.setTotalAmount(discountedAmount.max(BigDecimal.ZERO));
order.setDiscountAmount(discount.getDiscountAmount());
order.setCouponId(userCoupon.getId());
// 5. 更新优惠券状态
userCoupon.setStatus(1); // 已使用
userCoupon.setUseTime(new Date());
userCoupon.setOrderId(orderId);
userCouponRepository.save(userCoupon);
// 6. 保存订单
return orderRepository.save(order);
}
}
```
四、API接口设计
1. 优惠券领取接口
```
POST /api/coupons/receive
参数:
- userId: 用户ID
- templateId: 优惠券模板ID
响应:
{
"code": 200,
"message": "领取成功",
"data": {
"couponId": 12345,
"couponNo": "ABC12345678",
"expireTime": "2023-12-31 23:59:59"
}
}
```
2. 用户优惠券列表接口
```
GET /api/coupons/my
参数:
- userId: 用户ID
- status: 优惠券状态(0-未使用 1-已使用 2-已过期)
响应:
{
"code": 200,
"message": "成功",
"data": [
{
"couponId": 12345,
"couponNo": "ABC12345678",
"templateId": 1001,
"templateName": "满100减20",
"type": 1,
"discountValue": 20,
"minOrderAmount": 100,
"expireTime": "2023-12-31 23:59:59",
"status": 0
}
]
}
```
3. 优惠券核销接口
```
POST /api/orders/{orderId}/redeem-coupon
参数:
- userId: 用户ID
- couponNo: 优惠券码
响应:
{
"code": 200,
"message": "优惠券使用成功",
"data": {
"orderId": 12345,
"originalAmount": 150.00,
"discountAmount": 20.00,
"finalAmount": 130.00
}
}
```
五、关键考虑因素
1. 并发控制:优惠券领取和核销需要处理高并发场景,防止超发
2. 分布式锁:在优惠券发放时使用Redis分布式锁确保原子性
3. 幂等性设计:优惠券核销接口需要保证重复调用不会产生副作用
4. 过期处理:使用定时任务清理过期优惠券
5. 性能优化:优惠券适用范围检查需要高效,避免影响购物车结算性能
6. 数据一致性:确保优惠券状态和订单状态的最终一致性
六、扩展功能
1. 优惠券叠加使用:支持多张优惠券按规则叠加使用
2. 优惠券分享:用户可以将优惠券分享给好友
3. 优惠券裂变:邀请好友使用可获得额外优惠券
4. 优惠券组合:创建优惠券包,包含多种类型优惠券
5. 数据分析:统计优惠券领取、使用情况,优化营销策略
以上实现方案涵盖了美团买菜系统优惠券通用功能的核心逻辑,可根据实际业务需求进行调整和扩展。