diff --git a/changelogs-v2/2026-09/20_7990_车务看板订单详情按需求各返一组身份三元组-修改接口-管理后台.md b/changelogs-v2/2026-09/20_7990_车务看板订单详情按需求各返一组身份三元组-修改接口-管理后台.md new file mode 100644 index 00000000..9aa5f908 --- /dev/null +++ b/changelogs-v2/2026-09/20_7990_车务看板订单详情按需求各返一组身份三元组-修改接口-管理后台.md @@ -0,0 +1,378 @@ +--- +schema: "hl-changelog/v2" +ticket: "7990" +title: "看板订单详情按需求各返一组身份三元组" +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: "" +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。" +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 同步交接)