From 57a9e068ddd519e5cfa3f94e543be7add801dc21 Mon Sep 17 00:00:00 2001 From: API Changelog Bot Date: Tue, 22 Sep 2026 02:25:08 +0800 Subject: [PATCH] =?UTF-8?q?docs(changelog):=20#8056=20TRANSFER-only=20?= =?UTF-8?q?=E8=AE=A2=E5=8D=95=E7=94=A8=E8=BD=A6=E6=95=B0=E6=8D=AE=E9=9D=99?= =?UTF-8?q?=E9=BB=98=E4=B8=A2=E5=A4=B1=E4=BF=AE=E5=A4=8D=E4=BA=A4=E6=8E=A5?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 行程详情 / 确认前置 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) --- ...y订单用车数据静默丢失修复-修复-管理后台.md | 679 ++++++++++++++++++ 1 file changed, 679 insertions(+) create mode 100644 changelogs-v2/2026-09/22_8056_TRANSFER-only订单用车数据静默丢失修复-修复-管理后台.md diff --git a/changelogs-v2/2026-09/22_8056_TRANSFER-only订单用车数据静默丢失修复-修复-管理后台.md b/changelogs-v2/2026-09/22_8056_TRANSFER-only订单用车数据静默丢失修复-修复-管理后台.md new file mode 100644 index 00000000..7315070e --- /dev/null +++ b/changelogs-v2/2026-09/22_8056_TRANSFER-only订单用车数据静默丢失修复-修复-管理后台.md @@ -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` + +顶层字段: + +| 字段 | 类型 | 说明 | +|------|------|------| +| hotelGroup | HotelGroupVO | 配房需求+实配;本次未改动,结构与既有行为一致 | +| vehicleGroup | VehicleGroupVO | 配车需求+实配;本次修复的核心字段,见下方子表 | +| vehicleHistory | List\ | 已失活的配车需求历史(版本倒序);与 `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\ | 实际配车列表,见下表 | + +`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\ | 特殊诉求标签列表 | +| 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` + +顶层字段(`allPassed` 为唯一开关,`items`/`preview` 互斥): + +| 字段 | 类型 | 说明 | +|------|------|------| +| allPassed | Boolean | 是否全部通过;`true` 时 `items=null`、`preview` 有值,`false` 时 `items` 有值、`preview=null` | +| items | List\ | 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\ | 酒店列表(按行程城市分组) | +| staffs | List\ | 本单配置人员列表(报账人 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\ | 当前页记录(确定性排序:紧急组置顶→常规→终态沉底→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