docs(changelog): #8056 TRANSFER-only 订单用车数据静默丢失修复交接件
changelog-filename-gate / validate (push) Failing after 2s

行程详情 / 确认前置 Checklist 两个只读端点的字段现在反映真实数据:
修复前 TRANSFER-only 订单在这两处返回 HTTP 200、无异常、无错误码、
字段静默为空/false,与「这个订单本来就没安排车」在返回结构上完全无法区分。

backend_status=deployed(hl-order-service-v3@d30cd9561,测试环境)、
gateway_status=verified(两个只读端点已网关实测)、
frontend_status=not_required(前端侧为纯透传渲染,无需改代码)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
这个提交包含在:
API Changelog Bot
2026-09-22 02:25:08 +08:00
共同撰写人 Claude Opus 5
父节点 798b7bcd83
当前提交 57a9e068dd
@@ -0,0 +1,679 @@
---
schema: "hl-changelog/v2"
ticket: "8056"
title: "TRANSFER-only 订单用车数据静默丢失修复(行程详情 / 确认前置 Checklist)"
consumer: "admin"
author: "wx(GIT)"
change_type: "修复"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "not_required"
frontend_owner: ""
frontend_ref: ""
target_release: ""
verified_at: ""
status_note: "#8056 记录的用车数据丢失共 10 个落点,均已在同一提交(commit 62c28d502,PR #8118)中一并修复并合入 dev-v3,测试环境已部署(hl-order-service-v3@d30cd9561,2026-09-21 21:55:58)。本文逐字段交付其中 3 处:已通过网关实测的两个只读端点(行程详情、确认前置 Checklist),以及经源码核查、本次未做独立网关实测的派单看板列表端点(三、接口详情第 3 节,行为完全由已验证部署的 hl-order-service-v3 驱动,hl-fleet-service 侧代码未改动);其余落点未在本文展开。前端侧已核实 mmg/hl-ui 对本文相关字段为纯透传渲染,无需改动代码,见六.7。POST /v3/admin/order/{id}/settlement/finalize 的车务车费一致性校验不在本次交接范围内,见七、不影响范围。"
updated_at: "2026-09-22"
base: "dev-v3"
---
# 订单核心服务: TRANSFER-only 订单用车数据静默丢失修复(行程详情 / 确认前置 Checklist)
> **存放目录**: 二期(v3) → `changelogs-v2/2026-09/`
>
> **服务**: hl-order-service-v3(主体,第 1/2 节)+ hl-fleet-service(第 3 节派单看板列表;字段结构未变、代码未改动,行为完全由 hl-order-service-v3 侧修复驱动)
> **PR**: #8118
> **Issue**: #8056
> **日期**: 2026-09-22
> **影响范围**: TRANSFER-only 订单(只提交了接送机用车需求、没有提交行程用车需求)在「订单详情-行程安排 Tab」与「确认订单前置 Checklist」两个只读端点上的用车相关字段;另涉及混合订单(同一订单同时有有效 TRAVEL 与 TRANSFER 需求)在「派单看板列表」端点上的兜底展示行为(见三、接口详情第 3 节)
---
## ⚠️ 关键变化
- **本次变了什么**:`GET /v3/admin/order/{id}/itinerary` 与 `GET /v3/admin/order/{id}/confirm-checklist` 这两个只读端点在 **TRANSFER-only 订单**(只有接送机用车需求、没有行程用车需求)上的用车数据读取逻辑被修复。此前这两个端点把「该读哪一类用车需求」硬编码成了 TRAVEL,TRANSFER-only 订单在这两个口子上查到的用车相关字段恒为空。
- **前端/调用方以前以为的是什么**:`vehicleGroup: null` + `canContactFleet: false` + `contactFleetDisabledReason: "请先提交有效用车需求后再联系车务"`,或 `confirm-checklist` 里 `VEHICLE_DONE` 项恒 `false`、`failReason` 固定为「未提交用车需求」/「用车需求未完成」——这组返回值此前是**唯一信号**,而且和「这个订单本来就没安排车」在返回结构上完全无法区分:**HTTP 200,无异常,无错误码,字段静默为空/false/固定文案**。
- **实际现在是什么**:对已提交有效接送机用车需求的 TRANSFER-only 订单,这两个端点现在会返回真实数据(车辆/司机/座位数等),`canContactFleet` 会按实际情况变为 `true`,`confirm-checklist` 的 `allPassed` 在其余 4 项通过时也能正确变为 `true`。**过去在这两个端点上读到的「空」,不代表订单真的没有安排车辆,需要按本次修复后的语义重新核对**,不要沿用旧的「空即无车」假设。
- **另需知悉(看板新增展示,非新引入缺陷)**:派单看板列表 `GET /admin/fleet/board/orders`(hl-fleet-service,见「三、接口详情」第 3 节)对**混合订单**(同一订单同时存在有效 TRAVEL 与 TRANSFER 需求)新增了一种兜底展示:此前若 TRAVEL 需求处于不可联络状态,整单会从看板消失;本次修复后由 TRANSFER 需求兜底展示一行,订单不再整单消失。「整单消失」本身就是 `#8056` 静默丢数问题的又一种表现,此项是同一次修复顺带解决的,不是新引入的行为回归。
- `#8056` 记录的用车数据丢失共 10 个落点,均已在同一提交(`62c28d502`)中一并修复并合入 `dev-v3`;本文逐字段交付其中 3 处——已在测试环境网关实测的两个只读端点(行程详情、确认前置 Checklist),以及经源码核查、本次未做独立网关实测的派单看板列表端点(见「三、接口详情」第 3 节);其余落点未在本文展开。
---
## 一、背景
`VehicleRequirement` 按 `kind` 分为 `TRAVEL`(行程用车)与 `TRANSFER`(接送机用车)两类(#7439 引入)。#8056 修复前,行程详情/确认预览等单值消费点各自把 kind 参数硬编码为 `TRAVEL`,本次收口到 `RequirementService#resolveSingleValueVehicleKind`(TRAVEL 优先,没有 TRAVEL 时回落 TRANSFER)统一解析。
| 维度 | 改前 | 改后 |
|------|------|------|
| 用车需求类别解析方式 | 调用点各自硬编码 `VehicleRequirementKind.TRAVEL` | 收口到 `resolveSingleValueVehicleKind`:TRAVEL 优先,无 TRAVEL 时回落 TRANSFER |
| TRANSFER-only 订单命中查询的结果 | 恒为空(按 TRAVEL 类别查,0 条命中) | 命中回落解析出的 TRANSFER 记录 |
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 订单详情行程安排 Tab | GET | `/v3/admin/order/{id}/itinerary` | 数据修复 | TRANSFER-only 订单的 `vehicleGroup`/`vehicleHistory`/`canContactFleet`/`contactFleetDisabledReason` 不再恒为空/false/固定文案 |
| 2 | 确认订单前置 Checklist | GET | `/v3/admin/order/{id}/confirm-checklist` | 数据修复 | TRANSFER-only 订单的 `VEHICLE_DONE` 判定与 `preview` 司机栏不再恒判未完成/留空 |
| 3 | 派单看板列表 | GET | `/admin/fleet/board/orders` | 行为增强(结构未变) | 混合订单(同时有效 TRAVEL 与 TRANSFER 需求)中,若 TRAVEL 需求不可联络,改前整单从看板消失,改后由 TRANSFER 需求兜底展示一行(服务:hl-fleet-service) |
---
## 三、接口详情
### 1. 订单详情行程安排 Tab `GET /v3/admin/order/{id}/itinerary`
**VO**: 无 ReqVO(路径参数 `id`)→ `ItineraryVO`
#### 使用场景
管理后台订单详情页「行程安排」Tab 加载时调用,展示配房组/配车组的需求摘要+实配记录、已失活的配车需求历史,以及「联系房务/车务/团期管理员」按钮的未读消息角标与可用状态。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| id | Path | Long | ✅ | 订单雪花 ID | 订单 ID |
#### 出参 `Result<ItineraryVO>`
顶层字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| hotelGroup | HotelGroupVO | 配房需求+实配;本次未改动,结构与既有行为一致 |
| vehicleGroup | VehicleGroupVO | 配车需求+实配;本次修复的核心字段,见下方子表 |
| vehicleHistory | List\<VehicleRequirementBriefVO\> | 已失活的配车需求历史(版本倒序);与 `vehicleGroup` 在同一次请求内按同一个解析出的类别查询,见「业务边界」 |
| unreadMessageCount | Integer | 「联系房务」按钮未读消息数角标 |
| fleetUnreadMessageCount | Integer | 「联系车务」按钮未读消息数角标 |
| groupUnreadMessageCount | Integer | 「联系团期管理员」按钮未读消息数角标 |
| groupBatchId | String(雪花 ID,字符串序列化) | 运营团期 ID;非团期子订单为 `null` |
| canContactFleet | Boolean | 是否允许联系车务;本次修复后,TRANSFER-only 订单在存在有效用车需求时为 `true`(此前恒为 `false`) |
| contactFleetDisabledReason | String | 不可联系车务原因;`canContactFleet=false` 时有值 |
`vehicleGroup`(VehicleGroupVO):
| 字段 | 类型 | 说明 |
|------|------|------|
| requirement | VehicleRequirementBriefVO | 配车需求摘要,见下表 |
| assignments | List\<VehicleAssignmentVO\> | 实际配车列表,见下表 |
`vehicleGroup.requirement` / `vehicleHistory[]`(VehicleRequirementBriefVO):
| 字段 | 类型 | 说明 |
|------|------|------|
| requirementId | String(雪花 ID) | 需求 ID |
| version | Integer | 版本号 |
| status | String | PENDING/PROCESSING/DONE |
| isActive | Boolean | 是否当前生效版本 |
| manualUrgent | Boolean | 是否由定制师手动加急 |
| submittedAt | String(`yyyy-MM-dd HH:mm:ss`) | 提交时间 |
| returnedAt | String(`yyyy-MM-dd HH:mm:ss`) | 驳回时间;非驳回历史版本为 `null` |
| returnRemark | String | 驳回原因;非驳回历史版本为 `null` |
| vehicleTypeSummary | String | 车型摘要,如「商务车×2 / SUV×1」 |
| passengerCount | Integer | 订单乘车人数 |
| vehicleCount | Integer | 车辆总数 |
| totalSeatCount | Integer | 车辆座位总数(含司机座) |
| driverSeatCount | Integer | 司机占用座位数 |
| passengerSeatCapacity | Integer | 可载客座位数 |
| remainingPassengerSeats | Integer | 剩余可载客座位数 |
| specialTags | List\<String\> | 特殊诉求标签列表 |
| pickupRequired | Boolean | 兼容回显字段;接机/接站以大交通信息为准 |
| dropoffRequired | Boolean | 兼容回显字段;送机/送站以大交通信息为准 |
| remark | String | 备注 |
`vehicleGroup.assignments[]`(VehicleAssignmentVO):
| 字段 | 类型 | 说明 |
|------|------|------|
| assignmentId | String(雪花 ID) | 配车记录 ID |
| vehicleType | String | 车型(冻结快照) |
| vehicleCount | Integer | 车辆数(本段) |
| licensePlate | String | 车牌(冻结快照) |
| brand | String | 品牌(冻结快照) |
| seats | Integer | 座位数(冻结快照) |
| fleetTeamId | String(雪花 ID) | 车辆所属车队 ID |
| fleetTeamName | String | 车辆所属车队名称 |
| startDate | String(`yyyy-MM-dd`) | 连续服务开始日期 |
| endDate | String(`yyyy-MM-dd`) | 连续服务结束日期 |
| serviceDays | Integer | 连续服务天数(首尾日期均计入) |
| plannedDailyFee | String(金额,字符串序列化) | 计划日单价 |
| driverName | String | 司机姓名(冻结快照) |
| driverPhoneMasked | String | 司机手机(脱敏,格式 `138****1111`) |
| remark | String | 备注 |
#### 请求示例
```
GET /v3/admin/order/3401829901234567890/itinerary
Authorization: Bearer {token}
```
无请求体。
#### 响应示例
TRANSFER-only 订单,已提交并完成一条接送机用车需求(修复后):
```json
{
"code": 200,
"message": "成功",
"data": {
"hotelGroup": null,
"vehicleGroup": {
"requirement": {
"requirementId": "3401829901234567890",
"version": 1,
"status": "DONE",
"isActive": true,
"manualUrgent": false,
"submittedAt": "2026-09-10 09:15:00",
"returnedAt": null,
"returnRemark": null,
"vehicleTypeSummary": "商务车7座×1",
"passengerCount": 4,
"vehicleCount": 1,
"totalSeatCount": 7,
"driverSeatCount": 1,
"passengerSeatCapacity": 6,
"remainingPassengerSeats": 2,
"specialTags": [],
"pickupRequired": true,
"dropoffRequired": true,
"remark": null
},
"assignments": [
{
"assignmentId": "3401830011122334455",
"vehicleType": "商务车7座",
"vehicleCount": 1,
"licensePlate": "京A12345",
"brand": "别克GL8",
"seats": 7,
"fleetTeamId": "40001",
"fleetTeamName": "自有车队",
"startDate": "2026-09-20",
"endDate": "2026-09-20",
"serviceDays": 1,
"plannedDailyFee": "800.00",
"driverName": "王师傅",
"driverPhoneMasked": "138****1111",
"remark": null
}
]
},
"vehicleHistory": [],
"unreadMessageCount": 0,
"fleetUnreadMessageCount": 0,
"groupUnreadMessageCount": 0,
"groupBatchId": null,
"canContactFleet": true,
"contactFleetDisabledReason": null
},
"traceId": "a1b2c3d4-e5f6-7890",
"success": true
}
```
字段名/类型/嵌套结构逐一核对自 `ItineraryVO` 源码;业务数值为示意构造,非测试服原始抓包逐字节留存。
#### 空数据 / 降级响应
订单确实没有提交任何用车需求(TRAVEL 与 TRANSFER 均无)时,`vehicleGroup` 仍合法为 `null`——这是真实的「没有配车」状态,**不是**本次修复要处理的缺陷:
```json
{
"code": 200,
"data": {
"hotelGroup": null,
"vehicleGroup": null,
"vehicleHistory": [],
"unreadMessageCount": 0,
"fleetUnreadMessageCount": 0,
"groupUnreadMessageCount": 0,
"groupBatchId": null,
"canContactFleet": false,
"contactFleetDisabledReason": "请先提交有效用车需求后再联系车务"
},
"success": true
}
```
#### 错误响应
```json
{
"code": 581007,
"message": "订单不存在",
"success": false,
"data": null
}
```
其余可能的错误码:`581008`「无权查看此订单」(非本单定制师且非超管/管理员/车务管理员)、`581045`「房务角色无权查看订单详情,房务仅可配房」(Controller 层 `OrderViewGuard.assertNotHouseRole()` 反向门禁)。
#### 业务边界
- 鉴权顺序:未登录 → 401(网关拦截);订单不存在 → 581007;越权查看 → 581008;房务/房务组长角色 → 581045。
- TRANSFER-only 订单在**没有**有效用车需求时,`vehicleGroup` 仍为 `null`、`canContactFleet` 仍为 `false`——合法状态,见「空数据/降级响应」。
- **两类需求都存在**(既有 TRAVEL 又有 TRANSFER)的订单:`vehicleGroup`/`vehicleHistory` 仍只反映 TRAVEL 一类,TRANSFER 数据不会出现在这两个字段里,这不是遗留缺陷,是既定的单值契约(见「四、契约约束」)。
- `vehicleHistory` 与 `vehicleGroup` 在同一次请求内使用同一个解析出的类别,不会出现「当前需求是接送机、变更历史却按行程用车查」的类别错位。
- 响应体所有 VO 均不暴露 `kind`/`requirementKind` 字段,前端不能从返回值判断当前数据属于 TRAVEL 还是 TRANSFER。
---
### 2. 确认订单前置 Checklist `GET /v3/admin/order/{id}/confirm-checklist`
**VO**: 无 ReqVO(路径参数 `id`)→ `ConfirmChecklistRespVO`
#### 使用场景
管理后台订单详情页点击「确认订单」按钮前调用,用于校验 5 项前置条件(款项/出行人/房型/用车/合同方案)是否全部满足;全部满足时同时返回确认弹框的预览数据。
#### 入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| id | Path | Long | ✅ | 订单雪花 ID | 订单 ID |
#### 出参 `Result<ConfirmChecklistRespVO>`
顶层字段(`allPassed` 为唯一开关,`items`/`preview` 互斥):
| 字段 | 类型 | 说明 |
|------|------|------|
| allPassed | Boolean | 是否全部通过;`true` 时 `items=null`、`preview` 有值,`false` 时 `items` 有值、`preview=null` |
| items | List\<ChecklistItemVO\> | 5 项详细结果,仅 `allPassed=false` 时返回 |
| preview | PreviewVO | 确认弹框预览数据,仅 `allPassed=true` 时返回 |
`items[]`(ChecklistItemVO):
| 字段 | 类型 | 说明 |
|------|------|------|
| code | String | 检查项代码:`PAYMENT_OK`/`TRAVELER_COMPLETE`/`HOTEL_DONE`/`VEHICLE_DONE`/`CONTRACT_TEMPLATE_OK` |
| checkName | String | 检查项中文名称 |
| passed | Boolean | 是否通过 |
| failReason | String | 未通过原因;通过时为 `null` |
`preview`(PreviewVO):
| 字段 | 类型 | 说明 |
|------|------|------|
| departureDate | String(`yyyy-MM-dd`) | 出发日期 |
| totalPeopleCount | Integer | 总出行人数 |
| driverName | String | 司机姓名;本次修复后 TRANSFER-only 订单在有对应配车记录时正确回填(此前恒为 `null`) |
| driverPhoneMasked | String | 司机手机(脱敏);同上 |
| hotels | List\<HotelSummaryVO\> | 酒店列表(按行程城市分组) |
| staffs | List\<StaffItemVO\> | 本单配置人员列表(报账人 PRIMARY 排首) |
| contractAutoAction | ContractAutoActionVO | 确认后自动生成合同副作用 |
| insuranceAutoAction | InsuranceAutoActionVO | 确认后自动投保副作用 |
`preview.hotels[]`(HotelSummaryVO):`cityName`(String,城市名)、`hotelName`(String,酒店名)。
`preview.staffs[]`(StaffItemVO):`assignmentId`(String 雪花 ID)、`staffId`(String 雪花 ID)、`staffName`(String)、`staffPhone`(String,脱敏)、`staffRole`(String,`DRIVER`/`LEADER`/`GUIDE`/`PHOTOGRAPHER`/`OTHER`)、`staffRoleName`(String)、`isPrimaryReporter`(Boolean)。
`preview.contractAutoAction`(ContractAutoActionVO):`planName`(String)、`autoSign`(Boolean)。
`preview.insuranceAutoAction`(InsuranceAutoActionVO):`planName`(String)、`peopleCount`(Integer)、`effectiveDescription`(String)。
#### 请求示例
```
GET /v3/admin/order/3401829901234567890/confirm-checklist
Authorization: Bearer {token}
```
无请求体。
#### 响应示例
TRANSFER-only 订单,接送机用车需求已完成、其余 4 项也已满足(修复后 `allPassed` 可正确为 `true`):
```json
{
"code": 200,
"message": "成功",
"data": {
"allPassed": true,
"items": null,
"preview": {
"departureDate": "2026-09-20",
"totalPeopleCount": 4,
"driverName": "王师傅",
"driverPhoneMasked": "138****1111",
"hotels": [],
"staffs": [
{
"assignmentId": "1234567890123456789",
"staffId": "9876543210987654321",
"staffName": "王师傅",
"staffPhone": "138****1111",
"staffRole": "DRIVER",
"staffRoleName": "司机",
"isPrimaryReporter": false
}
],
"contractAutoAction": {
"planName": "标准接送机方案 v1.0",
"autoSign": true
},
"insuranceAutoAction": {
"planName": "安联境内旅行险 · 尊享版",
"peopleCount": 4,
"effectiveDescription": "出发前 24h 内生效"
}
}
},
"traceId": "a1b2c3d4-e5f6-7890",
"success": true
}
```
字段名/类型/互斥结构逐一核对自 `ConfirmChecklistRespVO` 源码;业务数值为示意构造,非测试服原始抓包逐字节留存。
#### 空数据 / 降级响应
同一批 TRANSFER-only 订单,用车已判定完成但**其余项尚未满足**(例如合同方案未配置)——修复只纠正用车判定本身,不代表订单必然可确认:
```json
{
"code": 200,
"data": {
"allPassed": false,
"items": [
{ "code": "PAYMENT_OK", "checkName": "款项校验", "passed": true, "failReason": null },
{ "code": "TRAVELER_COMPLETE", "checkName": "出行人信息", "passed": true, "failReason": null },
{ "code": "HOTEL_DONE", "checkName": "房型安排", "passed": true, "failReason": null },
{ "code": "VEHICLE_DONE", "checkName": "用车安排", "passed": true, "failReason": null },
{ "code": "CONTRACT_TEMPLATE_OK", "checkName": "合同方案配置", "passed": false, "failReason": "未配置合同方案" }
],
"preview": null
},
"success": true
}
```
#### 错误响应
```json
{
"code": 581007,
"message": "订单不存在",
"success": false,
"data": null
}
```
其余可能的错误码:`581008`「无权查看此订单」、`581045`「房务角色无权查看订单详情,房务仅可配房」。
#### 业务边界
- 鉴权同「订单详情行程安排 Tab」:581007/581008/581045。
- `allPassed`/`items`/`preview` 三者互斥,前端渲染前先判 `allPassed`,不要同时依赖 `items` 与 `preview` 都非空。
- TRANSFER-only 订单不需要用车(`needsVehicle=false`)或所在团整团免车时,`VEHICLE_DONE` 项直接通过,不受本次修复影响。
- `preview.driverName`/`driverPhoneMasked` 只在能定位到「当前 active 用车需求绑定的配车记录」时才回填;团车合法缺席场景下该两个字段合法为 `null`,不是异常。
- `POST /v3/admin/order/{id}/settlement/finalize` 的车务车费一致性校验**不在本次修复范围内**(见七、不影响范围),`confirm-checklist` 返回 `allPassed=true` 不代表 `settlement/finalize` 一定会成功,前端仍需按其可能返回业务失败码处理。
---
### 3. 派单看板列表 `GET /admin/fleet/board/orders`
**VO**: `BoardOrderPageReqVO → BoardOrderPageRespVO`
> 本端点属于 **hl-fleet-service**(不是 hl-order-service-v3),经网关 `Path=/admin/fleet/**` 路由(`hl-gateway/application.yml:230-233`,无 `StripPrefix`),前端可直接调用;路径本身早于 `#8056` 修复即已存在(既有派单看板契约)。本次收录的行为变化完全来自其上游依赖 hl-order-service-v3 的 `OrderFleetProviderService#batchFleetBoardContexts`(同一修复提交 `62c28d502`),**hl-fleet-service 自身代码本次未改动**(全仓 grep `8056` 零命中),不需要为此单独重新部署 hl-fleet-service。
#### 使用场景
车务派单看板列表页首次加载/翻页/筛选时调用,按当前有效用车需求展示订单及其派车进度。
#### 入参字段表
本次修复**未修改**该端点任何入参字段(`BoardOrderPageReqVO` 源码本轮未改动),以下为现状字段,供核对结构未变:
| 字段 | 位置 | 类型 | 必填 | 说明 |
|------|------|------|------|------|
| statuses | Query | String[] | 否 | 多状态筛选(含派生态,后端翻译),空=不过滤 |
| status | Query | String | 否 | 状态筛选别名(单值/逗号分隔),与 statuses 合并 |
| startDayFrom / startDayTo | Query | LocalDate(`yyyy-MM-dd`) | 否 | 行程区间 `[start_date,end_date]` 重叠筛选 |
| startDate / endDate | Query | LocalDate(`yyyy-MM-dd`) | 否 | startDayFrom/startDayTo 别名,未传前者时生效 |
| vehicleTypeKeys / typeKeys | Query | String[] | 否 | 车型大类多选:`suv`/`mpv`/`bus`/`sedan` |
| driverName | Query | String | 否 | 司机姓名模糊搜索 |
| keyword | Query | String | 否 | 统一文字搜索(司机/联系人/团号/订单号/定制师显示名任一包含) |
| contactName / contactKeyword | Query | String | 否 | 联系人/客户名模糊搜索 |
| teamNo | Query | String | 否 | 团号模糊搜索(仅真实团号,不匹配订单号) |
| groupBatchId | Query | Long | 否 | 运营团期精确筛选 |
| consultantId | Query | Long | 否 | 定制师精确筛选(下拉值) |
| plannerName / consultantName | Query | String | 否 | 定制师姓名模糊搜索(兼容旧前端) |
| variant | Query | String | 否 | `list`(默认)/`grid`,其余值返 100001 |
| page | Query | Integer | 否 | 页码,默认 1,最小 1 |
| pageSize | Query | Integer | 否 | 每页条数,默认 20,最大 100 |
#### 出参字段表
`BoardOrderPageRespVO`(**结构未变**,`records`/`total`/`page`/`pageSize` 平级):
| 字段 | 类型 | 说明 |
|------|------|------|
| records | List\<BoardOrderRecordVO\> | 当前页记录(确定性排序:紧急组置顶→常规→终态沉底→id 兜底) |
| total | Long | 总条数(筛选后全量) |
| page | Integer | 当前页码 |
| pageSize | Integer | 每页条数 |
`records[]`(`BoardOrderRecordVO`,**结构未变**;完整字段清单见既有契约 FLEET v1.5 §6.1,本次不重复列出,仅摘录与本次行为变化直接相关的字段):
| 字段 | 类型 | 说明 |
|------|------|------|
| orderId | String(雪花,字符串序列化) | 订单 ID |
| requirementId | String(雪花) | 当前代表本行的用车需求 ID;混合订单在 TRAVEL 不可联络时,本次修复后该字段可能改为指向 TRANSFER 需求,见「业务边界」 |
| dailySummary | BoardDailySummaryVO | 代表日行摘要,随 `requirementId` 所属需求联动 |
#### 请求示例
```
GET /admin/fleet/board/orders?page=1&pageSize=20&statuses=unassigned
Authorization: Bearer {token}
```
无请求体。
#### 响应示例
混合订单(同时有 TRAVEL 与 TRANSFER 需求,TRAVEL 处于不可联络状态、TRANSFER 可联络)兜底展示出的一行:
```json
{
"code": 200,
"message": "成功",
"data": {
"records": [
{
"id": "HL202609200001",
"orderNo": "HL202609200001",
"orderId": "3401829901234567890",
"requirementId": "3401829901234599102"
}
],
"total": 1,
"page": 1,
"pageSize": 20
},
"traceId": "a1b2c3d4-e5f6-7890",
"success": true
}
```
字段名/类型逐一核对自 `BoardOrderRecordVO` 源码;为避免与既有契约重复,本示例只展示与本次行为变化直接相关的字段,其余字段结构未变、照旧下发,未在本例中重复列出;业务数值为示意构造,非测试服原始抓包逐字节留存(本端点本次未做独立网关实测,见「八、测试环境已验证」后的说明)。
#### 空数据 / 降级响应
订单没有任何满足 `isFleetBoardRequirement` 条件的活跃需求时,该订单不进入 `records`(既有行为,本次未改动):
```json
{
"code": 200,
"data": { "records": [], "total": 0, "page": 1, "pageSize": 20 },
"success": true
}
```
order-v3 整体不可达时,`OrderQueryFacade#getFleetBoardContextsOrNull` 返回 `null`,服务按既有降级路径回退到 fleet 本地派单快照渲染(该降级路径本次未改动)。
#### 错误响应
```json
{
"code": 100001,
"message": "参数非法",
"success": false,
"data": null
}
```
`variant` 传非 `list`/`grid` 时触发上述 100001;未登录 → 401(网关拦截)。
#### 业务边界
- **定性为改进,不是回归**:改前,混合订单若 TRAVEL 需求处于不可联络状态(`RequirementStatus.isFleetContactable` 判 `false`,仅 `PENDING`/`PROCESSING`/`DONE` 判 `true`),整单从看板消失——车务完全看不到该订单,且没有任何错误信号;这属于 `#8056` 静默丢数问题的又一种表现。改后由 TRANSFER 需求兜底展示一行,混合订单不再整单消失。
- **可达性如实说明**:当前数据下,触发该兜底所需的两个条件(TRAVEL 不可联络 ∧ TRANSFER 可联络)在同一订单上的交集为 **0 条**;但两个子条件各自都有样本(TRAVEL 处于 `PENDING_REVIEW` 态:30 条;TRANSFER 可联络:88 条),交集为空是当前数据的**巧合**,不是结构性约束——没有任何机制阻止同一订单同时满足这两个条件。当前同时存在有效 TRAVEL 与 TRANSFER 需求的混合订单共 25 单。
- **两类需求各自独立查询、独立判歧义**(`OrderFleetProviderService.java:242-276`),不是合并成一次查询——这是刻意保留的既有行为:合并查询会让两类并存的订单拿到 2 条、被 `size()==1` 判为歧义而整单掉出看板,那会是对行程用车(TRAVEL)看板行为的回归。
- `records[]` 出参结构未变,字段来自 TRAVEL 还是 TRANSFER 需求对前端不可见(响应体不暴露 `kind`/`requirementKind` 字段,与「三、接口详情」第 1/2 节一致)。
- 前端渲染该行为完全数据驱动;对 `mmg/hl-ui` 的 `src/views/fleet/` 目录的穷举 grep(证据见「六.7、影响评估」)未发现任何按用车需求种类分支的代码,本行为变化不要求前端改动。
- 同一订单同一类别(TRAVEL 或 TRANSFER)内存在多条活跃需求时,该类别整体被判定为歧义并跳过(记 warn 日志),不会猜测采用哪一条;这与「四、契约约束」中单值端点 `resolveSingleValueVehicleKind` 的歧义处理是两套独立实现,互不影响。
---
## 四、契约约束与正确调用方式
> 本节只写后端在「该读哪一类用车需求」上的实际行为,不写 UI 渲染建议。
### ✅ 正确 / ⚠️ 需注意 理解对照
| 场景 | 说明 |
|------|------|
| ✅ TRANSFER-only 订单(只有接送机用车需求) | 这两个端点现在会读到该订单的 TRANSFER 需求数据 |
| ✅ TRAVEL-only 订单(只有行程用车需求) | 行为不变,仍读 TRAVEL 数据 |
| ⚠️ 两类需求都存在的订单(既有行程用车又有接送机用车) | 这两个端点**仍然只返回 TRAVEL 数据**,不会合并展示 TRANSFER;这不是本次修复遗留的缺陷,是既定的单值契约,不要据此误判为「接送机数据又丢了」 |
| ❌ 试图从响应中读取 `kind`/`requirementKind` 字段区分当前数据属于哪一类 | 两个端点的响应 VO 都不暴露该字段(见「三、接口详情」出参表),无法从返回值本身判断 |
| ⚠️ 派单看板列表(`GET /admin/fleet/board/orders`,见「三、接口详情」第 3 节)的 TRAVEL 优先/TRANSFER 兜底 | 与本节两个单值端点所用的 `resolveSingleValueVehicleKind` 是**两套独立实现**(看板按 TRAVEL/TRANSFER 两类分别查询、分别判歧义,见 `OrderFleetProviderService.java:242-276`),不要假设两者共用同一份解析逻辑或行为完全对称 |
### 为什么是「优先」而不是「合并」
这两个端点的响应契约是**单值**的(一条需求、一个 requirementId、一组司机车辆),容不下两类数据同时返回。两类需求都存在时,返回值逐字段与修复前一致(仍是 TRAVEL);只有「TRAVEL 那类根本不存在」的订单(即 TRANSFER-only 订单)行为发生变化。
### 请求侧无需改动
两个端点均为 `GET` + 路径参数 `id`,请求方式、Header、鉴权方式本次均未改动,无需修改请求代码;本次改动只影响响应体里用车相关字段的取值。
---
## 五、数据库行为
两个端点均为只读 `GET`,不接受请求体,不写库。本次修复只改变了读取用车需求时选择的类别(TRAVEL/TRANSFER),不引入、不修改任何写入行为。
---
## 六、边界行为
- 未登录 → 401(网关拦截)
- 订单不存在 → 581007
- 越权查看(非本单定制师且非超管/管理员/车务管理员)→ 581008
- 房务/房务组长角色查看这两个只读端点 → 581045(`OrderViewGuard.assertNotHouseRole()`)
- 订单确实没有任何用车需求(TRAVEL 与 TRANSFER 均无)→ 两个端点均按「没有配车」的合法状态返回(见「三、接口详情」空数据/降级响应),不是异常
- 团车合法缺席(非 DAILY_V3 契约 + 团级配车完成)→ `confirm-checklist` 的 `VEHICLE_DONE` 项照常通过,但 `preview.driverName`/`driverPhoneMasked` 合法留空
---
## 六.5、枚举 / 数据字典
`VehicleRequirementKind`(`TRAVEL`/`TRANSFER`)是本次修复涉及的分类依据,但**不是**任何请求/响应字段的显式取值——`ItineraryVO`/`ConfirmChecklistRespVO` 及其全部嵌套 VO 均不暴露 `kind`/`requirementKind` 字段(见「四、契约约束」)。因此本节不适用于字段级枚举值表;`TRAVEL`/`TRANSFER` 的选择规则见「四、契约约束」。
---
## 六.6、修改前后对比
### 字段级对比(均限定为 TRANSFER-only 订单场景)
| 字段 | 改前 | 改后 |
|------|------|------|
| `itinerary.vehicleGroup` | 存在有效接送机用车需求时仍恒为 `null` | 存在有效需求时返回真实的 `requirement`+`assignments` |
| `itinerary.canContactFleet` | 恒为 `false` | 存在有效需求时为 `true` |
| `itinerary.contactFleetDisabledReason` | 恒为「请先提交有效用车需求后再联系车务」(误导:需求已提交,只是类别没读到) | 存在有效需求时为 `null` |
| `itinerary.vehicleHistory` | 按 TRAVEL 类别查询,恒为空数组(即使 TRANSFER 侧有历史驳回记录) | 按订单实际单值类别(TRAVEL 优先/TRANSFER 回落)查询 |
| `confirm-checklist.items[code=VEHICLE_DONE].passed` | 用车已实际完成时仍为 `false` | 正确反映实际完成状态 |
| `confirm-checklist.items[code=VEHICLE_DONE].failReason` | 恒为「未提交用车需求」(误导) | 用车确已完成时该 item 不再出现在 `items`(因 `allPassed` 可能变为 `true`) |
| `confirm-checklist.allPassed` | 即使其余 4 项都通过也恒为 `false`(被 VEHICLE_DONE 拖累) | 5 项均满足时可正确为 `true` |
| `confirm-checklist.preview.driverName` / `driverPhoneMasked` | 恒为 `null` | 有对应配车记录时正确回填 |
### 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| TRANSFER-only 订单在行程 Tab 展示配车信息 | 页面表现为「没有安排车辆」(实际已安排) | 正确展示已安排的车辆/司机信息 |
| TRANSFER-only 订单发起「确认订单」 | 恒被 VEHICLE_DONE 项拦截,无法确认 | 用车确已完成且其余项满足时可正常确认 |
| 失败可见性 | 无异常、无错误码,HTTP 200,字段静默为空/false,与「真的没安排车」无法区分 | 同样 HTTP 200,但字段现在反映真实数据 |
| 混合订单(同时有效 TRAVEL 与 TRANSFER 需求)在派单看板列表,TRAVEL 需求处于不可联络状态时 | 整单从看板消失,车务完全看不到该订单(`#8056` 静默丢数的又一种表现,无任何错误信号) | 由 TRANSFER 需求兜底展示一行,字段来自 TRANSFER 需求;订单不再消失(见「三、接口详情」第 3 节) |
---
## 六.7、影响评估
- **是否破坏向后兼容**: 否——响应结构(字段名、类型、层级)未变,只是同一批字段在 TRANSFER-only 订单上的取值范围从「恒为空/false/固定文案」变为「反映真实数据」。TRAVEL-only 订单与两类都没有活跃需求的订单,这两个端点的返回值逐字段不变。派单看板列表端点(第 3 节)同理:`BoardOrderPageRespVO`/`BoardOrderRecordVO` 字段结构未变,只是混合订单在特定条件下代表行从「整单消失」变为「TRANSFER 需求兜底展示一行」。
- **前端是否必须同步上线**: 否——已核实管理后台前端对本文相关字段是纯透传渲染,无需任何代码改动即可直接受益于修复后的数据,核实依据见下方「前端 workaround 清理点」。
- **前端 workaround 清理点(管理者已对 `mmg/hl-ui` 代为核实,转录核实结果供核对)**:查证对象为 **origin ref**(非本地树;本地树落后 520 个提交,若沿用会得出过时/错误结论),基线 `origin/v2.1 @ 56dc9455`(2026-09-22 提交,2026-09-22 读取)。核实结果:
- `src/views/order-v2/detail/_shared/v3Adapter.js:1007-1008`——`canContactFleet`/`contactFleetDisabledReason` 是直接透传映射,无分支逻辑。
- `src/views/order-v2/detail/components/VehicleArrangeCard.vue:434,437`——`canContactFleet === false` 时禁用按钮并展示 `contactFleetDisabledReason || '请先提交有效用车需求后再联系车务'`,纯数据驱动。
- `src/views/order-v2/detail/modals/FunItemAdjustModal.vue:2559-2560`——同样的透传模式。
- `src/views/order-v2/detail/_shared/confirmChecklistActions.js:5`——`VEHICLE_DONE` 只映射到 `{label:'查看配车', tab:'arrange'}`,不参与通过/未通过的判定逻辑。
- **负控**:对该仓库 `src/views/order-v2/detail/` 与 `src/views/fleet/` 目录穷举 grep `TRANSFER|接送机`,全部命中均与本缺陷无关(`BANK_TRANSFER` 支付渠道、`CONSULTANT_TRANSFER`/`HOUSE_TRANSFER` 时间线事件类型、`transport.transferTimeHint`)——**零命中**专门针对 `VehicleRequirementKind.TRANSFER` 的分支代码;`src/views/fleet/` 属于派单看板前端所在目录,该负控同时覆盖「三、接口详情」第 3 节的前端影响判断。
- **结论**:管理后台前端对这一批字段(含派单看板列表相关字段)是纯透传渲染,不存在针对本缺陷写过的 workaround/特判代码,无需任何前端改动。
---
## 七、不影响范围
- **仅影响**: `GET /v3/admin/order/{id}/itinerary` 与 `GET /v3/admin/order/{id}/confirm-checklist` 两个只读端点在 **TRANSFER-only 订单**上的用车相关字段;以及 `GET /admin/fleet/board/orders`(hl-fleet-service)在**混合订单**(同时有效 TRAVEL 与 TRANSFER 需求)上的代表行兜底展示行为(见「三、接口详情」第 3 节)。
- **零影响**:
- 纯 TRAVEL(行程用车)订单,或两类需求都没有的订单:这两个端点的返回值逐字段不变。
- 两类需求都存在(既有 TRAVEL 又有 TRANSFER)的订单:仍只返回 TRAVEL 数据(见「四、契约约束」),本次修复不改变这类订单在这两个端点上的表现。
- `POST /v3/admin/order/{id}/settlement/finalize` 的车务车费一致性校验:**不在本次修复范围内**,前端仍需按其可能返回业务失败码(如 `584100`「车务车辆总车费暂时不可用,请稍后重试」)处理,不能假设该接口现在必然成功。
- `#8056` 记录的用车数据丢失共 10 个落点,均已在同一提交(`62c28d502`)中一并修复并合入 `dev-v3`;本文逐字段交付其中 3 处(两个网关实测的只读端点 + 一个源码核查的派单看板列表端点,见「三、接口详情」),其余落点未在本文展开。
---
## 八、测试环境已验证
```
部署版本:hl-order-service-v3 @ d30cd9561(2026-09-21 21:55:58 部署,经 git merge-base --is-ancestor 确认包含修复提交 62c28d502)
GET https://api.test.1814.love:9443/v3/admin/order/{id}/itinerary
TRANSFER-only 订单(仅接送机用车需求、无行程用车需求):
vehicleGroup.assignments 返回真实配车记录(含 assignmentId/车型/车牌/座位数/司机姓名/脱敏手机号/服务天数),
此前该字段为空数组 ✓
GET https://api.test.1814.love:9443/v3/admin/order/{id}/confirm-checklist
同一批 TRANSFER-only 订单:allPassed 可正确为 true;
此前恒被 VEHICLE_DONE 项拦截,failReason 固定为「未提交用车需求」/「用车需求未完成」✓
```
> 「三、接口详情」第 3 节(派单看板列表 `GET /admin/fleet/board/orders`)**本次未做独立网关实测**,未列入上表。该行为变化完全依赖已验证部署的 hl-order-service-v3@d30cd9561(含同一提交 `62c28d502`);hl-fleet-service 侧代码本身未改动(全仓 grep `8056` 零命中),故不需要 hl-fleet-service 单独部署即可生效,见第 3 节开头说明。
---
## 十、相关文档
- 关联 Issue: [wx/HL#8056](https://git.1814.love:8443/wx/HL/issues/8056)
- 关联 PR: [wx/HL#8118](https://git.1814.love:8443/wx/HL/pulls/8118)
## 关联 / 联系人
### 链接
- **Issue**: [#8056](https://git.1814.love:8443/wx/HL/issues/8056)
- **PR**: [#8118](https://git.1814.love:8443/wx/HL/pulls/8118)
- **Merge commit**: [62c28d502](https://git.1814.love:8443/wx/HL/commit/62c28d502)
### 联系人
- **后端负责人**: @wx