# feat(order-v2): /admin/order/list + /admin/order/{id} 综合改造（搜索 / 排序 / 详情行程 / 状态 tab）

> **仓库**: HL (后端 hl-order-service-v2)
> **关联 PR/Issue**: PR #2903 / #2906 / #2909 / #2914 / #2916（Closes #2899 #2905 #2908 #2912 #2915）
> **日期**: 2026-05-22
> **影响范围**: 管理后台「订单管理」列表 + 详情
> **接收方**: mmg (前端)
> **前端**: **需要改动 — 搜索框 placeholder / 排序下拉新增订金 / tab 切换迁到 displayStatus / 详情页渲染行程概览**

---

## 🚨 关键变化总览

| 维度 | 改动 | 前端动作 |
|---|---|---|
| 搜索 keyword | 现支持 **完整 11 位手机号精确匹配**（加密列等值），姓名/订单号/产品名仍 LIKE | placeholder 加"完整手机号" |
| 列表排序 | 新增 `depositAmount`（订金金额）+ `updateTime`（最近更新）排序 | 排序下拉加"订金金额"+"最近更新"选项 |
| 列表 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 | **订金金额（新增）** |
| `updateTime` ⭐ | desc / asc | **最近更新（新增，PR #2916）** — 业务场景：定制师/客服优先看"最近有动作"的订单 |
| 不传 / 未知值 | — | 兜底按 createTime DESC |

### 前端建议

排序下拉选项追加：
- "订金金额（高→低）/（低→高）"：财务对账场景（找订金大但未付尾款的订单优先催）
- "最近更新（新→旧）"：定制师工作流场景（看哪些订单今天有动静）—— 注意这是 `update_time` 不是 `create_time`，能把"老订单今天有退款 / 改单"的情况顶到前面

---

## 三、`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 | 单调递减 |
| 最近更新排序 | sortField=updateTime&sortOrder=desc | 与 createTime DESC 结果**不同**（老订单今天更新的会排到前面） |
| 待出行 tab | displayStatus=PENDING_DEPARTURE | 应命中（测试服 8 单） |
| 详情行程 | 打开任一已下单订单详情 | itinerary 非空数组 |
| 不传新参数 | 老接口调用方式 | 行为完全不变 |

---

## 八、关联

- PR #2903（itinerary）→ commit `c8f34411a`
- PR #2906（搜索+排序）→ commit `689d47013`
- PR #2909（手机号精确匹配，修 #2906 假阳性）→ commit `fbf803b1b`
- PR #2914（displayStatus 修待出行 tab）→ commit `8be30a562c`
- PR #2916（排序补 updateTime）→ commit `328691813c`
- Issue #2899 / #2905 / #2908 / #2912 / #2915 已全部 closed
- 负责人：yst（后端）
