--- schema: "hl-changelog/v2" ticket: "8430" title: "纯接送机订单看板详情改按接送机需求(TRANSFER)取数:门禁要求日、逐日方案、车数、进度、方案代际" 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: "PR #8452 已合入 dev-v3(squash 0bae84b59)。TEST 环境 hl-fleet-service 0bae84b59 于 2026-09-27 20:29:52 部署、21:12 与 21:24 两次复核未变,hl-gateway 71def6dc5;同一张纯接送机订单在部署前(fleet 267ab6906)与部署后各取一次详情做对照,结论见第八节。只改看板详情一个接口,看板列表未改;TRAVEL 与 TRANSFER 并存的订单行为不变。 前端实证(mmg 2026-09-28):结构/字段名/类型不变只变取值,前端已读 requirementIdentities[kind=TRANSFER](#7443/#7990),dispatchReadOnly 前端零消费,纯接送机订单详情自动有数据;判 not_required。" updated_at: "2026-09-27" base: "dev-v3" --- # fleet: 纯接送机订单看板详情改按接送机需求(TRANSFER)取数 **服务**: hl-fleet-service **PR**: `#8452`(已合入 `dev-v3`,squash `0bae84b59`) **Issue**: #8430 **日期**: 2026-09-27 **影响范围**: 管理后台车务看板「订单详情」,只影响**纯接送机订单**(只有 TRANSFER 用车需求、没有 TRAVEL 需求) --- ## ⚠️ 关键变化 🟡 **纯接送机订单的详情终于有内容了。** 改前这类订单打开详情,取数仍按(并不存在的)TRAVEL 需求走:`dailyVehiclePlan` 恒为空数组、`suggestedVehicleCount`/`actualVehicleCount` 恒为 0、进度停在第 1 步、接送机门禁的要求日取成了订单出发日与返回日。改后这些字段都按 TRANSFER 需求取值。**响应结构、字段名、字段类型都没有变**,变的只是纯接送机订单上这些字段的取值。 🟡 **顶层 `requirementId` / `requirementVersion` / `requirementSha256` 对纯接送机订单仍为 null**(设计如此)。要拿接送机需求的身份(例如调需求级确认接口),请用 `requirementIdentities` 里 `kind=TRANSFER` 那一条。 🟡 **纯接送机订单现在也可能返回 605311。** 这个错误码 #7990 就有,此前只会出现在 TRAVEL 订单上;改后同一判据也作用于 TRANSFER 需求,前端按已有的 605311 处理即可。 --- ## 一、背景 纯接送机订单(只下了接送机、没有行程用车)在车务看板打开详情时,后端 `BoardOrderService.queryOrderDetail` 只按 TRAVEL 需求取数。TRAVEL 缺席,于是窗口、门禁、逐日方案、车数、进度全部落空或落到订单快照兜底,车务在详情里看不到任何已派的车,也拼不出需求级确认要回传的方案代际。 本单改为:TRAVEL 缺席时,以 TRANSFER 需求作为「有效需求」取数。TRAVEL 存在时有效需求就是 TRAVEL,逐字沿用原逻辑。 --- ## 二、变更接口清单 | # | 接口 | 方法 | 路径 | 变更类型 | 说明 | |---|------|------|------|----------|------| | 1 | 车务看板订单详情 | GET | `/admin/fleet/board/orders/{orderId}` | 修改(取值) | 纯接送机订单按 TRANSFER 需求取数;结构不变 | --- ## 三、接口详情 ### 1. 车务看板订单详情 `GET /admin/fleet/board/orders/{orderId}` **VO**: `BoardOrderDetailVO` #### 使用场景 管理后台车务看板,点开一张订单的详情抽屉。纯接送机订单现在能看到逐日派车方案、实派车数、接送机缺口与进度,并能从 `requirementIdentities` 拿到接送机需求的版本、指纹与方案代际,用于需求级确认。 #### 入参字段表 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | orderId | Path | Long | 是 | 订单主键(雪花 ID) | 看板列表行里的订单 ID | #### 出参字段表 只列本单改变取值的字段,其余字段与改前一致。 | 字段 | 类型 | 说明 | |------|------|------| | requirementId | String | **取值不变**:纯接送机订单仍为 null(只指向 TRAVEL 需求) | | requirementVersion | Integer | **取值不变**:纯接送机订单仍为 null | | requirementSha256 | String | **取值不变**:纯接送机订单仍为 null | | requirementIdentities | List | 每条需求一个身份;纯接送机订单只有一条 `kind=TRANSFER`。**改后**其 `dispatchPlanGeneration` 按该需求自己的已定稿行计算(改前纯接送机订单恒为 null) | | requirementIdentities[].dispatchPlanGeneration | String | 该需求已定稿行的唯一方案代际;该需求还没有已定稿行、或已定稿行没有代际时为 null,前端按可空处理 | | pickupDropoffGate.arrivalRequiredDates | List | **改后**取自 TRANSFER 需求声明的接机服务日;改前取成订单出发日 | | pickupDropoffGate.departureRequiredDates | List | **改后**取自 TRANSFER 需求声明的送机服务日;改前取成订单返回日 | | pickupDropoffGate.missingPickupDates / missingDropoffDates | List | 按上面的要求日与实际标记参与的派车行重新计算 | | dailyVehiclePlan | List | **改后**返回 TRANSFER 需求的逐日派车行;改前纯接送机订单恒为空数组。已完结的行 `readOnly=true`、`readOnlyReason="派单已完结"` | | suggestedVehicleCount | Integer | **改后**按 TRANSFER 需求计算;改前恒为 0 | | actualVehicleCount | Integer | **改后**按 TRANSFER 需求的派车行计算;改前恒为 0 | | progressSteps | List | **改后**按 TRANSFER 需求的派车行推进;改前停在第 1 步 | | dispatchReadOnly | Boolean | 判据没变:行程已结束,或逐日方案**每一行**都已完结、服务日已过或已关账,满足其一即为 true。**改后**纯接送机订单的逐日方案有了行,第二条也会对它生效;改前它只在行程已结束时为 true | | dispatchReadOnlyReason | String | 行程已结束时为「行程已结束」;逐日方案全部只读时为「全部服务日期已过去、完结或关账」;不只读时为 null | #### 请求示例 ```http GET /admin/fleet/board/orders/2104161383220457474 Authorization: Bearer <车务账号 token> ``` #### 响应示例 测试服实测(纯接送机订单,部署后,截取关键字段;`dailyVehiclePlan` 共 7 行,只列两行): ```json { "code": 200, "message": "成功", "data": { "orderNo": "HL20260927184901765", "teamNo": "26-9179", "startDate": "2027-08-03", "endDate": "2027-08-09", "requirementId": null, "requirementVersion": null, "requirementSha256": null, "requirementIdentities": [ { "kind": "TRANSFER", "requirementId": "2104185762864115714", "requirementVersion": 5, "requirementSha256": "36bc63bf38bc881a7f5a48222538f232a304600cd970627247729262983b78e0", "dispatchPlanGeneration": "362573468547551232" } ], "suggestedVehicleCount": 1, "actualVehicleCount": 3, "dispatchReadOnly": false, "pickupDropoffGate": { "arrivalRequiredDates": ["2027-08-30", "2027-09-05"], "departureRequiredDates": ["2027-08-09"], "missingPickupDates": ["2027-08-30"], "missingDropoffDates": [], "declared": true, "satisfied": false }, "progressSteps": [ {"step": 1, "code": "ORDER_DETAIL", "status": "DONE"}, {"step": 2, "code": "DISPATCH", "status": "DONE"}, {"step": 3, "code": "PICKUP_DROPOFF", "status": "PROCESSING"}, {"step": 4, "code": "CONFIRM_EXECUTE", "status": "WAITING"} ], "dailyVehiclePlan": [ { "serviceDate": "2027-08-03", "assignmentId": "2104162972542935041", "planFinalized": true, "planState": "USED", "pickupParticipant": true, "assignmentStatus": "completed", "readOnly": true, "readOnlyReason": "派单已完结" }, { "serviceDate": "2027-09-05", "assignmentId": "2104183651266863105", "planFinalized": false, "planState": "UNPLANNED", "pickupParticipant": true, "pickupRequired": true, "assignmentStatus": "assigned", "readOnly": false, "readOnlyReason": null } ] }, "success": true } ``` #### 空数据 / 降级响应 - 纯接送机订单还没派过车:`dailyVehiclePlan` 为空数组、`actualVehicleCount=0`、`requirementIdentities[TRANSFER].dispatchPlanGeneration=null`,门禁要求日照常按 TRANSFER 需求给出。 - TRAVEL 与 TRANSFER 两条需求都取不到(订单服务不可达,`relatedDetailReady=false`):与改前完全一致,顶层字段走订单快照兜底。 #### 错误响应 ```json { "code": 605311, "message": "当前需求存在多个不透明派车方案代际,请联系车务核对派车方案后重试", "data": null, "success": false } ``` | 错误码 | 触发条件 | |---|---| | 605311 | 有效需求(纯接送机订单即 TRANSFER 需求)的已定稿派车行出现一个以上方案代际,或无代际的老定稿行与有代际的行混在一起。看板不猜当前代际,直接返回该码(HTTP 200)。错误码本身不是新增的(#7990),本单之后纯接送机订单也会走到这个判据 | #### 业务边界 - 只改看板**详情**;看板**列表**未改。同一张纯接送机订单在列表与详情里的派车状态、车辆、需求身份已实测一致;列表的 `currentStep` 按 3 步编号、详情的 `progressSteps` 按 4 步编号,两者本来就不同源,不是本单引入的差异。 - TRAVEL 与 TRANSFER 并存的订单:有效需求就是 TRAVEL,每个字段的取值与改前逐字相同。 - 打开详情不会改变 TRANSFER 需求的状态(只有 TRAVEL 需求会在打开详情时被置为处理中,这一点没变)。 - `dispatchReadOnly` 的判据没变(行程已结束,或逐日方案每一行都已完结、服务日已过或已关账),但纯接送机订单现在有了逐日方案行,所以「全部完结」的纯接送机订单行程未结束也会整单只读,原因「全部服务日期已过去、完结或关账」。单行是否只读看 `dailyVehiclePlan[].readOnly`。 - 纯接送机订单调需求级确认接口时,`expectedRequirementVersion` / `expectedRequirementSha256` / `expectedPlanGeneration` 取 `requirementIdentities` 中 `kind=TRANSFER` 那一条;`dispatchPlanGeneration` 为 null 时说明该需求还没有唯一的已定稿方案,此时不能按「已定稿方案」确认。 --- ## 四、契约约束与正确调用方式 纯接送机订单的需求身份一律从 `requirementIdentities` 取,不要读顶层 `requirementId`: ```js const transfer = (detail.requirementIdentities || []).find(x => x.kind === 'TRANSFER'); // 纯接送机订单:顶层 requirementId 为 null,身份一律从 transfer 取 const expected = transfer && { expectedRequirementVersion: transfer.requirementVersion, expectedRequirementSha256: transfer.requirementSha256, expectedPlanGeneration: transfer.dispatchPlanGeneration, // 可能为 null }; ``` --- ## 五、数据库行为 无 DDL、无写入。本单只改详情接口的内存组装,打开详情不写任何表。 --- ## 六、边界行为 ### 605311 的触发面变大 判据本身没变(有效需求的已定稿行只能有一个方案代际,老的无代际定稿行不能与有代际的行混在一起),变的是它现在也作用于纯接送机订单的 TRANSFER 需求。前端沿用 605311 现有的提示即可,不需要新分支。 ### 方案代际为 null `requirementIdentities[TRANSFER].dispatchPlanGeneration` 在该需求还没有已定稿行、或已定稿行没有代际时为 null。这时详情照常返回,只是不能按「已定稿方案」做需求级确认。 ### 只读判据 `dispatchReadOnly` 的判据没变,有两条,满足其一即为 true: 1. 行程已结束(订单返回日早于今天),原因「行程已结束」; 2. 逐日方案每一行都已完结、服务日已过或服务日已关账,原因「全部服务日期已过去、完结或关账」。 改前纯接送机订单的逐日方案恒为空,第 2 条对它永不成立;改后它有了逐日方案行,TRANSFER 的派车行全部完结时,即使行程还没结束,整单也会只读。实测订单还有三行在途,所以为 `false`。单行只读看 `dailyVehiclePlan[].readOnly`,已完结行为 `true`、原因「派单已完结」。 ## 六.6、修改前后对比 ### 字段级对比 同一张纯接送机订单实测: | 字段 | 改前(fleet `267ab6906`) | 改后(fleet `0bae84b59`) | |------|------|------| | `pickupDropoffGate.arrivalRequiredDates` | `["2027-08-03"]`(订单出发日) | `["2027-08-30","2027-09-05"]`(TRANSFER 需求的接机服务日) | | `pickupDropoffGate.departureRequiredDates` | `["2027-08-09"]`(订单返回日) | `["2027-08-09"]` | | `dailyVehiclePlan` | 0 行 | 7 行(4 行已完结 + 09-05 三行已派) | | `suggestedVehicleCount` / `actualVehicleCount` | 0 / 0 | 1 / 3 | | `progressSteps` 状态 | 进行中 / 待处理 / 待处理 / 待处理 | 已完成 / 已完成 / 进行中 / 待处理 | | `requirementIdentities[TRANSFER].dispatchPlanGeneration` | null | `362573468547551232` | | 顶层 `requirementId` | null | null(不变) | 送机要求日改前改后恰好相同(订单返回日刚好也是 TRANSFER 需求的送机日),区分改前改后的是接机要求日。 --- ## 六.7、影响评估 | 维度 | 评估 | |---|---| | 接口结构 | 无变化,不新增、不删除字段 | | 纯接送机订单 | 详情字段从空值 / 0 / 订单日期兜底变为按 TRANSFER 需求取值;可能新出现 605311;派车行全部完结时 `dispatchReadOnly` 会变为 true | | TRAVEL 订单、TRAVEL 与 TRANSFER 并存订单 | 无变化 | | 看板列表 | 无变化 | | 数据库 | 无 DDL、无写入 | ## 七、不影响范围 - 看板列表、派车 / 改派 / 接送机配置 / 需求级确认等写接口:未改。 - TRAVEL 订单、TRAVEL 与 TRANSFER 并存的订单:取值不变。 - 响应结构:没有新增或删除字段。 --- ## 八、测试环境已验证 **环境**:TEST,hl-fleet-service `0bae84b59`(2026-09-27 20:29:52 部署,21:12 与 21:24 两次复核未变),hl-gateway `71def6dc5`,测试专用车务账号。 1. **同一订单改前改后对照**(第六节):部署前在 fleet `267ab6906` 上取详情,接机要求日为订单出发日、逐日方案 0 行、车数 0/0;部署后同一订单接机要求日为 TRANSFER 服务日、逐日方案 7 行、车数 1/3,`requirementIdentities[TRANSFER]` 带方案代际。 2. **接送机配置写口与详情门禁一致**:另建一张只有送机方向的纯接送机订单,接送机配置接口响应里的门禁与随后看板详情返回的门禁逐字段相同(`arrivalRequiredDates=[]`、`departureRequiredDates=["2027-10-19"]`、`satisfied=true`);只有送机方向时接机要求日为空,不会拿订单出发日硬凑。 3. **拿详情里的身份做需求级确认**:用同一订单详情里 `requirementIdentities[TRANSFER]` 的需求 ID、版本、指纹、代际调 `POST /admin/fleet/assignments/requirements/{requirementId}/confirm` 成功(`confirmed=true`、`finalPlanPublished=true`),服务端接受了详情给出的四个值。 4. **列表与详情一致**:同一订单在看板列表(`GET /admin/fleet/board/orders`)中的派车状态、车辆、需求身份与详情一致。 5. **单测**:看板详情新增 7 条纯接送机订单用例(含全部已完结、旧版在途行、平移重绑行、同一需求多代 605311、不置处理中、顶层身份为 null),合并后在 dev-v3 上复跑 234/0 通过。 --- ## 九、相关历史 PR - PR [#8451](https://git.1814.love/wx/HL/pulls/8451)(#8429):接送机最终方案发布三写口统一判据,本单实测纯接送机订单详情里的门禁与它的接送机配置写口响应逐字段一致。 ## 十、相关文档 - **Issue**: [#8430](https://git.1814.love/wx/HL/issues/8430) - **PR**: [#8452](https://git.1814.love/wx/HL/pulls/8452) - **前序**: [#7990](https://git.1814.love/wx/HL/issues/7990)(`requirementIdentities` 与 605311 的来源)、[#8429](https://git.1814.love/wx/HL/issues/8429)(接送机最终方案发布判据) ## 关联 / 联系人 - **后端负责人**: @wx