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 段实证
59 KiB
schema, ticket, title, consumer, author, change_type, backend_status, gateway_status, frontend_status, frontend_owner, frontend_ref, target_release, verified_at, status_note, updated_at, base
| schema | ticket | title | consumer | author | change_type | backend_status | gateway_status | frontend_status | frontend_owner | frontend_ref | target_release | verified_at | status_note | updated_at | base |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| hl-changelog/v2 | 7443 | TRANSFER 派车行确认/改派基线复核修复(605905/605041 不再误判) | admin | wx(GIT) | 修改接口 | deployed | verified | not_required | 【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)不动。 | 2026-09-20 | 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 |
请求示例
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 是否匹配无关紧要。
响应示例
{
"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 的成功响应形态。
错误响应
{
"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 | 差异说明 |
请求示例
POST /admin/fleet/assignments/1934567890123457100/confirm
{
"sendItinerarySms": true,
"requestId": "fleet-final-confirm-20260918-0001"
}
上例 assignmentId=1934567890123457100 是一条挂在 kind=TRANSFER 需求下的派车行;本次修复前
该请求必返 605041。
响应示例
{
"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,但不是"空数据"形态);
失败时按下方错误响应返回,不存在中间态。
错误响应
{
"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) |
请求示例
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。
响应示例
{
"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 无其它车辆时为空数组而非缺失字段);
失败时按下方错误响应返回。
错误响应
{
"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描述的是"存在时必填",不是 "恒存在"。
请求示例
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 等其余字段不受影响,正常返回)。
响应示例
{
"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 成因不受影响。
{
"code": 200,
"message": "成功",
"data": {
"canonicalSnapshot": null
},
"success": true
}
错误响应
本端点的 canonicalSnapshot 解析路径不抛错误码(这正是「使用场景」里描述的"坏法是静默的");
resolveCanonicalSnapshot 内部锁获取失败时会抛 CommonErrorCode.RESOURCE_LOCKED
(hl-common-core/.../CommonErrorCode.java:67-68),但这是既有的锁竞争兜底,与本次修复的身份
比对分支无关,本次未改动:
{
"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/015c7cdd0c(Step2 canonical 快照 +BaselineRequirementSelector):Step2CanonicalSnapshotServiceTest.java+59 行(新增BaselineRequirementSelector.java+62 行为非测试代码);提交信息称Step2CanonicalSnapshotServiceTest23/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 AC-24
- PR: 尚未创建(代码见本地分支
feature/7443-rest-ac,三个提交 abd435c15(前三个端点,rebase 后哈希; 原09e9d7e50内容相同,rebase 到dev-v3@701898e2f后哈希改变)、 694e405fd(refinalizeFinalSnapshot)、 15c7cdd0c(Step2 canonical 快照 +BaselineRequirementSelector)——这些链接在 PR 合入前可能 404,仅作提交号留档) - 前序文档(本文订正其中一处表述,详见「⚠️ 关键变化」):
changelogs-v2/2026-09/17_7443_团期身份失败关闭与看板矩阵按团筛选-修改接口-管理后台.md、changelogs-v2/2026-09/18_7443_接送机用车需求分叉PR1派车入参新增需求类别kind-修改接口-管理后台.md
关联 / 联系人
链接
- Issue: #7443 AC-24
- PR: 尚未创建;三个提交
abd435c15/
694e405fd/
15c7cdd0c(本地分支
feature/7443-rest-ac,已 rebase 到dev-v3@701898e2f,尚未合入)
联系人
- 后端: @wx