--- schema: "hl-changelog/v2" ticket: "7443" title: "调整订单「车辆安排」页同页提交 行程用车(TRAVEL) + 接送机用车(TRANSFER) 两类需求" consumer: "admin" author: "wx(GIT)" change_type: "修改接口" backend_status: "deployed" gateway_status: "verified" frontend_status: "pending" frontend_owner: "mmg" frontend_ref: "" target_release: "" verified_at: "2026-09-20" status_note: "后端已合入 dev-v3(PR #8024,squash 提交 920f29d76)并部署测试服:deploy-status.sh 回读 hl-order-service-v3 = dev-v3 @ 920f29d76、BEHIND=0/N、STATE=ok,两实例滚动重启均 UP,判据 git merge-base --is-ancestor 920f29d76 920f29d76 = true。gateway_status=verified 的依据是四条真实网关调用而非源码推断:①读口双槽(含阳性对照:未提交需求的订单两个字段都是 null,证明不是恒有值)②一次 submit 同时提交两份 → HTTP 200,落库两条 active 行,fleet 分别为 bus/19座/1台 与 mpv/7座/2台 ③调整记录两条 label 原文不同 ④无大交通时 809002。🔴 一条必须连着读的限定:本文只验证了「提交」这一段。同一次实测观测到 TRANSFER 需求同步给车务的 outbox 命令持续失败(order_fleet_command_outbox command_type=RECONCILE,TRAVEL=SUCCEEDED 而 TRANSFER=PENDING/retry_count=2,last_error_message='Fleet 用车需求换版失败: code=605905, message=需求版本过期'),根因是 hl-fleet-service AssignmentService:11492 requireCurrentVehicleRequirementForMutation 取当前需求时不带 kind、恒取 TRAVEL,与 TRANSFER 的 requirementId 比对必然不等。即:前端按本文接完即可正常提交并回显,但提交出去的接送机需求在修复该缺陷之前到不了车务侧,端到端业务尚未打通。该缺陷已在修(属 #7990 那一族),修好后另发交接件,不影响本文的前端契约。" updated_at: "2026-09-20" base: "dev-v3" --- # order-v3: 调整订单「车辆安排」页同页提交 行程用车 + 接送机用车 两类需求 > **服务**: hl-order-service-v3(adjustment 层,唯一变化点) > **影响范围**: 管理后台「调整订单 → 车辆安排」页。此前该页**结构上只能提交行程用车**,本次补上接送机用车的读口与写口。 ## 这次修的是什么 「车辆安排」页此前只有一个用车需求槽。页面上那张接送机卡片(航班号、抵离时间、接送备注) 来自 `vehicleTransportSummary`,它是**只读的大交通摘要**——没有车型、座位数、数量输入, 也没有对应的提交字段。于是业务上「接机用什么车、用几辆」**没有任何录入通道**。 后端的用车需求模型本身早就支持两类并存:表 `order_vehicle_requirement` 的 `requirement_kind` 取 `TRAVEL`(行程用车,服务日=行程日)/ `TRANSFER`(接送机,服务日=航班日), 唯一键 `(order_id, active_kind)` ⇒ **同一订单两类 active 需求合法并存,各有自己的 `fleet` 与 `service_dates`**。 缺口只在 adjustment 这一层把 kind 焊死成了 TRAVEL。本次把它透出来。 ## 接口变更 ### 1. `GET /v3/admin/order/{orderId}/adjustment/snapshot`(读) **新增响应字段 `transferRequirement`**,类型与既有 `vehicleRequirement` 完全相同。 | 字段 | 说明 | |---|---| | `vehicleRequirement` | **行为不变**,仍是 `TRAVEL`(行程用车)。⚠️ 字段名与类型均未改动,现网页面无需调整 | | `transferRequirement` | **新增**,`TRANSFER`(接送机用车)。该订单没有接送机需求时为 `null` | 两个字段的结构(同一个 VO): | 字段 | 类型 | 说明 | |---|---|---| | `id` | String | 需求 id | | `version` | Integer | 版本号 | | `status` | String | 需求状态(如 `PENDING_REVIEW`) | | `fleet` | Array | **车辆组合**,元素 `{vehicleType, seats, count}` —— 即「用什么车、几座、几辆」 | | `specialTags` | Array<String> | 特殊诉求标签 | | `pickupRequired` | Boolean | 是否需要平台接机/接站 | | `dropoffRequired` | Boolean | 是否需要平台送机/送站 | | `remark` | String | 备注 | | `claimerId` / `claimerName` / `claimedAt` | - | 抢单人信息 | | `vehicleType` / `requiredSeats` | - | ⚠️ **旧兼容字段,恒为 `null`**。车型与座位已迁入 `fleet`,不要再读这两个 | `vehicleTransportSummary`(大交通摘要)**保持原样不变**,仍是只读展示用。 ### 2. `POST /v3/admin/order/{orderId}/adjustment/submit`(写) **`updates` 下新增 `transferRequirement`**,结构与既有 `updates.vehicleRequirement` 完全相同 (`fleet` / `specialTags` / `pickupRequired` / `dropoffRequired` / `remark`)。 - 只传 `vehicleRequirement` → **行为与改动前逐字节一致**(行程用车) - 只传 `transferRequirement` → 只提交接送机用车 - **两个都传 → 一次提交落两条需求**,这正是「同页提交」要的效果 调整记录(`adjustment-record`)里两者会产出**可区分的两条** `VEHICLE_REQ` 条目: `行程用车需求已调整` / `接送机用车需求已调整`。改动前恒为「车辆需求已调整」,两条无法区分。 🔴 **`serviceDates` 前端不传**。服务日由后端派生:`TRAVEL` 取行程日,`TRANSFER` 取**大交通的航班/车次日期** (客人可能提前一天到、返程后一天走,所以允许落在行程日窗口之外)。 ## 🔴 前端必须处理的前置:没有大交通时提交接送机需求会失败 `TRANSFER` 的服务日来自大交通,**空集合不会兜底成行程日**(那样会把航班日写错、 派车日期落到需求单声明范围之外)。因此该订单没有录入大交通行程时, 提交 `transferRequirement` 会返回 **`809002`**。 ⇒ **建议前端在没有大交通时把接送机那一段的输入置灰**,并提示「请先录入大交通行程」, 不要让用户填完再吃一个错误码。判断依据用同一份快照里的 `vehicleTransportSummary.hasPickupTime` (为 `false` 时它还会给出 `emptyText`,当前文案是「暂无接送机时间」)。 ⚠️ 两份同时提交而接送机这半抛 809002 时,**整笔调整回滚**(一次 submit = 一笔原子调整), 行程用车那半也不会落库。所以置灰比事后补救重要。 ## 环境开关 `hl.order.requirement.transfer-kind-submit-enabled` - **代码默认 `false`**;生产环境未开,提交 `kind=TRANSFER` 返 `809009` - **测试服已置 `true`**(2026-09-19 15:33:32 发布,随 order-v3 重启生效) - ⚠️ 该配置无 `@RefreshScope`,改完必须重启服务才生效 ## 联调样本 | | | |---|---| | 订单 | `2101219133700952066`(订单号 `HL20260919155734730`,散客单) | | 特征 | 已录大交通 2 条;`TRAVEL` 与 `TRANSFER` 两类需求**均已存在**,可直接用来验双槽回显 | ## 实测读数(2026-09-20,测试服真实网关调用) 后端版本:`hl-order-service-v3` @ **`920f29d76`**(PR #8024 squash 合入 dev-v3)。 **① 读口双槽 —— 带阳性对照** | 订单 | `vehicleRequirement` | `transferRequirement` | |---|---|---| | `2101566624467419137`(提交前) | `null` | `null` | | `2101219133700952066`(两类都有) | `{id:2101219223517659138, fleet:[{mpv,7,1}]}` | `{id:2101219393722634242, fleet:[{mpv,7,1}]}` | ⚠️ 第二行两者的 `fleet` 恰好相同(样本单本身如此),**所以"两个字段不同"这件事不能拿来证明按 kind 取数生效**; 真正证明它的是第一行那个全 `null` 的阳性对照,以及两者 `requirementId` 不同。 **② 同页提交(本文的核心)** ``` POST /v3/admin/order/2101566624467419137/adjustment/submit updates.vehicleRequirement = {fleet:[{vehicleType:"bus", seats:19, count:1}]} updates.transferRequirement = {fleet:[{vehicleType:"mpv", seats:7, count:2}]} → HTTP 200 {"code":200,"data":{"success":true}} ``` 落库 `order_vehicle_requirement` 两条 active 行: | `active_kind` | `fleet` | `service_dates` | |---|---|---| | `TRAVEL` | `[{"count":1,"seats":19,"vehicleType":"bus"}]` | `["2026-10-20","2026-10-21","2026-10-22"]`(行程三天) | | `TRANSFER` | `[{"count":2,"seats":7,"vehicleType":"mpv"}]` | `["2026-10-20","2026-10-23"]`(**接机日 + 送机日**) | 🔴 **两个 `service_dates` 一个都不是前端传的**,全部由后端按 kind 派生——这是「前端不传日期」这条约定的硬证据。 **③ 调整记录可区分** `GET /v3/admin/order/{id}/adjustment-record` → 本次 `changeCount:2`,两条 `type=VEHICLE_REQ`, `label` 分别是 `行程用车需求已调整` / `接送机用车需求已调整`。改动前两条 label 完全相同、事后分不出改的是哪一份。 **④ 无大交通的失败形态** 订单 `2101567681155207169`(同产品、已付款、**无大交通**),只提交 `transferRequirement`: → HTTP **200** + `{"code":809002,"message":"接送机需求缺少服务日期,请先补齐大交通信息"}` (提交前已确认测试服 Nacos `hl.order.requirement.transfer-kind-submit-enabled: true`, 所以这个 809002 不是被开关挡住的假象。) ## 🔴 本文的覆盖边界:只验到「提交」为止 同一次实测里观测到:TRANSFER 需求同步给车务的 outbox 命令**持续失败** (`order_fleet_command_outbox`,`command_type=RECONCILE`:TRAVEL `SUCCEEDED`, **TRANSFER `PENDING` / `retry_count=2` / `last_error_message="Fleet 用车需求换版失败: code=605905, message=需求版本过期"`**)。 ⇒ **前端按本文接完即可正常提交并回显,但提交出去的接送机需求目前到不了车务侧。** 该缺陷在 fleet(`AssignmentService:11492` 取当前需求不带 kind、恒取 TRAVEL),已在修, 修好后另发交接件。**它不改变本文的前端契约**,可以并行开工。 ⚠️ 写下这段是因为「四条实测全达成」这个汇总句**丢掉边界之后会变强**—— 会被读成「接送机用车已经端到端可用」,而那句话今天还不成立。