# 营销与优惠模块业务说明

> 本文档描述营销活动与优惠券体系，涵盖活动类型、优惠券生命周期、优惠计算逻辑及叠加规则。
> 帮助开发人员快速理解"为什么这样设计"以及各环节的特殊处理方式。

---

## 一、营销活动体系

### 1.1 三套活动体系

| 体系 | 核心实体 | 管理入口 | 用途 |
|------|---------|---------|------|
| 营销活动 | MarketingActivity | `MarketingActivityController` | 首单立减/折扣/满减 |
| 定时折扣 | TimedDiscountActivity | `TimedDiscountController` | 按时间段自动执行的折扣 |
| 邀请有礼 | InviteActivity | `InviteActivityAdminController` | 邀请奖励活动 |

**统一活动管理**：`UnifiedActivityService` / `UnifiedActivityController` 提供三套活动的统一查询和管理入口。

#### 为什么有三套活动体系而非统一一套？

三套体系虽同属"营销"，但业务差异很大，强行合并会增加复杂度：
- **营销活动**（MarketingActivity）：面向设备端计算的即时优惠，有时间范围、预算控制，由运营人员配置
- **定时折扣**（TimedDiscountActivity）：按每天时段自动执行（如夜场22:00-06:00打折），有库存关联的阶梯折扣逻辑，可自动执行
- **邀请有礼**（InviteActivity）：奖励机制完全不同（发放优惠券给邀请人/被邀请人），有每日限额、首单门槛等专属逻辑

`UnifiedActivityService` 在查询层面做统一聚合，但各体系保持独立的 CRUD 和业务逻辑。

### 1.2 MarketingActivity 实体

| 字段 | 类型 | 说明 |
|------|------|------|
| id | Long | 雪花算法主键 |
| activityName | String | 活动名称 |
| activityType | Integer | 活动类型：1-首单立减 2-优惠折扣 3-商品满减 4-邀请有礼 |
| activityDesc | String | 活动描述 |
| startTime / endTime | LocalDateTime | 活动有效期 |
| status | Integer | 状态：0-草稿 1-已发布 2-进行中 3-已暂停 4-已结束 |
| discountValue | BigDecimal | 优惠值（立减金额/折扣率/减免金额） |
| minAmount | BigDecimal | 最低消费金额（满减门槛） |
| maxDiscount | BigDecimal | 最大优惠金额（折扣活动上限） |
| applyScope | Integer | 适用范围：1-全部门店 2-指定门店 |
| deviceScope | Integer | 设备范围：1-全部设备 2-指定设备 |
| productScope | Integer | 商品范围：1-全部商品 2-指定商品 |
| totalBudget / usedBudget | BigDecimal | 预算控制 |
| deleted | Integer | 逻辑删除 |

### 1.3 活动类型与计算规则

#### 类型1：首单立减 (FIRST_ORDER_DISCOUNT)

```
条件：用户之前无已完成订单（isFirstOrder 判断）
优惠 = min(discountValue, orderAmount)
结果：可能免单（优惠 >= 订单金额）
```

#### 类型2：优惠折扣 (DISCOUNT)

```
折扣率 discountValue：0 < discountValue <= 1（如0.8表示8折）
优惠 = orderAmount × (1 - discountValue)，保留2位小数
如有 maxDiscount：优惠 = min(计算优惠, maxDiscount)
```

#### 类型3：商品满减 (FULL_REDUCTION)

```
门槛：orderAmount >= minAmount
满足：优惠 = discountValue（固定减免金额）
不满足：优惠 = 0
```

### 1.4 活动状态流转 (`ActivityStatusEnum`)

```
DRAFT(0, 草稿)
    ↓ publish
PUBLISHED(1, 已发布) → 到达开始时间自动 → ONGOING(2, 进行中)
    ↓ pause                              ↓ pause
PAUSED(3, 已暂停) → resume → ONGOING    ↓ 到达结束时间
    ↓                                    ↓
                                       ENDED(4, 已结束)
```

状态操作权限：
- 可编辑：仅 DRAFT
- 可发布：仅 DRAFT
- 可暂停：仅 ONGOING
- 可恢复：仅 PAUSED
- 可删除：DRAFT 或 ENDED

### 1.5 关联关系表

| 关联表 | 说明 |
|--------|------|
| t_activity_shop | 活动-门店关联（applyScope=2时） |
| t_activity_device | 活动-设备关联（deviceScope=2时） |
| t_activity_product | 活动-商品关联（productScope=2时） |

#### 营销活动与优惠券的关系设计

**关系模型**：一个营销活动可以关联多个优惠券模板（1:N），优惠券模板的 `activityId` 字段指向所属活动，但优惠券也可以独立存在（`activityId` 为空）。

**设计考量**：
- 活动和优惠券解耦，优惠券可独立发放（如日常运营券），也可归属某个活动（如春节大促券）
- 活动详情页可展示关联的优惠券列表，便于运营人员统一管理
- `activityId` 为可选字段，不强制绑定

---

## 二、定时折扣体系

### 2.1 TimedDiscountActivity 实体

| 字段 | 类型 | 说明 |
|------|------|------|
| id | Long | 雪花算法主键 |
| activityName | String | 活动名称 |
| startTime / endTime | LocalTime | 折扣时段（精确到时分秒，如 22:00:00 - 06:00:00） |
| discountType | Integer | 折扣类型：1-固定折扣 2-阶梯折扣 3-固定价格 |
| discountValue | BigDecimal | 折扣值 |
| minDiscountPrice | BigDecimal | 最低折扣价（底线价格） |
| applyScope / deviceScope / productScope | Integer | 适用范围 |
| productTypes | String | 商品分类（逗号分隔） |
| priorityProductTypes | String | 优先折扣商品分类 |
| minStock / maxStock | Integer | 库存范围筛选 |
| status | Integer | 状态 |
| autoExecute | Integer | 是否自动执行 |
| notifyOnExecute | Integer | 执行时是否通知 |

### 2.2 折扣类型 (`DiscountTypeEnum`)

| code | 枚举值 | 说明 |
|------|--------|------|
| 1 | FIXED_DISCOUNT | 固定折扣（如统一8折） |
| 2 | TIERED_DISCOUNT | 阶梯折扣（库存越少折扣越大） |
| 3 | FIXED_PRICE | 固定价格（如夜场统一3元） |

#### 定时折扣的业务场景

定时折扣主要解决**临期/夜间清库存**问题：
- **夜场折扣**：22:00-06:00 折扣销售，提高夜间销量
- **临期清仓**：库存低于阈值时自动加大折扣，减少损耗
- **固定价格**：特定时段统一定价（如夜场饮品一律3元），简化计价

**阶梯折扣**是定时折扣独有的机制：根据当前库存量动态调整折扣力度（库存越少折扣越大），由 `minStock` / `maxStock` 字段定义库存区间。

### 2.3 关联表

| 关联表 | 说明 |
|--------|------|
| t_timed_discount_device | 折扣-设备关联 |
| t_timed_discount_shop | 折扣-门店关联 |
| t_timed_discount_product | 折扣-商品关联 |
| t_timed_discount_record | 折扣执行记录 |
| t_timed_discount_statistics | 折扣统计 |

---

## 三、优惠券体系

### 3.1 CouponTemplate 实体（优惠券模板）

| 字段 | 类型 | 说明 |
|------|------|------|
| id | Long | 雪花算法主键 |
| couponName | String | 优惠券名称 |
| couponType | Integer | 类型：1-满减券 2-折扣券 3-立减券 4-商品券 |
| couponDesc | String | 描述 |
| discountValue | BigDecimal | 优惠值（金额/折扣率） |
| minAmount | BigDecimal | 最低消费金额（满减门槛） |
| maxDiscount | BigDecimal | 最大优惠金额（折扣券上限） |
| totalCount | Integer | 发放总量 |
| remainCount | Integer | 剩余库存 |
| receiveLimit | Integer | 每人限领数量 |
| validType | Integer | 有效期类型：1-固定时间 2-相对时间 |
| validStartTime / validEndTime | LocalDateTime | 固定有效期 |
| validDays | Integer | 相对有效天数（领取后N天有效） |
| applyScope | Integer | 适用范围：1-全部门店 2-指定门店 |
| productScope | Integer | 商品范围：1-全部商品 2-指定商品 |
| activityId | Long | 关联营销活动ID（可选） |
| status | Integer | 状态：0-禁用 1-启用 |
| receiveType | String | 发放类型：Collect-主动领取，Release-系统发放 |

### 3.2 优惠券类型与计算 (`CouponTypeEnum`)

| couponType | 名称 | discountValue含义 | 计算方式 |
|-----------|------|-------------------|---------|
| 1 | 满减券 | 减免金额（元） | 订单金额 >= minAmount → 优惠 = discountValue |
| 2 | 折扣券 | 折扣率（如0.8=8折） | 优惠 = orderAmount × (1 - discountValue/10)，有 maxDiscount 上限 |
| 3 | 立减券 | 抵扣金额（元） | 优惠 = discountValue（无门槛） |
| 4 | 商品券 | 无意义 | 免费兑换指定商品，不用于金额扣减 |

### 3.3 UserCoupon 实体（用户优惠券）

| 字段 | 类型 | 说明 |
|------|------|------|
| id | Long | 雪花算法主键 |
| couponCode | String | 优惠券编码（唯一） |
| templateId | Long | 优惠券模板ID |
| userId | Long | 用户ID |
| orderId | Long | 使用订单ID（使用后写入） |
| status | Integer | 状态：0-未使用 1-已使用 2-已过期 |
| receiveTime | LocalDateTime | 领取时间 |
| useTime | LocalDateTime | 使用时间 |
| validStartTime / validEndTime | LocalDateTime | 有效期 |
| discountAmount | BigDecimal | 实际优惠金额（使用时记录） |

### 3.4 优惠券状态 (`CouponStatusEnum`)

| code | 枚举值 | 说明 |
|------|--------|------|
| 0 | UNUSED | 未使用 |
| 1 | USED | 已使用 |
| 2 | EXPIRED | 已过期 |

### 3.5 关联表

| 关联表 | 说明 |
|--------|------|
| t_coupon_shop | 优惠券-门店关联 |
| t_coupon_product | 优惠券-商品关联 |
| t_coupon_distribute | 优惠券发放记录 |

#### 优惠券发放类型 (`receiveType`)

| 类型 | 说明 |
|------|------|
| Collect | 主动领取：用户在小程序“领券中心”点击领取 |
| Release | 系统发放：系统自动发放（如邀请奖励、新人礼包） |

#### 优惠券库存防超卖

高并发领取场景下使用 Redis 原子操作扣减库存：
```java
// Redis原子扣减
String key = "coupon:stock:" + templateId;
Long remain = redisTemplate.opsForValue().decrement(key);
if (remain < 0) {
    redisTemplate.opsForValue().increment(key);  // 回滚
    return false;
}
```

同时使用 Redis 分布式锁防止重复领取：
```java
String lockKey = "coupon:receive:" + userId + ":" + templateId;
Boolean acquired = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", Duration.ofMinutes(1));
```

---

## 四、优惠计算与叠加规则

### 4.1 优惠叠加规则

| 优惠类型 | 是否可叠加 | 计算顺序 |
|---------|-----------|---------|
| 多个营销活动 | 可叠加 | 设备端计算 |
| 优惠券 | 一次仅用1张 | 服务端计算 |
| 营销活动 + 优惠券 | 可叠加 | 先营销活动，后优惠券 |

### 4.2 优惠券自动扣减流程（订单回调时触发）

#### 为什么是自动扣减？

智能售卖机没有“确认订单”环节，用户关门即结算。系统无法等待用户选择优惠券，因此采用自动匹配策略：优先使用最先到期的符合条件的券。

**设计考量**：
- 优先到期策略避免优惠券过期浪费，符合用户利益
- 一次仅用1张优惠券，避免复杂叠加计算
- 异常不阻断：优惠券计算失败时返回原金额，不影响正常订单流程

```
HahaCallbackServiceImpl.processCouponDiscount()
    ↓
1. 查询用户所有可用优惠券（status=0 且未过期）
    ↓
2. 按到期时间升序排序（优先使用最先到期的券）
    ↓
3. 遍历优惠券，检查适用条件：
   - 订单金额 >= 优惠券满减门槛（minAmount）
   - 适用范围匹配（applyScope）
    ↓
4. 使用第一张符合条件的优惠券
    ↓
5. 计算优惠金额（根据 couponType）
    ↓
6. 确保优惠金额 <= 订单金额（paidAmount >= 0）
    ↓
7. 标记优惠券为已使用（useCoupon）
    ↓
8. 更新订单 discountAmount / paidAmount
    ↓
9. 返回最终实付金额（用于支付扣费）
```

### 4.3 优惠券使用校验（UserCouponServiceImpl.useCoupon）

```
1. 验证优惠券归属（userId 匹配）
2. 检查状态（must be UNUSED）
3. 检查有效期（validEndTime）
4. 更新状态为 USED，写入 orderId / useTime / discountAmount
```

### 4.4 优惠金额公式

```
最终实付 = totalAmount - discountAmount
discountAmount = 营销活动优惠（设备端已扣减，不可见） + 优惠券优惠（服务端扣减）

注意：设备端传来的 totalAmount 已包含营销活动优惠
      服务端 discountAmount 仅记录优惠券优惠部分
      营销活动优惠金额不单独持久化到订单表
```

#### 为什么营销活动优惠金额不持久化到订单表？

营销活动优惠是**动态计算**的：同一个活动在不同订单中的优惠金额不同（如折扣率取决于订单金额），且活动配置可能随时修改。将计算结果持久化到订单表既不必要也不合理——订单创建时优惠已确定，后续活动修改不影响已有订单。

如果将来需要统计营销活动优惠总额，可通过 `MarketingRuleService.calculateOrderDiscount()` 重新计算或从统计表 `t_marketing_statistics` 获取。

---

## 五、邀请有礼体系

### 5.1 InviteActivity 实体

| 字段 | 类型 | 说明 |
|------|------|------|
| id | Long | 雪花算法主键 |
| activityName / activityDesc | String | 活动名称/描述 |
| startTime / endTime | LocalDateTime | 活动有效期 |
| status | Integer | 状态：0-草稿 1-已发布 2-进行中 3-已暂停 4-已结束 |
| couponTemplateId | Long | 邀请人奖励优惠券模板ID |
| inviteeCouponTemplateId | Long | 被邀请人奖励优惠券模板ID（双向奖励） |
| dailyLimit | Integer | 每人每日最大邀请数 |
| totalLimit | Integer | 每人活动期间最大邀请总数 |
| requireFirstOrder | Integer | 是否要求完成首单：0-否 1-是 |
| minOrderAmount | BigDecimal | 首单最低金额要求 |
| inviteCodePrefix | String | 邀请码前缀 |
| applyScope / productScope | Integer | 适用范围 |

### 5.2 邀请记录 (InviteRecord)

| 关键字段 | 说明 |
|---------|------|
| inviterUserId | 邀请人 |
| inviteeUserId | 被邀请人 |
| inviteCode | 邀请码 |
| status | 0-待激活 1-已激活 2-已失效 3-已奖励 |

### 5.3 邀请奖励 (InviteReward)

| 关键字段 | 说明 |
|---------|------|
| rewardType | 0-邀请人奖励 1-被邀请人奖励 |
| triggerType | 0-首单完成触发 1-邀请绑定触发 |
| couponTemplateId | 奖励的优惠券模板 |
| userCouponId | 发放的用户优惠券ID |
| status | 0-待发放 1-已发放 2-发放失败 |

### 5.4 邀请激活触发

订单完成时（`payStatus = PAID`），`InviteActivityServiceImpl` 检查被邀请人是否有待激活的邀请记录，满足条件后发放奖励优惠券。

---

## 六、核心代码文件索引

| 文件 | 职责 |
|------|------|
| `haha-entity/.../MarketingActivity.java` | 营销活动实体 |
| `haha-entity/.../TimedDiscountActivity.java` | 定时折扣活动实体 |
| `haha-entity/.../InviteActivity.java` | 邀请有礼活动实体 |
| `haha-entity/.../CouponTemplate.java` | 优惠券模板实体 |
| `haha-entity/.../UserCoupon.java` | 用户优惠券实体 |
| `haha-service/.../MarketingRuleServiceImpl.java` | 营销规则计算（首单/折扣/满减） |
| `haha-service/.../MarketingActivityServiceImpl.java` | 营销活动CRUD |
| `haha-service/.../UserCouponServiceImpl.java` | 优惠券领取/使用/查询 |
| `haha-service/.../CouponTemplateServiceImpl.java` | 优惠券模板管理 |
| `haha-service/.../TimedDiscountServiceImpl.java` | 定时折扣管理 |
| `haha-service/.../InviteActivityServiceImpl.java` | 邀请有礼管理 |
| `haha-service/.../UnifiedActivityServiceImpl.java` | 统一活动管理 |
| `haha-service/.../HahaCallbackServiceImpl.java` | 订单回调中优惠券自动扣减 |
| `haha-admin/.../MarketingActivityController.java` | 运营平台-营销活动API |
| `haha-admin/.../CouponController.java` | 运营平台-优惠券管理API |
| `haha-admin/.../TimedDiscountController.java` | 运营平台-定时折扣API |
| `haha-admin/.../InviteActivityAdminController.java` | 运营平台-邀请有礼API |
| `haha-admin/.../UnifiedActivityController.java` | 运营平台-统一活动API |
| `haha-miniapp/.../AppCouponController.java` | 小程序-优惠券API |
| `haha-miniapp/.../InviteController.java` | 小程序-邀请API |
| `haha-common/.../enums/ActivityTypeEnum.java` | 活动类型枚举 |
| `haha-common/.../enums/ActivityStatusEnum.java` | 活动状态枚举 |
| `haha-common/.../enums/CouponTypeEnum.java` | 优惠券类型枚举 |
| `haha-common/.../enums/CouponStatusEnum.java` | 优惠券状态枚举 |
| `haha-common/.../enums/DiscountTypeEnum.java` | 折扣类型枚举 |
