From 24dde670d11ffa7398f17cd0933316ad24b1a8b3 Mon Sep 17 00:00:00 2001 From: API Changelog Bot Date: Sun, 20 Sep 2026 17:52:04 +0800 Subject: [PATCH] =?UTF-8?q?docs(changelog):=20#7443=20AC-26=20TRANSFER=20?= =?UTF-8?q?=E6=B4=BE=E8=BD=A6=E8=A1=8C=E5=9B=9B=E7=AB=AF=E7=82=B9=E7=BD=91?= =?UTF-8?q?=E5=85=B3=E5=AE=9E=E6=B5=8B=E6=94=B6=E5=B0=BE=EF=BC=8Cgateway?= =?UTF-8?q?=5Fstatus=20=E5=9B=9E=E5=A1=AB=20verified?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 四个既有端点(candidates/change/confirm(assignmentId)/confirm(requirementId))经网关 逐一实测通过;此前缺失的 confirm(requirementId) 借新上线的 GET /admin/fleet/board/orders/{orderId} requirementIdentities 字段取得 expectedRequirementVersion/Sha256/expectedPlanGeneration 真值补齐验证。 Co-Authored-By: Claude Sonnet 5 --- ...¦行确认改派基线复核修复-修改接口-管理后台.md | 985 ++++++++++++++++++ 1 file changed, 985 insertions(+) create mode 100644 changelogs-v2/2026-09/20_7443_TRANSFER派车行确认改派基线复核修复-修改接口-管理后台.md diff --git a/changelogs-v2/2026-09/20_7443_TRANSFER派车行确认改派基线复核修复-修改接口-管理后台.md b/changelogs-v2/2026-09/20_7443_TRANSFER派车行确认改派基线复核修复-修改接口-管理后台.md new file mode 100644 index 00000000..62085f5d --- /dev/null +++ b/changelogs-v2/2026-09/20_7443_TRANSFER派车行确认改派基线复核修复-修改接口-管理后台.md @@ -0,0 +1,985 @@ +--- +schema: "hl-changelog/v2" +ticket: "7443" +title: "TRANSFER 派车行确认/改派基线复核修复(605905/605041 不再误判)" +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: "【2026-09-20 AC-26 收尾】gateway_status 回填 verified,四个端点(「二、变更接口清单」全部条目)均经网关实测(cw_test_7443 账号,dev-v3 测试服,fleet 部署 dev-v3@311dc92ee,2026-09-20 17:46:56,含 PR #7952/c527e48c3 + #8048 + #8042;取证前后各跑一次 deploy-status.sh,两次 fleet/order-v3 commit 一致,证据有效):(1) POST /admin/fleet/assignments/candidates(requirementId=2101219393722634242):canonicalSnapshot 非 null;(2) POST /admin/fleet/assignments/{id}/change(TRANSFER 派车行 2101174428044824578):code=200;(3) POST /admin/fleet/assignments/{id}/confirm(TRANSFER 派车行 2101174428711686146):code=200/confirmed=true/assignmentStatus=assigned(此前记录的 582093 已由 #8037 修复);(4) POST /admin/fleet/assignments/requirements/{requirementId}/confirm(TRANSFER 需求 2101567456596365313,订单 2101566624467419137):借新上线的 GET /admin/fleet/board/orders/{orderId} 返回的 requirementIdentities(#7990/#8048 一并带来的新字段,含 requirementVersion/requirementSha256/dispatchPlanGeneration 三个本端点必需的真值,此前全仓无接口能给出)取得 expectedRequirementVersion=1/expectedRequirementSha256=814e3f85.../expectedPlanGeneration=359984828583645184,groups[] 取自 candidates 返回的 retainedGroupIds 两个 assignmentGroupId,实测 code=200/confirmed=true/finalPlanPublished=true。四项证据补全,判据满足,回填 verified。" +updated_at: "2026-09-20" +base: "dev-v3" +--- + +# fleet/order-v3: TRANSFER 派车行确认/改派基线复核修复(605905/605041 不再误判) + +> **存放目录**: changelogs-v2/{YYYY-MM}/ +> +> **服务**: hl-fleet-service、hl-order-service-v3(共享 hl-common-core 的 Feign DTO) +> **PR**: 尚未创建(三个提交 `abd435c15`/`694e405fd`/`15c7cdd0c`,本地分支 `feature/7443-rest-ac`,尚未合入 `dev-v3`) +> **Issue**: #7443 AC-24 +> **日期**: 2026-09-18 +> **影响范围**: 管理后台车务派单弹窗——按需求原子确认、车务确认执行、修改派单、查询派单候选资源(Step2 canonical 快照)四个既有端点,仅当对象(派车行/需求/候选查询)属于 `kind=TRANSFER`(接送机)时行为发生变化;另有一处内部作业端点(重发最终方案快照)不面向 admin 前端 + +--- + +## ⚠️ 关键变化 + +**一句话**:`kind=TRANSFER` 建出来的派车行,以前建得出来但操作不动——现在通了。三个既有端点 +(按需求原子确认、车务确认执行、修改派单)的入参、出参**零变化**,变的只是内部"拿哪条需求当 +基线比对"的逻辑。 + +**根因**(写给前端知道以前为什么会失败):fleet 侧这三个端点的最终基线复核,原来一律拿 +`OrderFleetDetailContextDTO.vehicleRequirement` 当基线;而 order-v3 提供这个字段时**恒为 +TRAVEL**(`OrderFleetProviderService.java:1027-1029` 写死取 `kind=TRAVEL` 那条需求)。于是一条 +`requirement_id` 指向 TRANSFER 需求的派车行,在第一道 `requirementId` 比对上必然不等,从而被 +判定为"需求已过期/已变化"而拒绝——**接送机的车派得出来,却一步都确认/改派不了**。 + +**修法**:`OrderFleetDetailContextDTO`(`hl-common-core`)新增**可空**字段 +`transferVehicleRequirement`;order-v3 一并把订单当前生效的 TRANSFER 需求带回(服务日未回填时 +降级为 `null`,不让整个车务详情上下文报错)。fleet 侧新增私有方法 +`baselineRequirementFor(requirementId, context)`:传入的 `requirementId` 命中 +`transferVehicleRequirement` 就用那条,否则原样回退 `vehicleRequirement`(TRAVEL)—— +**TRAVEL 派车行的比对结果与改动前逐字相同**。 + +> ⚠️ **订正 2026-09-18 早些时候的说法**(本目录 `17_7443_*.md`、`18_7443_接送机用车需求分叉…` +> 两份文档的「六、边界行为」都写"确认、改派、基线变更复验三处内部调用……对 TRANSFER 派车行 +> 一律 605041"):这句话对 `POST /{assignmentId}/confirm` 与 `POST /{assignmentId}/change` +> 准确,但对 `POST /requirements/{requirementId}/confirm` **不准确**——经 2026-09-18 源码复核 +> (`AssignmentService.java:3567-3583` 的 `assertRequirementPreflightVersion`),该端点有一道 +> **更早**触发的版本预检门禁,直接读 `context.getVehicleRequirement()`(恒 TRAVEL)与请求体里的 +> `requirementId`(TRANSFER)比对,必不等 ⇒ **实际可观测的报错是 605905(需求版本过期), +> 不是 605041**;请求根本走不到后面真正会抛 605041 的 `assertFinalConfirmationBaseline` +> (`:3974`)。两处的根因完全相同(都是"拿 `context.getVehicleRequirement()` 当基线"), +> 本次修复对两处一并修,但**前端排查历史工单日志时请按实际报错码 605905 去找**,不要按 +> 605041 去找。 + +**不影响范围(前端最关心的一句)**:TRAVEL 派车行的确认/改派/复核行为**逐字未变**;上游 +`PUT /v3/admin/order/{id}/vehicle-requirement` 传 `kind=TRANSFER` 的写口目前仍然关着 +(809009,见 `18_7443_接送机用车需求分叉…` 一文),本次修复**不改变这一点**——没有那个开关, +生产环境仍然造不出真实的 TRANSFER 需求;本次修复解决的是"已经有一条 TRANSFER 需求/派车行时, +这三个端点能不能正常操作它"。 + +> **前端行动项**:无。本次三个端点的请求体、响应体结构一个字段都没有变,不需要改任何代码。 +> 如果贵方之前按 `18_7443_接送机用车需求分叉…` 文档的提示"前端本版暂缓接入 TRANSFER 派车行的 +> 确认/改派"搁置了相关联调,这条限制在**这三个端点自身**的维度已经解除;但由于上游写口 +> (809009)仍未开放,测试环境目前仍无法通过正常业务流程产出可供联调的 TRANSFER 数据。 + +> 🆕 **2026-09-18 当日追加两个提交,同一根因的另外两处落点**(分支已 rebase 到最新 `dev-v3`, +> 现为三个提交): +> +> **① `refinalizeFinalSnapshot`**(`AssignmentService.java:13092`,提交 `694e405fd`)——一个 +> **内部作业端点**(`POST /internal/fleet/jobs/vehicle-assignment-snapshot/refinalize`, +> `FleetJobInternalController`),不面向 admin 前端,供运维/作业系统补发存量订单的最终方案 +> 快照。坏法与前三处同形:读 `context.getVehicleRequirement()`(恒 TRAVEL),入参 +> `requirementId` 指向 TRANSFER 需求时必判"身份错位"而拒绝。⚠️ **源码注释与提交信息把这里的 +> 错误码写成了"605036",经核实是笔误**:该方法唯一的身份不一致分支 +> (`AssignmentService.java:13096-13098`)实际 `throw new BusinessException( +> AssignmentErrorCode.REQUIREMENT_VERSION_EXPIRED)`,即 **605905**("需求版本过期"), +> 605036 是另一个不相关的码(`CROSS_RESIDENT_CONFIRMATION_REQUIRED`,"司机与车辆不是常驻组合")。 +> 本文档按实测的 605905 记录,不采信注释里的 605036。这一处**不进本文档「二/三」的接口清单** +> (非 admin 端点),详见下方「六、边界行为」。 +> +> **② Step2 canonical 快照**(`Step2CanonicalSnapshotService.java:100`,提交 +> `15c7cdd0c`)——**这一处前端可见,是本次追加里最重要的一处**。四步向导第②步选车/选司机页 +> 用的 `POST /admin/fleet/assignments/candidates` 端点(`AssignmentCandidateRespVO +> .canonicalSnapshot` 字段)此前同样恒读 `context.getVehicleRequirement()`(TRAVEL)。 +> **坏法是静默的**:身份比对(`identityConsistent`)不等时**不抛任何错误码**,只有一条 +> `log.warn("Step2 快照身份不一致,拒绝建快照/更新")`,方法直接 `return null`——调用方拿到的是 +> **一个空字段,不是一次失败**。运营端表现为"四步向导第②步网格出不来",且**没有任何错误码指向 +> 成因**,排查只能靠日志。已收录为本文档新增的「三、接口详情」第 4 条,详见该节与「六、边界 +> 行为」。 + +--- + +## 二、变更接口清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---|------|------|------|----------|------| +| 1 | 按需求原子确认 | POST | `/admin/fleet/assignments/requirements/{requirementId}/confirm` | 行为修复(入参/出参契约不变) | `kind=TRANSFER` 需求不再在预检门禁上必现 605905 | +| 2 | 车务确认执行 | POST | `/admin/fleet/assignments/{assignmentId}/confirm` | 行为修复(入参/出参契约不变) | `kind=TRANSFER` 派车行的 holding 收尾与 ASSIGNED 复核两条内部分支不再必现 605041 | +| 3 | 修改派单 | POST | `/admin/fleet/assignments/{assignmentId}/change` | 行为修复(入参/出参契约不变) | `kind=TRANSFER` 派车行改派不再必现 605041 | +| 4 | 查询派单候选资源 | POST | `/admin/fleet/assignments/candidates` | 行为修复(入参/出参契约不变) | `kind=TRANSFER` 需求的 Step2 canonical 快照(`canonicalSnapshot` 字段)不再静默返回 `null` | + +> 另有一处**内部作业端点**(不进本表)同根因修复:`POST /internal/fleet/jobs/vehicle-assignment-snapshot/refinalize`, +> 详见「⚠️ 关键变化」①与「六、边界行为」。 + +--- + +## 三、接口详情 + +### 1. 按当前派车方案代际原子确认全部执行段 `POST /admin/fleet/assignments/requirements/{requirementId}/confirm` + +**VO**: `ConfirmRequirementReqVO → ConfirmRequirementRespVO` + +#### 使用场景 + +车务在派单弹窗对某条用车需求下**当前有效的全部执行段**做原子最终确认(#5827 起新派车提交即 +派定,本端点服务"已派定组的复核"与"存量 holding 组的收尾确认")。本次修复前,若 +`requirementId` 指向一条 `kind=TRANSFER`(接送机)需求,请求会在最早一道版本预检门禁 +(`assertRequirementPreflightVersion`)上必然判定"需求已过期"而拒绝,即使前端刚从看板拉取的 +`expectedRequirementVersion`/`expectedRequirementSha256` 完全正确也一样;修复后该门禁按 +`requirementId` 正确择取 TRANSFER 需求做比对,version/sha256 匹配时可正常通过。 + +#### 入参(本次零新增/零变化,全量 7 个字段逐一核对源码) + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| requirementId | Path | Long | 是 | - | 当前用车需求 ID;本次修复后,传一条 `kind=TRANSFER` 需求的 ID 时不再必然被判定为版本过期 | +| orderId | Body | Long | 是 | - | 订单 ID | +| requestId | Body | String | 是 | ≤64 | 幂等请求标识 | +| expectedRequirementVersion | Body | Integer | 是 | - | 预期当前有效用车需求版本 | +| expectedRequirementSha256 | Body | String | 是 | 64 位小写十六进制 | Board 返回的当前用车需求 canonical SHA-256 | +| expectedPlanGeneration | Body | Long | 是 | - | 预期当前最终派车方案代际 | +| groups[] | Body | Array | 是 | ≤50 项 | 当前有效执行段精确集合及各段行程短信选择 | +| groups[].assignmentGroupId | Body | Long | 是 | - | 当前有效派车组 ID | +| groups[].sendItinerarySms | Body | Boolean | 是 | - | 是否向本执行段司机发送行程短信 | + +#### 出参(本次零新增/零变化,全量字段) + +| 字段 | 类型 | 说明 | +|------|------|------| +| requirementId | String(雪花 ID,Long 转字符串序列化) | 用车需求 ID | +| dispatchPlanGeneration | String(雪花 ID) | 已确认的最终派车方案代际 | +| confirmed | Boolean | 整组是否原子确认成功 | +| finalPlanPublished | Boolean | 本次是否发布了最终方案快照;false 表示确认已成功但订单车控仍为处理中 | +| groups[] | Array | 各执行段确认结果 | +| groups[].assignmentId | String(雪花 ID) | 代表派单 ID | +| groups[].assignmentGroupId | String(雪花 ID) | 派车组 ID | +| groups[].assignmentStatus | String | 派单状态 | +| groups[].confirmedAt | LocalDateTime | 车务最终确认时间 | +| groups[].sendItinerarySms | Boolean | 是否选择发送本段行程短信 | +| groups[].itinerarySmsEventId | String(雪花 ID,未发送为 null) | 行程短信 Outbox 事件 ID | +| groups[].itinerarySmsStatus | String | 行程短信状态 | +| groups[].itineraryUrl | String | 本段电子行程单 H5 链接;签发不可用时为 null | + +#### 请求示例 + +```json +POST /admin/fleet/assignments/requirements/1934567890123457001/confirm +{ + "orderId": 1934567890123456789, + "requestId": "confirm-req-20260918-0001", + "expectedRequirementVersion": 3, + "expectedRequirementSha256": "a1b2c3d4e5f60718293a4b5c6d7e8f90112233445566778899aabbccddeeff", + "expectedPlanGeneration": 5, + "groups": [ + {"assignmentGroupId": 1934567890123457100, "sendItinerarySms": true} + ] +} +``` + +上例 `requirementId=1934567890123457001` 是一条 `kind=TRANSFER` 需求;本次修复前该请求必返 +605905,version/sha256 是否匹配无关紧要。 + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": { + "requirementId": "1934567890123457001", + "dispatchPlanGeneration": "5", + "confirmed": true, + "finalPlanPublished": true, + "groups": [ + { + "assignmentId": "1934567890123457100", + "assignmentGroupId": "1934567890123457100", + "assignmentStatus": "assigned", + "confirmedAt": "2026-09-18 15:00:00", + "sendItinerarySms": true, + "itinerarySmsEventId": "1934567890123457200", + "itinerarySmsStatus": "PENDING", + "itineraryUrl": "https://h5.example.com/#/itinerary/eyJvcmRlcklkIjoi..." + } + ] + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +N/A。本端点是需求级原子确认动作,要么全部执行段确认成功并返回完整 `groups[]`,要么整体失败 +关闭返回错误响应;不存在"部分成功"或 `data` 为空数组/null 的成功响应形态。 + +#### 错误响应 + +```json +{ + "code": 605905, + "message": "需求版本过期", + "data": null, + "success": false +} +``` + +> ⚠️ **响应信封订正**:`Result` 只有 `code`/`message`/`data`/`traceId` 四个字段,业务失败时 +> `code` 本身就是业务错误码,HTTP 状态码固定 200,**不存在独立的 `errorCode` 字段**; +> `success` 是 `Result.isSuccess()`(`code==200`)序列化出的真实字段。此端点未被 +> Controller 专门 catch,异常经 `GlobalExceptionHandler.handleBusiness` 兜底,`data` +> 固定为 `null`(不含 `dailyDifferences`,与下面两个端点不同)。 + +#### 业务边界 + +- 本次修复前,仅因 `requirementId` 指向 `kind=TRANSFER` 需求就必然命中上面这条 605905,即便 + `expectedRequirementVersion`/`expectedRequirementSha256` 与前端刚拉取的完全一致;修复后只在 + 需求确实已发生变化(如被他人换版/改派)时才会命中 +- 若通过预检门禁后,在需求级原子确认循环内部(`assertFinalConfirmationBaseline`, + `AssignmentService.java:3974`)仍检出基线不一致,同样按 605041 + (`FINAL_CONFIRMATION_BASELINE_MISMATCH`,"订单行程或用车派单已变化,请按逐日差异处理后 + 重试")拒绝;但该调用点不经 Controller 的专门 catch,响应 `data` 固定为 `null`,**不返回 + `dailyDifferences`**(与「2. 车务确认执行」「3. 修改派单」两个端点不同,那两处会返回结构化 + 差异) +- `kind=TRAVEL` 需求的确认流程、错误码语义与本次改动前逐字一致 + +--- + +### 2. 车务确认执行(holding→assigned) `POST /admin/fleet/assignments/{assignmentId}/confirm` + +**VO**: `ConfirmReqVO → ConfirmRespVO` + +#### 使用场景 + +两类入口共用本端点:①存量 holding 组的收尾确认;②已派定组的复核——重跑最终确认基线 + 行程 +短信决策一致性校验 + 接送机门禁,通过后重发最终方案快照。本次修复覆盖这两条内部分支 +(holding 收尾在 `AssignmentService.java:4246/4279`,ASSIGNED 复核经 +`revalidateAssignedAfterBaselineChange` 于 `:4462`)对 `kind=TRANSFER` 派车行的基线选取—— +两处修复前都直接读 `context.getVehicleRequirement()`(恒 TRAVEL),派车行的 +`assignmentId` 无论对应哪条需求,基线比对都必然落到 TRAVEL 上。 + +#### 入参(本次零新增/零变化,全量 5 个字段) + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| assignmentId | Path | Long | 是 | - | 派单 ID;本次修复后,若该行属于 `kind=TRANSFER` 需求,不再必然 605041 | +| sendItinerarySms | Body | Boolean | 是 | - | 是否在最终确认后向该车师傅发送行程短信 | +| requestId | Body | String | 是 | ≤64 | 幂等请求标识 | +| vehicleFeeTotal | Body | BigDecimal | 否 | 已废弃,传值即被拒绝 | 历史字段,最终总车费已改为逐日车费只读合计 | +| vehicleFeeAdjustmentReason | Body | String | 否 | ≤256,已废弃,传值即被拒绝 | 历史字段,逐日车费调整原因应在派车保存时提交 | + +#### 出参(本次零新增/零变化,全量字段,含 `sideEffects`/`dailyDifferences` 展开) + +| 字段 | 类型 | 说明 | +|------|------|------| +| confirmed | Boolean | 是否已完成最终确认 | +| assignmentStatus | String | 派单状态(assigned=已派定) | +| stageCode | String | 生命周期阶段码 | +| stageLabel | String | 生命周期阶段文案 | +| currentStep | Integer | 当前三阶段步骤(订单详情/排车/确认执行) | +| assignmentGroupId | String(雪花 ID) | 派车组 ID(同一辆车连续每日切片共用) | +| vehicleFeeTotal | String(BigDecimal 转字符串) | 最终确认后的该车本派车段总车费 | +| vehicleFeeSource | String | 最终总车费来源:AUTO、MANUAL | +| vehicleFeeAdjustmentReason | String | 手工总车费调整原因 | +| confirmedAt | LocalDateTime | 车务确认派车时间 | +| sendItinerarySms | Boolean | 车务是否选择向该车师傅发送行程短信 | +| itinerarySmsEventId | String(雪花 ID) | 行程短信可靠事件 ID;不发送为空 | +| itinerarySmsStatus | String | 确认响应中的初始短信状态(PENDING,NOT_SENT) | +| itineraryUrl | String | 电子行程单 H5 签名链接 | +| sideEffects | Object | 副作用执行结果,见下方展开 | +| sideEffects.vehicleStatusUpdated | String | 车辆状态更新结果(idle,busy,maint;不涉及车为 null) | +| sideEffects.driverStatusUpdated | String | 司机状态更新结果(busy,idle;不涉及司机为 null) | +| sideEffects.reconPrepRowsCreated | Integer | 对账预备单创建条数(M1 降级恒返 0) | +| sideEffects.reconPrepMarkedCanceled | Integer | 对账预备单标记取消条数(M1 降级恒返 0) | +| dailyDifferences | Array | 确认失败时的逐日基线差异;成功时为空 | +| dailyDifferences[].serviceDate | LocalDate | 服务日 | +| dailyDifferences[].differenceType | String | 差异类型枚举,见「六.5」 | +| dailyDifferences[].differenceLabel | String | 差异中文名称 | +| dailyDifferences[].assignmentId | String(雪花 ID) | 派单 ID | +| dailyDifferences[].assignmentSlotId | String(雪花 ID) | 稳定车辆槽位 ID | +| dailyDifferences[].passengerCount | Integer | 订单乘客人数(不含司机) | +| dailyDifferences[].message | String | 差异说明 | + +#### 请求示例 + +```json +POST /admin/fleet/assignments/1934567890123457100/confirm +{ + "sendItinerarySms": true, + "requestId": "fleet-final-confirm-20260918-0001" +} +``` + +上例 `assignmentId=1934567890123457100` 是一条挂在 `kind=TRANSFER` 需求下的派车行;本次修复前 +该请求必返 605041。 + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": { + "confirmed": true, + "assignmentStatus": "assigned", + "stageCode": "assigned", + "stageLabel": "已派车", + "currentStep": 3, + "assignmentGroupId": "1934567890123457100", + "vehicleFeeTotal": "700.00", + "vehicleFeeSource": "AUTO", + "vehicleFeeAdjustmentReason": null, + "confirmedAt": "2026-09-18 15:05:00", + "sendItinerarySms": true, + "itinerarySmsEventId": "1934567890123457200", + "itinerarySmsStatus": "PENDING", + "itineraryUrl": "https://h5.example.com/#/itinerary/eyJvcmRlcklkIjoi...", + "sideEffects": { + "vehicleStatusUpdated": "busy", + "driverStatusUpdated": "busy", + "reconPrepRowsCreated": 0, + "reconPrepMarkedCanceled": 0 + }, + "dailyDifferences": null + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +N/A。本端点确认成功时恒返回完整对象(`sideEffects` 各字段可能为 null,但不是"空数据"形态); +失败时按下方错误响应返回,不存在中间态。 + +#### 错误响应 + +```json +{ + "code": 605041, + "message": "订单行程或用车派单已变化,请按逐日差异处理后重试", + "data": { + "confirmed": false, + "dailyDifferences": [ + { + "serviceDate": null, + "differenceType": "REQUIREMENT_VERSION_MISMATCH", + "differenceLabel": "用车需求已更新", + "assignmentId": "1934567890123457100", + "assignmentSlotId": "1934567890123457099", + "passengerCount": null, + "message": "用车需求已更新,请刷新后按最新需求重新派车" + } + ] + }, + "success": false +} +``` + +#### 业务边界 + +- 本次修复前,`kind=TRANSFER` 派车行走 holding 收尾确认或 ASSIGNED 复核任一分支都**必现** + 605041,与请求体字段是否正确无关(两条分支都不接受调用方传 `requirementId`,只能按 + `assignmentId` 反查行、再拿上下文里恒为 TRAVEL 的需求比对);修复后按该行自己的 + `requirementId` 正确匹配 TRANSFER 需求 +- 若基线确实发生变化(如换版、需求被取消),仍会返回 605041 及 `data.dailyDifferences`,与本次 + 改动前逐字一致;前端展示优先用 `differenceLabel`/`message`,`differenceType` 仅供逻辑判断 +- `kind=TRAVEL` 派车行的确认流程、`sideEffects`/`dailyDifferences` 语义与本次改动前逐字一致 +- 旧 HOLD 通知结果仍在确认中的场景(605042 等)不在本次修复范围内,行为未变 + +--- + +### 3. 修改派单(按槽位和生效日) `POST /admin/fleet/assignments/{assignmentId}/change` + +**VO**: `ChangeAssignmentReqVO → ChangeAssignmentRespVO` + +#### 使用场景 + +按稳定车辆槽位和生效日原子换车、换司机或同时替换;改派恒执行最终基线复核。本次修复覆盖 +`doChangeInLock` 内部对 `kind=TRANSFER` 派车行的基线选取(`AssignmentService.java:5848` 附近, +修复前 `assertFinalConfirmationBaseline` 的 `requirementOverride` 显式传 `null`,回退读 +`context.getVehicleRequirement()` 恒为 TRAVEL)。 + +#### 入参(本次零新增/零变化,全量 14 个字段,含嵌套 `dailyVehicleFees[]`) + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| assignmentId | Path | Long | 是 | - | 当前车辆槽位任一派单 ID;本次修复后,若该行属于 `kind=TRANSFER` 需求,不再必然 605041 | +| effectiveDate | Body | LocalDate | 是 | - | 改派生效日(含当日) | +| serviceDates | Body | Array\ | 否 | - | 按天改派限定的服务日期集;空=生效日起连续后缀整体改派 | +| newVehicleId | Body | Long | 否 | - | 新车辆 ID;不换车可空 | +| newDriverId | Body | Long | 否 | - | 新司机 ID;不换司机可空 | +| sendItinerarySms | Body | Boolean | 否 | 不传按 false | 是否向改派后的师傅发送行程短信 | +| holdMode | Body | Integer | 否 | 已废弃,服务端忽略 | #5827 起一律一步派定,勿再传 | +| messageTemplateId | Body | Long | 否 | 已废弃 | 不再消费 | +| customBody | Body | String | 否 | ≤4000,已废弃 | 不再消费 | +| protocolPrice | Body | BigDecimal | 否 | ≥0.00,整数最多10位/小数最多2位 | 新协议价日单价;可空按价格日历计算 | +| vehicleFeeTotal | Body | BigDecimal | 否 | 已废弃,传值即被拒绝 | 历史字段 | +| vehicleFeeAdjustmentReason | Body | String | 否 | ≤256 | 新派车段单日车费与价格日历不一致时的调整原因 | +| dailyVehicleFees[] | Body | Array | 否 | - | 新派车段逐日车费;未提交的收费日按价格日历取价 | +| dailyVehicleFees[].serviceDate | Body | LocalDate | 是(数组项内) | - | 车费服务日期 | +| dailyVehicleFees[].price | Body | BigDecimal | 是(数组项内) | ≥0.00 | 本次派车单日车费;只覆盖本次派车,不回写价格日历 | +| retainedVehicleFeeTotal | Body | BigDecimal | 否 | 已废弃,传值即被拒绝 | 历史字段 | +| retainedVehicleFeeAdjustmentReason | Body | String | 否 | ≤256,已废弃 | 历史字段 | +| chargeableServiceDates | Body | Array\ | 否 | - | 生效日起收取车费的服务日期;不传沿用原值 | +| vehicleFeeWaiverReason | Body | String | 否 | ≤256 | 免费服务日原因;全部免费时必填 | +| confirmAllServiceDatesFree | Body | Boolean | 否 | - | 目标日期全部免费二次确认 | +| confirmCrossResident | Body | Boolean | 否 | - | 跨常驻车显式确认 | +| reason | Body | String | 是 | ≤256 | 修改原因 | +| requestId | Body | String | 是 | ≤64 | 请求幂等 ID | + +#### 出参(本次零新增/零变化,全量字段,含 `otherVehicles[]`/`dailyVehicleFees[]`/`dailyDifferences[]` 展开) + +| 字段 | 类型 | 说明 | +|------|------|------| +| assignmentId | String(雪花 ID) | 新派单锚点 ID | +| assignmentSlotId | String(雪花 ID) | 稳定车辆槽位 ID;改派前后保持不变 | +| previousAssignmentGroupId | String(雪花 ID) | 被替换的旧派车组 ID | +| newAssignmentGroupId | String(雪花 ID) | 新派车组 ID | +| assignmentStatus | String | 新派车组状态 | +| effectiveDate | LocalDate | 改派实际生效日(含当日) | +| affectedDays | Integer | 本次原子替换的逐日切片数量 | +| protocolPrice | String(BigDecimal 转字符串) | 新协议价日单价快照(元/车天) | +| vehicleFeeAutoTotal | String(BigDecimal) | 改派后价格日历自动合计参考 | +| vehicleFeeAutoComplete | Boolean | 改派后自动合计是否覆盖全部计费服务日 | +| vehicleFeeTotal | String(BigDecimal) | 改派后新派车段逐日车费只读合计 | +| vehicleFeeSource | String | 改派后总车费来源:AUTO、MANUAL、INCOMPLETE | +| vehicleFeeAdjustmentReason | String | 改派后单日车费调整原因 | +| dailyVehicleFees[] | Array | 改派后逐日价格日历参考价和本次派车价 | +| dailyVehicleFees[].serviceDate | LocalDate | 服务日期 | +| dailyVehicleFees[].chargeable | Boolean | 是否收取车费 | +| dailyVehicleFees[].calendarPrice | String(BigDecimal) | 车型价格日历参考价;缺价为空 | +| dailyVehicleFees[].assignmentPrice | String(BigDecimal) | 本次派车单日车费;收费日必有值 | +| dailyVehicleFees[].source | String | 价格来源:CALENDAR、OVERRIDE、FREE、MISSING | +| dailyVehicleFees[].calendarPriceMissing | Boolean | 价格日历是否缺价 | +| otherVehicleCount | Integer | 同订单其它有效车辆槽位数 | +| warningCode | String | 强提示代码;无其它车辆时为空 | +| warningMessage | String | 强提示文案;无其它车辆时为空 | +| otherVehicles[] | Array | 同订单其它有效车辆摘要 | +| otherVehicles[].assignmentSlotId | String(雪花 ID) | 其它稳定车辆槽位 ID | +| otherVehicles[].vehiclePlate | String | 当前车牌号 | +| otherVehicles[].driverName | String | 当前司机名称 | +| otherVehicles[].startDate | LocalDate | 当前有效服务日起 | +| otherVehicles[].endDate | LocalDate | 当前有效服务日止 | +| sendItinerarySms | Boolean | 车务本次是否选择向改派后的师傅发送行程短信 | +| itinerarySmsEventId | String(雪花 ID) | 行程短信可靠事件 ID;未勾选发送为空 | +| itinerarySmsStatus | String | 本次改派的初始短信状态(PENDING,NOT_SENT) | +| dailyDifferences[] | Array | 改派最终派定失败时的逐日基线差异(结构同「2. 车务确认执行」的 `dailyDifferences`) | + +#### 请求示例 + +```json +POST /admin/fleet/assignments/1934567890123457100/change +{ + "effectiveDate": "2026-10-11", + "newVehicleId": 1934567890123456701, + "newDriverId": 1934567890123456702, + "sendItinerarySms": false, + "reason": "司机临时无法执行,替换车辆和司机", + "requestId": "change-20260918-0001" +} +``` + +上例 `assignmentId=1934567890123457100` 是一条挂在 `kind=TRANSFER` 需求下的派车行;本次修复前 +该请求必返 605041。 + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": { + "assignmentId": "1934567890123457300", + "assignmentSlotId": "1934567890123457099", + "previousAssignmentGroupId": "1934567890123457100", + "newAssignmentGroupId": "1934567890123457300", + "assignmentStatus": "assigned", + "effectiveDate": "2026-10-11", + "affectedDays": 1, + "protocolPrice": "1688.00", + "vehicleFeeAutoTotal": "1688.00", + "vehicleFeeAutoComplete": true, + "vehicleFeeTotal": "1688.00", + "vehicleFeeSource": "AUTO", + "vehicleFeeAdjustmentReason": null, + "dailyVehicleFees": [ + { + "serviceDate": "2026-10-11", + "chargeable": true, + "calendarPrice": "1688.00", + "assignmentPrice": "1688.00", + "source": "CALENDAR", + "calendarPriceMissing": false + } + ], + "otherVehicleCount": 0, + "warningCode": null, + "warningMessage": null, + "otherVehicles": [], + "sendItinerarySms": false, + "itinerarySmsEventId": null, + "itinerarySmsStatus": "NOT_SENT", + "dailyDifferences": null + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +N/A。本端点改派成功时恒返回完整对象(`otherVehicles` 无其它车辆时为空数组而非缺失字段); +失败时按下方错误响应返回。 + +#### 错误响应 + +```json +{ + "code": 605041, + "message": "订单行程或用车派单已变化,请按逐日差异处理后重试", + "data": { + "dailyDifferences": [ + { + "serviceDate": null, + "differenceType": "REQUIREMENT_VERSION_MISMATCH", + "differenceLabel": "用车需求已更新", + "assignmentId": "1934567890123457100", + "assignmentSlotId": "1934567890123457099", + "passengerCount": null, + "message": "用车需求已更新,请刷新后按最新需求重新派车" + } + ] + }, + "success": false +} +``` + +#### 业务边界 + +- 本次修复前,`kind=TRANSFER` 派车行改派**必现** 605041,与请求体字段是否正确无关(`change` + 端点不接受调用方传 `requirementId`,只能按 `assignmentId` 反查所属行,再拿上下文里恒为 TRAVEL + 的需求比对);修复后按该行自己的 `requirementId` 正确匹配 TRANSFER 需求 +- 若基线确实发生变化,仍会返回 605041 及 `data.dailyDifferences`,与本次改动前逐字一致 +- 同订单存在其它车辆槽位时仍返回 `warningCode=ORDER_HAS_OTHER_VEHICLES` 及摘要,前端仍须强提示 + (行为未变) +- `kind=TRAVEL` 派车行的改派流程、错误码语义与本次改动前逐字一致 + +--- + +### 4. 查询派单候选资源 `POST /admin/fleet/assignments/candidates` + +**VO**: `AssignmentCandidateReqVO → AssignmentCandidateRespVO` + +> ⚠️ **本节范围声明**:本端点是派单弹窗车辆/司机候选查询的既有端点,入参 24 个字段、出参 +> 涵盖车辆候选、司机候选、常驻关系、动态筛选项等一整套与本次修复无关的结构。**本次修复只影响 +> 出参里的 `canonicalSnapshot` 一个字段**(及其内部 `resolveCanonicalSnapshot` 的基线选取逻辑), +> 下方入参/出参表**只列与本次修复直接相关的字段**,其余约 20 个入参字段与 `vehicles[]`/ +> `drivers[]`/`fleetTeamFacets[]`/`vehicleTypeFacets[]`/`selectedDriverResidentVehicle` 等 +> 出参结构本次**零改动**,未逐一列出。 + +#### 使用场景 + +四步向导第②步(选车/选司机)加载页面时调用。本次修复前,若 `requirementId` 指向一条 +`kind=TRANSFER`(接送机)需求,响应里 `vehicles[]`/`drivers[]` 等候选数据正常返回,但 +**`canonicalSnapshot` 字段恒为 `null`**——`Step2CanonicalSnapshotService.resolveCanonicalSnapshot` +内部的 `identityConsistent` 身份比对拿 `context.getVehicleRequirement()`(恒 TRAVEL)与请求的 +`requirementId`(TRANSFER)比对,必不等,**不抛错误码,只打一条 `log.warn` 后直接返回 `null`**。 +运营端表现为"网格出不来",没有任何错误提示能指向成因。修复后 `Step2CanonicalSnapshotService` +与 `AssignmentService` 共用新抽出的 `BaselineRequirementSelector.select(requirementId, context)` +按请求自己的 `requirementId` 正确择取 TRANSFER 需求。 + +**可验证的效果句**(工单 #7443 AC-24 给定判据):对一条 `kind=TRANSFER` 的活跃需求调 +`resolveCanonicalSnapshot`(该 `requirementId`),返回非 `null`,且 `editableServiceDates` +恰为该需求的两条航班日、`cells` 覆盖这两日、其中已派车那一日的 cell `used = "USED"` 且 +`assignmentId` 指向该派车行。 + +#### 入参(本节只列与本次修复相关的字段;其余约 20 个字段本次零改动,未列出) + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| orderId | Body | Long | 改派排除自身时必填 | - | 当前订单 ID | +| requirementId | Body | Long | 改派排除自身时必填 | - | 当前用车需求 ID;本次修复后,传一条 `kind=TRANSFER` 需求的 ID 时 `canonicalSnapshot` 不再必然为 `null` | + +#### 出参(本节只列 `canonicalSnapshot` 极其内部结构;`vehicles[]`/`drivers[]`/其余顶层字段本次零改动,未列出) + +| 字段 | 类型 | 说明 | +|------|------|------| +| canonicalSnapshot | Object | Step2 canonical full snapshot;无 `requirementId` 或需求上下文不可用时为 `null`(本次修复前,`kind=TRANSFER` 需求也会落入这个 `null`,是本次修复的对象) | +| canonicalSnapshot.planGeneration | String(雪花 ID,`required=true`) | 当前计划代际(不透明令牌) | +| canonicalSnapshot.snapshotVersion | String(雪花 ID,`required=true`) | 当前快照版本;同代内容修订递增 | +| canonicalSnapshot.retainedGroupIds | Array\(`required=true`) | 有序 RetainedGroupSet:稳定派车组 ID | +| canonicalSnapshot.editableServiceDates | Array\(`required=true`) | 完整有序可编辑服务日集合 | +| canonicalSnapshot.cells | Array(`required=true`) | 每个派车组×服务日笛卡尔积位置唯一 cell | +| canonicalSnapshot.cells[].groupId | String(雪花 ID) | 稳定派车组 ID | +| canonicalSnapshot.cells[].serviceDate | LocalDate | 服务日期 | +| canonicalSnapshot.cells[].assignmentId | String(雪花 ID) | 当天逐日派车行 ID;无行为 null | +| canonicalSnapshot.cells[].used | String | `USED`=实际用车/`UNUSED`=显式不用车/`null`=无切片行 | +| canonicalSnapshot.cells[].readOnly | Boolean | 该 cell 是否只读 | +| canonicalSnapshot.cells[].vehicleId | String(雪花 ID) | 当天车辆 ID | +| canonicalSnapshot.cells[].driverId | String(雪花 ID) | 当天司机 ID | +| canonicalSnapshot.selectedVehicle | Object | 已选车辆最小脱敏展示快照;未选为 null(结构未动) | +| canonicalSnapshot.selectedDriver | Object | 已选司机最小脱敏展示快照;未选为 null(结构未动) | + +> `CanonicalSnapshotVO` 五个 `required=true` 字段(`planGeneration`/`snapshotVersion`/ +> `retainedGroupIds`/`editableServiceDates`/`cells`)是 **Swagger 注解表达的契约意图,不是 +> 运行时观测**——它们只在 `canonicalSnapshot` 本身非 `null` 时必然存在;`canonicalSnapshot` +> 整体为 `null` 时这五个字段自然也拿不到。`Swagger required` 描述的是"存在时必填",不是 +> "恒存在"。 + +#### 请求示例 + +```json +POST /admin/fleet/assignments/candidates +{ + "orderId": 1934567890123456789, + "requirementId": 1934567890123457001, + "startDate": "2026-10-11", + "endDate": "2026-10-17", + "vehiclePage": 1, + "vehiclePageSize": 20, + "driverPage": 1, + "driverPageSize": 20 +} +``` + +上例 `requirementId=1934567890123457001` 是一条 `kind=TRANSFER` 需求;本次修复前 +`data.canonicalSnapshot` 必为 `null`(`vehicles`/`drivers` 等其余字段不受影响,正常返回)。 + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": { + "canonicalSnapshot": { + "planGeneration": "1934567890123457500", + "snapshotVersion": "1", + "retainedGroupIds": ["1934567890123457100"], + "editableServiceDates": ["2026-10-11", "2026-10-17"], + "cells": [ + { + "groupId": "1934567890123457100", + "serviceDate": "2026-10-11", + "assignmentId": "1934567890123457100", + "used": "USED", + "readOnly": false, + "vehicleId": "2064998142394183681", + "driverId": "2065272150960357378" + }, + { + "groupId": "1934567890123457100", + "serviceDate": "2026-10-17", + "assignmentId": null, + "used": null, + "readOnly": false, + "vehicleId": null, + "driverId": null + } + ], + "selectedVehicle": null, + "selectedDriver": null + } + }, + "success": true +} +``` + +(省略号:本响应实际还含 `vehicles`/`drivers`/`fleetTeamFacets` 等与本次修复无关的字段, +上例只展示 `canonicalSnapshot`。) + +#### 空数据 / 降级响应 + +`canonicalSnapshot` 为 `null` 是本端点**合法的既有降级形态**,不是本次修复要消灭的东西—— +未传 `requirementId`、或需求上下文确实不可达(如 809007 服务日未回填导致 +`transferVehicleRequirement` 也取不到)时,`canonicalSnapshot` 仍然合法为 `null`。本次修复只 +消灭了"`requirementId` 合法指向一条**存在**的 TRANSFER 需求,却因基线选错而被误判身份不一致" +这一种 `null` 成因;其余合法 `null` 成因不受影响。 + +```json +{ + "code": 200, + "message": "成功", + "data": { + "canonicalSnapshot": null + }, + "success": true +} +``` + +#### 错误响应 + +本端点的 `canonicalSnapshot` 解析路径**不抛错误码**(这正是「使用场景」里描述的"坏法是静默的"); +`resolveCanonicalSnapshot` 内部锁获取失败时会抛 `CommonErrorCode.RESOURCE_LOCKED` +(`hl-common-core/.../CommonErrorCode.java:67-68`),但这是既有的锁竞争兜底,与本次修复的身份 +比对分支无关,本次未改动: + +```json +{ + "code": 100503, + "message": "资源被占用,请稍后重试", + "data": null, + "success": false +} +``` + +#### 业务边界 + +- 🔴 **本条【假设】未验证,请 mmg 自验**:`canonicalSnapshot` 为 `null` 时四步向导第②步前端 + 会表现出什么效果(空白网格/报错提示/直接不可操作),本单**没有测试前端行为**,不替前端下 + 结论。上文"运营看到网格出不来"是问题背景陈述,不是对当前前端代码行为的实测断言 +- `canonicalSnapshot` 为 `null` 与"请求本身失败"是两回事:响应 `code` 仍是 200、`success` + 仍是 `true`,`vehicles[]`/`drivers[]` 等其余候选数据不受影响,只有这一个字段拿不到 +- `kind=TRAVEL` 需求的 `canonicalSnapshot` 解析行为逐字不变 +- 本条修复与本文档前三条同根因、同修法(`BaselineRequirementSelector`),但触发路径不同 + (前三条走 HTTP 错误响应,这一条走静默降级字段),前端排查时不能用同一套"看错误码"的思路 + +--- + +## 四、契约约束与正确调用方式 + +> 本节只写后端接受/拒绝 payload 的规则,不写 UI 渲染建议。 + +| 场景 | 做法 | +|------|------| +| `requirementId`/`assignmentId` 指向一条 `kind=TRANSFER` 需求/派车行 | 三个端点的请求体格式**不需要任何调整**——与 `kind=TRAVEL` 完全一样传参即可,fleet 内部会自动按行/需求自己的 ID 择基线 | +| `POST /requirements/{id}/confirm` 拿到 605905 | 与本次修复前不同:现在**只**代表需求版本确实过期(被他人换版/改派),需要重新拉取最新的 `expectedRequirementVersion`/`expectedRequirementSha256` 后重试,**不再**是"TRANSFER 需求天生用不了这个端点"的信号 | +| `POST /{assignmentId}/confirm`、`POST /{assignmentId}/change` 拿到 605041 | 同上,现在只代表基线确实变化,按 `data.dailyDifferences[].differenceLabel`/`message` 提示用户刷新重试 | +| 需要创建/查询一条真实的 `kind=TRANSFER` 需求用于联调 | 目前仍需 SQL 直接构造(上游写口 809009 未开放),不在本次修复范围内 | + +--- + +## 五、数据库行为 + +| 操作 | 影响 | +|------|------| +| `kind=TRANSFER` 派车行调用上述三接口,基线校验通过 | 与 `kind=TRAVEL` 完全相同的既有落库逻辑(`fleet_assignment` 状态流转、`confirmed_at`/`assignment_status` 等列更新),不因 `kind` 不同而有任何额外列或额外写入 | +| `kind=TRANSFER` 派车行调用上述三接口,基线校验未通过(需求确实已变化) | 与 `kind=TRAVEL` 相同:整体拒绝,不落库,不改变已有行状态 | +| `kind=TRAVEL` 派车行调用上述三接口 | 落库行为与本次改动前逐字一致 | +| `OrderFleetDetailContextDTO` 新增字段 `transferVehicleRequirement` | 纯内存态 Feign 响应字段,不对应任何新增数据库列 | + +--- + +## 六、边界行为 + +- `transferVehicleRequirement` 为 `null` 的三种合法情形(源码 javadoc 逐字, + `OrderFleetProviderService.java:336-354`):①该订单没有接送机需求(绝大多数订单); + ②接送机需求存在但服务日尚未回填(809007 降级为 `null`,不让整个订单车务详情报错); + ③订单已终态。三种情形下 `baselineRequirementFor` 都会回退到 `vehicleRequirement`(TRAVEL), + 行为与改动前一致 +- 服务日尚未回填的 `kind=TRANSFER` 需求:本次修复**不改变**这类需求仍无法被正常操作的事实 + (`transferVehicleRequirement` 取不到即为 `null`,三端点仍按原口径拒绝);服务日回填属 + #7443 AC-6~9 范畴,已于今天早些时候的 PR #7911/#7912/#7913 交付(见 + `18_7443_接送机用车需求分叉…` 文档) +- `POST /requirements/{requirementId}/confirm` 对 `kind=TRANSFER` 的实际可观测报错是 + **605905**(`assertRequirementPreflightVersion` 更早触发的预检门禁),不是 605041——本条 + 订正了 2026-09-18 早些时候 `17_7443_*.md`/`18_7443_接送机用车需求分叉…` 两份文档"三处均 + 605041"的表述,理由与出处见上方「⚠️ 关键变化」 +- `POST /{assignmentId}/confirm`、`POST /{assignmentId}/change` 对 `kind=TRANSFER` 的实际可 + 观测报错才是 605041 +- 本次修复还覆盖了一处**非 HTTP 端点**的内部判据:`isRequirementEligibleForReopen` + (`AssignmentService.java:9135`,commit 内注释标注"第 4 个落点")。它被两条异步补偿链路调用 + (`AssignmentLifecycleOutboxEffectService.java:136` 撤销"整段不用车"声明后的需求重开、`:264` + 取消派车后的补偿重开),修复前对 `kind=TRANSFER` 需求恒返回 `false`,两条补偿链路对接送机 + 需求整条失效(需求会卡在 `DONE`/`PROCESSING` 不自愈,且不报任何错误)。前端**无法直接感知** + 这一点——它不产生任何同步 HTTP 响应,只在"撤销不用车声明"或"取消已派车后需求应否被重开"的 + 排查场景可能相关,记录于此供后续问题定位参考 +- **`refinalizeFinalSnapshot`(内部作业端点,提交 `694e405fd`,`AssignmentService.java:13092`)**: + 与 `isRequirementEligibleForReopen` 同形,入参 `requirementId` 由 + `FleetJobInternalController`(`POST /internal/fleet/jobs/vehicle-assignment-snapshot/refinalize`) + 原样传入、不带 `kind`,修复前恒读 `context.getVehicleRequirement()`(TRAVEL),TRANSFER 需求 + 进来在 `:13096-13098` 的身份比对上必然不等,**整条重发最终方案快照的补偿链路对 TRANSFER + 不可用**。抛出的错误码是 **605905**(`REQUIREMENT_VERSION_EXPIRED`,"需求版本过期")—— + ⚠️ 该方法的源码注释与提交信息把这里写成"报 605036",经对照 `:13097-13098` 的实际 + `throw` 语句核实为**笔误**(605036 是 `CROSS_RESIDENT_CONFIRMATION_REQUIRED`,"司机与车辆 + 不是常驻组合",与本方法无关),本文档按源码实测的 605905 记录。本端点不面向 admin 前端, + 不进「二、变更接口清单」 +- **Step2 canonical 快照(提交 `15c7cdd0c`,`Step2CanonicalSnapshotService.java:100`)**: + 详见「三、接口详情」第 4 条。补充一点边界行为——`identityConsistent` 的身份比对**除 + `requirementId` 外还比对 `orderId`**(`Step2CanonicalSnapshotService.java:136-154`),本次 + 修复只解决了 `requirementId` 因恒读 TRAVEL 而误判的那部分;若 `orderId` 本身传错, + 修复前后同样会被判定"身份不一致"并静默返回 `null`,这不是本次修复要处理的场景 +- **`BaselineRequirementSelector`(提交 `15c7cdd0c`,`assignment/support/` 新增类)**:原来 + fleet 内部只有 `AssignmentService` 私有方法 `baselineRequirementFor` 一份择基线逻辑,本次 + 抽成独立静态工具类 `BaselineRequirementSelector.select(requirementId, context)`, + `AssignmentService`(确认/改派/复验/重开/重发快照五处)与 `Step2CanonicalSnapshotService` + (候选查询一处)共用同一份实现(CODE_RULES §15.7 对称子域禁镜像重复)。这是**内部重构,不 + 改变任何对外行为或字段**,此处只作记录,不影响上面任何一条边界行为 +- `kind=TRAVEL` 派车行三个端点、`isRequirementEligibleForReopen`、`refinalizeFinalSnapshot`、 + Step2 canonical 快照的全部行为逐字不变(`baselineRequirementFor`/`BaselineRequirementSelector` + 对非 TRANSFER 命中的 `requirementId` 一律回退到原 `context.getVehicleRequirement()`) +- 本次修复零 DB 迁移,不改变 `fleet_assignment`/`vehicle_requirement` 任一张表结构 +- 上游 `PUT /v3/admin/order/{id}/vehicle-requirement` 传 `kind=TRANSFER` 的写口仍然关着 + (809009,`transfer-kind-submit-enabled` 默认 `false`),本次修复**不改变这一点**——没有这个 + 开关,生产环境依然造不出真实的 TRANSFER 需求;本次修复只在"已经存在一条 TRANSFER 需求/ + 派车行(如通过 SQL 构造,或未来开关打开后)"的前提下起作用 + +--- + +## 六.5 枚举(本次不新增错误码,以下为本次行为直接相关的既有错误码) + +| 错误码 | 常量名 | 文案 | 触发位置 | +|--------|--------|------|----------| +| 605905 | `REQUIREMENT_VERSION_EXPIRED` | 需求版本过期 | `assertRequirementPreflightVersion`(`AssignmentService.java:3567-3583`),`POST /requirements/{id}/confirm` 专属预检门禁;同码也是内部作业端点 `refinalizeFinalSnapshot`(`:13096-13098`)的身份不一致分支,见「六、边界行为」 | +| 605041 | `FINAL_CONFIRMATION_BASELINE_MISMATCH` | 订单行程或用车派单已变化,请按逐日差异处理后重试 | `assertFinalConfirmationBaseline`(`AssignmentService.java:4644` 起),`POST /{assignmentId}/confirm`、`POST /{assignmentId}/change` 及 `POST /requirements/{id}/confirm` 内部循环共用 | + +> ⚠️ **Step2 canonical 快照(`POST /admin/fleet/assignments/candidates` 的 `canonicalSnapshot` +> 字段)没有对应错误码**——它的身份不一致分支(`identityConsistent` 判 false)走的是 +> `return null` + `log.warn`,不抛任何 `IErrorCode`。上表只覆盖会抛错误码的场景,`canonicalSnapshot` +> 为 `null` 的排查请走「三、接口详情」第 4 条,不要在这张表里找它的错误码——它没有。 + +**`dailyDifferences[].differenceType` 允许值**(既有枚举,本次未新增,仅在 605041 场景可能 +出现):`REQUIREMENT_VERSION_MISMATCH` / `ORDER_DATE_MISMATCH` / `ITINERARY_DATE_MISMATCH` / +`ASSIGNMENT_DATE_MISSING` / `ASSIGNMENT_DATE_EXTRA` / `HEADCOUNT_BASELINE_MISMATCH`。 + +--- + +## 六.6、修改前后对比 + +### 字段级对比 + +| 字段 | 改前 | 改后 | +|------|------|------| +| `OrderFleetDetailContextDTO.transferVehicleRequirement`(`hl-common-core`,fleet↔order-v3 内部 Feign 契约,前端不可见) | 不存在 | 新增可空字段,携带订单当前生效的 `kind=TRANSFER` 需求 | +| 四个端点(confirmRequirement/confirm/change/candidates)的请求体、响应体字段 | — | **零变化**,本次不涉及任何 ReqVO/RespVO 字段增删(`canonicalSnapshot` 及其内部字段结构不变,只是不再必然为 `null`) | +| `AssignmentService.baselineRequirementFor` 的实现 | 内联逻辑 | 委托给新抽出的 `BaselineRequirementSelector.select`(内部重构,行为等价) | + +### 行为级对比 + +| 行为 | 改前 | 改后 | +|------|------|------| +| `POST /requirements/{id}/confirm` 对 `kind=TRANSFER` 需求 | 必现 605905(`assertRequirementPreflightVersion` 恒判需求已过期) | version/sha256 匹配时正常通过 | +| `POST /{assignmentId}/confirm` 对 `kind=TRANSFER` 派车行(holding 收尾 + ASSIGNED 复核两条分支) | 必现 605041 | 基线一致时正常确认为 `assigned` | +| `POST /{assignmentId}/change` 对 `kind=TRANSFER` 派车行 | 必现 605041 | 基线一致时正常改派 | +| `isRequirementEligibleForReopen`(内部,两条异步补偿链路) | 对 `kind=TRANSFER` 需求恒返 `false`,重开补偿失效且无报错 | 按该需求自己的 `requirementId` 正确判定 | +| `refinalizeFinalSnapshot`(内部作业端点) | 对 `kind=TRANSFER` 需求必现 605905,重发快照补偿失效 | 按入参 `requirementId` 正确判定 | +| `POST /admin/fleet/assignments/candidates` 的 `canonicalSnapshot` 字段(对 `kind=TRANSFER` 需求) | **静默**恒为 `null`,不抛任何错误码 | `requirementId`/`orderId` 身份一致时返回正常快照 | +| `kind=TRAVEL` 上述全部行为 | — | 逐字不变 | + +--- + +## 六.7、影响评估 + +- **是否破坏向后兼容**: 否。三个端点入参/出参零变化,`OrderFleetDetailContextDTO` 只新增 + 可空字段(纯增量),TRAVEL 行为逐字不变 +- **前端是否必须同步上线**: 否。本次不涉及任何前端可见的请求/响应契约变更,不需要改代码。 + 仅供知悉:`18_7443_接送机用车需求分叉…` 文档里"前端本版暂缓接入 TRANSFER 派车行确认/改派" + 的限制,在这三个端点自身维度已解除;但上游 809009 写口未开放,测试环境目前仍无法通过正常 + 业务流程产出可联调的 TRANSFER 数据(见「七、不影响范围」) +- **前端 workaround 清理点**: 无(此前未要求前端做任何 workaround,只是要求暂缓接入) +- **数据**: 无需迁移,本次零 DB 迁移 + +--- + +## 七、不影响范围 + +- **仅影响**: 管理后台车务派单弹窗——`POST /requirements/{id}/confirm`、 + `POST /{assignmentId}/confirm`、`POST /{assignmentId}/change`、 + `POST /admin/fleet/assignments/candidates` 四个既有端点对 `kind=TRANSFER` + 对象(派车行/需求/候选查询)的内部基线选取逻辑;以及一处不面向 admin 前端的内部作业端点 + `POST /internal/fleet/jobs/vehicle-assignment-snapshot/refinalize` +- **零影响**: + - 四个端点自身的请求体、响应体字段结构(零新增/零删除/零改名) + - `kind=TRAVEL` 派车行/需求在这四个端点的全部行为 + - `POST /admin/fleet/assignments/batch`(批量创建)、`PUT /pickup-dropoff-config` + (接送机配置)——两者的 `kind=TRANSFER` 基线复核缺口已由今天早些时候的 PR #7913 + 单独修复(见 `18_7443_接送机用车需求分叉…` 文档),与本次修复是同一根因的两个不同修复点 + - 单笔创建派单 `POST /admin/fleet/assignments`(该端点无本次改动涉及的基线复核缺口) + - 取消派单 `DELETE /{assignmentId}`、司机拒接 `POST /{assignmentId}/driver-reject`、软清 + `POST /{assignmentId}/soft-clear-assignment`、撤销取消 `POST /{assignmentId}/restore-cancel`、 + 提前完结 `POST /{assignmentId}/early-complete`、行程短信查询/重发、一键重派推荐等其余端点 + - `fleet_assignment`/`vehicle_requirement` 数据库表结构(零迁移) + - 上游 `PUT /v3/admin/order/{id}/vehicle-requirement` 传 `kind=TRANSFER` 的写口现状 + (809009 仍未开放,不受本次修复影响) + - 看板列表、矩阵日订单等只读查询接口 + +--- + +## 部署清单(本单改了 hl-common-core,CODE_RULES §16.6) + +本单在 `hl-common-core` 新增了字段(`OrderFleetDetailContextDTO.transferVehicleRequirement`)。 +按 CODE_RULES §16.6,**共享 jar 变了行为就变了,部署时全部消费方必须一起滚**,不能只滚 +`hl-fleet-service`/`hl-order-service-v3` 这两个直接改代码的服务。 + +⚠️ **消费方清单不能按 `pom.xml` 直接依赖关系查**(2026-09-18 实测教训): +`grep -rl 'hl-common-core' */pom.xml` 的结果里 **order-v3、fleet、user、resource、 +product-v2、mp 一个都没有**——它们全部经 `hl-common-web`(本身声明 `hl-common-core` 依赖) +传递引入,直接查 `pom.xml` 会把本单真正改动、也真正需要重启的这两个服务漏掉。 + +| 部署单位 | 与 `hl-common-core` 的依赖关系 | 是否需要本次一起滚 | +|---|---|---| +| hl-gateway | 直接依赖(`pom.xml` 实测命中) | 是(§16.6 全消费方一起滚) | +| hl-user-service | 经 `hl-common-web` 传递依赖 | 是 | +| hl-resource-service | 经 `hl-common-web` 传递依赖 | 是 | +| hl-product-service-v2 | 经 `hl-common-web` 传递依赖 | 是 | +| hl-order-service-v3 | 经 `hl-common-web` 传递依赖 | **是(本单直接改了这个服务的生产代码)** | +| hl-mp-service | 经 `hl-common-web` 传递依赖 | 是 | +| hl-fleet-service | 经 `hl-common-web` 传递依赖 | **是(本单直接改了这个服务的生产代码)** | +| hl-finance | 直接依赖(`pom.xml` 实测命中) | 不独立部署——`packaging=jar`,无 `spring-boot-maven-plugin`,是 order-v3 的库依赖,随 order-v3 一起滚即可,不是第 8 个部署单位 | + +⇒ **实际部署单位共 7 个**:gateway / user / resource / product-v2 / order-v3 / mp / fleet。 +部署顺序、回滚预案等具体操作步骤本文档不展开(属发布流程,不是本 changelog 的职责范围); +这里只交付"漏了会出什么问题"的判据——`hl-common-core` 里的类是所有 7 个服务共享的同一份 +class 定义,只滚一部分服务会导致该部分服务与其余服务对 `OrderFleetDetailContextDTO` 的 +序列化/反序列化预期不一致(新滚的服务发出/接收带 `transferVehicleRequirement` 字段的 +JSON,未滚的服务用旧版本类反序列化——**Jackson 对未知字段默认静默丢弃,不会报错**,但会让 +这次修复在未滚的服务视角里"看起来没生效")。 + +--- + +## 八、测试环境已验证 + +> ⚠️ **本节如实说明:本次修复尚未通过网关/端到端验证。** + +- 三个提交 `abd435c15`/`694e405fd`/`15c7cdd0c` 位于本地分支 `feature/7443-rest-ac`(已 rebase + 到最新 `dev-v3`),**尚未合入 `origin/dev-v3`**(2026-09-18 复核:分支落在 + `origin/dev-v3@701898e2f` 之上 3 个提交,`git log origin/dev-v3..HEAD --oneline` 精确列出 + 这三个;`origin/dev-v3:hl-common/hl-common-core/.../OrderFleetDetailContextDTO.java` 实测 + 确认无 `transferVehicleRequirement` 字段) +- 2026-09-18 现场 `deploy-status.sh` 复测(合入前最后一次核对):测试环境 `hl-fleet-service` + 跑在 `dev-v3` 分支的 `cdf763de6`、`hl-order-service-v3` 跑在 `dev-v3` 分支的 + `701898e2f`(0 behind,即当前 `origin/dev-v3` 的真实 HEAD)——均**不含**本次三个提交 +- 因此本节**没有**网关请求/响应实测数据;上方「三、接口详情」的请求/响应示例是依据源码 VO + 字段结构构造的示意,不是网关捕获的原始报文,取值仅供理解结构,不代表真实业务数据 +- 代码层佐证(随三个提交一并提交,本次撰写过程中**未在本会话重新执行**,通过与否以合并前的 + CI/单测结果为准,行数据/测试数字均取自各提交的 `git show --stat` 与提交信息实测): + - `abd435c15`(第一批,前三个端点):`AssignmentServiceTest.java` +471 行、 + `AssignmentServiceNoVehicleDeclarationTest.java` +25 行、 + `OrderFleetProviderServiceTest.java` +81 行、`RequirementServiceTest.java` +73 行 + - `694e405fd`(`refinalizeFinalSnapshot`):`AssignmentServiceTest.java` +119 行;提交信息称 + fleet 定向 626/0/0/0 + - `15c7cdd0c`(Step2 canonical 快照 + `BaselineRequirementSelector`): + `Step2CanonicalSnapshotServiceTest.java` +59 行(新增 `BaselineRequirementSelector.java` + +62 行为非测试代码);提交信息称 `Step2CanonicalSnapshotServiceTest` 23/0/0/0(+1), + fleet 定向合计 714/0/0/0 +- **发布前置条件**:需先将 `feature/7443-rest-ac` 合入 `dev-v3` 并部署到测试环境(含 + `hl-common-core` 触及的全部 7 个部署单位,见上方「部署清单」),完成四个端点对 + `kind=TRANSFER` 的网关实测后,方可将本文档 `backend_status` 改为 `deployed`、 + `gateway_status` 改为 `verified` 并推送 + +--- + +## 十、相关文档 + +- Issue: [#7443](https://git.1814.love:8443/wx/HL/issues/7443) AC-24 +- PR: 尚未创建(代码见本地分支 `feature/7443-rest-ac`,三个提交 + [abd435c15](https://git.1814.love:8443/wx/HL/commit/abd435c15)(前三个端点,rebase 后哈希; + 原 `09e9d7e50` 内容相同,rebase 到 `dev-v3@701898e2f` 后哈希改变)、 + [694e405fd](https://git.1814.love:8443/wx/HL/commit/694e405fd)(`refinalizeFinalSnapshot`)、 + [15c7cdd0c](https://git.1814.love:8443/wx/HL/commit/15c7cdd0c)(Step2 canonical 快照 + + `BaselineRequirementSelector`)——这些链接在 PR 合入前可能 404,仅作提交号留档) +- 前序文档(本文订正其中一处表述,详见「⚠️ 关键变化」): + `changelogs-v2/2026-09/17_7443_团期身份失败关闭与看板矩阵按团筛选-修改接口-管理后台.md`、 + `changelogs-v2/2026-09/18_7443_接送机用车需求分叉PR1派车入参新增需求类别kind-修改接口-管理后台.md` + +## 关联 / 联系人 + +### 链接 + +- **Issue**: [#7443](https://git.1814.love:8443/wx/HL/issues/7443) AC-24 +- **PR**: 尚未创建;三个提交 + [abd435c15](https://git.1814.love:8443/wx/HL/commit/abd435c15)/ + [694e405fd](https://git.1814.love:8443/wx/HL/commit/694e405fd)/ + [15c7cdd0c](https://git.1814.love:8443/wx/HL/commit/15c7cdd0c)(本地分支 + `feature/7443-rest-ac`,已 rebase 到 `dev-v3@701898e2f`,尚未合入) + +### 联系人 + +- **后端**: @wx