# 【前端接线·管理后台】「新建订单」向导第1步「选主题」接口对照——后端核心全已实现，仅需前端接线（含联动第2步「选产品」）

> 页面：管理后台 → 订单 → 新建订单（`order-v2/new`）第1步「选主题」、联动第2步「选产品」 | 服务：hl-product-service-v2（order-picker 端点）+ hl-order-service-v3（热门下单量，内部 Feign）| **无 PR，后端零改动** | 负责：mmg
> 核验方式：2026-06-17 源码全链路审计（热门链路追到 order-v3 Mapper 真查订单表）+ 测试服 admin token 经网关 9443 实测 3 端点全 200

## ⚠️ 关键判定：核心接口全部已实现且实测可用，没有缺失的必备接口

这个页面用到的后端接口**全部已上线**，热门标识是**真实计算**（不是写死/Mock）。需要前端处理的只有 **2 处「前端本地实现」**（搜索、最近选过）、**1 处「确认删除」**（分类 tab，wx 已批注不需要）、**1 处「待 wx 拍板」**（卡片地区标签）。

> 术语对照（必读，否则字段对不上）：前端「**主题**」= 后端「**产品线 ProductLine**」(lineId)；前端「**产品/版本**」= 后端「**产品 Product**」(productId)；前端「**SKU/规格/档位**」= 后端「**档位 tier**」(tierSeq)。

## 接口对照总表

| 页面数据块 | 后端落地 | 取数方式 | 前端动作 |
|---|---|---|---|
| 主题卡片网格 | ✅ 已实现 | `GET /admin/product/line/order-picker` | 直接接 |
| 「热」角标 + 「热门主题·直接进入」 | ✅ 已实现（真实算） | 同上 `hot` 字段；入口=`filter(hot==true)` | 本地筛，**不要新接口** |
| 顶部统计头（在售数/SKU总/最低人均） | ⚠️ 无接口 | 从上面列表前端派生 | 本地算 |
| 搜索框（主题/目的地/关键词） | ⚠️ 无入参 | 在已返回列表本地模糊匹配 | 本地搜 |
| 分类筛选 tab（西藏/云南/日本…） | ✅ 确认删除 | 后端无目的地数据源，无法过滤 | **直接删** |
| 「最近选过」 | ⚠️ 无接口 | localStorage 本地历史 | 本地存取 |
| 第2步「选择产品」 | ✅ 已实现 | `GET /admin/product/item/order-picker?lineId=` | 直接接 |
| 选档位/规格 | ✅ 已实现 | `GET /admin/product/item/{productId}/tiers` | 直接接 |
| 网关路由 | ✅ 全放行 | `/admin/product/**` → product-v2 | 经网关 9443 调通 |
| 卡片「国内·西藏」地区标签 | ✅ wx 拍 A：不显示 | 第1步主题 VO 不返回，后端零改动 | **地区标签不显示** |

## 1. 第1步「选主题」主接口（直接接）

**`GET /admin/product/line/order-picker`**（经网关 9443，路由已配 `/admin/product/**`）

- **入参**：无（不分页，一次性返回全部在售主题；数据权限由后端按当前管理员自动过滤）
- **返回**：`Result<List<OrderPickerThemeVO>>`

| 字段 | 类型 | 含义 |
|---|---|---|
| `lineId` | String（雪花，序列化为字符串） | 主题ID，**下一步选产品要传它** |
| `name` | String | 主题名称（布达拉宫朝圣…） |
| `description` | String | 主题描述 |
| `coverImageUrl` | String | 主题封面图 URL |
| `productType` | String | `CORE`=核心产品 / `GROUP`=小蒙马 / `CUSTOM`=私人定制 |
| `skuCount` | Integer | 该主题下已上架产品的档位总数（= 页面「X 个 SKU」） |
| `fromPrice` | **String**（金额，可能为 null） | 最低成人起价（= 页面「起 ¥XXXX / 人」；全无价返 `null`，前端兜底） |
| `hot` | Boolean | 是否热门（见第 2 节） |

- 只返「ACTIVE + 下挂 ≥1 个已上架产品」的在售主题，按排序号升序。
- ⚠️ `fromPrice` 是**字符串**（后端 `@JsonSerialize(ToStringSerializer)`，防大数精度丢失），前端展示前自行 `parseFloat`。

请求示例：
```
GET https://api.test.1814.love:9443/admin/product/line/order-picker
Authorization: Bearer {adminToken}
```
响应示例（节选，实测）：
```json
{
  "code": 200,
  "data": [
    { "lineId": "2056937785918369794", "name": "呼伦贝尔产品-yst", "productType": "CORE",
      "skuCount": 6, "fromPrice": "3105.00", "hot": true,
      "coverImageUrl": "https://hlgl-test.oss-cn-beijing.aliyuncs.com/...png", "description": "..." },
    { "lineId": "2043694738304880641", "name": "草原深度游", "productType": "CORE",
      "skuCount": 3, "fromPrice": "2980.00", "hot": false, "coverImageUrl": "...", "description": "..." }
  ]
}
```

## 2. 热门（「热」角标 + 「热门主题·直接进入」入口）——真实落地，无需新接口

- `hot` 是**唯一数据源**：近 90 天下单量在当前返回列表内**降序排名前 30%（向上取整、至少 1 名）且下单量 > 0** → `true`。
- 链路已核到底：`product-v2` → 内部 Feign → `order-v3 GET /v3/internal/order/stats/product-order-count` → 真 `selectCount` 查 `order_main` 表（**非 Mock**）。实测当前测试库返回 1 个 `hot=true` 主题，证明实时计算生效。
- **「热门主题·直接进入」不是独立接口** = 同一份 order-picker 列表里 `hot==true` 的子集，前端本地 `filter` 即可，**不要再找/要求新接口**。
- **降级行为**：order-v3 不可用时，列表照常返回但所有 `hot=false`（不报错、不阻断），前端按 `hot=false` 正常渲染即可（此时无角标、无热门入口属预期降级）。
- 口径说明（非 bug，设计如此）：热门按「**下单量（含未支付/已取消单）**」算，不是「成交量」；在售主题很少时可能出现「几乎都标热」。如要改成「成交热」口径需 wx 另拍，目前保持现状。

## 3. 统计头 / 搜索框 / 最近选过 —— 前端本地实现，后端无接口（也不需要）

- **统计头**（在售主题数 / SKU 总数 / 最低人均）：无独立接口，从第 1 节列表直接派生：
  - 在售主题数 = `list.length`
  - SKU 总数 = `Σ skuCount`
  - 最低人均 = `min(fromPrice)`（后端口径是「最低成人**起价**」，与「最低人均」基本等价；后端无单独「人均」概念）
- **搜索框**：`order-picker` 零入参、不分页、一次返全量（量级 = 公司在售主题数，很小）。前端在已返回列表上按 `name` / `description` 本地模糊匹配即可，**后端无需加参**。
- **「最近选过」**：后端无最近浏览接口（全范围 grep `recent/viewed/footprint/history` 仅命中小程序 C 端，与后台无关）。前端用 `localStorage` 自存浏览历史，无需对接后端。

## 4. 分类筛选 tab（全部/西藏/内蒙古/云南…）—— 确认删除

- wx 批注「不需要」正确，**额外实锤**：`product_line` 表**根本没有** region / 目的地 / 分类 任何结构化列（只有 name / product_type / description / seasons / tags / cover…），后端本就**无法**按目的地过滤主题。
- → 前端**直接删**这排 tab，**不要让后端补地区维度**（那是另一个数据建模工程，非本页范畴）。

## 5. 第2步「选择产品」+ 选档位（直接接）

**5.1 选产品列表**：`GET /admin/product/item/order-picker?lineId={第1步选中的lineId}&keyword=&pageNo=1&pageSize=20`
- `lineId` **必填**；`keyword` 可选（按产品名模糊）；分页。
- 返回 `PageResult<OrderPickerProductVO>`，字段（实测全有）：

| 字段 | 类型 | 含义 |
|---|---|---|
| `productId` | String | 产品ID（前端「版本」） |
| `name` / `subtitle` | String | 产品名 / 副标题 |
| `tripDays` / `tripNights` | Integer | 行程天数 / 晚数 |
| `tags` | List\<String> | 产品标签（**实测返回 `["国内"]` 之类**） |
| `coverImageUrl` | String | 封面图 |
| `startPrice` | String（金额，可 null） | 起步价（各档位未来可售最低成人价的最小值） |
| `tierPrices` | List | 档位起价列表（档位序号/名称/描述/该档起价） |
| `hot` | Boolean | 是否热销（同主题热门口径，order-v3 90 天下单量前 30%） |
| `nextSaleDate` | LocalDate（可 null） | 最近可售日（CORE/CUSTOM 取价格日历最早未来可售日，GROUP 取最近未来班期出发日） |

**5.2 选档位/规格**：`GET /admin/product/item/{productId}/tiers`
- 返回 `List`，字段 `tierSeq`（规格序号，1 起）/ `tierName`（如「舒适」）/ `tierDescription`。
- `tierSeq` 是**后续算价/下单的入参**，选档后带上。

## 6. 网关路由（无缺口）

- `/admin/product/**` → `lb://hl-product-service-v2`，**同时覆盖** `/admin/product/line/**` 与 `/admin/product/item/**`，两个 order-picker 端点经网关 9443 正常调通。
- order-v3 的 `/v3/internal/...` 是 product-v2 内部 Feign LB 直连（不经网关，admin token 直调会被安全过滤返 403），**前端不直接调它**，不算路由缺口。

## ✅ 已拍板：卡片「国内·西藏」地区标签 —— 不显示（wx 2026-06-17 拍 A）

页面每张主题卡片左上原有「国内·西藏 / 国内·云南」地区标签。第1步 `order-picker`（`OrderPickerThemeVO`）**不返回任何标签字段**（数据虽存在于 `product_line.tags`、第2步产品 VO 也返 `tags`，但第1步主题 VO 未透出）。

**结论：分类 tab 既已删，卡片地区标签一并不显示，后端零改动。** 前端去掉卡片上的地区标签，不需要地区字段。（备查：若日后又要显示，最低成本是后端给 `OrderPickerThemeVO` 补 `tags` 字段，1 个字段、低风险，但「国内·」国别前缀无干净来源。）

## 测试服实测（admin token 经网关 9443，2026-06-17）

| 端点 | 结果 |
|---|---|
| `GET /admin/product/line/order-picker` | code=200，9 个在售主题，SKU 总 33，最低起价 ¥10，热门 1 个（`hot=true` 实时算出） |
| `GET /admin/product/item/order-picker?lineId=...` | code=200，返回字段含 productId/name/subtitle/tripDays/tripNights/tags/coverImageUrl/startPrice/tierPrices/hot/nextSaleDate |
| `GET /admin/product/item/{productId}/tiers` | code=200，返回 tierSeq/tierName/tierDescription |

## 前端 TODO 清单

1. 第1步卡片网格：接 `GET /admin/product/line/order-picker`（无参）。
2. 「热」角标：用返回项 `hot` 渲染；「热门主题·直接进入」= 本地 `filter(hot==true)`，不要新接口。
3. 统计头：本地算（`length` / `Σ skuCount` / `min(fromPrice)`）。
4. 搜索框：本地按 `name`/`description` 匹配，不传后端。
5. **删除分类筛选 tab**（后端无数据源）。
6. 「最近选过」：`localStorage` 本地实现。
7. 第2步选产品：`GET /admin/product/item/order-picker?lineId={上一步lineId}`；选档位 `GET /admin/product/item/{productId}/tiers`，带 `tierSeq` 进算价/下单。
8. 卡片地区标签：**不显示**（wx 已拍 A，后端零改动）。
9. 金额字段 `fromPrice`/`startPrice` 是**字符串**，展示前 `parseFloat`，注意 `null` 兜底。
