# PRD：优惠券功能

**版本**: v1.1
**日期**: 2026-03-03
**项目**: 万车充运营管理平台
**状态**: 草稿

---

## 变更记录

| 版本 | 日期 | 变更内容 |
|------|------|---------|
| v1.0 | 2026-03-03 | 初稿：积分兑换洗车券功能 |
| v1.1 | 2026-03-03 | 新增：券创建权限体系（平台人员 vs 运营商人员）；新增充电折扣券规划；更新数据库设计与业务规则 |

---

## 一、背景与目标

### 背景

平台现有积分系统（`member_points_info`）支持充电消费奖励积分，但积分使用场景单一（仅支持抵扣充电费用）。为提升用户留存与活跃度，引入优惠券体系，首期以「积分兑换洗车券」为切入点，同时建立支持平台与运营商双层管理的券权限体系，为后续充电折扣券等功能奠定基础。

### 目标

- 为用户提供积分消耗渠道，提升积分价值感知
- 通过洗车权益与充电绑定，增强用户充电粘性
- 建立可扩展的优惠券基础架构，支持后续更多券类型
- 建立平台与运营商双层券管理权限，支持精细化运营

### 成功指标

- 积分兑换率（兑换用户数 / 持有积分用户数）≥ 15%
- 洗车券核销率 ≥ 60%
- 功能上线后次月充电频次同比提升 ≥ 5%

---

## 二、用户角色

| 角色 | 类型 | 说明 |
|------|------|------|
| **平台管理员** | 创券方 | 平台运营人员，可创建全平台范围或指定运营商范围的优惠券 |
| **运营商管理员** | 创券方 | 运营商后台人员，只能创建本运营商范围内可用的优惠券 |
| **会员用户** | 使用方 | 通过充电获得积分，在 App/小程序中兑换并使用券 |
| **合作洗车商** | 核销方 | 扫码核销洗车券（可通过独立 H5 页面或 API 对接） |

---

## 三、券可用范围权限设计

### 3.1 核心规则

| 创建人角色 | 可设置的最大可用范围 | 说明 |
|-----------|-------------------|------|
| **平台管理员** | 全平台 | 可选：全平台 / 指定运营商 / 指定站点 |
| **运营商管理员** | 本运营商 | 可选：本运营商全部站点 / 指定站点（仅限本运营商下） |

### 3.2 可用范围枚举

| 范围值 | 说明 | 可创建角色 |
|--------|------|----------|
| `1` - 全平台 | 所有运营商、所有站点均可使用 | 仅平台管理员 |
| `2` - 指定运营商 | 仅指定运营商下的站点可使用 | 平台管理员（可多选）；运营商管理员（固定为本运营商） |
| `3` - 指定站点 | 仅指定站点可使用 | 平台管理员；运营商管理员（仅限本运营商下的站点） |

### 3.3 业务规则

1. 运营商管理员创建券时，`scope_type` 最高只能选 `2`（本运营商），**不能选** `1`（全平台）
2. 运营商管理员创建的指定站点券，`scope_merchant_id` 强制写入本运营商 ID，不可修改
3. 平台管理员可查看所有运营商的券；运营商管理员只能查看和管理本运营商创建的券
4. 用户兑换或领取券时，系统根据券的 `scope_type` 及关联数据校验该用户是否符合领取资格（如充电站是否在券可用范围内）
5. 充电折扣券使用时额外校验：本次充电所在站点是否在券的可用范围内

---

## 四、功能范围（首期 v1.0）

### 4.1 功能清单

| 模块 | 功能点 | 优先级 |
|------|--------|--------|
| 券模板管理 | 平台管理员创建/编辑/下架洗车券模板 | P0 |
| 券模板管理 | 运营商管理员创建/编辑/下架洗车券模板 | P0 |
| 券模板管理 | 设置券可用范围（全平台 / 指定运营商 / 指定站点） | P0 |
| 券模板管理 | 设置积分兑换比例（N 积分 = 1 张券） | P0 |
| 券模板管理 | 设置券有效期（固定日期 / 领取后 N 天） | P0 |
| 积分兑换 | 用户查看可兑换券列表 | P0 |
| 积分兑换 | 用户用积分兑换洗车券 | P0 |
| 积分兑换 | 兑换记录查询 | P0 |
| 我的券包 | 查看持有的洗车券（未使用/已使用/已过期） | P0 |
| 券核销 | 展示券码（二维码 / 核销码） | P0 |
| 券核销 | 合作商扫码核销 | P0 |
| 数据看板 | 兑换量、核销量、积分消耗统计 | P1 |
| 运营配置 | 单用户每日/每月兑换上限 | P1 |
| 运营配置 | 券库存上限控制 | P1 |

### 4.2 首期不包含

- 现金购买券
- 券转赠
- 充电折扣券（规划于 v1.2）
- 第三方洗车平台 API 对接（仅本地核销）

---

## 五、业务流程

### 5.1 创建券模板流程

```
管理员进入券模板管理页
    → 选择券类型（首期仅洗车券）
    → 系统根据登录角色自动限制可用范围选项
        ├─ 平台管理员：可选 全平台 / 指定运营商 / 指定站点
        └─ 运营商管理员：可选 本运营商 / 本运营商下指定站点
    → 填写积分兑换价格、库存、有效期、兑换限额等配置
    → 提交 → 系统二次校验 scope 权限边界
    → 保存，默认下架状态，手动上架后对用户可见
```

### 5.2 积分兑换流程

```
用户进入积分商城
    → 系统查询该用户可见的洗车券列表
      （过滤逻辑：券为上架状态 + 未过期 + 有库存 + 用户所在运营商/站点在券可用范围内）
    → 用户查看洗车券兑换入口（展示所需积分、库存、有效期）
    → 确认兑换
    → 系统校验：积分是否足够 + 库存是否充足 + 兑换限额检查
    → 原子操作：扣减积分 + 生成用户券记录
    → 写入积分变更日志（member_points_record，类型=积分兑换消耗）
    → 返回兑换成功，展示券码
```

### 5.3 洗车券核销流程

```
用户出示洗车券二维码
    → 洗车商扫码 → 调用核销接口
    → 系统校验：券是否有效 + 是否已使用 + 是否在有效期内
    → 更新券状态为「已使用」，记录核销时间、核销门店
    → 核销成功响应
```

---

## 六、数据库设计

### 6.1 券模板表 `coupon_template`

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | bigint PK | 主键 |
| `name` | varchar(50) | 券名称，如"洗车券" |
| `type` | tinyint | 券类型：1=洗车券 2=充电折扣券 |
| `creator_type` | tinyint | 创建人类型：1=平台管理员 2=运营商管理员 |
| `creator_merchant_id` | bigint | 创建人所属运营商 ID（平台管理员为 NULL） |
| `scope_type` | tinyint | 可用范围：1=全平台 2=指定运营商 3=指定站点 |
| `points_cost` | int | 兑换所需积分（洗车券用） |
| `discount_rate` | decimal(4,2) | 折扣比例（充电折扣券用，如 0.85 表示 85 折） |
| `stock_total` | int | 总库存（-1 表示不限制） |
| `stock_remain` | int | 剩余库存 |
| `validity_type` | tinyint | 有效期类型：1=固定日期 2=领取后N天 |
| `valid_start_time` | datetime | 固定有效期开始（validity_type=1） |
| `valid_end_time` | datetime | 固定有效期结束（validity_type=1） |
| `valid_days` | int | 领取后有效天数（validity_type=2） |
| `daily_limit` | int | 单用户每日兑换上限（0=不限） |
| `monthly_limit` | int | 单用户每月兑换上限（0=不限） |
| `status` | tinyint | 状态：0=下架 1=上架 |
| `description` | varchar(500) | 券使用说明 |
| `create_by` | varchar(64) | 创建人账号 |
| `create_time` | datetime | 创建时间 |
| `update_time` | datetime | 更新时间 |
| `del_flag` | char(1) | 删除标志 |

### 6.2 券可用范围明细表 `coupon_template_scope`

> 用于存储 `scope_type=2`（指定运营商）或 `scope_type=3`（指定站点）时的具体关联数据

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | bigint PK | 主键 |
| `template_id` | bigint | 关联券模板 ID |
| `scope_type` | tinyint | 范围类型：2=运营商 3=站点 |
| `scope_id` | bigint | 运营商 ID 或站点 ID |

### 6.3 用户券表 `member_coupon`

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | bigint PK | 主键 |
| `coupon_no` | varchar(32) | 券编号（唯一，用于核销） |
| `template_id` | bigint | 关联券模板 ID |
| `member_id` | bigint | 关联会员 ID |
| `coupon_type` | tinyint | 券类型快照：1=洗车券 2=充电折扣券 |
| `points_cost` | int | 兑换时消耗的积分（快照，折扣券为 0） |
| `discount_rate` | decimal(4,2) | 折扣比例快照（洗车券为 NULL） |
| `status` | tinyint | 状态：0=未使用 1=已使用 2=已过期 |
| `source` | tinyint | 来源：1=积分兑换 |
| `exchange_time` | datetime | 兑换时间 |
| `expire_time` | datetime | 过期时间 |
| `use_time` | datetime | 核销时间 |
| `use_store_id` | bigint | 核销门店/站点 ID |
| `use_operator` | varchar(64) | 核销操作人 |
| `create_time` | datetime | 创建时间 |
| `del_flag` | char(1) | 删除标志 |

### 6.4 依赖现有表

- `member_points_info`：积分余额更新
- `member_points_record`：新增消耗类型（`type=3` 积分兑换）
- `pile_merchant_info`：运营商信息（校验 creator_merchant_id）
- `pile_station_info`：站点信息（校验 scope 范围）

---

## 七、接口设计

### App/小程序端

| 接口 | 方法 | 说明 |
|------|------|------|
| `/coupon/template/list` | GET | 获取用户可兑换券模板列表（按用户所属范围过滤） |
| `/coupon/exchange` | POST | 积分兑换（入参：templateId） |
| `/coupon/my/list` | GET | 我的券包列表（入参：status） |
| `/coupon/my/detail/{couponNo}` | GET | 券详情（含核销二维码） |

### 管理后台

| 接口 | 方法 | 说明 |
|------|------|------|
| `/admin/coupon/template/list` | GET | 券模板列表（运营商管理员仅返回本运营商数据） |
| `/admin/coupon/template/add` | POST | 新增券模板（含 scope 权限校验） |
| `/admin/coupon/template/edit` | PUT | 编辑券模板 |
| `/admin/coupon/template/changeStatus` | PUT | 上下架 |
| `/admin/coupon/record/list` | GET | 兑换记录查询（运营商管理员仅查本运营商数据） |
| `/admin/coupon/verify` | POST | 核销券（入参：couponNo） |
| `/admin/coupon/stats` | GET | 数据统计（运营商管理员仅查本运营商数据） |

---

## 八、关键业务规则

1. **原子性保障**：扣减积分与生成券记录必须在同一事务中完成，防止积分扣了但券没生成
2. **库存防超卖**：使用数据库乐观锁（`UPDATE ... WHERE stock_remain >= 1`）或 Redis 原子操作控制库存
3. **幂等兑换**：前端提交带客户端请求 ID，防止重复点击导致多次扣分
4. **券码安全**：`coupon_no` 使用 UUID 或带校验位的随机串，不可预测
5. **过期处理**：Quartz 定时任务每日凌晨扫描并更新过期券状态
6. **积分变更日志**：兑换成功后写入 `member_points_record`，确保积分流水可追溯
7. **Scope 权限隔离**：
   - 运营商管理员提交创建/编辑接口时，后端强制校验 `scope_type <= 2`，且 `creator_merchant_id` 必须与登录账号所属运营商一致，防止越权
   - 运营商管理员只能查看和操作本运营商创建的券模板，列表接口自动按 `creator_merchant_id` 过滤
8. **折扣券使用校验**（v1.2 实现）：充电结算时校验充电站是否在折扣券的 `scope` 范围内，不在范围内的券不可抵扣

---

## 九、非功能需求

| 项目 | 要求 |
|------|------|
| 性能 | 兑换接口 P99 < 500ms |
| 并发 | 支持同一券模板 100 QPS 并发兑换 |
| 可靠性 | 积分扣减与券生成保证原子性 |
| 安全 | 核销接口需鉴权；创券接口需强制 scope 边界校验，防止运营商越权 |

---

## 十、数据库建表语句

```sql
-- ----------------------------
-- 券模板表
-- ----------------------------
CREATE TABLE `coupon_template` (
    `id`                   BIGINT        NOT NULL AUTO_INCREMENT COMMENT '主键',
    `name`                 VARCHAR(50)   NOT NULL COMMENT '券名称，如洗车券',
    `type`                 TINYINT       NOT NULL DEFAULT 1 COMMENT '券类型：1=洗车券 2=充电折扣券',
    `creator_type`         TINYINT       NOT NULL DEFAULT 1 COMMENT '创建人类型：1=平台管理员 2=运营商管理员',
    `creator_merchant_id`  BIGINT                 DEFAULT NULL COMMENT '创建人所属运营商ID，平台管理员为NULL',
    `scope_type`           TINYINT       NOT NULL DEFAULT 1 COMMENT '可用范围：1=全平台 2=指定运营商 3=指定站点',
    `points_cost`          INT                    DEFAULT NULL COMMENT '兑换所需积分（洗车券使用）',
    `discount_rate`        DECIMAL(4, 2)          DEFAULT NULL COMMENT '折扣比例（充电折扣券使用，如0.85表示85折）',
    `stock_total`          INT           NOT NULL DEFAULT -1 COMMENT '总库存，-1表示不限制',
    `stock_remain`         INT           NOT NULL DEFAULT -1 COMMENT '剩余库存，-1表示不限制',
    `validity_type`        TINYINT       NOT NULL DEFAULT 2 COMMENT '有效期类型：1=固定日期 2=领取后N天',
    `valid_start_time`     DATETIME               DEFAULT NULL COMMENT '固定有效期开始时间（validity_type=1时有效）',
    `valid_end_time`       DATETIME               DEFAULT NULL COMMENT '固定有效期结束时间（validity_type=1时有效）',
    `valid_days`           INT                    DEFAULT NULL COMMENT '领取后有效天数（validity_type=2时有效）',
    `daily_limit`          INT           NOT NULL DEFAULT 0 COMMENT '单用户每日兑换上限，0=不限',
    `monthly_limit`        INT           NOT NULL DEFAULT 0 COMMENT '单用户每月兑换上限，0=不限',
    `status`               TINYINT       NOT NULL DEFAULT 0 COMMENT '状态：0=下架 1=上架',
    `description`          VARCHAR(500)           DEFAULT NULL COMMENT '券使用说明',
    `create_by`            VARCHAR(64)            DEFAULT '' COMMENT '创建人账号',
    `create_time`          DATETIME               DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `update_time`          DATETIME               DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `del_flag`             CHAR(1)       NOT NULL DEFAULT '0' COMMENT '删除标志：0=存在 2=删除',
    PRIMARY KEY (`id`),
    KEY `idx_creator_merchant` (`creator_merchant_id`, `status`, `del_flag`),
    KEY `idx_scope_type` (`scope_type`, `status`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4
  COMMENT = '优惠券模板表';


-- ----------------------------
-- 券可用范围明细表（scope_type=2 或 3 时使用）
-- ----------------------------
CREATE TABLE `coupon_template_scope` (
    `id`          BIGINT  NOT NULL AUTO_INCREMENT COMMENT '主键',
    `template_id` BIGINT  NOT NULL COMMENT '关联券模板ID',
    `scope_type`  TINYINT NOT NULL COMMENT '范围类型：2=运营商 3=站点',
    `scope_id`    BIGINT  NOT NULL COMMENT '运营商ID 或 站点ID',
    PRIMARY KEY (`id`),
    KEY `idx_template_id` (`template_id`),
    UNIQUE KEY `uk_template_scope` (`template_id`, `scope_type`, `scope_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4
  COMMENT = '优惠券可用范围明细表';


-- ----------------------------
-- 用户券表
-- ----------------------------
CREATE TABLE `member_coupon` (
    `id`            BIGINT        NOT NULL AUTO_INCREMENT COMMENT '主键',
    `coupon_no`     VARCHAR(32)   NOT NULL COMMENT '券编号（唯一，用于核销）',
    `template_id`   BIGINT        NOT NULL COMMENT '关联券模板ID',
    `member_id`     BIGINT        NOT NULL COMMENT '关联会员ID',
    `coupon_type`   TINYINT       NOT NULL COMMENT '券类型快照：1=洗车券 2=充电折扣券',
    `points_cost`   INT           NOT NULL DEFAULT 0 COMMENT '兑换时消耗的积分快照，折扣券为0',
    `discount_rate` DECIMAL(4, 2)          DEFAULT NULL COMMENT '折扣比例快照，洗车券为NULL',
    `status`        TINYINT       NOT NULL DEFAULT 0 COMMENT '状态：0=未使用 1=已使用 2=已过期',
    `source`        TINYINT       NOT NULL DEFAULT 1 COMMENT '来源：1=积分兑换',
    `exchange_time` DATETIME               DEFAULT NULL COMMENT '兑换时间',
    `expire_time`   DATETIME      NOT NULL COMMENT '过期时间',
    `use_time`      DATETIME               DEFAULT NULL COMMENT '核销时间',
    `use_store_id`  BIGINT                 DEFAULT NULL COMMENT '核销门店/站点ID',
    `use_operator`  VARCHAR(64)            DEFAULT NULL COMMENT '核销操作人',
    `create_time`   DATETIME               DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `del_flag`      CHAR(1)       NOT NULL DEFAULT '0' COMMENT '删除标志：0=存在 2=删除',
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_coupon_no` (`coupon_no`),
    KEY `idx_member_id` (`member_id`, `status`),
    KEY `idx_template_id` (`template_id`),
    KEY `idx_expire_time` (`expire_time`, `status`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4
  COMMENT = '会员优惠券表';
```

> **索引说明**
> - `coupon_template.idx_creator_merchant`：运营商管理员查看本运营商券模板列表的高频查询
> - `coupon_template_scope.uk_template_scope`：防止同一模板下重复添加相同范围记录
> - `member_coupon.idx_expire_time`：供 Quartz 定时任务扫描过期券使用
> - `member_coupon.uk_coupon_no`：唯一索引防止重复券码、保证核销安全

---

## 十一、后续迭代规划

| 阶段 | 功能 |
|------|------|
| v1.1 | 第三方洗车平台 API 对接（自动核销） |
| v1.2 | 充电折扣券：运营商/平台创建折扣券，充电结算时自动抵扣，校验站点范围 |
| v1.3 | 运营活动：充电满额赠券、节日送券 |
| v2.0 | 积分商城完整体验（多商品、排行榜） |
