# Feature Specification: 商家促销 + 优惠券系统

**Feature Branch**: `008-promotion-coupon`  
**Created**: 2026-05-29  
**Status**: Draft  
**Input**: 参考美团外卖商家端营销活动机制，为 foodie 系统实现促销和优惠券功能

## 背景

foodie 系统目前缺少商家端的营销工具。商家无法设置满减、折扣等促销活动，也无法发放优惠券。需要参考美团外卖的机制，实现一套适合 foodie 的促销 + 优惠券系统。

### 旧代码处理

系统中已有旧的促销/优惠券代码，**全部废弃不使用，不修改**：

| 旧代码 | 位置 | 说明 |
|--------|------|------|
| `SalesPromotion.java` | ruoyi-system/domain | 旧促销活动实体，表 `sales_promotion` |
| `VipQuanyi.java` | ruoyi-system/domain | 旧优惠券模板，表 `vip_quanyi` |
| `VipUserQuanyi.java` | ruoyi-system/domain | 旧用户券，表 `vip_user_quanyi` |
| `SalesPromotionController` | ruoyi-admin/app/promotion | 平台管理端促销接口 |
| `ShSalesPromotionController` | ruoyi-admin/app/mendian | 商家端促销接口 |
| `VipUserQuanyiController` | ruoyi-admin/app/user | 用户端优惠券接口 |

**原则：所有新功能新建文件、新表、新接口，不动旧代码。**

### 新表命名规则

所有新建数据库表使用 `promotion_` 前缀，与旧表区分：

| 新表名 | 说明 |
|--------|------|
| `promotion_activity` | 促销活动表（替代旧 `sales_promotion`） |
| `promotion_activity_rule` | 促销规则明细表 |
| `promotion_coupon_batch` | 券批次表（替代旧 `vip_quanyi`） |
| `promotion_coupon_rule` | 券规则表 |
| `promotion_user_coupon` | 用户券表（替代旧 `vip_user_quanyi`） |

### 工作范围

只有两部分：
1. **后端 API**（Java/Spring Boot）— 新建 Entity、Mapper、Service、Controller，包含商家端管理接口和用户端接口（领券、优惠券查询、算价计算）
2. **商家 PC 端前端**（foodie-store Vue.js）— 促销管理页面 + 优惠券管理页面

用户端（小程序）的前端界面不在本 spec 范围内，后续单独做。但后端提供的用户端接口（FR-011~022）在本期实施。

### 美团商家营销活动分类

美团外卖商家端有16种营销活动，分为三类：
- **促销类**（直接影响价格）：满减、折扣商品、第二份半价、新客立减、减配送费、满减运费、爆品
- **优惠券类**（用户领券后使用）：店内领券、店外发券、下单返券、集点返券、收藏有礼、售卖代金券
- **其他营销**：满赠、买赠、超值换购、好友助力、美团会员红包

### foodie 范围裁剪

本需求只实现以下功能：

**促销（4种）**：满减、折扣商品（逐商品设折扣）、第二份半价、新客立减

**不做**：爆品、买赠、减配送费、满减运费、好友助力等

**优惠券**：只做"店内领券"一种发放方式

**不做**：店外发券、下单返券、集点返券、收藏有礼、售卖代金券

### 促销的作用对象

促销不是都作用于同一个价格层级：

| 层级 | 作用对象 | 包含的促销类型 |
|------|---------|-------------|
| 订单级 | 订单总价 | 满减、新客立减 |
| 商品级 | 指定商品单价 | 折扣商品、第二份半价 |

### 互斥规则

参考美团做法，用户下单时系统自动计算两条路径，选最优惠的：

- **满减、折扣商品、第二份半价 → 三选一**，系统算两遍自动选最优
  - 路径A（走折扣/第二份半价）：系统分别计算「折扣」和「第二份半价」各一遍，取两者中更优的作为路径A结果。折扣商品按折扣价，第二份半价商品按半价规则，非活动商品原价
  - 路径B（走满减）：所有商品恢复原价，匹配满减最高可达档位，再减满减金额
- **新客立减** → 可与以上任意一种叠加
- **互斥在下单时判断**，不限制商家创建活动

### 叠加计算顺序

```
商品原价 → 促销（满减/折扣/第二份半价，三选一）→ 商家满减券 = 实付金额
商品券单独作用在指定商品上，不与满减券叠加
一个订单最多：1个促销 + 1张满减券
```

**同享券门槛基准**：满减券的 threshold（使用门槛）基于促销后的金额判断（如促销后¥33，券要求满30，¥33≥¥30 可用）。

**商品券与促销互斥规则**：商品券对应的商品如果正在参加折扣或第二份半价活动，该商品券不可用于该商品（放入不可用券列表），避免折上折。

## User Scenarios & Testing

### User Story 1 - 商家创建促销活动 (Priority: P1)

商家在后台创建促销活动（满减/折扣/第二份半价/新客立减），活动生效后用户下单时自动享受优惠。未开始的活动可以修改和删除，参考美团商家端交互。

**Why this priority**: 促销是商家最核心的营销需求，没有促销就没有优惠，后续优惠券也无法叠加使用。

**Independent Test**: 商家创建一个满减活动（满20减5），用户下单满20元后自动减5元，订单金额正确。

**操作权限矩阵（参考美团）**:

| 活动状态 | 编辑 | 删除 | 结束 |
|----------|------|------|------|
| 未开始(0) | ✅ | ✅ | ❌ |
| 进行中(1) | ❌ | ❌ | ✅ |
| 已结束(2) | ❌ | ❌ | ❌ |

**Acceptance Scenarios**:

1. **Given** 商家未创建任何促销活动, **When** 商家创建满减活动（满20减5 / 满40减12）, **Then** 活动列表显示进行中，用户端店铺显示满减标签
2. **Given** 商家已创建折扣活动（宫保鸡丁7折、麻婆豆腐8折）, **When** 用户下单宫保鸡丁(¥20) + 麻婆豆腐(¥18), **Then** 折扣价 ¥14 + ¥14.4 = ¥28.4
3. **Given** 商家同时有满减（满40减12）和折扣（宫保鸡丁7折¥14）, **When** 用户下单宫保鸡丁(¥20) + 鸡腿饭(¥25) = ¥45, **Then** 系统算两遍：走折扣¥39 vs 走满减¥33，自动选满减
4. **Given** 商家创建新客立减3元 + 满减（满40减12）, **When** 新用户下单¥45, **Then** 先减满减12 = ¥33，再减新客立减3 = ¥30
5. **Given** 折扣活动采用逐商品设折扣, **When** 商家选择可乐设5折、宫保鸡丁设7折、麻婆豆腐设8折, **Then** 可乐¥6×0.5=¥3，宫保鸡丁¥20×0.7=¥14，麻婆豆腐¥18×0.8=¥14.4

---

### User Story 2 - 商家创建优惠券 (Priority: P2)

商家创建优惠券（满减券/商品券/免配送费券），设置库存和有效期。用户进店时看到领券区，领取后下单使用。未开始和进行中的优惠券可以修改，未开始的可以删除。

**Why this priority**: 优惠券在促销基础上进一步促进复购和转化，但依赖促销系统先完成。

**Independent Test**: 商家创建"满30减5"优惠券100张，用户进店领取，下单满30元时选择使用，实付减5元。

**操作权限矩阵（参考美团）**:

| 券状态 | 编辑 | 删除 | 下架 |
|--------|------|------|------|
| 未开始(0) | ✅ | ✅ | ❌ |
| 进行中(1) | ✅ | ❌ | ✅ |
| 已结束(2) | ❌ | ❌ | ❌ |
| 已下架(3) | ❌ | ❌ | ❌ |

**Acceptance Scenarios**:

1. **Given** 商家创建满减券（同享券，满30减5，100张）, **When** 用户进店看到领券区并领取, **Then** 用户"我的优惠券"列表显示该券，状态为未使用
2. **Given** 用户已领取满减券（满30减5）且商家有满减活动（满40减12）, **When** 用户下单¥45, **Then** 先减满减12=¥33，再减券5=¥28（同享券可叠加）
3. **Given** 用户已领取互斥券（满30减5）, **When** 用户下单且订单有满减活动, **Then** 互斥券不可与满减叠加，用户只能二选一
4. **Given** 商家创建商品折扣券（宫保鸡丁5折）, **When** 用户下单宫保鸡丁(¥20)并使用该券, **Then** 宫保鸡丁按¥10计算
5. **Given** 优惠券库存为100张，已领取100张, **When** 新用户进店, **Then** 领券区显示"已领完"

---

### User Story 3 - 用户下单时优惠计算和展示 (Priority: P3)

用户在结算页看到优惠明细，系统自动选择最优方案，用户可手动切换。

**Why this priority**: 结算页的优惠展示和计算是用户最终看到的结果，需要在促销和优惠券都完成后实现。

**Independent Test**: 用户选好商品进入结算页，页面展示优惠明细（满减/折扣自动选最优 + 可用券列表），选择券后实付金额实时更新。

**Acceptance Scenarios**:

1. **Given** 商家有满减（满40减12）和折扣（宫保鸡丁7折）, **When** 用户进入结算页, **Then** 系统展示两条路径对比，默认选最优
2. **Given** 用户有3张可用券, **When** 用户点击优惠券行, **Then** 弹出券列表，可用券和不可用券分Tab展示，选中券后金额重算
3. **Given** 用户选了互斥券, **When** 订单同时有满减活动, **Then** 满减自动取消，金额重新计算

---

### Edge Cases

- 商家创建了满减活动后又创建折扣活动，两个同时进行中，用户下单时怎么处理？→ 系统自动算两遍选最优，不限制商家创建
- 用户领了券但下单时未达到门槛（券要求满30但订单只有25）→ 券在"不可用"Tab显示，提示"未达到¥30"
- 用户领券后商家修改/下架了该券批次 → 已领取的券仍可使用（按领取时的规则），但不能再领取新券
- 满减活动设了3个档位，订单金额正好在两个档位之间 → 命中低档位（如¥39命中满20减5而非满40减12）
- 第二份半价商品用户只买了1件 → 不享受优惠，按原价
- 第二份半价商品用户买了多件 → 每2件为一组，每组第2件半价（如买3件：第1、3件全价，第2件半价；买4件：第1、3件全价，第2、4件半价）
- 商家结束促销活动时，正在进行中的订单 → 不影响已下单的订单，只影响新订单
- 优惠券过期 → 过期券自动标记为已过期，不可使用
- 折扣活动中同一商品重复添加 → 前端限制同一商品只能添加一次，重复选择时提示并忽略
- 多用户同时领取同一优惠券（库存仅剩1张）→ 数据库原子扣减 `WHERE remain_count > 0`，只有一人成功，其余返回"已领完"

### 算价精度规则

- 所有金额计算使用 `BigDecimal`，中间过程不截断，最终结果 `ROUND_HALF_UP`（四舍五入）保留2位小数
- 最终实付金额最低为 ¥0.01，不出现零或负数
- 两条路径金额完全相等时，默认选路径B（满减路径），因满减对商家毛利更优
- 无促销活动时两条路径结果相同，直接返回原价
- 优惠券有效期计算：`expire_time = 领取日期 + valid_days 天后的 23:59:59`（如6月1日领券，valid_days=7，则6月8日 23:59:59 过期）
- 用户券过期判断：查询时实时判断 `expire_time < NOW()` 则标记为已过期（status→2），不依赖定时任务
- 同一门店同一促销类型只允许一个进行中的活动，创建时校验
- 结束活动不可逆，已结束活动不可重新激活
- 商品价格由后端根据 productId 从数据库查询，不信任前端传入的价格

## Clarifications

### Session 2026-06-01

- Q: 折扣活动中同一商品重复添加怎么处理？ → A: 前端限制同一商品只能添加一次，重复选择时提示并忽略（改为逐商品设折扣，不再有折扣区分组概念）
- Q: 折扣商品采用「折扣区分组」还是「逐商品设折扣」？ → A: 改为逐商品设折扣（参考美团），每个商品独立设置折扣率，不再使用折扣区/折扣档位分组。调研记录见 `docs/research-meituan-discount-product.md`
- Q: 新客立减的"新客"定义？ → A: 该门店首次下单的用户（该门店无已完成订单记录）
- Q: 算价接口前端传入价格有篡改风险？ → A: 算价接口只传productId+quantity，后端根据productId查实际价格
- Q: 第二份半价属于路径A还是独立路径？ → A: 作为路径A的变体，系统分别算折扣和第二份半价，取更优作为路径A结果
- Q: 同享券门槛基于促销前还是促销后金额？ → A: 基于促销后金额
- Q: 商品在折扣活动中，商品券能否用于该商品？ → A: 不可用，放入不可用券列表
- Q: 金额精度和舍入规则？ → A: BigDecimal计算，ROUND_HALF_UP保留2位小数，最低¥0.01
- Q: 免配送费券如何获取配送费金额？ → A: 算价接口新增可选参数deliveryFee，前端传入当前配送费，后端计算min(coupon.amount, deliveryFee)作为减免额
- Q: 删除促销活动是物理删除还是软删除？ → A: 物理删除(DELETE)，因为未开始活动不会有订单引用，安全且简单
- Q: 编辑进行中的优惠券时，已领取用户的券规则是否跟随变更？ → A: 只影响新领取的券，已领取的券保持原规则不变（快照机制）
- Q: 免配送费券在叠加计算顺序中的位置？ → A: 在促销+满减券之后独立叠加：实付 = 促销后金额 - 券减免 - min(配送费券.amount, deliveryFee)
- Q: 优惠券删除条件？ → A: 仅当 status=0 且 receivedCount=0 时允许删除（双重校验，防御性编程）

### Session 2026-06-02

- Q: 折扣率允许范围和 UI 控件？ → A: 1折~9.9折（10%~99%），步长 0.1，用 -/+ 按钮或直接输入。超出范围前端阻止
- Q: 用户领券并发超发怎么防止？ → A: 数据库原子扣减：`UPDATE remain_count = remain_count - 1, received_count = received_count + 1 WHERE id = #{id} AND remain_count > 0`，affected rows=0 则返回"已领完"

### Session 2026-09-02（优惠幅度 20% 限制，防止恶意竞争）

- Q: 限制范围只包括优惠券吗？ → A: 促销活动一起限制。满减/折扣促销、满减券、商品券、免配送费券都受限；第二杯半价豁免（商家无可设金额参数、结构性封顶该商品小计的25%）
- Q: "总优惠幅度不能大于20%"的执行口径？ → A: 双层执行：①创建/修改时各项自身 ≤ 比例 × 各自锚点（满减档位=门槛、折扣=8折下限、商品抵用券=商品价格）；②算价时总账预算 = 比例 × 订单商品原价，按 促销→新客立减→券 的顺序层层封顶。不做创建时跨活动叠加预算（曾评估"同店叠加总账"方案，因门槛交点计算复杂且会误拦小券组合而放弃，改由算价侧兜底）
- Q: 剩余预算不够抵券面额时怎么处理？ → A: 部分抵扣（实减 = min(面额, 剩余预算)），不是整张作废——规则统一、能把20%空间用满
- Q: 第二杯半价计入算价预算吗？ → A: 不占预算也不被封顶，否则其本身的25%会被砍到20%等于变相废掉该功能
- Q: 免配送费券怎么处理？ → A: 创建时必须有使用门槛且减免 ≤ 比例×门槛（无门槛拒绝并提示设门槛）；算价时计入预算（现有实现免配送费券与满减券同路径、从餐费里减，计入可顺带堵存量离谱券）
- Q: 新客立减为什么创建时不校验？ → A: 只有减免额没有门槛、创建时无分母；由算价预算统一封顶（实减 = min(设置额, 剩余预算)）
- Q: 开关怎么做？ → A: sys_config 参数 promotion.discount.limit.enabled（开关）/ promotion.discount.limit.ratio（比例%，默认20），后台"参数管理"页启停，前端零改动；关闭=行为与现状完全一致
- Q: 存量怎么处理？ → A: 创建侧不追溯（已被领取且规则未变的编辑不受影响，仅规则实际被替换时校验新值）；算价侧存量自动被预算封顶
- Q: 互斥券路径怎么算？ → A: 互斥券取消促销+新客后，其自身减免同样受预算封顶

## Requirements

### Functional Requirements

#### 促销活动管理（商家端）

- **FR-001**: 商家 MUST 能创建满减活动，支持设置多个档位（如满20减5 / 满40减12 / 满60减20）
- **FR-002**: 商家 MUST 能创建折扣商品活动，采用**逐商品设折扣**方式（参考美团）：从商品列表勾选参与的商品，每个商品单独设置折扣率。一个活动内不同商品可以有不同折扣率（如宫保鸡丁7折、麻婆豆腐8折、可乐5折）。不使用「折扣区/折扣档位」分组概念。折扣率范围：10%~99%（即1折~9.9折），步长 0.1，前端用 -/+ 按钮或直接输入
- **FR-003**: 商家 MUST 能创建第二份半价活动，从商品列表勾选参加的商品
- **FR-004**: 商家 MUST 能创建新客立减活动，设置固定减免金额。「新客」定义为：该门店首次下单的用户（该门店无已完成订单记录）
- **FR-005**: 商家 MUST 能查看活动列表（进行中/未开始/已结束），能结束进行中的活动
- **FR-005a**: 商家 MUST 能修改未开始的活动（编辑活动名称、时间、规则），复用创建对话框回填数据
- **FR-005b**: 商家 MUST 能删除未开始的活动（物理删除活动及关联规则），删除前需二次确认
- **FR-006**: 系统 MUST 展示互斥规则提示：满减、折扣、第二份半价三选一，新客立减可叠加

#### 优惠券管理（商家端）

- **FR-007**: 商家 MUST 能创建满减券，设置同享/互斥属性、使用门槛、减免金额、发放总量、有效天数
- **FR-008**: 商家 MUST 能创建商品券（折扣券/抵用券），关联指定商品，设置折扣率。商品券优惠作用于该商品的每一件（如买3个宫保鸡丁，5折券则全部按5折计算）
- **FR-008a**: 商家 MUST 能创建免配送费券，设置使用门槛（满X元可用）、减免金额（固定减免配送费Y元，减免金额≥实际配送费时等同于免配送费）、同享/互斥属性、发放总量、有效天数。算价接口新增可选参数 `deliveryFee`，前端传入当前配送费，后端计算 `min(coupon.amount, deliveryFee)` 作为实际减免额
- **FR-009**: 商家 MUST 能查看优惠券列表，展示领取/使用情况、库存，能下架进行中的优惠券
- **FR-009a**: 商家 MUST 能修改未开始/进行中的优惠券（编辑名称、时间、规则等），复用创建对话框回填数据
- **FR-009b**: 商家 MUST 能删除未开始的优惠券（物理删除券批次+规则），仅当 status=0 且 receivedCount=0 时允许删除，删除前需二次确认
- **FR-010**: 优惠券发放方式只有一种：店内领券（用户进店时在领券区领取）

#### 用户端接口（后端API本期实施，前端界面[延后]由小程序实施）

- **FR-011**: 用户 MUST 能在店铺首页看到领券区 → 后端提供「查询门店可领优惠券列表」接口
- **FR-012**: 用户 MUST 能领取优惠券，每人每批次限领1张 → 后端提供「领取优惠券」接口
- **FR-013**: 用户 MUST 能在"我的优惠券"中查看已领取的券 → 后端提供「查询用户优惠券列表」接口
- **FR-014**: 用户 MUST 能在结算页看到促销优惠明细 → 后端提供「计算订单优惠」接口
- **FR-015**: 用户 MUST 能在结算页选择使用/不使用优惠券 → 由算价接口参数控制
- **FR-016**: 系统 MUST 在结算页自动计算满减和折扣两条路径，默认选最优 → 算价接口返回两条路径对比

#### 算价逻辑（后端Service本期实施，前端展示[延后]由小程序实施）

- **FR-017**: 下单时系统 MUST 同时计算两条路径：
  - 路径A（走折扣）：折扣商品按折扣价，非折扣商品原价，再扣券
  - 路径B（走满减）：所有商品恢复原价，扣满减，再扣券
- **FR-018**: 系统 MUST 默认选择实付金额更低的路径
- **FR-019**: 用户 MUST 能在结算页手动切换路径 → 算价接口支持指定路径参数
- **FR-020**: 同享券 MUST 能与满减/折扣叠加使用。满减券的 threshold 基于促销后金额判断
- **FR-021**: 互斥券 MUST 不能与满减/折扣/第二份半价叠加
- **FR-022**: 一个订单最多使用 1个促销 + 1张满减券。商品券对应的商品如正在参加折扣/第二份半价活动，该券不可用于该商品

#### 优惠幅度限制（2026-09-02 追加，平台开关控制）

- **FR-023**: 平台 MUST 提供优惠幅度限制开关：sys_config 参数 promotion.discount.limit.enabled（true/false）与 promotion.discount.limit.ratio（百分比，默认20）。关闭时系统行为与现状完全一致
- **FR-024**: 开关启用时，商家新增/修改以下内容 MUST 被校验（超限拒绝，ServiceException 指明具体档位/券）：
  - 满减促销：每个档位 减免 ≤ 比例 × 门槛
  - 折扣促销：每个商品折扣率 ≥ (1 − 比例)，如20%时最低8折
  - 满减券：减免 ≤ 比例 × 使用门槛
  - 商品折扣券：折扣率 ≥ (1 − 比例)
  - 商品抵用券：减免 ≤ 比例 × 商品价格
  - 免配送费券：必须设使用门槛，且减免 ≤ 比例 × 门槛
  - 第二杯半价、新客立减：创建时不校验
- **FR-025**: 开关启用时，算价 MUST 执行总优惠预算：预算 = 比例 × 订单商品原价合计，按 促销（三选一）→ 新客立减 → 优惠券 顺序层层封顶，每层实减 = min(该层优惠额度, 剩余预算)（部分抵扣）；第二杯半价胜出的促销减免不占预算也不封顶；免配送费券计入预算；互斥券路径同样受预算约束；实付最低兜底沿用现状
- **FR-026**: 结算页可用券列表 MUST 按剩余预算反映实际可抵额度，可抵为 0 的券标记不可用（原因"本单优惠已达优惠上限"）
- **FR-027**: 算价明细与 pos_order_promotion 快照 MUST 反映预算封顶后的实减金额，被封顶的项标注"已达优惠上限"；预览与下单走同一算价入口，金额一致

### Key Entities

- **促销活动 (promotion_activity)**: 商家创建的促销规则，包含类型（满减/折扣/第二份半价/新客立减）、状态、时间范围
- **促销规则 (promotion_activity_rule)**: 促销活动的具体规则数据，一个活动对应多条规则（满减档位/折扣商品/第二份半价商品）
- **券批次 (promotion_coupon_batch)**: 优惠券模板，包含券名称、类型、库存、有效期
- **券规则 (promotion_coupon_rule)**: 优惠券的使用规则，包含门槛、金额、折扣率、同享/互斥属性
- **用户券 (promotion_user_coupon)**: 用户领取的券实例，绑定用户和门店，记录状态和使用情况

## Data Model

所有新建表使用 `promotion_` 前缀，与旧表（`sales_promotion`、`vip_quanyi`、`vip_user_quanyi`）完全独立。

### promotion_activity — 促销活动表

```sql
CREATE TABLE promotion_activity (
  id              BIGINT AUTO_INCREMENT PRIMARY KEY,
  store_id        BIGINT       NOT NULL COMMENT '门店ID',
  type            TINYINT      NOT NULL COMMENT '类型: 1=满减 2=折扣 3=第二份半价 4=新客立减',
  name            VARCHAR(100) NOT NULL COMMENT '活动名称',
  status          TINYINT      DEFAULT 0 COMMENT '0=未开始 1=进行中 2=已结束',
  start_time      DATETIME     NOT NULL COMMENT '开始时间',
  end_time        DATETIME     NOT NULL COMMENT '结束时间',
  create_time     DATETIME     DEFAULT CURRENT_TIMESTAMP,
  update_time     DATETIME     DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  INDEX idx_store_type (store_id, type)
) COMMENT '促销活动表';
```

### promotion_activity_rule — 促销规则明细表

```sql
CREATE TABLE promotion_activity_rule (
  id              BIGINT AUTO_INCREMENT PRIMARY KEY,
  activity_id     BIGINT   NOT NULL COMMENT '关联促销活动',
  product_id      BIGINT   DEFAULT NULL COMMENT '商品ID（折扣/第二份半价用）',
  threshold       DECIMAL(10,2) DEFAULT NULL COMMENT '满减门槛（满X元）',
  reduce_amount   DECIMAL(10,2) DEFAULT NULL COMMENT '减免金额（满减/新客立减）',
  discount_rate   DECIMAL(3,2)  DEFAULT NULL COMMENT '折扣率（0.70=7折 / 0.50=半价）',
  min_quantity    INT       DEFAULT NULL COMMENT '最低数量（第二份半价=2）',
  INDEX idx_activity (activity_id)
) COMMENT '促销规则明细';
```

### promotion_coupon_batch — 券批次表（模板）

```sql
CREATE TABLE promotion_coupon_batch (
  id                BIGINT AUTO_INCREMENT PRIMARY KEY,
  store_id          BIGINT       NOT NULL COMMENT '门店ID',
  name              VARCHAR(100) NOT NULL COMMENT '券名称',
  coupon_type       TINYINT      NOT NULL COMMENT '1=满减券 2=商品券 3=免配送费券',
  total_count       INT          NOT NULL COMMENT '发放总量',
  remain_count      INT          NOT NULL COMMENT '剩余数量',
  received_count    INT          DEFAULT 0 COMMENT '已领取数量',
  status            TINYINT      DEFAULT 0 COMMENT '0=未开始 1=进行中 2=已结束 3=已下架',
  start_time        DATETIME     NOT NULL COMMENT '领取开始时间',
  end_time          DATETIME     NOT NULL COMMENT '领取结束时间',
  valid_days        INT          NOT NULL COMMENT '领取后有效天数',
  create_time       DATETIME     DEFAULT CURRENT_TIMESTAMP,
  update_time       DATETIME     DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  INDEX idx_store_status (store_id, status)
) COMMENT '券批次表(模板)';
```

### promotion_coupon_rule — 券规则表

```sql
CREATE TABLE promotion_coupon_rule (
  id                BIGINT AUTO_INCREMENT PRIMARY KEY,
  batch_id          BIGINT   NOT NULL COMMENT '关联券批次',
  product_id        BIGINT   DEFAULT NULL COMMENT '商品ID（商品券用，满减券=NULL）',
  is_mutex          TINYINT  DEFAULT 0 COMMENT '0=同享券 1=互斥券（仅满减券使用；商品券此字段为NULL，互斥逻辑由促销活动判断）',
  threshold         DECIMAL(10,2) DEFAULT NULL COMMENT '使用门槛：满减券=促销后订单金额门槛，商品券=该商品小计金额门槛(单价×数量)',
  amount            DECIMAL(10,2) DEFAULT NULL COMMENT '减免金额（满减券/抵用券）',
  discount_rate     DECIMAL(3,2)  DEFAULT NULL COMMENT '折扣率（商品折扣券用）',
  INDEX idx_batch (batch_id)
) COMMENT '券规则';
```

### promotion_user_coupon — 用户券表（实例）

```sql
CREATE TABLE promotion_user_coupon (
  id                BIGINT AUTO_INCREMENT PRIMARY KEY,
  user_id           BIGINT   NOT NULL COMMENT '用户ID',
  batch_id          BIGINT   NOT NULL COMMENT '券批次ID',
  store_id          BIGINT   NOT NULL COMMENT '门店ID',
  status            TINYINT  DEFAULT 0 COMMENT '0=未使用 1=已使用 2=已过期 3=冻结',
  order_id          BIGINT   DEFAULT NULL COMMENT '使用的订单ID',
  receive_time      DATETIME NOT NULL COMMENT '领取时间',
  use_time          DATETIME DEFAULT NULL COMMENT '使用时间',
  expire_time       DATETIME NOT NULL COMMENT '过期时间',
  INDEX idx_user_status (user_id, status),
  INDEX idx_store (store_id)
) COMMENT '用户领券记录';
```

### pos_order_promotion — 订单优惠明细表

下单时快照记录每个订单命中的优惠信息，优惠类型级别（每条记录 = 一种优惠），不做 SKU 级分摊。由算价接口在下单时写入。

```sql
CREATE TABLE pos_order_promotion (
  id              BIGINT AUTO_INCREMENT PRIMARY KEY,
  order_id        BIGINT       NOT NULL COMMENT '关联订单ID',
  promo_type      TINYINT      NOT NULL COMMENT '优惠类型: 1=促销活动 2=商家优惠券',
  promo_sub_type  TINYINT      DEFAULT NULL COMMENT '促销子类型: 1=满减 2=折扣 3=第二份半价 4=新客立减 (promo_type=1时有效)',
  promo_id        BIGINT       DEFAULT NULL COMMENT '促销活动ID(promo_type=1) 或 券批次ID(promo_type=2)',
  user_coupon_id  BIGINT       DEFAULT NULL COMMENT '用户券ID (仅优惠券 promo_type=2 时有值, 关联 promotion_user_coupon.id)',
  promo_name      VARCHAR(200) NOT NULL COMMENT '快照名称: 如"午市满减(满40减12)"',
  promo_detail    VARCHAR(500) DEFAULT NULL COMMENT '快照详情JSON: 如{"threshold":40,"reduce":12}',
  reduce_amount   DECIMAL(10,2) NOT NULL COMMENT '减免金额',
  path_summary    VARCHAR(500) DEFAULT NULL COMMENT '路径对比摘要, 仅第一条记录有值, 如"满减路径¥33 vs 折扣路径¥39, 选择满减"',
  create_time     DATETIME     DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_order (order_id)
) COMMENT '订单优惠明细';
```

#### 字段说明

| 字段 | 作用 | 示例 |
|------|------|------|
| `promo_type` | 区分是促销还是优惠券 | 1=促销, 2=优惠券 |
| `promo_sub_type` | 促销的具体子类型 | 1=满减, 2=折扣, 3=第二份半价, 4=新客立减；优惠券时为 null |
| `promo_id` | 关联原始活动或券批次 | promo_type=1 时为 promotion_activity.id；promo_type=2 时为 promotion_coupon_batch.id |
| `user_coupon_id` | 关联用户领取的券实例 | promo_type=2 时为 promotion_user_coupon.id；促销时为 null |
| `promo_name` | 下单时的快照名称 | "午市满减(满40减12)" |
| `promo_detail` | 规则快照 JSON | `{"type":"manjian","threshold":40,"reduce":12}` |
| `reduce_amount` | 实际减免金额 | 12.00 |
| `path_summary` | 路径对比摘要 | "满减路径¥33 vs 折扣路径¥39, 选择满减"，仅第一条记录有值 |

#### 示例数据

订单：宫保鸡丁¥20 + 鸡腿饭¥25 = ¥45，商家有满减(满40减12)和折扣(宫保鸡丁7折)，用户有满30减5同享券，新用户

| promo_type | promo_sub_type | promo_name | reduce_amount | path_summary |
|---|---|---|---|---|
| 1(促销) | 1(满减) | 午市满减(满40减12) | 12.00 | 满减路径¥33 vs 折扣路径¥39, 选择满减 |
| 2(优惠券) | null | 满30减5(同享券) | 5.00 | null |
| 1(促销) | 4(新客立减) | 新客立减 | 3.00 | null |

#### 与 pos_order 现有字段的关系

下单时双写：
- `pos_order` 的 `mdSalesReduction`、`mdDiscountAmount` 等汇总字段照常写入
- `pos_order_promotion` 写入每条优惠明细（快照）

查询场景：
- 订单列表：只看 `pos_order` 汇总字段
- 订单详情/退款：查 `pos_order_promotion` 获取明细

#### 设计决策

- **单表扁平设计**：一张表存储所有优惠明细，每条记录代表一种优惠，一个订单通常 3-5 条记录
- **优惠类型级别**：不做 SKU 级优惠分摊，所有优惠都是商家级别
- **快照机制**：下单时将促销名称、规则、金额拍快照存入，后续促销活动修改不影响已下单的订单
- **路径对比摘要**：`path_summary` 字段记录系统计算的路径对比结果（如满减 vs 折扣），仅存在该订单的第一条明细记录上，其余记录该字段为 null
- **pos_order 现有字段保留**：不删除现有的 `mdSalesReduction`、`mdDiscountAmount` 等字段，新表明细作为补充信息

### 表关系

```
促销模块:
  promotion_activity 1 → N promotion_activity_rule

优惠券模块:
  promotion_coupon_batch 1 → 1 promotion_coupon_rule
  promotion_coupon_batch 1 → N promotion_user_coupon

订单优惠:
  pos_order 1 → N pos_order_promotion
  pos_order_promotion N → 1 promotion_activity (promo_type=1 时)
  pos_order_promotion N → 1 promotion_coupon_batch (promo_type=2 时)
  pos_order_promotion N → 1 promotion_user_coupon (promo_type=2 时)
```

## Success Criteria

### Measurable Outcomes

- **SC-001**: 商家能在3分钟内完成一个满减活动的创建（包括设置3个档位）
- **SC-002**: 商家能在3分钟内完成一个折扣活动的创建（选择5个商品 + 逐个设折扣率）
- **SC-003**: 商家能在2分钟内完成一张优惠券的创建
- **SC-004**: 用户下单时优惠计算正确，结算页实时展示优惠明细
- **SC-005**: 满100%覆盖互斥规则：满减/折扣/第二份半价三选一，新客立减可叠加

## Assumptions

- **旧代码不动**：`SalesPromotion`、`VipQuanyi`、`VipUserQuanyi` 等旧实体和旧接口全部废弃，新功能完全新建文件
- **新表前缀 `promotion_`**：与旧表（`sales_promotion`、`vip_quanyi`）完全独立
- **工作范围**：后端 API（商家端管理接口 + 用户端接口）+ 商家 PC 端前端（foodie-store）
- **用户端前端不在本范围**：小程序端的领券页面、结算页展示后续单独做，但后端接口（FR-011~022）本期实施
- 促销和优惠券都是门店级别（storeId），不是跨店通用
- `pos_order_promotion`（订单优惠明细表）：下单时快照记录优惠明细，单表扁平设计，优惠类型级别，不做 SKU 分摊。本期建表并实现写入逻辑
- 现有 `pos_order` 表的优惠汇总字段（`mdSalesReduction`、`mdDiscountAmount` 等）保留不动，`pos_order_promotion` 明细表作为补充
- 平台券不在本需求范围内，只做商家级促销和商家券
- 优惠幅度限制（2026-09-02）：开关默认值在 SQL 中为启用(true)，上线前可在 updatesql/sql.md 调整；创建侧校验存量不追溯，算价侧预算对存量自动生效
- SQL 变更写入 `updatesql/sql.md`，由开发者手动执行

## 参考资源

- 美团外卖商家活动创建攻略（知乎）
- 美团外卖优惠规则（官方）
- 美团外卖平台市场营销规则（rules-center.meituan.com）
- 优惠券系统设计（知乎）
- 大厂优惠券系统设计（掘金）
- Demo文件：`meituan-promotion-types-demo.html`、`merchant-promotion-ui-demo.html`、`meituan-checkout-demo.html`
- [美团折扣商品调研报告](../../docs/research-meituan-discount-product.md) — 美团/饿了么折扣商品设置方式对比，逐商品设折扣 vs 折扣区分组
