--- schema: "hl-changelog/v2" ticket: "7990" title: "看板订单详情按需求各返一组身份三元组" consumer: "admin" author: "wx(GIT)" change_type: "修改接口" backend_status: "deployed" gateway_status: "verified" frontend_status: "verified" frontend_owner: "mmg" frontend_ref: "662310eac02ef5afc84c81b242949ca536c79489" target_release: "v2.1" verified_at: "2026-09-21" status_note: "backend 已部署到 311dc92ee(hl-fleet-service,2026-09-20 17:46:56,STATE=ok;jar 字节口径已验:605311 命中 2 处,阳性对照老码 605015 命中 1 处,两者都非零说明检索方法本身有效)。gateway_status=verified 依据 2026-09-20 18:0x 经测试网关(api.test.1814.love:9443,账号 cw_test_7443)实测 GET /admin/fleet/board/orders/2101566624467419137 返 HTTP 200(该订单同时有 TRAVEL 与 TRANSFER 两条已确认需求,修复前此形态必返 500),requirementIdentities 返两项:TRAVEL(requirementId=2101567456462147586, version=1, sha256=bcfea7cf…c5cb, generation=359969759900602368) 与 TRANSFER(requirementId=2101567456596365313, version=1, sha256=814e3f85…c21c, generation=359984828583645184)。该组值随后被原样用于 POST /admin/fleet/assignments/requirements/2101567456596365313/confirm,返 code=200, confirmed=true, finalPlanPublished=true (两个执行段均 assigned)——即本字段不只是返回了,而是真的能驱动接送机需求确认走通,这是本篇的效果判据。frontend_status=pending:响应结构新增字段,前端取接送机确认参数必须改用 requirementIdentities 里 kind 匹配的那一项,顶层三字段恒指 TRAVEL。 前端实证翻 not_required(mmg 2026-09-20,挂起期判定):requirementIdentities/605311 全仓零命中;需求级确认取参 useAssignFlow.js 用顶层 requirementSha256(恒 TRAVEL),对现行 TRAVEL 流程逐字正确;500 修复与 605311 由拦截器透 message 自动受益。TRANSFER 接入时(#7443 启动)义务已入前端 memory:取参必须 requirementIdentities.find(kind==='TRANSFER')(禁顶层三字段与 driverConfirmationSummary.dispatchPlanGeneration),且接送机天然多段须走需求级确认端点(单车 confirm 恒 605057)。【mmg 2026-09-21 交付,not_required 翻 verified】requirementIdentities 已随 #7443 C 段消费(hl-admin v2.1 662310ea):AssignModal requirementScopedOrder 按 kind 匹配项覆写 requirementId/requirementVersion/requirementSha256/dispatchPlanGeneration(顶层三字段与整单 summary 均禁用,代际 null 透传),kind 切换走 initializationIdentity 统一重置;新派 batch 取参义务已落地。需求级确认端点按 kind 取参目前仅新派链路使用,confirmHold 复核(整单 activeAssignments 无法按 kind 拆 groups)留后续项。" updated_at: "2026-09-20" base: "dev-v3" --- # fleet: 看板订单详情按需求各返一组身份三元组 > **存放目录**: 二期(v3) → `changelogs-v2/{YYYY-MM}/` > > **服务**: hl-fleet-service (8009) > **PR**: #8048 > **Issue**: #7990 > **日期**: 2026-09-20 > **影响范围**: 管理后台派单看板订单详情页接送机需求确认流程 --- ## ⚠️ 关键变化 **本版修复了一个先前必 500 的缺陷**:同一订单**同时有行程用车与接送机两条已确认需求**时,`GET /admin/fleet/board/orders/{orderId}` 每次都抛 `IllegalStateException` ⇒ HTTP 500「系统繁忙」。现已正常返回;真出现无法判定的代际冲突时返业务码 **605311**(HTTP 200,消息「当前需求存在多个不透明派车方案代际,请联系车务核对派车方案后重试」)。 --- ## 一、背景 **同一订单为何会同时有行程用车与接送机?** 订单的出行人日程包含接客、行程、送客三段。接客与送客涉及接送机,中间行程使用行程用车。当出行时间长/人数多时,接机日与送机日往往**不相邻**,落成两个独立服务窗口,系统会生成两条 active 用车需求: - **TRAVEL**(行程用车):行程期间实际用车 - **TRANSFER**(接送机):接机日 + 送机日之间无直接包含关系 这在[存量库中确实存在](#存量问题),不是边角情形。 --- ## 二、变更接口清单 | # | 接口 | 方法 | 路径 | 变更类型 | 说明 | |---|------|------|------|----------|------| | 1 | 订单详情 | GET | `/admin/fleet/board/orders/{orderId}` | 响应体新增字段 + 行为修复 | 新增 `requirementIdentities` 数组;修复两条需求并存时的 500 缺陷 | --- ## 三、接口详情 ### 1. 订单详情 `GET /admin/fleet/board/orders/{orderId}` **VO**: `BoardOrderDetailVO → Result` #### 使用场景 管理后台派单看板左侧点开某订单,右侧弹窗 Step 1(订单详情)加载该接口数据。其中**新增的 `requirementIdentities` 数组是接送机需求确认的唯一参数来源**——前端在该页面右上方有「需求级确认」按钮,用户点击后需要从该数组中取参数调用 `POST /admin/fleet/assignments/requirements/{requirementId}/confirm`。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | orderId | Path | Long | ✅ | 雪花 ID | 订单 ID(订单号) | #### 出参 `Result` **顶层结构**: | 字段 | 类型 | 说明 | |------|------|------| | code | Integer | 业务状态码;200=成功,605311=需求代际冲突,其他=失败 | | message | String | 人类可读的状态消息 | | data | BoardOrderDetailVO | 订单详情对象(详见下表);失败时为 null | | success | Boolean | 是否成功 | **BoardOrderDetailVO 关键字段**(仅列新增 + 涉及接送机的字段): | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | id | String | ✅ | 订单 ID(订单号);如「HL20260708144554930」 | | orderNo | String | ✅ | 订单号(同 id) | | teamNo | String | - | 团号;如「26-0095」 | | requirementId | Long(序列化为 String) | ✅ | **当前有效用车需求 ID;恒指 TRAVEL(行程用车),语义不变** | | requirementVersion | Integer | ✅ | **当前有效用车需求版本号;恒指 TRAVEL,语义不变** | | requirementSha256 | String | ✅ | **当前有效用车需求 SHA-256;恒指 TRAVEL,语义不变** | | **requirementIdentities** | **`List`** | **✅** | **【新增】本订单当前并存的全部用车需求身份;TRAVEL 在前、TRANSFER 在后;无接送机需求时只有一条** | | customerName | String | ✅ | 客户名 | | headcount | Integer | ✅ | 人数 | | startDate | LocalDate | ✅ | 出团日 | | endDate | LocalDate | ✅ | 结束日 | | dailyVehiclePlan | `List` | ✅ | 逐日逐车最终计划;【改】接送机待派车行现在可见(修复前被隐藏) | | activeAssignments | `List` | ✅ | 全部有效派车组 | | driverConfirmationSummary | DriverConfirmationSummaryVO | ✅ | 司机确认汇总;**dispatchPlanGeneration 是整单维度(两条需求并存时恒 null),不能用来确认接送机,需改用 `requirementIdentities[].dispatchPlanGeneration`** | | (其他字段) | - | - | 与本次变更无关,详见 swagger | **RequirementIdentityVO 结构**: | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | kind | String | ✅ | 需求类别:`TRAVEL`(行程用车)/ `TRANSFER`(接送机);enum 值见 §6.5 | | requirementId | Long | ✅ | 用车需求 ID;雪花序列化为字符串 | | requirementVersion | Integer | ✅ | 用车需求版本号;需求级确认时原样回传给 `expectedRequirementVersion` | | requirementSha256 | String | ✅ | 用车需求 canonical SHA-256(64 位小写十六进制);需求级确认时原样回传给 `expectedRequirementSha256` | | dispatchPlanGeneration | Long | - | 该需求当前最终派车方案代际;该需求下定稿行的代际不唯一(含尚未定稿)时为 null;需求级确认时回传给 `expectedPlanGeneration`;注意:**不要用顶层 `driverConfirmationSummary.dispatchPlanGeneration`**,那个字段是整单维度计算的(两条需求并存时恒 null),本字段才是当前需求的真实代际 | #### 请求示例 ```json GET /admin/fleet/board/orders/HL20260708144554930 ``` #### 响应示例 ```json { "code": 200, "message": "成功", "data": { "id": "HL20260708144554930", "orderNo": "HL20260708144554930", "teamNo": "26-0095", "requirementId": "70123456789", "requirementVersion": 3, "requirementSha256": "abc123def456abc123def456abc123def456abc123def456abc123def456ab", "requirementIdentities": [ { "kind": "TRAVEL", "requirementId": "70123456789", "requirementVersion": 3, "requirementSha256": "abc123def456abc123def456abc123def456abc123def456abc123def456ab", "dispatchPlanGeneration": "1934567890123456789" }, { "kind": "TRANSFER", "requirementId": "70123456790", "requirementVersion": 2, "requirementSha256": "fed654cba987fed654cba987fed654cba987fed654cba987fed654cba987fe", "dispatchPlanGeneration": "1934567890123456790" } ], "customerName": "赵先生", "headcount": 3, "startDate": "2026-05-06", "endDate": "2026-05-11", "pickupAt": "hailar", "dropoffAt": "hailar", "productName": "额吉的故乡 v9", "productType": "CORE", "dailyVehiclePlan": [], "vehicleSlots": [], "activeAssignments": [], "driverConfirmationSummary": { "dispatchPlanGeneration": null, "requiredSegmentCount": 0, "confirmedSegmentCount": 0, "pendingSegmentCount": 0, "rejectedSegmentCount": 0, "ambiguousSegmentCount": 0, "allDriverConfirmed": false, "allExecutionConfirmed": false } }, "success": true } ``` #### 空数据 / 降级响应 ```json { "code": 503001, "message": "下游服务暂时不可用,请稍后重试", "success": false, "data": null } ``` #### 错误响应 **场景 1:同一条需求存在多个不透明派车方案代际**(新增,修复了原来必 500 的情况) ```json { "code": 605311, "message": "当前需求存在多个不透明派车方案代际,请联系车务核对派车方案后重试", "success": false, "data": null } ``` **场景 2:订单不存在** ```json { "code": 404, "message": "订单不存在", "success": false, "data": null } ``` #### 业务边界 - **原有字段不变**:`requirementId`、`requirementVersion`、`requirementSha256` 三个平铺字段恒指 **TRAVEL**(行程用车),语义与行为完全不变;旧前端不读 `requirementIdentities` 时逐字不变 - **存在性保证**:`requirementIdentities` 至少包含一条 TRAVEL 类型的需求;若订单有接送机需求,会多一条 TRANSFER 类型 - **顺序**:TRAVEL 恒在数组前,TRANSFER 恒在后(若有);便于前端按索引取值 - **接送机需求确认**:前端**必须从 `requirementIdentities` 中选择 `kind === 'TRANSFER'` 的那一条**,提取其 `requirementVersion`、`requirementSha256`、`dispatchPlanGeneration` 作为需求级确认端点 `POST /admin/fleet/assignments/requirements/{requirementId}/confirm` 的入参 `expectedRequirementVersion`、`expectedRequirementSha256`、`expectedPlanGeneration`;**禁止使用顶层的三个平铺字段**(那些恒指 TRAVEL) - **代际字段注意**:`requirementIdentities[].dispatchPlanGeneration` 可能为 null(该需求下尚未定稿、或定稿行的代际不唯一);此时前端调用需求级确认端点时仍需携带,服务端会自行处理;**不要用 `driverConfirmationSummary.dispatchPlanGeneration`**(整单维度,两条需求并存时恒 null) - **新生成订单**:该端点 M1 本地快照的 `requirementIdentities` 会按最新需求逐条填充;若 Feign 拉 order-v3 失败降级,仍返 200,不要当作无需求 --- ## 四、契约约束与正确调用方式 ### ✅ 正确 / ❌ 错误 payload 对照 **需求级确认前端取参示例**: | 场景 | 前端取值来源 | payload | |------|-----------|---------| | ✅ 行程用车确认 | `requirementIdentities.find(x => x.kind === 'TRAVEL')` | `{ "orderId": 123, "expectedRequirementVersion": 3, "expectedRequirementSha256": "abc123...", "expectedPlanGeneration": 1934567890123456789, "requestId": "...", "groups": [...] }` | | ✅ 接送机确认 | `requirementIdentities.find(x => x.kind === 'TRANSFER')` | `{ "orderId": 123, "expectedRequirementVersion": 2, "expectedRequirementSha256": "fed654...", "expectedPlanGeneration": 1934567890123456790, "requestId": "...", "groups": [...] }` | | ❌ 混合取值 | 分别从 `requirementIdentities` 与顶层字段混选 | 请求虽不会因此拒绝,但会导致版本/摘要不匹配,服务端拒绝整组确认(605054 错误码) | | ❌ 用顶层字段确认接送机 | `orderId: 123, expectedRequirementVersion: detail.requirementVersion` 等 | 顶层字段恒指 TRAVEL,接送机版本号无法提供,确认失败(605054 版本不匹配) | ### 接送机为何不能用单车端点(`POST /admin/fleet/assignments/{assignmentId}/confirm`) 接送机需求天然拆成接机日 + 送机日两个**不相邻的服务段**: - 接机日:2026-05-06,派车组 assignmentGroupId_1 - 行程:2026-05-07 ~ 2026-05-10,派车组 assignmentGroupId_2(行程用车) - 送机日:2026-05-11,派车组 assignmentGroupId_3 接送机对应两个 assignmentGroupId(接机和送机),每个都有多个 assignmentId(历史改派)。单车端点 `confirm(assignmentId)` 按单个 ID 操作,会触发校验: ``` IF 当前方案内该 assignmentId 所属 assignmentGroupId 包含多个 assignmentId(即多个执行段) THEN 拒绝并返 605057「当前方案包含多个执行段,请刷新并整组确认」 ``` 接送机由于天然多段,**任何时刻都会触发 605057**。因此前端必须引导用户用需求级确认端点 `POST /admin/fleet/assignments/requirements/{requirementId}/confirm`(按需求原子性整组提交),不要使用单车端点。 --- ## 五、数据库行为 无写操作;纯读取。响应字段由如下源头构造: - `requirementIdentities[].requirementId/requirementVersion/requirementSha256`:来自 fleet_assignment 快照 - `requirementIdentities[].dispatchPlanGeneration`:来自 fleet_assignment.plan_finalized_generation(若该需求下多条定稿行代际不唯一或不定稿则为 null) - `requirementIdentities[].kind`:由 `requirement_type` 枚举判定(TRAVEL / TRANSFER) --- ## 六、边界行为 - **订单不存在** → 404 - **下游 order-v3 Feign 降级** → 返 M1 本地快照数据,仍构造 `requirementIdentities`,不 500 不阻断页面 - **该订单零需求**(业务上不应出现,但 M1 快照理论可能)→`requirementIdentities` 为空数组 - **只有 TRAVEL 需求**(无接送机)→ `requirementIdentities` 仅包含一条 TRAVEL 记录 - **两条需求代际冲突**(同一条需求下多条定稿行代际不唯一)→ 该条需求的 `dispatchPlanGeneration` 为 null;若同时触发整单级代际无效(§六.1a)则返 605311 --- ## 六.5、枚举 / 数据字典 ### kind(需求类别) **所属字段**: `requirementIdentities[].kind` | **类型**: `String` | 值 | 中文 | 说明 | |----|------|------| | `TRAVEL` | 行程用车 | 出行行程期间的用车需求;恒为主需求 | | `TRANSFER` | 接送机 | 出行接客或送客期间的接送机需求;可能与 TRAVEL 并存(不相邻服务日) | --- ## 六.6、修改前后对比 ### 字段级对比 | 字段 | 改前 | 改后 | |------|------|------| | requirementIdentities | 无 | 新增数组,每项含 kind/requirementId/requirementVersion/requirementSha256/dispatchPlanGeneration 五元组 | | requirementId(顶层) | 指当前有效需求 ID | 改后恒指 TRAVEL;若仅有一条需求则仍指该需求;语义无实质变化 | | requirementVersion(顶层) | 指当前有效需求版本 | 改后恒指 TRAVEL;语义无实质变化 | | requirementSha256(顶层) | 指当前有效需求摘要 | 改后恒指 TRAVEL;语义无实质变化 | ### 行为级对比 | 行为 | 改前 | 改后 | |------|------|------| | 订单内同时有 TRAVEL 与 TRANSFER 两条定稿需求 | 抛 `IllegalStateException` → HTTP 500「系统繁忙」;前端分不清是服务挂了还是数据自相矛盾;接送机确认无从下手 | 正常返 200 + 完整 `requirementIdentities` 数组;前端根据数组各自提取参数调用需求级确认端点;接送机需求可确认 | | 同一需求下多条定稿行代际不唯一 | 抛 `IllegalStateException` → HTTP 500 | 返业务码 605311(HTTP 200)+ message 提示「请联系车务核对派车方案」;前端据此提示用户而非重试 | | 接送机待派车行的可见性 | 被行程用车的代际筛掉,隐藏不可见 | 改后按需求各自筛代际,接送机的待派车行若无定稿锚点则正常显示 | --- ## 六.7、影响评估 - **是否破坏向后兼容**:否。`requirementIdentities` 是纯增量(新字段);原有三个平铺字段语义与行为完全不变;旧前端不读新字段时行为逐字不变 - **前端是否必须同步上线**: - 行程用车确认:**否**,可延续原流程(用顶层字段) - 接送机需求确认:**是**,必须从 `requirementIdentities` 提参,否则无法取到接送机版本号/摘要(接送机确认按钮点不动) - **前端 workaround 清理点**: - 删除「顶层字段用于确认接送机」的代码路由(若有) - 接送机需求确认时改为 `find(x => x.kind === 'TRANSFER')` 取参 - 单车端点(`POST .../assignments/{assignmentId}/confirm`)对接送机返 605057 时,提示「请用需求级确认」而不是重试 --- ## 七、不影响范围 - **仅影响**:管理后台派单看板订单详情页「需求级确认」流程(接送机确认入口) - **零影响**: - 管理后台订单列表 - 行程用车单车端点确认流程(`POST .../assignments/{assignmentId}/confirm`) - C 端算价、行程详情 - 订单创建、编辑接口 - 存量数据(不迁移;接送机需求现存储为业务数据,查询时动态构造) --- ## 八、测试环境已验证 **存量问题**:全库已有 4 个订单同时存在 TRAVEL + TRANSFER 两条已确认需求(2026-09-20 21:00 前后数据快照): ``` SELECT order_id, COUNT(DISTINCT requirement_type) as cnt FROM fleet_assignment WHERE requirement_type IN ('TRAVEL', 'TRANSFER') GROUP BY order_id HAVING cnt > 1 AND status != 'canceled'; → 4 rows ``` 这些订单在修复前**每次查看都 500**;修复后正常返回 `requirementIdentities` 完整数据。 **后端取证**(基于测试用例 + 部署验证): - ✅ GET /admin/fleet/board/orders/10 → 200 + `requirementIdentities` 含 TRAVEL 和 TRANSFER 两条记录 - ✅ TRAVEL 和 TRANSFER 的 SHA-256 各不相同、各自 64 位小写十六进制 - ✅ dispatchPlanGeneration 字段存在、可为 null(未定稿时) - ✅ 同一条需求多代际冲突 → 605311(HTTP 200)而非 500 - ✅ 接送机待派车行在 dailyVehiclePlan 中可见(修复前被筛掉) **网关实测**:另一代理正在进行,待读数回填(placeholder 状态)。 --- ## 十、相关文档 - **Issue**: [#7990](https://git.1814.love:8443/wx/HL/issues/7990) - **PR**: [#8048](https://git.1814.love:8443/wx/HL/pulls/8048) - **Merge commit**: [311dc92ee](https://git.1814.love:8443/wx/HL/commit/311dc92ee) --- ## 关联 / 联系人 ### 链接 - **Issue**: [#7990](https://git.1814.love:8443/wx/HL/issues/7990) - **PR**: [#8048](https://git.1814.love:8443/wx/HL/pulls/8048) - **Merge commit**: [311dc92ee](https://git.1814.love:8443/wx/HL/commit/311dc92ee) ### 联系人 - **后端负责人**: @wx - **前端接收方**: mmg(hl-ui 管理后台) - **相关接口**: 需求级确认端点 `POST /admin/fleet/assignments/requirements/{requirementId}/confirm`(由 #7989 同步交接)