文件
hl-api-changelog/changelogs-v2/2026-09/20_7443_TRANSFER派车行确认改派基线复核修复-修改接口-管理后台.md
T
Mimingguang ac12dabe20
changelog-filename-gate / validate (push) Failing after 2s
docs(changelog): #7443 C 车务侧+13_7439/18_7443/20_7990 前端已交付 verified(hl-admin v2.1 662310ea/6c091ef24)
18_7443 挂起期回头补落地(派车弹窗 kind 切换+batch/pickup-dropoff-config 显式 kind);
20_7990 requirementIdentities 已消费;13_7439 硬契约点 A+B 已补(809008/显式 kind/reject 走 query);
20_7443 AC-24 维持 not_required 仅补 C 段实证
2026-09-21 17:52:22 +08:00

986 行
59 KiB
Markdown
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
---
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。 前端实证维持 not_required(mmg 2026-09-20):四端点入参/出参零变化、TRAVEL 逐字不变、TRANSFER 前端不可达(809009 开关仍关,#7443 维持挂起);canonicalSnapshot=null 自验:AssignModal applyCanonicalSnapshotToDraft 首行 `!snapshot` 静默 return,不报错、草稿保持现状,合法降级形态既有处理;#7443 启动条件更新为 产品排期+809009 开关+outbox 修复交接件(AC-24 本件已销),已入前端 memory。【mmg 2026-09-21 C 段实证补充】#7443 C 交付(hl-admin v2.1 662310ea)再次确认本件前端零改动:四端点请求/响应契约逐字未变,改派按 assignmentId 反查服务端自取基线,TRANSFER 行零改动可用;canonicalSnapshot=null 合法降级既有处理(applyCanonicalSnapshotToDraft 首行静默 return)不动。"
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\<LocalDate\> | 否 | - | 按天改派限定的服务日期集;空=生效日起连续后缀整体改派 |
| 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\<LocalDate\> | 否 | - | 生效日起收取车费的服务日期;不传沿用原值 |
| 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\<String\>(`required=true`) | 有序 RetainedGroupSet:稳定派车组 ID |
| canonicalSnapshot.editableServiceDates | Array\<LocalDate\>(`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