43 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 | 8429 | 车务最终实派方案发布判据统一(批量派车 batch 补上满派与陈旧定稿守卫,已完结行越窗豁免)、三个写口新增未发布原因字段、602202 按声明车数放宽 | admin | wx(GIT) | 修改接口 | deployed | verified | verified | mmg | 72c28d07a0defe478cce3b97b9ffaf33e8137bc7 | v2.1 | 2026-09-28 | PR #8451 已合入 dev-v3(合并提交 267ab6906)。TEST 环境 hl-fleet-service 267ab6906 于 2026-09-27 19:40:15 部署,hl-gateway 71def6dc5 同期在线;2026-09-27 19:58-20:27 在该组合上网关实测 batch/pd/confirm 三写口(见「八、测试环境已验证」):越窗已完结行 batch 与 pd 答案从一真一假变为都真;602202 改按声明车数动态判定(count=2 时第 3 辆拒绝、降为 count=1 后第 2 辆即拒);三个接口响应均带 finalPlanNotPublishedReason 字段。前端已交付(72c28d07):batch 失败按 finalPlanNotPublishedReason 精确指引(仅无原因/GATE_UNSATISFIED 跳第③步,方案不完整/代际/载客量留第②步),confirm 多组 HTTP 成功补查 finalPlanPublished=false 改警示而非「已完成派单」;useAssignFlow 两 spec 84 例全绿。 | 2026-09-27 | dev-v3 |
fleet: 车务最终实派方案发布判据统一 + 未发布原因字段
服务: hl-fleet-service(端口 8161/8261)
PR: #8451(已合入 dev-v3,合并提交 267ab6906)
Issue: #8429
日期: 2026-09-27
影响范围: 管理后台车务看板的批量派车、接送机配置、需求确认三个接口;接送机派车对同日同方向多辆车的上限校验
⚠️ 关键变化
🔴 三个写口统一判据。 改前:batch 只看接送机门禁(满足即发布),pd 与 confirm 还要过满派拓扑与陈旧定稿两道守卫。改后:三个接口都走完整判据(陈旧定稿 → 方案代际 → 满派拓扑 → 门禁)。结果:同一份派单数据走 batch 和 pd 可能得到不同答案的情况消失;batch 在「方案不完整」时会从原来的 finalPlanPublished=true 变成 false + PLAN_INCOMPLETE。
🟡 新字段 finalPlanNotPublishedReason。 三个接口(batch、pd、confirm)响应都新增这个字段。发布时为 null;不发布时回「第一个没通过的判据」原因,供前端对运营给出有针对性的提示。取值共五个(通用)+ 两个(pd 专属)。
🟡 已完结行越窗豁免。 改大交通或换版后,已完结日落在新服务日之外。改前会让方案判「不完整」永久发不出去;改后只对在途行检查窗口,已完结行豁免。
🟢 602202 按声明车数放宽。 TRANSFER 需求下同日同方向标「参与」的派车行数上限从「1」改为「声明车数」(count)。当 count 缺失或 ≤0 时按 1。code 不变,文案扩展(含日期、方向、标记条数、派车行 ID 列表、声明车数)。TRAVEL 需求不受影响。
一、背景
问题现象(测试服 2026-09-27 实测):同一批派单数据,pd 配齐接送机后响应 finalPlanPublished=false,同一条 batch(无新增行)7 秒后响应 true。根因是三个写口走不同判据——batch 只过门禁漏斗,pd 还要过满派守卫,当方案含「越窗已完结行」时满派判定会卡住。
两个上游缺口:
- #5810 在「最终确认基线」与「方案代际」两处给已完结行豁免,满派拓扑这一处漏了;
- #7067 统一了发布入口但 batch 路径返工时没有带上满派与陈旧定稿的守卫。
业务定案(wx 2026-09-27):
- TRANSFER 的 count = 同日同方向实际要派的车数。
- count 不是发布门禁;实派即权威,派少照样发。
- 602202 以 count 为上限,不再「超过 1 就拒」。
二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|---|---|---|---|---|
| 1 | 批量派车 | POST | /admin/fleet/assignments/batch |
修改 | 判据改完整(新增满派与陈旧定稿检查);新增 finalPlanNotPublishedReason;已完结行越窗豁免 |
| 2 | 接送机配置 | PUT | /admin/fleet/assignments/pickup-dropoff-config |
修改 | 新增 finalPlanNotPublishedReason;602202 上限按声明车数;已完结行越窗豁免 |
| 3 | 需求确认 | POST | /admin/fleet/assignments/requirements/{requirementId}/confirm |
修改 | 新增 finalPlanNotPublishedReason;已完结行越窗豁免 |
三、接口详情
1. 批量派车 POST /admin/fleet/assignments/batch
VO: BatchCreateAssignmentReqVO → BatchAssignmentWriteRespVO
使用场景
车务第②步:提交逐日最终方案。改后走完整发布判据,方案不完整时会从原来的 true 变成 false。
入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| orderId | Body | Long | ✅ | 订单 ID(雪花) | 不变 |
| orderNo | Body | String | ❌ | - | 不变:订单号冗余 |
| requirementId | Body | Long | ✅ | 用车需求 ID | 不变:当前生效用车需求 ID |
| kind | Body | String | ❌ | TRANSFER / TRAVEL |
不变,与本单无关:不传按 TRAVEL 解析(见下方业务边界「kind 省略」) |
| startDate | Body | String | ✅ | yyyy-MM-dd | 不变(此前遗漏未列):用车开始日期;TRANSFER 请传接送机服务日最早一天 |
| endDate | Body | String | ✅ | yyyy-MM-dd | 不变(此前遗漏未列):用车结束日期 |
| pickupAt | Body | String | ❌ | - | 不变:接客地 |
| dropoffAt | Body | String | ❌ | - | 不变:送客地 |
| headcount | Body | Integer | ❌ | - | 不变:乘客人数 |
| confirmNoVehicleServiceDates | Body | Boolean | ❌ | - | 不变:逐日计划未覆盖全部服务日(存在不配车日)时的显式二次确认 |
| sendItinerarySms | Body | Boolean | ❌ | 不传按 false | 不变:是否向本批各车师傅发送行程短信,整批统一决策 |
| skipCityJunctionException | Body | Boolean | ❌ | - | 不变:跳过城市衔接例外 |
| fromEntry | Body | String | ❌ | - | 不变:操作来源 |
| requestId | Body | String | ✅ | 长度 ≤64 | 改:此前误写为非必填,源码 @NotBlank,批次级幂等请求标识不能为空 |
| dailyPlan | Body | List<DailyPlanItem> | ✅ | 最多 4000 项 | 不变:按行程日的完整配车列表,需求日期窗内未出现的服务日视为该日不配车 |
| dailyPlan[].serviceDate | Body | String | ✅ | yyyy-MM-dd | 不变 |
| dailyPlan[].vehicleId | Body | Long | ✅ | 车辆 ID | 不变 |
| dailyPlan[].driverId | Body | Long | ✅ | 司机 ID | 不变 |
| dailyPlan[].assignmentPrice | Body | BigDecimal | ❌ | ≥0.00,整数位≤10 小数位≤2 | 不变:本车当天实际价格,未传按车型价格日历参考价兜底 |
| dailyPlan[].priceAdjustmentReason | Body | String | ❌ | 长度 ≤256 | 不变:实际价格与参考价不一致时的调整原因 |
| dailyPlan[].confirmCrossResident | Body | Boolean | ❌ | - | 不变:跨常驻车显式确认 |
出参字段表
| 字段 | 类型 | 说明 |
|---|---|---|
| assignments | List<Item> | 不变:本次提交生效的派单结果;Item.fleetItemIndex 去槽位化(#7067)后恒为 null(字段保留兼容),Item.assignment 是完整 AssignmentWriteRespVO(见响应示例,关键字段 id【不是 assignmentId】、assignmentStatus【提交即派定,恒 "assigned" 小写】) |
| finalPlanPublished | Boolean | 语义不变,取值可能改变:true = 本次发布了最终方案;false 时见下一字段 |
| finalPlanNotPublishedReason | String | 新增。发布时为 null;不发布时取值见下表。返回第一个没通过的判据原因 |
| pickupDropoffGate | PickupDropoffGateVO | 不变:含 arrivalRequiredDates/departureRequiredDates/missingPickupDates/missingDropoffDates/declared/satisfied(见响应示例) |
| failedFleetItemIndex | Integer | 不变 |
| dailyDifferences | List<...> | 不变:成功时为 null(不是空数组),仅最终派定失败时回逐日基线差异 |
finalPlanNotPublishedReason 取值(五个值,按判据顺序,只返回首个未通过的):
| 取值 | 触发条件 |
|---|---|
STALE_FINALIZED_PLAN |
存在按旧需求定稿的陈旧行,需车务对当前需求重新确认 |
INVALID_PLAN_GENERATION |
方案代际不一致(部分行已定稿、部分未定稿或代际不同) |
PLAN_INCOMPLETE |
满派拓扑不完整:有逻辑 key 没派车、缺司机、在途行越窗、同 key 多行等 |
CAPACITY_INSUFFICIENT |
未定稿分支当日载客量不足覆盖需求人数(仅未定稿分支出现) |
GATE_UNSATISFIED |
大交通要求的接/送机日没有配车 |
请求示例
实测请求(2026-09-27 20:00:24,测试专用车务账号,见 wf/8429/ac2/result.md AC-2 第一步):
{
"orderId": 2104161383220457474,
"requirementId": 2104178895018459138,
"kind": "TRANSFER",
"startDate": "2027-08-15",
"endDate": "2027-08-30",
"headcount": 2,
"requestId": "fleetqa-ac2_step1_batch-xxxxxxxxxx",
"dailyPlan": [
{
"serviceDate": "2027-08-15",
"vehicleId": 2104029270659780610,
"driverId": 2104029157140910081,
"assignmentPrice": "300.00",
"priceAdjustmentReason": "按订单约定价格"
},
{
"serviceDate": "2027-08-30",
"vehicleId": 2104029270659780610,
"driverId": 2104029157140910081,
"assignmentPrice": "300.00",
"priceAdjustmentReason": "按订单约定价格"
}
]
}
响应示例
提交新行、门禁未配齐(不发布)——实测原文(wf/8429/ac2/out/batch_ac4_assign3.json,AC-4 三车同时新建):
{
"code": 200,
"message": "成功",
"data": {
"assignments": [
{
"fleetItemIndex": null,
"assignment": {
"id": "2104183651266863105",
"assignmentGroupId": "362573467578667009",
"assignmentSlotId": "362573467578667009",
"assignmentStatus": "assigned",
"stageCode": "assigned",
"stageLabel": "已派车",
"currentStep": 3,
"skippedStepCodes": [],
"protocolPrice": "300.00",
"vehicleFeeAutoTotal": "0.00",
"vehicleFeeAutoComplete": false,
"vehicleFeeTotal": "300.00",
"vehicleFeeSource": "MANUAL",
"vehicleFeeAdjustmentReason": "按订单约定价格",
"dailyVehicleFees": null,
"holdSentAt": null,
"confirmedAt": "2026-09-27 20:17:31",
"sideEffects": {
"vehicleStatusUpdated": "busy",
"driverStatusUpdated": "busy",
"reconPrepRowsCreated": 0,
"reconPrepMarkedCanceled": null
},
"sendItinerarySms": false,
"itinerarySmsEventId": null,
"itinerarySmsStatus": "NOT_SENT",
"dailyDifferences": null
}
}
],
"finalPlanPublished": false,
"finalPlanNotPublishedReason": "GATE_UNSATISFIED",
"pickupDropoffGate": {
"arrivalRequiredDates": ["2027-08-30", "2027-09-05"],
"departureRequiredDates": ["2027-08-09"],
"missingPickupDates": ["2027-08-30", "2027-09-05"],
"missingDropoffDates": [],
"declared": true,
"satisfied": false
},
"failedFleetItemIndex": null,
"dailyDifferences": null
},
"traceId": null,
"success": true
}
上例响应只返回 1 条
assignments记录,即便当次提交/相关联的行更多——assignments[]只包含本次调用新建的派车行(与库内既有行 diff 后的itemsToCreate),已存在且未变化的行不会重复出现。不要用assignments数组长度判断本次提交是否被完整接受,应以finalPlanPublished/finalPlanNotPublishedReason为准。
原样重放、已发布(本次未产生新建行)——实测原文(wf/8429/ac2/out/batch_ac2_step3_batch_replay.json):
{
"code": 200,
"message": "成功",
"data": {
"assignments": [],
"finalPlanPublished": true,
"finalPlanNotPublishedReason": null,
"pickupDropoffGate": {
"arrivalRequiredDates": ["2027-08-15", "2027-08-30"],
"departureRequiredDates": ["2027-08-09"],
"missingPickupDates": [],
"missingDropoffDates": [],
"declared": true,
"satisfied": true
},
"failedFleetItemIndex": null,
"dailyDifferences": null
},
"traceId": null,
"success": true
}
空数据 / 降级响应
派单成功但方案不完整时(缺接/送机日、缺司机等)返回 finalPlanPublished=false + 原因(无空态)。
网关降级无额外变化。
错误响应
{
"code": 605062,
"message": "派车日期不在用车需求服务范围内",
"data": null,
"success": false
}
| 错误码 | 触发条件 |
|---|---|
| 605062 | 提交项的服务日期越出当前需求日期窗(不变);既有的窗外在途行不再报错,仅新提交项受限 |
| 605912 | 派车快照缺少需求车型:所选车辆无车型大类且型号名无法归一(不变) |
| 605905 | 需求版本过期:锁内重读的当前生效需求 ID/版本与请求不一致(不变);kind 省略解析出的需求与传入 requirementId 不一致时也走这条 |
| 100001 | 参数非法,含「已完结日改派」(该日已完结不可改派)与请求携带已移除旧字段两类场景(不变) |
业务边界
- 满派拓扑缺口场景:需求下某些必须配车的服务日本次未提交任何
dailyPlan行覆盖(而非"某行漏填司机"——dailyPlan[].driverId本身必填,无法提交出一条缺司机的行),门禁满足时改前发布、改后不发布(PLAN_INCOMPLETE)。运营需按原因提示补齐缺失服务日的派车行。 - 已完结行:越窗已完结行不再卡发布,该行在其他维度(车、司机、形态)仍需满足完整性条件。
- 空方案(dailyPlan 为空):只过门禁判定,满派守卫不适用。
- 重复调用(requestId 相同):按幂等复用,返回相同结果。
assignments只回新建行:数组只包含本次调用新建的派车行,已存在且未变化的行不重复返回;判断提交是否被接受用finalPlanPublished/finalPlanNotPublishedReason,不要用数组长度或内容对比。- 旧字段一律 400:
items/chargeableServiceDates/vehicleFeeWaiverReason/confirmAllServiceDatesFree/holdMode(顶层)、dailyPlan[].fleetItemIndex/used/pickupParticipant(逐日项)已移除,请求体携带任一个即触发 Bean Validation 400(不进入业务判据)。 - 该接口重置本次覆盖服务日的接送标记:
batch落库时会把本次dailyPlan覆盖到的每个服务日的 pickup/dropoff 参与标记重置,即使该日车辆/司机未变——先调batch再调pickup-dropoff-config的调用顺序下,batch覆盖到的日期需要重新配置接送标记,不能假设旧标记还在。 kind省略时的解析行为(#8453,纯 TRANSFER 订单既有缺陷,非本次改动引入):kind不是必填字段,省略时按TRAVEL解析当前需求(RequirementKindResolver.resolveForMutation,AssignmentService.java:8088),与「接送机配置」接口共用同一个解析器(AssignmentService.java:1228)。纯 TRANSFER 订单(无 TRAVEL 需求)省略kind时会解析不到当前需求;requirementId传了值但与解析结果不一致会触发 605905(需求版本过期)。该缺陷已在「接送机配置」接口实测复现(见该接口业务边界的日志对照),本接口调用同一个解析器,结构上同样受影响,但本轮未针对batch单独造数复现——TRANSFER 场景请始终显式传kind: "TRANSFER",跟踪见#8453。
2. 接送机配置 PUT /admin/fleet/assignments/pickup-dropoff-config
VO: PickupDropoffConfigReqVO → PickupDropoffConfigRespVO
使用场景
车务第③步:配置派车行是否参与接机/送机。改后新增未发布原因字段,602202 上限按声明车数。本接口仍保留「跃迁才发布」规则(门禁从不满足→满足时发布),且已完结行越窗不再阻止发布。
入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| orderId | Body | Long | ✅ | 订单 ID | 不变 |
| requirementId | Body | Long | ✅ | 用车需求 ID | 不变:当前生效用车需求 ID |
| kind | Body | String | ❌ | TRANSFER / TRAVEL |
改,与本单无关:不传按 TRAVEL 解析(见下方业务边界「kind 省略」,纯 TRANSFER 订单请务必显式传) |
| requestId | Body | String | ✅ | 长度 ≤64 | 改:此前误写为非必填,源码 @NotBlank |
| items | Body | List<ConfigItem> | ✅ | 最多 4000 项 | 不变:要标记接送机参与的派车行集合;该订单+当前需求下未出现在 items 中的生效派车行两个方向标志一律归零(全量覆盖语义) |
| items[].assignmentId | Body | Long | ✅ | 须是当前需求下的生效派车行 | 不变 |
| items[].pickupParticipant | Body | Boolean | ✅ | - | 不变:当日该车是否参与 ARRIVAL 接机 |
| items[].dropoffParticipant | Body | Boolean | ✅ | 与 pickupParticipant 不能同为 false | 不变:当日该车是否参与 DEPARTURE 送机;同一行两个标志不能同为 false(不参与的行不要出现在 items 里,否则 400) |
出参字段表
| 字段 | 类型 | 说明 |
|---|---|---|
| finalPlanPublished | Boolean | 不变:是否已发布最终方案 |
| finalPlanNotPublishedReason | String | 新增。七个值(五个通用 + 两个本接口专属),发布时为 null |
| requirementReopened | Boolean | 不变:是否已把订单拉回处理中 |
| reopenBlockedReason | String | 不变:拉回失败的原因 |
| pickupDropoffGate | PickupDropoffGateVO | 不变:当前门禁状态 |
finalPlanNotPublishedReason 取值(七个值):
| 取值 | 出现场景 |
|---|---|
| 前五个 | 同 batch(陈旧定稿、代际不一致、拓扑不完整、载客量不足、门禁不满足) |
NO_GATE_TRANSITION |
本接口专属:本次配置前后门禁没有从不满足→满足 |
PICKUP_DROPOFF_GATE_DISABLED |
本接口专属:接送机门禁开关关闭或无当前生效需求,本接口退回纯写口 |
请求示例
实测请求(2026-09-27 20:02:18,测试专用车务账号,见 wf/8429/ac2/result.md AC-2 第二步、一次性提交两行):
{
"orderId": 2104161383220457474,
"requirementId": 2104178895018459138,
"kind": "TRANSFER",
"requestId": "fleetqa-pd-ac2_step2_pd-xxxxxxxxxx",
"items": [
{
"assignmentId": 2104166776231403521,
"pickupParticipant": true,
"dropoffParticipant": false
},
{
"assignmentId": 2104179344782008321,
"pickupParticipant": true,
"dropoffParticipant": false
}
]
}
响应示例
门禁尚未满足(部分补标,仍缺 08-15)——实测原文(wf/8429/ac2/out/pd_ac2_step2_pd.json):
{
"code": 200,
"message": "成功",
"data": {
"finalPlanPublished": false,
"finalPlanNotPublishedReason": "NO_GATE_TRANSITION",
"requirementReopened": false,
"reopenBlockedReason": null,
"pickupDropoffGate": {
"arrivalRequiredDates": ["2027-08-15", "2027-08-30"],
"departureRequiredDates": ["2027-08-09"],
"missingPickupDates": ["2027-08-15"],
"missingDropoffDates": [],
"declared": true,
"satisfied": false
}
},
"traceId": null,
"success": true
}
门禁已满足但无新跃迁(重复提交与已生效配置相同的标记)——实测原文(wf/8429/ac2/out/pd_ac4_reset_v2.json):
{
"code": 200,
"message": "成功",
"data": {
"finalPlanPublished": false,
"finalPlanNotPublishedReason": "NO_GATE_TRANSITION",
"requirementReopened": false,
"reopenBlockedReason": null,
"pickupDropoffGate": {
"arrivalRequiredDates": ["2027-08-15", "2027-08-30"],
"departureRequiredDates": ["2027-08-09"],
"missingPickupDates": [],
"missingDropoffDates": [],
"declared": true,
"satisfied": true
}
},
"traceId": null,
"success": true
}
首次把门禁从「不满足」配到「满足」时(即上面「请求示例」一次性提交两行的那次调用)响应形状与上两例相同,字段取值为
finalPlanPublished=true、finalPlanNotPublishedReason=null、pickupDropoffGate.satisfied=true、missingPickupDates/missingDropoffDates均为空数组(实测记录见wf/8429/ac2/result.mdAC-2 第二步,本节不重复整段 JSON)。
空数据 / 降级响应
无额外空态。门禁不满足或未跃迁时 finalPlanPublished=false + 原因;拉回失败时 requirementReopened=true + 拉回失败原因。
错误响应
实测原文(wf/8429/ac2/out/pd_ac4_pd3.json,声明车数=2 时追加第 3 辆):
{
"code": 602202,
"message": "2027-09-05 的 ARRIVAL 接机 已有 3 条派车行标记参与(派车行 2104183651266863105,2104183651463995394,2104183651883425793), 超过接送机需求声明的 2 辆",
"data": null,
"traceId": null,
"success": false
}
换版把声明车数降为 1 后,同样 2 辆车即触发(wf/8429/ac2/out/pd_ac4_pd2_after_reduce.json):
{
"code": 602202,
"message": "2027-09-05 的 ARRIVAL 接机 已有 2 条派车行标记参与(派车行 2104183651266863105,2104183651463995394), 超过接送机需求声明的 1 辆",
"data": null,
"traceId": null,
"success": false
}
| 错误码 | 触发条件 |
|---|---|
| 602202 | 语义改:TRANSFER 需求,同日同方向标「参与」的派车行数 超过声明车数(改前是固定「超过 1」,现改按 RequirementSnapshot.fleet[].count 动态判定);失败时整批原子回滚(实测复核:失败调用前后 SQL 标记值完全一致);文案含日期、方向、标记条数、派车行 ID 列表、声明车数(见上两例) |
| 605905 | 需求版本过期:锁内重读的当前生效需求 ID 或版本与写命令不一致(不变) |
| 605913 | 接送机配置无效:items[].assignmentId 对应的派单行不存在、不属于当前需求,或已是终态(已取消/无车无司机)(不变) |
业务边界
-
602202 的「标记参与」判据:跳过 CANCELED 与 EXCEPTION 行,COMPLETED 照计(已完结日重新配置时它仍代表现状)。
-
TRAVEL 需求不受 602202 约束(TRANSFER-only),同日多行参与接机仍合法。
-
count 缺失或 ≤0 时:602202 上限按 1 计(与改前行为等价)。
-
改前配置数据里已完结日:越窗不再阻止发布;该行仍需满足其他完整性条件(有车有司机、形态合法等)。
-
items是全量覆盖语义:请求里未列出的行,其 pickup/dropoff 标记会被隐式重置为false;只想改动部分行时也必须把其余仍需保留标记的行一并带上,否则会被静默清零(实测wf/8429/ac2/result.md第 85 行记录同一现象)。 -
kind省略时的解析行为(#8453,纯 TRANSFER 订单实测确认的既有缺陷,非本次改动引入):不传kind时RequirementKindResolver.resolveForMutation按TRAVEL解析当前生效需求;若该订单没有 TRAVEL 需求,解析结果为currentRequirement=null,依赖它的校验(含 602202)会被静默跳过而不报错,接口返回看似正常的 200且finalPlanNotPublishedReason=PICKUP_DROPOFF_GATE_DISABLED——该原因值本意是"这条需求 kind 不启用接送机门禁",用在这里具有误导性(实际是 kind 解析失配,不是真的门禁禁用)。实测对照(同一份 3 行items、同一账号重放):- 漏传
kind(wf/8429/ac2/run_ac4_pd3.log20:19:37):http=200 code=200 finalPlanPublished=False reason=PICKUP_DROPOFF_GATE_DISABLED,602202 判定被跳过。 - 补传
kind=TRANSFER(wf/8429/ac2/run_ac4_pd3b.log20:23:21):同一份items立即http=200 code=602202,报文与上方错误响应一致。
纯 TRANSFER 订单调用本接口必须显式传
kind=TRANSFER,否则会静默绕过声明车数上限校验且不产生任何报错信号。该问题已登记为独立缺陷跟踪#8453;在该单修复上线前,前端调用侧须将kind作为纯 TRANSFER 场景的强制字段随每次请求携带。 - 漏传
3. 需求确认 POST /admin/fleet/assignments/requirements/{requirementId}/confirm
VO: ConfirmRequirementReqVO → ConfirmRequirementRespVO
使用场景
车务最终确认需求(第④步)。改后新增未发布原因字段,已完结行越窗不再阻止发布。
入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| requirementId | Path | Long | ✅ | 用车需求 ID | 不变 |
| orderId | Body | Long | ✅ | @NotNull |
订单 ID |
| requestId | Body | String | ✅ | @NotBlank @Size(max=64) |
幂等请求标识 |
| expectedRequirementVersion | Body | Integer | ✅ | @NotNull |
预期当前有效用车需求版本 |
| expectedRequirementSha256 | Body | String | ✅ | @NotBlank @Pattern(^[0-9a-f]{64}$) |
Board 返回的当前用车需求 canonical SHA-256 |
| expectedPlanGeneration | Body | Long | ✅ | @NotNull |
预期当前最终派车方案代际 |
| groups | Body | List<GroupDecisionVO> | ✅ | @NotEmpty @Size(max=50) |
当前有效执行段精确集合及各段行程短信选择;漏传/多传任一执行段触发 605056(见下方错误响应与业务边界) |
| groups[].assignmentGroupId | Body | Long | ✅ | @NotNull |
当前有效派车组 ID |
| groups[].sendItinerarySms | Body | Boolean | ✅ | @NotNull |
是否向本执行段司机发送行程短信 |
出参字段表
| 字段 | 类型 | 说明 |
|---|---|---|
| requirementId | Long | 不变:用车需求 ID |
| dispatchPlanGeneration | Long | 不变:已确认的最终派车方案代际 |
| confirmed | Boolean | 不变:整组是否原子确认成功 |
| finalPlanPublished | Boolean | 不变:本次是否发布了最终方案;false 表示确认已成功但订单车控仍为处理中(接送机未配齐或方案未派满),需按 605914/605915 提示补齐后重试 |
| finalPlanNotPublishedReason | String | 新增(#8429)。五个通用值,发布时为 null(本接口不含 pd 专属的两个值) |
| groups | List<GroupResultVO> | 不变:各执行段确认结果 |
| groups[].assignmentId | Long | 代表派单 ID |
| groups[].assignmentGroupId | Long | 派车组 ID;历史行无 assignmentGroupId 时回退下发 assignmentId,对任何真实行恒非空 |
| groups[].assignmentStatus | String | 派单状态;实测恒为小写 assigned(#5827 提交即派定后,确认阶段不再有其他取值) |
| groups[].confirmedAt | LocalDateTime | 车务最终确认时间;实测序列化为 "yyyy-MM-dd HH:mm:ss"(无 T 分隔符,见下方响应示例) |
| groups[].sendItinerarySms | Boolean | 是否选择发送本段行程短信 |
| groups[].itinerarySmsEventId | Long | 行程短信 Outbox 事件 ID;未发送为 null |
| groups[].itinerarySmsStatus | String | 行程短信状态 |
| groups[].itineraryUrl | String | 本段电子行程单 H5 链接;签发不可用时为 null |
finalPlanNotPublishedReason 取值(五个值,同 batch):
| 取值 | 触发条件 |
|---|---|
STALE_FINALIZED_PLAN |
存在按旧需求定稿的陈旧行 |
INVALID_PLAN_GENERATION |
方案代际不一致 |
PLAN_INCOMPLETE |
满派拓扑不完整 |
CAPACITY_INSUFFICIENT |
未定稿分支载客量不足 |
GATE_UNSATISFIED |
大交通要求的接/送机日未配车 |
请求示例
实测原文(wf/8429/ac2/out/confirm_req_body.json,AC-5,groups 精确传当前 2 个 assigned 执行段):
{
"orderId": 2104161383220457474,
"requestId": "fleetqa-ac5-confirm-8337937430",
"expectedRequirementVersion": 4,
"expectedRequirementSha256": "24a2233c35565f31d14ecaaba4cf47abc56c592b5a0bb6b78f927ba4a949ca95",
"expectedPlanGeneration": 362569745205170176,
"groups": [
{
"assignmentGroupId": 362556592543109120,
"sendItinerarySms": false
},
{
"assignmentGroupId": 362569161085423616,
"sendItinerarySms": false
}
]
}
响应示例
确认成功且方案发布——实测原文(wf/8429/ac2/out/confirm_resp.json):
{
"code": 200,
"message": "成功",
"data": {
"requirementId": "2104178895018459138",
"dispatchPlanGeneration": "362569745205170176",
"confirmed": true,
"finalPlanPublished": true,
"finalPlanNotPublishedReason": null,
"groups": [
{
"assignmentId": "2104166776231403521",
"assignmentGroupId": "362556592543109120",
"assignmentStatus": "assigned",
"confirmedAt": "2026-09-27 19:10:28",
"sendItinerarySms": false,
"itinerarySmsEventId": null,
"itinerarySmsStatus": "NOT_SENT",
"itineraryUrl": null
},
{
"assignmentId": "2104179344782008321",
"assignmentGroupId": "362569161085423616",
"assignmentStatus": "assigned",
"confirmedAt": "2026-09-27 20:00:24",
"sendItinerarySms": false,
"itinerarySmsEventId": null,
"itinerarySmsStatus": "NOT_SENT",
"itineraryUrl": null
}
]
},
"traceId": null,
"success": true
}
确认成功但未发布(
confirmed=true, finalPlanPublished=false, finalPlanNotPublishedReason="GATE_UNSATISFIED"等五值之一)的响应形状与上例相同,只是这三个字段取值不同——本轮测试未构造出该分支的原始抓包,字段定义见上方出参表与源码ConfirmRequirementRespVO.java,取值语义见「finalPlanNotPublishedReason 取值」表。
空数据 / 降级响应
无额外空态。确认成功但未发布时返回 confirmed=true + finalPlanPublished=false + 原因。
错误响应
按日志重建(wf/8429/ac2/ac2.log:48,字段形状与本文档其余错误响应一致的 Result 包装):
{
"code": 605056,
"message": "执行段集合已变化,请刷新后重试",
"data": null,
"traceId": null,
"success": false
}
| 错误码 | 触发条件 |
|---|---|
| 605056 | groups 未精确等于当前有效执行段集合(多传/少传任一均拒绝,非 602202/#8429 引入,不变但实测新确认;文档 @ApiOperation 未列出此码,属既有文档缺口)—— 实测:首次误传全部 6 个历史执行段(含已 completed)报本码,改传当前 2 个 assigned 执行段后成功 |
| 605914 | 大交通要求接机的日期未配置接机车辆(不变) |
| 605915 | 大交通要求送机的日期未配置送机车辆(不变) |
| 605062 | 存在派车日期越出当前需求日期窗的在途槽位行(不变) |
| 605037 / 605038 | 车辆维保或停用 / 司机休假或待激活,不可派(不变) |
| 605059 | 同一 requestId 用于了不同的确认内容,须换新 requestId 重试(不变) |
| 605063 | 原子确认回执已损坏,无法幂等重放;该码不可自愈,前端不得自动重试或静默轮询,须提示用户联系管理员(不变) |
业务边界
- 确认与发布分离:确认本身与最终方案发布是两件事,
confirmed=true不代表finalPlanPublished=true;前端以两个字段组合判断下一步。 - 已完结行越窗:不再拒绝确认;确认成功但未发布时按原因(通常是门禁不满足或方案不完整)回到前面的步骤补齐。
- 行程短信与电子行程单:只在
finalPlanPublished=true时发送(不变)。 groups必须是精确集合,不是增量:提交前须先取当前全部有效执行段(如通过看板/详情接口)再据其构造groups,多传已完结的历史执行段或少传遗漏均触发 605056;本次实测已验证该约束真实存在(见上方错误响应)。- 605063 终态:命中该码时前端应停止对同一
requestId的自动重试,引导人工介入,重试不会改变结果。
四、契约约束与正确调用方式
判断发布结果
对所有三个接口,推荐判断顺序:
1. 若 finalPlanPublished = true
→ 最终方案已发布,订单车控推进到 DONE(不变)
2. 若 finalPlanPublished = false 且 reason = GATE_UNSATISFIED
→ 接送机未配齐,回到第③步配接送机
3. 若 reason = PLAN_INCOMPLETE
→ 派车方案不完整(缺车、缺司机等),回到第②步调整派车
4. 若 reason = STALE_FINALIZED_PLAN 或 INVALID_PLAN_GENERATION
→ 需重新确认需求(第④步 confirm),或方案代际变化需重新派车
5. 若 reason = NO_GATE_TRANSITION(仅 pd)
→ 本次未触发新的跃迁,重复保存或调整门禁再试
前端不需要做的
- ❌ 按
finalPlanPublished=false推断"哪里出了问题",必须用finalPlanNotPublishedReason字段精确判断。 - ❌ 拦截错误码后自行决定前端跳转(跳转策略参考上面的判断顺序,但最权威的来源是后端原因字段)。
五、数据库行为
无 DDL。改动全在判据(读侧)与新字段(响应序列化)。
| 对象 | 改前 | 改后 |
|---|---|---|
| 派车行(已完结) | 越窗时满派判据返 false,方案永不发布 | 越窗时 dateValid 豁免,其他条件仍检查 |
| 派车行(在途) | 越窗时满派判据返 false | 不变 |
| 最终方案快照 | 发布条件只看门禁(batch 路径),或门禁+满派(pd/confirm 路径) | 三条路径统一走完整判据 |
| finalPlanNotPublishedReason 响应字段 | 无 | 新增;发布时为 null;不发布时为首个未通过的原因 |
| 602202 上限 | 1(TRANSFER-only) | max(1, 需求声明车数)(TRANSFER-only) |
六、边界行为
能否从 true 变成 false
- 能(方案不完整场景):改前 batch 可能因只看门禁而返 true,改后补上满派检查可能返 false。前端需捕获这个变化。
- 缓解:原因字段让前端精确提示运营下一步;越窗已完结行豁免缓解了大部分场景。
已完结行的豁免形式
- 只豁免
dateValid(日期窗口检查);不豁免其他条件(有车有司机、形态合法、每个 key 恰一条)。 - 只豁免 COMPLETED 行;HOLDING / ASSIGNED 越窗照旧拒。
- 不影响其他判据(陈旧定稿、方案代际、载客量、门禁都不变)。
版本联动
本单改动不涉及其他服务。下游 order-v3 对 finalPlanPublished 的消费逻辑不变(true 时更新车控为 DONE,false 时保持原状)。
六.6、修改前后对比
字段级对比
| 字段 | 改前 | 改后 |
|---|---|---|
batch / pd / confirm finalPlanNotPublishedReason |
无 | 新增;发布时 null,不发布时为首个未通过判据的原因(五个通用值或两个 pd 专属值) |
pd finalPlanPublished(已完结行越窗场景) |
false | true(已完结行越窗不再阻止) |
batch finalPlanPublished(必须配车的服务日本次未提交 dailyPlan 行覆盖) |
true | false + PLAN_INCOMPLETE(补上满派检查) |
| 602202 上限 | 1(TRANSFER-only) | max(1, 需求声明车数)(TRANSFER-only) |
行为级对比
| 行为 | 改前 | 改后 |
|---|---|---|
| 三个接口发布判据 | batch 只过门禁;pd/confirm 过满派+陈旧定稿+门禁 | 三者走同一判据:陈旧定稿→代际→满派→门禁 |
| 越窗已完结行 | 满派判据返 false,方案永不发布 | 豁免 dateValid;其他条件仍检查 |
batch 存在必须配车服务日未提交 dailyPlan 行覆盖 |
门禁满足时发布(true) | 满派检查拦住,不发布(false + PLAN_INCOMPLETE) |
| 602202 文案(TRANSFER) | 「同一天同一方向只能有一条」 | 「{日期} 的 {方向} 已有 {已标条数} 条…超过接送机需求声明的 {声明车数} 辆」 |
六.7、影响评估
- 是否破坏向后兼容: 否。所有接口路径、入参、出参结构不变;错误码与文案(602202 除外)不变。只是三个接口新增一个字段,且 batch 的发布结果在「方案不完整」时取值会改变。
- 前端是否必须同步上线: 否。不改前端时:
- 管理员的调用流程与改前一致(只是 batch 时 false 需要按新原因字段判断下一步,而原来的 false 本身就很少出现);
- 新字段为 null 时前端可以忽略(安全字段,只在不发布时有值)。
- 前端 workaround 清理点:
- 如果前端有"batch 返 false 就兜底改 pd"之类的旧补偿逻辑,需以新原因字段为准判断是「接送机未配」还是「方案不完整」,而不是盲目走第③步。
- 602202 的提示文案可从「不能多配」改为「不能超过声明车数」(文案已自解释)。
- 已完结行越窗的"方案卡死"问题随之消失,如有针对性的降级/提示可撤。
七、不影响范围
- 其他接口:fleet 其他调用方、order-v3、mp 端、网关路由全不受影响。
- 操作链路:第①②③④步的入参、错误码(除 602202 上限外)、取消/删除操作一概不变。
- 数据结构:派车行、最终方案快照、需求实体的存储结构不变。
- 已发布的快照:改前发的最终方案快照不重放,不在版本号上变化。
八、测试环境已验证
环境:TEST,hl-fleet-service 267ab6906 于 2026-09-27 19:40:15 部署,hl-gateway 71def6dc5 同期在线(deploy-status 于 20:00 前后与 20:25:46 两次核对读数一致,全程无重新部署)。以下均为 2026-09-27 19:58-20:27 网关实测(测试专用车务账号,role=车务),原始请求/响应/SQL/日志见 wf/8429/ac2/result.md 及同目录 out/*.json。
AC-2:越窗已完结行不再卡发布,batch 与 pd 答案一致 —— 已验证 通过
三步链路(同一需求 requirementId=2104178895018459138,换版后 serviceDates 含新日期 2027-08-30):
- batch 提交 [(08-15,车), (08-30,车)]:
finalPlanPublished=false, finalPlanNotPublishedReason=GATE_UNSATISFIED;fleet 日志同步打出「接送机未配齐,暂不发布最终方案快照」。 - pd 一次性补标 08-15+08-30 两日接机:
finalPlanPublished=true, finalPlanNotPublishedReason=null,pickupDropoffGate.satisfied=true;此后日志不再出现「未配齐」提示。 - 原样重放 batch(20:02:43,未提交新行):仍
finalPlanPublished=true, finalPlanNotPublishedReason=null。
SQL 复核(20:02:43 之后):该需求名下 6 行全部 dispatch_plan_finalized=1,含越窗已完结行(08-03/08-04,plan_finalized_requirement_id 已指向当前需求)——越窗已完结行豁免、仍计入当前需求判定,符合预期。batch 与 pd 对同一份数据不再出现「一真一假」。
AC-4:602202 改按声明车数动态判定 —— 已验证 通过
声明车数=2 时(需求 2104183379455049729,3 行落库 assigned):2 辆标接机 http=200 code=200;追加第 3 辆 → http=200 code=602202,文案含日期(2027-09-05)、方向(ARRIVAL)、3 个派车行 ID、声明车数「2」;失败调用前后 SQL 标记值完全一致,确认整批原子回滚。换版把声明车数降为 1(需求 2104185762864115714)后,同样 2 辆车即触发 602202,文案里的声明车数同步变为「1」。两级阈值均实测复现,判据已从固定值改为动态读需求声明车数。
AC-5:三写口统一暴露 finalPlanNotPublishedReason —— 已验证 通过
- batch(即 AC-2 第一步):字段值
GATE_UNSATISFIED。 - pd(AC-2 第二步同配置换新 requestId 重放,20:09:26):字段值
NO_GATE_TRANSITION(门禁已满足,重复提交不构成新的跃迁,不重新发布)。 - confirm:
groups精确传当前 2 个 assigned 执行段后,响应data中存在finalPlanNotPublishedReason键,值为null(已发布状态),见「三、接口详情」需求确认小节的响应示例。
三个写口的响应体均带该字段,取值随各自判据结果变化,字段本身在「发布」与「未发布」两种结果下都稳定出现。
已知边界
- 旧回执重放(二期未上生产,仅测试服):本版本前生成的 confirm 回执若重放,可能出现
finalPlanPublished=false而原因为null。前端以finalPlanPublished为准,不要用「原因是否为空」反推是否已发布。 - count 缺失:按 1 计,与改前等价。
- confirm 的
groups必须精确等于当前有效执行段集合:本轮实测中途误传过全部历史执行段(含已 completed),触发 605056,详见「三、接口详情」需求确认小节的错误响应。
九、相关历史 PR
| PR | Issue | 说明 | 是否仍有效 |
|---|---|---|---|
| — | #7067 | 统一发布入口,batch 补充门禁漏斗 | ⚠️ batch 返工时漏了满派与陈旧定稿守卫,本单补齐 |
| — | #5810 | 已完结行随平移与豁免(基线、代际) | ⚠️ 满派拓扑这一处漏了,本单补齐 |
| — | #7443 | 602202 首次引入(上限 1) | ⚠️ 上限被本单改为声明车数 |
| 本 PR #8451 | #8429 | 三个写口统一判据、已完结行越窗豁免、602202 按声明车数、新增未发布原因字段 | ✅ 最新 |
十、相关文档
- Issue: #8429
- PR: #8451
- 上游缺口: #5810(已完结行口径)、#7067(发布统一)、#7443(602202 来源)
- 本轮实测新确认的既有缺陷: #8453(接送机配置
kind省略时绕过 605905/602202/门禁判据,纯 TRANSFER 订单需显式传kind;非本单引入,详见「三、接口详情」批量派车与接送机配置两节的业务边界)
关联 / 联系人
链接
联系人
- 后端负责人: @wx