feat(order-v2): admin 订单列表+详情综合改造 (搜索/排序/行程/状态 tab)

覆盖今日 4 PR:
- PR #2903 详情新增 itinerary (景点+酒店 按天)
- PR #2906 列表 keyword 加联系电话 + 排序加 depositAmount
- PR #2909 完整 11 位手机号精确加密匹配 (修 #2906 假阳性)
- PR #2914 displayStatus 参数对齐原型 5 tab (修待出行 0 单 BUG)

前端动作:
- 搜索框 placeholder 提示完整手机号
- 排序下拉新增"订金金额"
- tab 切换迁到 displayStatus 参数
- 详情页"行程概览"段渲染 itinerary 字段

向后兼容: 不读新字段/不传新参数行为完全不变。

关联: Closes #2899 #2905 #2908 #2912
这个提交包含在:
yaosutu 2026-05-22 18:20:05 +08:00
父节点 933adcdcad
当前提交 bf1788ae5e

查看文件

@ -0,0 +1,225 @@
# feat(order-v2): /admin/order/list + /admin/order/{id} 综合改造(搜索 / 排序 / 详情行程 / 状态 tab
> **仓库**: HL (后端 hl-order-service-v2)
> **关联 PR/Issue**: PR #2903 / #2906 / #2909 / #2914Closes #2899 #2905 #2908 #2912
> **日期**: 2026-05-22
> **影响范围**: 管理后台「订单管理」列表 + 详情
> **接收方**: mmg (前端)
> **前端**: **需要改动 — 搜索框 placeholder / 排序下拉新增订金 / tab 切换迁到 displayStatus / 详情页渲染行程概览**
---
## 🚨 关键变化总览
| 维度 | 改动 | 前端动作 |
|---|---|---|
| 搜索 keyword | 现支持 **完整 11 位手机号精确匹配**(加密列等值),姓名/订单号/产品名仍 LIKE | placeholder 加"完整手机号" |
| 列表排序 | 新增 `depositAmount`(订金金额)排序 | 排序下拉加"订金金额"选项 |
| 列表 tab 筛选 | 新增 `displayStatus` 参数,**原型 5 tab 必须用这个**status 是内部细粒度,对不上 tab | tab 切换的 status 参数改名为 `displayStatus` |
| 订单详情 | 新增 `itinerary` 字段(行程按天结构化,含景点 + 酒店) | 详情页"行程概览"段渲染 |
---
## 一、`GET /admin/order/list` 搜索框keyword
### 改前
`keyword` 对 4 字段做 LIKE 模糊匹配:`orderNo` / `contactName` / `productName`(联系电话 LIKE 是一次预备改造,对加密列实际无效)。
### 改后
| 输入 | 行为 |
|---|---|
| 完整 11 位手机号(如 `17600545225` | **后端加密后等值匹配 contact_phone 密文列** → 精确命中所有持此号订单 |
| 部分手机号(如 `176``5225` | 不识别为手机号,不查 contact_phone部分号匹不到加密订单是天然限制 |
| 姓名(`米明光` | 仍 LIKE 模糊匹配 contact_name |
| 订单号(`HL20260509` | 仍 LIKE 模糊匹配 order_no |
| 产品名(`冻干粉` | 仍 LIKE 模糊匹配 product_name |
| 不传 / 空 | 不过滤(行为不变) |
### 前端建议
搜索框 placeholder 改为类似:
```
姓名 / 完整手机号 / 订单号 / 产品名
```
不要再写"手机号"模糊(部分手机号匹不到,会误导用户)。
---
## 二、`GET /admin/order/list` 排序sortField
### 改后枚举
| sortField | sortOrder | 含义 |
|---|---|---|
| `createTime` | desc默认 / asc | 创建时间 |
| `departureDate` | desc / asc | 出发日期 |
| `totalPrice` | desc / asc | 订单金额 |
| `depositAmount` ⭐ | desc / asc | **订金金额(新增)** |
| 不传 / 未知值 | — | 兜底按 createTime DESC |
### 前端建议
排序下拉选项追加"订金金额(高→低)/(低→高)",业务场景是财务对账(找订金大但未付尾款的订单优先催)。
---
## 三、`GET /admin/order/list` 订单 tab 筛选 ⚠️ 重要
### 改前
只有 `status` 参数(匹配内部 `OrderStatus` 枚举 11 状态)。**原型"待出行" tab 传 `status=PENDING_DEPARTURE` 命中 0 单**(数据库实际分散在 DEPOSIT_PAID / CONFIRMED 等状态)。
### 改后
新增 `displayStatus` 参数,与 `status` 并存AND 联合)。**原型 5 tab 必须用 `displayStatus`**
| 原型 Tab | 前端传 `displayStatus` 值 | 后端命中范围 |
|---|---|---|
| 全部 | (不传) | 所有未删除订单 |
| 待支付 | `PENDING_PAY` | 内部 status = PENDING_PAY |
| **待出行** | `PENDING_DEPARTURE` | **折叠** DEPOSIT_PAID / PAID / CONFIRMED / PENDING_BALANCE / PENDING_DEPARTURE 5 个内部状态 |
| 待评价 | `PENDING_REVIEW` | (状态机决定) |
| 已完成 | `COMPLETED` | 内部 status = COMPLETED且已评价 |
| 售后中 | `AFTER_SALE` | **折叠** REFUNDING / 部分 REFUNDED |
| 已取消 | `CANCELLED` | **折叠** CANCELLED / 部分 REFUNDED |
### 前端动作
按 tab 切换的参数从 `status` 改为 `displayStatus`。两个值的枚举不一样,**千万不要混用** —— 把内部 `status=PENDING_DEPARTURE` 当成 tab 传过去会拿到 0 单。
### 测试服实测对照
| 请求 | 命中 |
|---|---|
| `?displayStatus=PENDING_DEPARTURE` | **8 单** ⭐ |
| `?displayStatus=CANCELLED` | 26 单 |
| `?displayStatus=AFTER_SALE` | 2 单 |
| `?status=PENDING_DEPARTURE`(错用法) | 0 单 ❌ |
---
## 四、`GET /admin/order/{orderId}` 详情新增 `itinerary` 字段
### 字段结构
`OrderDetailVO` 新增:
```typescript
itinerary: OrderItineraryDayVO[] // 全部天数,不截断
OrderItineraryDayVO {
dayNumber: number // 1, 2, 3...
dayTitle: string // "京都·古都漫步"
route: string // "大阪 → 京都"
attractions: ItineraryAttractionVO[] // 当天 SCENIC + ACTIVITY 节点
hotels: ItineraryHotelVO[] // 当天酒店(已按订单档位 tierSeq 过滤)
}
ItineraryAttractionVO {
resourceId: string // Long 序列化为字符串
name: string // "清水寺"
nodeType: 'SCENIC' | 'ACTIVITY'
startTime: string | null // "09:30"
durationMinutes: number | null
description: string | null
coverImageUrl: string | null
}
ItineraryHotelVO {
hotelId: string
hotelName: string // "京都八坂町京町家"
roomTypeName: string // "豪华双床房"
}
```
### 注意点
- **数据来源是订单下单时固化的产品快照**,不会随产品变化
- 老订单(快照 key 是 `itineraryDays`)也兼容解析
- 快照损坏时 `itinerary` 返回空数组(不会让整个详情接口挂)
- `hotels` 已经按订单 `tierSeq` 过滤 —— 不要担心一天显示 3 档候选酒店
### 真实示例(订单 `HL20260509114457-6539`
```json
{
"itinerary": [
{
"dayNumber": 1,
"dayTitle": "京都古都漫步",
"route": "海拉尔区 → 额尔古纳市",
"attractions": [
{
"name": "中俄边境公路(卡线)",
"nodeType": "SCENIC",
"description": "沿额尔古纳河蜿蜒前行约200公里...",
"coverImageUrl": "https://hlgl-test.oss.../62943d9b....jpg"
},
{
"name": "驯鹿拉车体验",
"nodeType": "ACTIVITY",
"description": "..."
}
// ... 一天可能 6-7 个节点
],
"hotels": [
{ "hotelName": "呼伦贝尔香格里拉大酒店", "roomTypeName": "普通标间" }
]
}
// ... 多天行程
]
}
```
### 前端建议
详情页"行程概览"段:
- 按 `dayNumber` 排序,渲染 D1 / D2 / D3...
- 每天先 `route` + `dayTitle`,下面 `attractions` 列表 + `hotels` 列表(末日可能 `hotels` 为空,正常)
- `attractions` 可按 `nodeType` 分组("景点 SCENIC" / "活动 ACTIVITY"),也可不分组直接列
- `coverImageUrl` 是 OSS 完整 URL 可直接 `<img src>`
---
## 五、向后兼容
| 老前端不传新参数 / 不读新字段 | 行为 |
|---|---|
| 仅用 `status`(不用 `displayStatus` | 完全不变(仍按内部状态 eq |
| 不读 `itinerary` | 完全不变(多了字段不影响其他) |
| 不传 `sortField=depositAmount` | 完全不变 |
| keyword 输部分手机号 | 不影响其他字段 LIKE 命中(只是不再误命中加密列的明文残留) |
---
## 六、错误码 / 异常
无新增错误码。所有改动都是请求参数扩展或响应字段新增。
---
## 七、测试建议(前端联调时验证)
| 验证项 | 操作 | 期望 |
|---|---|---|
| 完整手机号搜 | 搜 `17600545225` | 精确命中(如有该号订单) |
| 部分号搜 | 搜 `176` | 不命中加密订单(只命中订单号/姓名/产品名含 "176" 的) |
| 订金排序 | sortField=depositAmount&sortOrder=desc | 单调递减 |
| 待出行 tab | displayStatus=PENDING_DEPARTURE | 应命中(测试服 8 单) |
| 详情行程 | 打开任一已下单订单详情 | itinerary 非空数组 |
| 不传新参数 | 老接口调用方式 | 行为完全不变 |
---
## 八、关联
- PR #2903itinerary→ commit `c8f34411a`
- PR #2906(搜索+排序)→ commit `689d47013`
- PR #2909(手机号精确匹配,修 #2906 假阳性)→ commit `fbf803b1b`
- PR #2914displayStatus 修待出行 tab→ commit `8be30a562c`
- Issue #2899 / #2905 / #2908 / #2912 已全部 closed
- 负责人yst后端