Files
number/后端架构设计/06-优惠券与活动系统设计.md

676 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 优惠券系统 & 活动系统 — 前后端开发方案
> 基于现有项目架构Java Spring Boot + MyBatis-Plus + RabbitMQ + Redis / Vue 3 + Ant Design Vue
> 创建日期2026-03-21
---
## 一、总体架构
```
┌─────────────────────────────────────────────────────────────┐
│ 用户端 (Vue 3) │
│ 领券中心 │ Skill详情(券选择) │ 下单(券抵扣) │ 活动专区 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 后端服务 (Spring Boot) │
├─────────────────────────────────────────────────────────────┤
│ coupon 模块 │ promotion 模块 │
│ ├ CouponTemplate │ ├ PromotionActivity │
│ ├ UserCoupon │ ├ PromotionSkill │
│ ├ CouponService │ ├ FlashSaleSession │
│ └ CouponController │ ├ PromotionService │
│ │ └ PromotionController │
├─────────────────────────────────────────────────────────────┤
│ order 模块(改造) │
│ ├ previewOrder → 增加优惠券/活动价计算 │
│ ├ createOrder → 增加券核销 + 活动价锁定 │
│ └ cancelOrder → 增加券退回 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ MySQL (coupon_templates / user_coupons / │
│ promotion_activities / promotion_skills / │
│ flash_sale_sessions) │
│ Redis (库存预扣 / 防刷 / 活动缓存) │
│ RabbitMQ (券过期 / 活动开始结束 / 秒杀异步下单) │
└─────────────────────────────────────────────────────────────┘
```
---
## 二、优惠券系统
### 2.1 数据库设计
#### coupon_templates — 优惠券模板(管理员创建)
```sql
CREATE TABLE coupon_templates (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL COMMENT '券名称',
coupon_type ENUM('full_reduce','discount','fixed') NOT NULL COMMENT '满减/折扣/立减',
-- 优惠规则JSON灵活扩展
threshold_amount DECIMAL(10,2) DEFAULT 0.00 COMMENT '使用门槛金额(0=无门槛)',
discount_amount DECIMAL(10,2) DEFAULT 0.00 COMMENT '满减/立减金额',
discount_rate DECIMAL(3,2) DEFAULT 1.00 COMMENT '折扣率(0.80=8折)',
max_discount_amount DECIMAL(10,2) COMMENT '折扣券最大优惠金额(封顶)',
-- 适用范围
scope_type ENUM('all','category','skill') DEFAULT 'all' COMMENT '全场/指定分类/指定Skill',
scope_ids JSON COMMENT '适用的分类ID或SkillID数组',
-- 发放规则
total_count INT NOT NULL COMMENT '发行总量(-1=不限)',
issued_count INT DEFAULT 0 COMMENT '已发放数量',
per_user_limit INT DEFAULT 1 COMMENT '每人限领张数',
-- 时间
valid_type ENUM('fixed','relative') DEFAULT 'fixed' COMMENT '固定时段/领取后N天',
valid_start DATETIME COMMENT 'fixed模式: 生效开始时间',
valid_end DATETIME COMMENT 'fixed模式: 生效结束时间',
valid_days INT COMMENT 'relative模式: 领取后N天有效',
-- 状态
status ENUM('draft','active','paused','expired','exhausted') DEFAULT 'draft',
description VARCHAR(500) COMMENT '使用说明',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_status (status),
INDEX idx_coupon_type (coupon_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='优惠券模板表';
```
#### user_coupons — 用户持有的优惠券
```sql
CREATE TABLE user_coupons (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
template_id BIGINT NOT NULL COMMENT '券模板ID',
coupon_code VARCHAR(32) NOT NULL UNIQUE COMMENT '券码(唯一)',
status ENUM('unused','used','expired','returned') DEFAULT 'unused',
-- 冗余快照(下单时用,避免回查模板)
coupon_type VARCHAR(20) NOT NULL,
threshold_amount DECIMAL(10,2) DEFAULT 0.00,
discount_amount DECIMAL(10,2) DEFAULT 0.00,
discount_rate DECIMAL(3,2) DEFAULT 1.00,
max_discount_amount DECIMAL(10,2),
scope_type VARCHAR(20) DEFAULT 'all',
scope_ids JSON,
-- 有效期(计算后的绝对时间)
valid_start DATETIME NOT NULL,
valid_end DATETIME NOT NULL,
-- 使用记录
used_order_id BIGINT COMMENT '使用的订单ID',
used_at DATETIME COMMENT '使用时间',
received_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '领取时间',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id),
INDEX idx_user_status (user_id, status),
INDEX idx_template_id (template_id),
INDEX idx_valid_end (valid_end),
INDEX idx_coupon_code (coupon_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户优惠券表';
```
### 2.2 后端模块设计
```
module/coupon/
├── controller/
│ ├── CouponController.java # 用户端: 领券/我的券/可用券查询
│ └── AdminCouponController.java # 管理端: 券模板CRUD/发放/统计
├── dto/
│ ├── CouponTemplateCreateDTO.java # 创建券模板
│ ├── CouponTemplateUpdateDTO.java # 更新券模板
│ └── CouponIssueDTO.java # 手动发券(指定用户)
├── entity/
│ ├── CouponTemplate.java
│ └── UserCoupon.java
├── repository/
│ ├── CouponTemplateRepository.java
│ └── UserCouponRepository.java
├── service/
│ ├── CouponService.java
│ └── impl/CouponServiceImpl.java
├── vo/
│ ├── CouponTemplateVO.java
│ ├── UserCouponVO.java
│ └── CouponCalcResultVO.java # 优惠计算结果
└── task/
└── CouponExpireTask.java # 定时过期未使用券
```
### 2.3 核心 API 设计
| 方法 | 路径 | 说明 | 权限 |
|------|------|------|------|
| GET | `/api/coupons/available` | 可领取的券列表 | 登录 |
| POST | `/api/coupons/{templateId}/receive` | 用户领券 | 登录 |
| GET | `/api/coupons/my` | 我的优惠券(分tab: unused/used/expired) | 登录 |
| GET | `/api/coupons/usable?skillIds=1,2` | 下单时可用的券(传入SkillID自动匹配) | 登录 |
| POST | `/api/coupons/calc` | 计算优惠金额(预览) | 登录 |
| — | — | **管理端** | — |
| POST | `/api/admin/coupons/templates` | 创建券模板 | ADMIN |
| PUT | `/api/admin/coupons/templates/{id}` | 编辑券模板 | ADMIN |
| PUT | `/api/admin/coupons/templates/{id}/status` | 上架/暂停/下架 | ADMIN |
| GET | `/api/admin/coupons/templates` | 券模板列表(分页+筛选) | ADMIN |
| POST | `/api/admin/coupons/issue` | 手动发券给指定用户 | ADMIN |
| GET | `/api/admin/coupons/stats/{templateId}` | 单券统计(发放/使用/过期) | ADMIN |
### 2.4 核心业务逻辑
#### 领券流程
```
1. 校验券模板状态(active) + 时间有效 + 库存充足
2. Redis 原子扣减库存: DECR coupon:stock:{templateId}(防超发)
3. 校验用户领取上限: COUNT user_coupons WHERE user_id AND template_id
4. 生成券码(UUID短码): 插入 user_coupons快照券规则
5. DB 更新 issued_count最终一致Redis为准
6. 若DB插入失败 → Redis INCR 回补库存
```
#### 下单优惠计算(改造现有 OrderServiceImpl.previewOrder
```
1. 原有逻辑:计算 totalAmount / 积分抵扣
2. 新增步骤:
a. 若传入 couponId → 查 user_coupons 校验(unused + 未过期 + 适用范围匹配)
b. 根据 coupon_type 计算优惠:
- full_reduce: totalAmount >= threshold → 减 discount_amount
- discount: totalAmount * discount_rate不超过 max_discount_amount
- fixed: 直接减 discount_amount
c. couponDeductAmount = min(优惠金额, totalAmount)
d. 优惠券抵扣在积分抵扣之前: finalAmount = totalAmount - couponDeduct
e. 再对 finalAmount 计算积分抵扣
```
#### 下单时核销券
```
1. createOrder 增加 couponId 参数
2. CAS 更新 user_coupons SET status='used', used_order_id=?, used_at=NOW()
WHERE id=? AND status='unused' AND valid_end > NOW()
3. 若 rows=0 → 抛异常"优惠券已使用或已过期"
4. orders 表新增字段:
- coupon_id BIGINT COMMENT '使用的优惠券ID'
- coupon_deduct_amount DECIMAL(10,2) DEFAULT 0.00 COMMENT '优惠券抵扣金额'
```
#### 取消/退款时退券
```
cancelOrder / approveRefund 中:
IF order.coupon_id IS NOT NULL:
UPDATE user_coupons SET status='returned', used_order_id=NULL, used_at=NULL
WHERE id=couponId AND status='used'
— 注: returned状态的券不可再次使用防止薅羊毛循环退券重用
— 也可按业务需求改为退回unused状态需评估风险
```
#### 定时过期
```
CouponExpireTask (@Scheduled cron="0 0 2 * * ?"):
UPDATE user_coupons SET status='expired'
WHERE status='unused' AND valid_end < NOW()
— 分批执行每批1000条防止大事务
```
### 2.5 Redis Key 设计
| Key | 说明 | TTL |
|-----|------|-----|
| `coupon:stock:{templateId}` | 券库存(原子扣减防超发) | 券过期后清理 |
| `coupon:receive:lock:{userId}:{templateId}` | 领券防重复(分布式锁) | 5秒 |
| `coupon:user:count:{userId}:{templateId}` | 用户已领数量缓存 | 1小时 |
---
## 三、活动系统(限时折扣 / 秒杀)
### 3.1 说明
现有 `activity` 模块是**内容运营型活动**(轮播展示、跳转链接),不包含价格逻辑。
本次新建 `promotion` 模块,专注**营销促销**:限时折扣、满减活动、秒杀。
### 3.2 数据库设计
#### promotion_activities — 促销活动主表
```sql
CREATE TABLE promotion_activities (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(200) NOT NULL COMMENT '活动名称',
promo_type ENUM('time_discount','flash_sale','full_reduce') NOT NULL
COMMENT '限时折扣/秒杀/满减活动',
-- 活动规则
discount_rate DECIMAL(3,2) COMMENT '限时折扣率(0.70=7折)',
threshold_amount DECIMAL(10,2) COMMENT '满减门槛',
reduce_amount DECIMAL(10,2) COMMENT '满减金额',
-- 时间
start_time DATETIME NOT NULL,
end_time DATETIME NOT NULL,
-- 状态
status ENUM('draft','pending','active','ended','cancelled') DEFAULT 'draft'
COMMENT 'draft=草稿 pending=待开始 active=进行中 ended=已结束',
-- 展示
cover_image VARCHAR(500) COMMENT '活动封面图',
description TEXT COMMENT '活动描述',
banner_url VARCHAR(500) COMMENT '活动Banner图',
sort_order INT DEFAULT 0,
-- 限制
user_limit INT DEFAULT 0 COMMENT '每用户限购次数(0=不限)',
created_by BIGINT COMMENT '创建管理员ID',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_status (status),
INDEX idx_promo_type (promo_type),
INDEX idx_time (start_time, end_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='促销活动表';
```
#### promotion_skills — 活动关联Skill活动价 / 库存)
```sql
CREATE TABLE promotion_skills (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
activity_id BIGINT NOT NULL,
skill_id BIGINT NOT NULL,
original_price DECIMAL(10,2) NOT NULL COMMENT '原价快照',
promo_price DECIMAL(10,2) NOT NULL COMMENT '活动价',
-- 库存(秒杀/限量用)
total_stock INT DEFAULT -1 COMMENT '活动库存(-1=不限)',
sold_count INT DEFAULT 0 COMMENT '已售数量',
sort_order INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (activity_id) REFERENCES promotion_activities(id),
UNIQUE KEY uk_activity_skill (activity_id, skill_id),
INDEX idx_skill_id (skill_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动Skill关联表';
```
#### flash_sale_sessions — 秒杀场次(可选,秒杀专用)
```sql
CREATE TABLE flash_sale_sessions (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
activity_id BIGINT NOT NULL,
session_name VARCHAR(100) COMMENT '场次名(如: 10点场/14点场)',
start_time DATETIME NOT NULL,
end_time DATETIME NOT NULL,
status ENUM('pending','active','ended') DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (activity_id) REFERENCES promotion_activities(id),
INDEX idx_activity_id (activity_id),
INDEX idx_time (start_time, end_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='秒杀场次表';
```
### 3.3 后端模块设计
```
module/promotion/
├── controller/
│ ├── PromotionController.java # 用户端: 活动列表/详情/活动价查询
│ └── AdminPromotionController.java # 管理端: 活动CRUD/关联Skill/统计
├── dto/
│ ├── PromotionCreateDTO.java
│ ├── PromotionUpdateDTO.java
│ └── PromotionSkillDTO.java # 添加活动Skill
├── entity/
│ ├── PromotionActivity.java
│ ├── PromotionSkill.java
│ └── FlashSaleSession.java
├── repository/
│ ├── PromotionActivityRepository.java
│ ├── PromotionSkillRepository.java
│ └── FlashSaleSessionRepository.java
├── service/
│ ├── PromotionService.java
│ └── impl/PromotionServiceImpl.java
├── vo/
│ ├── PromotionActivityVO.java
│ ├── PromotionSkillVO.java
│ └── FlashSaleVO.java
└── task/
├── PromotionStatusTask.java # 定时更新活动状态(pending→active→ended)
└── FlashSaleWarmUpTask.java # 秒杀预热: 库存加载到Redis
```
### 3.4 核心 API 设计
| 方法 | 路径 | 说明 | 权限 |
|------|------|------|------|
| GET | `/api/promotions` | 进行中的活动列表 | 公开 |
| GET | `/api/promotions/{id}` | 活动详情(含活动Skill列表+活动价) | 公开 |
| GET | `/api/promotions/skill/{skillId}` | 查询某Skill当前参与的活动(有则返回活动价) | 公开 |
| GET | `/api/promotions/flash-sales` | 秒杀场次列表(含倒计时) | 公开 |
| — | — | **管理端** | — |
| POST | `/api/admin/promotions` | 创建促销活动 | ADMIN |
| PUT | `/api/admin/promotions/{id}` | 编辑活动 | ADMIN |
| PUT | `/api/admin/promotions/{id}/status` | 手动开始/暂停/结束 | ADMIN |
| POST | `/api/admin/promotions/{id}/skills` | 批量添加活动Skill(设置活动价) | ADMIN |
| DELETE | `/api/admin/promotions/{id}/skills/{skillId}` | 移除活动Skill | ADMIN |
| GET | `/api/admin/promotions` | 活动列表(分页+筛选) | ADMIN |
| GET | `/api/admin/promotions/{id}/stats` | 活动数据(浏览/下单/成交) | ADMIN |
### 3.5 核心业务逻辑
#### Skill详情页 — 活动价展示
```
SkillService.getDetail(skillId):
1. 查 skills 表获取原价
2. 查 promotion_skills JOIN promotion_activities
WHERE skill_id=? AND status='active' AND NOW() BETWEEN start_time AND end_time
3. 若命中活动 → 返回 promoPrice + 活动标签 + 倒计时endTime
4. 优先级: 秒杀 > 限时折扣 > 满减同一Skill只取优先级最高的活动
```
#### 下单时活动价锁定(改造 OrderServiceImpl.createOrder
```
1. 创建订单时,检查 skillIds 是否命中活动
2. 若命中:
a. 校验活动状态(active) + 时间范围
b. 校验库存: Redis DECR promo:stock:{activityId}:{skillId}
c. 使用 promo_price 替代 skill.price 计算 totalAmount
d. order_items 新增字段:
- activity_id BIGINT COMMENT '活动ID'
- promo_price DECIMAL(10,2) COMMENT '活动价快照'
3. 若未命中活动 → 原逻辑不变
— 注: 活动价与优惠券可叠加(视业务决策,可配置互斥)
```
#### 秒杀流程(高并发防护)
```
1. 预热(活动开始前5分钟):
FlashSaleWarmUpTask → Redis SET promo:stock:{activityId}:{skillId} = total_stock
2. 秒杀下单:
a. 前端按钮 → POST /api/orders (带 activityId)
b. 后端先 Redis 校验:
- 防刷: SET NX promo:lock:{userId}:{activityId}:{skillId} EX 5 (5秒内不可重复)
- 扣库存: DECR promo:stock:{activityId}:{skillId}
- 若 < 0 → INCR 回补 → 返回"已售罄"
c. Redis通过后 → 发送 MQ 异步创建订单
d. 前端轮询订单状态 or WebSocket推送
3. 补偿:
- 订单超时取消/退款 → INCR 回补 Redis 库存
- DB sold_count 定时与 Redis 同步校正
```
#### 定时任务 — 活动状态流转
```
PromotionStatusTask (@Scheduled fixedRate=60000):
1. UPDATE promotion_activities SET status='active'
WHERE status='pending' AND start_time <= NOW()
2. UPDATE promotion_activities SET status='ended'
WHERE status='active' AND end_time < NOW()
3. 活动结束后清理 Redis 库存缓存
```
### 3.6 Redis Key 设计
| Key | 说明 | TTL |
|-----|------|-----|
| `promo:stock:{activityId}:{skillId}` | 秒杀/限量库存 | 活动结束后清理 |
| `promo:lock:{userId}:{activityId}:{skillId}` | 秒杀防刷锁 | 5秒 |
| `promo:active:list` | 进行中活动列表缓存 | 1分钟 |
| `promo:skill:{skillId}` | Skill当前活动价缓存 | 1分钟 |
---
## 四、订单模块改造
### 4.1 orders 表新增字段
```sql
ALTER TABLE orders ADD COLUMN coupon_id BIGINT COMMENT '使用的优惠券ID' AFTER points_deduct_amount;
ALTER TABLE orders ADD COLUMN coupon_deduct_amount DECIMAL(10,2) DEFAULT 0.00 COMMENT '优惠券抵扣金额' AFTER coupon_id;
```
### 4.2 order_items 表新增字段
```sql
ALTER TABLE order_items ADD COLUMN activity_id BIGINT COMMENT '促销活动ID' AFTER skill_cover;
ALTER TABLE order_items ADD COLUMN promo_price DECIMAL(10,2) COMMENT '活动价快照' AFTER activity_id;
```
### 4.3 previewOrder 改造
```
入参新增: Long couponId, Long activityId(可选)
计算顺序:
1. 获取Skill价格 → 检查是否命中活动 → 用活动价替代原价
2. 计算 totalAmount (可能已是活动价)
3. 若有 couponId → 计算优惠券抵扣 → couponDeductAmount
4. afterCouponAmount = totalAmount - couponDeductAmount
5. 计算积分抵扣(基于 afterCouponAmount)
6. cashAmount = afterCouponAmount - pointsDeductAmount
返回 OrderPreviewVO 新增:
- couponDeductAmount
- couponName (所用券名)
- activityName (活动名)
- originalTotalAmount (原总价,用于前端划线价)
- savedAmount (总共节省金额)
```
### 4.4 createOrder 改造
```
入参新增: Long couponId
核心流程:
1~3. 原有校验 + 活动价替代 + 券核销
4. 重新计算: totalAmount → couponDeduct → pointsDeduct → cashAmount
5. 写入 orders (含 coupon_id, coupon_deduct_amount)
6. 写入 order_items (含 activity_id, promo_price)
7. 冻结积分 + 发MQ
```
---
## 五、前端方案
### 5.1 技术栈
- Vue 3 + Ant Design Vue + Element Plus
- Pinia 状态管理
- Axios 请求
### 5.2 新增页面/组件
#### 用户端
| 路由 | 文件 | 说明 |
|------|------|------|
| `/user/coupons` | `views/user/coupons.vue` | 我的优惠券(Tab: 未使用/已使用/已过期) |
| `/coupons` | `views/coupon/list.vue` | 领券中心(可领券列表+一键领取) |
| `/promotions` | `views/promotion/list.vue` | 活动专区(进行中活动列表) |
| `/promotions/:id` | `views/promotion/detail.vue` | 活动详情(活动Skill列表+活动价) |
| `/flash-sale` | `views/promotion/flash-sale.vue` | 秒杀专区(场次+倒计时+秒杀按钮) |
| — | `components/CouponCard.vue` | 优惠券卡片组件(领取/使用) |
| — | `components/CouponSelect.vue` | 下单时券选择弹窗 |
| — | `components/PromoTag.vue` | 活动价标签(Skill卡片/详情页) |
| — | `components/CountdownTimer.vue` | 倒计时组件 |
#### 管理端
| 路由 | 文件 | 说明 |
|------|------|------|
| `/admin/coupons` | `views/admin/coupons.vue` | 券模板管理(CRUD+状态切换) |
| `/admin/coupons/:id` | `views/admin/coupon-detail.vue` | 券统计详情(发放/使用率) |
| `/admin/promotions` | `views/admin/promotions.vue` | 促销活动管理(CRUD+关联Skill) |
| `/admin/promotions/:id` | `views/admin/promotion-detail.vue` | 活动详情(关联Skill管理+数据统计) |
### 5.3 现有页面改造
#### Skill详情页 (`views/skill/detail.vue`)
```
改造点:
1. 价格区域: 原价 + 活动价(划线价) + 活动标签 + 倒计时
2. 购买弹窗: 增加"选择优惠券"入口 → CouponSelect弹窗
3. 价格预览: 显示 原价 - 活动优惠 - 券优惠 - 积分抵扣 = 实付金额
```
#### 订单详情页 (`views/order/detail.vue`)
```
改造点:
1. 金额明细: 增加"优惠券抵扣"行 + "活动优惠"行
2. 显示使用的券名称 + 活动名称
```
#### 首页 (`views/home/index.vue`)
```
改造点:
1. 新增"限时秒杀"模块(横向滚动,显示倒计时+秒杀价)
2. 新增"领券中心"入口(悬浮icon或Banner位)
3. 活动Banner(复用现有轮播图linkType新增 promotion 类型)
```
### 5.4 API Service 新增
```javascript
// service/couponApi.js
export const couponApi = {
// 用户端
getAvailable: () => get('/api/coupons/available'),
receiveCoupon: (templateId) => post(`/api/coupons/${templateId}/receive`),
getMyCoupons: (params) => get('/api/coupons/my', { params }),
getUsableCoupons: (skillIds) => get('/api/coupons/usable', { params: { skillIds: skillIds.join(',') } }),
calcDiscount: (data) => post('/api/coupons/calc', data),
}
// service/promotionApi.js
export const promotionApi = {
// 用户端
getActiveList: () => get('/api/promotions'),
getDetail: (id) => get(`/api/promotions/${id}`),
getSkillPromo: (skillId) => get(`/api/promotions/skill/${skillId}`),
getFlashSales: () => get('/api/promotions/flash-sales'),
}
```
### 5.5 关键交互流程
#### 领券流程
```
领券中心页面:
1. 加载可领券列表(带已领/已抢光状态)
2. 点击"立即领取" → loading + 防抖
3. 成功 → 动画(券飞入钱包) + toast提示
4. 失败 → 提示(已领完/超过限领数/已领过)
```
#### 下单选券流程
```
Skill详情页 → 点击购买:
1. 调用 previewOrder(不带券) → 显示基础价格
2. 底部显示"有N张可用优惠券" → 点击展开 CouponSelect
3. CouponSelect: 显示可用券列表(按优惠金额排序,最优推荐)
4. 选中券 → 重新调用 previewOrder(带couponId) → 更新价格预览
5. 确认下单 → createOrder(带couponId)
```
#### 秒杀流程
```
秒杀专区页:
1. 显示场次列表 + 倒计时(距开始/距结束)
2. 未开始: 按钮灰色"即将开始" + 倒计时
3. 进行中: 按钮红色"立即抢购" / 已售罄灰色
4. 点击抢购 → 弹窗确认 → POST 下单
5. 后端返回"排队中" → 前端轮询订单状态(每2秒)
6. 成功 → 跳转支付页 / 失败 → 提示"未抢到"
```
---
## 六、实施计划
### Phase 1: 优惠券系统7-8天
| 天数 | 后端任务 | 前端任务 |
|------|---------|---------|
| D1 | 建表 + Entity/Repository | — |
| D2 | CouponService(创建模板/领券/查可用券) | — |
| D3 | CouponService(优惠计算/核销/退券) + Redis库存 | CouponCard组件 + 领券中心页 |
| D4 | AdminCouponController + 管理端API | 我的优惠券页 |
| D5 | OrderService改造(previewOrder/createOrder增加券) | CouponSelect选券弹窗 |
| D6 | 取消退款退券 + 定时过期任务 | Skill详情页+订单详情改造 |
| D7 | 联调 + 边界测试 | 管理端券模板管理页 |
| D8 | 压测领券并发 + 修复 | 联调 + UI打磨 |
### Phase 2: 活动系统 — 限时折扣5-6天
| 天数 | 后端任务 | 前端任务 |
|------|---------|---------|
| D9 | 建表 + Entity/Repository | — |
| D10 | PromotionService(CRUD/状态流转/活动价查询) | 活动专区列表页 |
| D11 | Skill详情接入活动价 + Order改造 | 活动详情页 + PromoTag组件 |
| D12 | AdminPromotionController + 关联Skill | Skill详情页活动价展示 |
| D13 | 定时任务(状态流转) + 联调 | 管理端活动管理页 |
| D14 | 集成测试 + 修复 | 首页活动模块 + 联调 |
### Phase 3: 秒杀系统4-5天可独立排期
| 天数 | 后端任务 | 前端任务 |
|------|---------|---------|
| D15 | 秒杀场次表 + 预热任务 + Redis库存 | — |
| D16 | 秒杀下单(Redis防刷+MQ异步) | 秒杀专区页 + 倒计时组件 |
| D17 | 补偿(取消回补库存) + 库存校正 | 秒杀下单交互(轮询/状态) |
| D18 | 压测(JMeter模拟并发抢购) | 联调 + 异常处理 |
| D19 | 修复 + 优化 | UI打磨 + 最终联调 |
**总计:约 17-19 天**优惠券8天 + 限时折扣6天 + 秒杀5天
---
## 七、注意事项
### 安全
- **领券防刷**: Redis分布式锁 + 用户限领校验(DB唯一约束兜底)
- **金额服务端计算**: 前端只展示,**所有优惠金额在后端重新计算**,不信任客户端传入
- **库存超卖防护**: Redis原子操作(DECR) + DB乐观锁(sold_count CAS)双重保障
- **优惠叠加控制**: 默认活动价+优惠券可叠加,但需配置互斥开关
### 性能
- **热点数据缓存**: 活动列表/活动价/券库存走RedisDB只做持久化
- **秒杀异步**: 下单请求先入MQ异步消费创建订单前端轮询结果
- **批量操作**: 券过期/活动状态更新分批执行(每批1000条)
### 一致性
- **券库存**: Redis预扣 → DB写入 → 失败回补Redis最终一致
- **活动库存**: Redis + DB双写定时任务校正偏差
- **退款退券**: 事务内同步处理(券退回 + 库存回补)
### 与现有系统集成
- **会员折扣**: 会员等级折扣与活动价/优惠券的优先级: 活动价 > 优惠券 > 会员折扣
- **积分抵扣**: 积分在最终实付金额(扣除活动优惠和券优惠后)上计算
- **MQ补偿**: 复用现有 CompensationService秒杀/券相关MQ失败自动写入补偿表
---
*本文档将随开发进度持续更新*