# 早鸟优惠计划管理 CRUD 接口（管理后台）

- **端类型**：管理后台
- **变更类型**：新增接口
- **日期**：2026-06-20
- **PR**：[#4076](https://git.1814.love:8443/wx/HL/pulls/4076)
- **负责人**：yst（腰苏图）

---

## 接口背景

早鸟优惠（Early Bird Discount）是订单 v3 新增的优惠机制，允许管理员为指定产品配置日期区间 x 人数区间 x 折扣方式三维优惠计划。下单时系统自动匹配最优方案并应用优惠。

本次（PR #4076）新增管理后台侧 CRUD 6 个接口，供管理员对早鸟计划进行增删改查、启停操作。

---

## 变更清单

| # | 方法 | 路径 | 说明 |
|---|------|------|------|
| 1 | GET | /v3/admin/order/early-bird/list | 早鸟计划分页列表 |
| 2 | GET | /v3/admin/order/early-bird/{planId} | 早鸟计划详情 |
| 3 | POST | /v3/admin/order/early-bird | 创建早鸟计划 |
| 4 | PUT | /v3/admin/order/early-bird/{planId} | 修改早鸟计划（不影响已下单订单） |
| 5 | DELETE | /v3/admin/order/early-bird/{planId} | 删除早鸟计划 |
| 6 | PUT | /v3/admin/order/early-bird/{planId}/toggle | 启用 / 禁用早鸟计划 |

全部为新增接口，无前版存量接口被替换。
---

## 接口详情

| 项目 | 说明 |
|------|------|
| **认证** | 需要管理后台 JWT Token（Authorization: Bearer token） |
| **Base URL** | /v3/admin/order/early-bird |
| **幂等性** | POST 创建非幂等（同参数可重复创建多个计划）；PUT 修改按 planId 幂等覆盖 |
| **限流** | 无独立限流规则，走全局网关限流 |
| **内容类型** | 请求体 Content-Type: application/json；GET 请求均为 Query 参数 |

---

## 接口入参

### 4.1 GET /v3/admin/order/early-bird/list — 分页列表

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| pageNo | Integer | 是 | 页码，从 1 开始 |
| pageSize | Integer | 是 | 每页条数，建议 10 / 20 |

### 4.2 GET /v3/admin/order/early-bird/{planId} — 详情

路径参数 planId（Long）：早鸟计划 ID。

### 4.3 POST /v3/admin/order/early-bird — 创建

请求体（EarlyBirdPlanSaveReqVO）：

| 字段 | 类型 | 必填 | 校验规则 | 说明 |
|------|------|------|----------|------|
| planName | String | 是 | NotBlank | 计划名称 |
| remark | String | 否 | 无 | 优惠描述 |
| discountType | String | 否 | 枚举三选一 | 折扣方式，默认 AMOUNT_TOTAL（见枚举节） |
| discountAmount | BigDecimal | 条件必填 | AMOUNT_PER_PERSON 或 AMOUNT_TOTAL 时必填 | 优惠金额（元） |
| discountPercent | BigDecimal | 条件必填 | PERCENT 时必填，值区间 (0, 100) 开区间 | 折扣率，80=8折（非立减百分比） |
| priority | Integer | 否 | >=0，默认 0 | 优先级，同区间多命中时大者优先 |
| minPeople | Integer | 是 | >=1 | 最低适用人数（含） |
| maxPeople | Integer | 否 | >=1 且 >=minPeople | 最高适用人数（含），不填=不限 |
| startDate | LocalDate | 是 | yyyy-MM-dd | 生效开始日期 |
| endDate | LocalDate | 是 | yyyy-MM-dd，需 >=startDate | 生效结束日期 |
| productIds | List<Long> | 否 | 无 | 关联产品 ID 列表，空=不限产品 |
| applicableTravelerTypes | List<String> | 否 | 枚举值见下 | 适用人群 code 列表，空=后端自动填充成人+儿童+小童（排婴儿） |

### 4.4 PUT /v3/admin/order/early-bird/{planId} — 修改

路径参数 planId（Long）。请求体与 4.3 完全一致，全字段覆盖更新。修改不影响已下单订单中已匹配的早鸟优惠。

### 4.5 DELETE /v3/admin/order/early-bird/{planId} — 删除

路径参数 planId（Long）。无请求体。

### 4.6 PUT /v3/admin/order/early-bird/{planId}/toggle — 启用 / 禁用

| 参数 | 类型 | 位置 | 说明 |
|------|------|------|------|
| planId | Long | Path | 早鸟计划 ID |
| enabled | Boolean | Query | true=启用；false=禁用 |

禁用后该计划不再参与下单时自动匹配。
---

## 出参字段

所有接口返回 Result<T> 包装，成功 code=200。列表接口返回 Result<PageResult<EarlyBirdPlanVO>>，创建/详情返回 Result<EarlyBirdPlanVO>，修改/删除/toggle 返回 Result<Void>。

### EarlyBirdPlanVO 字段表

| 字段 | 类型 | 说明 |
|------|------|------|
| planId | Long（字符串序列化） | 计划 ID，雪花 ID，前端用字符串接收防精度丢失 |
| planName | String | 计划名称 |
| remark | String | 优惠描述，可为 null |
| discountType | String | 折扣方式枚举 code |
| discountAmount | BigDecimal | 优惠金额，PERCENT 类型时为 null |
| discountPercent | BigDecimal | 折扣率（0~100），非 PERCENT 时为 null；80=8折 |
| priority | Integer | 优先级 |
| minPeople | Integer | 最低适用人数（含） |
| maxPeople | Integer | 最高适用人数（含），null=不限 |
| peopleRangeText | String | 后端拼好的人数区间文案，如 3-5 人 / 3 人及以上 |
| discountText | String | 后端拼好的折扣说明，如 每人立减 100 元 / 整单立减 500 元 / 8 折优惠 |
| startDate | LocalDate（yyyy-MM-dd） | 生效开始日期 |
| endDate | LocalDate（yyyy-MM-dd） | 生效结束日期 |
| enabled | Boolean | 是否启用 |
| applicableTravelerTypes | List<String> | 适用人群 code 列表；配置为空时后端回填 [ADULT, CHILD, YOUNG_CHILD] |
| applicableTravelerTypesText | String | 后端拼好的适用人群文案，如 成人/儿童/小童 |
| productIds | List<Long> | 关联产品 ID 列表，空=不限产品 |
| productNames | List<String> | 关联产品名称，下标与 productIds 对齐；产品已删除时填 产品[ID]已删除 |
| products | List<{productId:Long, name:String}> | 关联产品结构化列表 |
| createTime | LocalDateTime | 创建时间 |
| updateTime | LocalDateTime | 最后更新时间 |

---

## 枚举 / 数据字典

### discountType — 折扣方式

| code | 中文 | 计算说明 |
|------|------|----------|
| AMOUNT_PER_PERSON | 每人立减 | 优惠金额 = discountAmount x 计费人数 |
| AMOUNT_TOTAL | 整单立减（默认） | 优惠金额 = discountAmount |
| PERCENT | 按比例打折 | 优惠金额 = 总价 x (100 - discountPercent) / 100 |

discountPercent 语义：值为折扣率，80 表示 8折，不是打 8 折的优惠百分比。立减额 = 总价 x (100 - 80) / 100 = 总价 x 0.2。

### applicableTravelerTypes — 适用人群

| code | 中文 | 人数匹配规则 |
|------|------|------------|
| ADULT | 成人 | 计入人数 |
| CHILD | 儿童 | 计入人数 |
| YOUNG_CHILD | 小童 | 计入人数 |
| BABY | 婴儿 | 默认不计入人数，需显式配置才计入 |

人数匹配口径：命中方案的 applicableTravelerTypes 中包含哪几类，就统计哪几类的人数判断是否满足 minPeople/maxPeople 区间。默认配置排除婴儿。

---

## 错误码

早鸟模块错误码段位：581600 – 581699

| 错误码 | 说明 | 触发场景 |
|--------|------|----------|
| 581600 | 早鸟计划不存在 | planId 查不到对应记录 |
| 581601 | 早鸟计划已禁用 | 对已禁用计划执行不允许操作 |
| 581602 | 当前日期不在有效期内 | 当前日期早于 startDate 或晚于 endDate |
| 581603 | 人数不满足最低人数要求（{0}） | 计费人数 < minPeople，{0} 为最低人数阈值 |
| 581604 | 人数超过最高人数限制（{0}） | 计费人数 > maxPeople，{0} 为最高人数阈值 |
| 581605 | PERCENT 时折扣率不能为空 | discountType=PERCENT 但 discountPercent 未传 |
| 581606 | {0} 类型时优惠金额不能为空 | 立减类型但 discountAmount 未传，{0} 为类型名 |
| 581607 | 最高人数不能小于最低人数 | maxPeople < minPeople |
| 581608 | 结束日期不能早于开始日期 | endDate < startDate |
| 581609 | 适用人群包含非法取值：{0} | applicableTravelerTypes 有不在枚举范围的 code |
| 581610 | 产品已存在生效时间与人数区间重叠的早鸟计划，不能重复创建或启用 | 同产品同时间段同人数区间已有启用计划 |
---

## 示例

### 8.1 典型成功 — 创建早鸟计划（整单立减）

POST /v3/admin/order/early-bird，Content-Type: application/json

请求体：

    {
      "planName": "暑期早鸟立减300",
      "remark": "7月出发整单立减300元",
      "discountType": "AMOUNT_TOTAL",
      "discountAmount": 300,
      "priority": 10,
      "minPeople": 2,
      "maxPeople": 6,
      "startDate": "2026-07-01",
      "endDate": "2026-07-31",
      "productIds": [1900000000001, 1900000000002],
      "applicableTravelerTypes": ["ADULT", "CHILD", "YOUNG_CHILD"]
    }

响应：

    {
      "code": 200,
      "msg": "success",
      "data": {
        "planId": "1920000000001234",
        "planName": "暑期早鸟立减300",
        "remark": "7月出发整单立减300元",
        "discountType": "AMOUNT_TOTAL",
        "discountAmount": 300.00,
        "discountPercent": null,
        "priority": 10,
        "minPeople": 2,
        "maxPeople": 6,
        "peopleRangeText": "2-6 人",
        "discountText": "整单立减 300 元",
        "startDate": "2026-07-01",
        "endDate": "2026-07-31",
        "enabled": true,
        "applicableTravelerTypes": ["ADULT", "CHILD", "YOUNG_CHILD"],
        "applicableTravelerTypesText": "成人/儿童/小童",
        "productIds": [1900000000001, 1900000000002],
        "productNames": ["云南大理亲子7日游", "丽江雪山5日徒步"],
        "products": [
          {"productId": 1900000000001, "name": "云南大理亲子7日游"},
          {"productId": 1900000000002, "name": "丽江雪山5日徒步"}
        ],
        "createTime": "2026-06-20T10:00:00",
        "updateTime": "2026-06-20T10:00:00"
      }
    }

### 8.2 边界情况 — 折扣率类计划（不限人数、不限产品）

maxPeople 不传=不限；productIds 不传=不限产品；applicableTravelerTypes 不传=后端回填默认值。

请求体：

    {
      "planName": "年底全产品9折",
      "discountType": "PERCENT",
      "discountPercent": 90,
      "minPeople": 1,
      "startDate": "2026-12-01",
      "endDate": "2026-12-31"
    }

响应：

    {
      "code": 200,
      "msg": "success",
      "data": {
        "planId": "1920000000001235",
        "planName": "年底全产品9折",
        "remark": null,
        "discountType": "PERCENT",
        "discountAmount": null,
        "discountPercent": 90.00,
        "priority": 0,
        "minPeople": 1,
        "maxPeople": null,
        "peopleRangeText": "1 人及以上",
        "discountText": "9 折优惠",
        "startDate": "2026-12-01",
        "endDate": "2026-12-31",
        "enabled": true,
        "applicableTravelerTypes": ["ADULT", "CHILD", "YOUNG_CHILD"],
        "applicableTravelerTypesText": "成人/儿童/小童",
        "productIds": [],
        "productNames": [],
        "products": [],
        "createTime": "2026-06-20T11:00:00",
        "updateTime": "2026-06-20T11:00:00"
      }
    }

### 8.3 业务失败 — 产品时间区间人数区间重叠（581610）

该产品+时间段+人数区间已有另一启用计划。请求体：

    {
      "planName": "冲突测试计划",
      "discountType": "AMOUNT_TOTAL",
      "discountAmount": 100,
      "minPeople": 2,
      "maxPeople": 5,
      "startDate": "2026-07-01",
      "endDate": "2026-07-15",
      "productIds": [1900000000001]
    }

响应：

    {
      "code": 581610,
      "msg": "产品已存在生效时间与人数区间重叠的早鸟计划，不能重复创建或启用",
      "data": null
    }
---

## 业务边界

**适用场景**：管理员新建/修改/删除早鸟优惠配置；对已有计划快速启停；查询当前所有早鸟计划状态。

**不适用场景**：小程序用户侧查看早鸟优惠（走 internal Feign 接口）；下单时自动匹配优惠（订单服务内部逻辑，前端无需干预）；修改不追溯已下单订单中已应用的优惠金额。

**特殊边界**：

- 同一产品在同一时间段+人数区间内只允许存在一个启用状态的计划（581610）；禁用状态不参与冲突检测
- discountPercent=80 表示 8折，不是优惠 80%；前端展示需转换为 X折格式
- planId 为雪花 ID，JSON 已序列化为字符串，前端用字符串类型接收
- applicableTravelerTypes 传空或不传，后端自动填充默认值 [ADULT, CHILD, YOUNG_CHILD] 并持久化

---

## 修改前后对比

新增接口，无前版对比。

---

## 影响评估 / 回滚

新增接口，不影响存量功能，无需回滚策略。如需下线，在网关层屏蔽 /v3/admin/order/early-bird 前缀即可。

---

## 注意事项

1. discountPercent 语义陷阱：80=8折，不是立减 80%。前端展示请转换为 X折，勿直接显示 80%。
2. planId 雪花 ID 序列化：响应中已序列化为字符串；调用修改/删除/toggle 时路径参数直接传字符串即可。
3. applicableTravelerTypes 回填：创建时不传或传空，后端自动填充默认值并持久化；详情/列表返回实际存储值（不再是空数组）。
4. toggle 启用触发重叠校验（581610）：将已禁用计划重新启用时，若同时间+人数区间已有其他启用计划，会拒绝启用。
5. PUT 修改是全字段覆盖，不支持 PATCH 语义，未传的选填字段会被重置为 null/默认值。
6. productNames/products 降级：关联产品已删除时，productNames 对应位置返回 产品[ID]已删除，前端按需降级处理。

---

## 关联 / 联系人

| 项目 | 内容 |
|------|------|
| PR | [#4076 早鸟优惠管理后台 CRUD](https://git.1814.love:8443/wx/HL/pulls/4076) |
| 后端负责人 | yst（腰苏图） |
| 变更服务 | hl-order-service-v3 |
