文件
hl-api-changelog/changelogs-v2/2026-09/27_8429_车务最终实派方案发布判据统一与未发布原因字段-修改接口-管理后台.md
T

43 KiB
原始文件 Blame 文件历史

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 还要过满派守卫,当方案含「越窗已完结行」时满派判定会卡住。

两个上游缺口:

  1. #5810 在「最终确认基线」与「方案代际」两处给已完结行豁免,满派拓扑这一处漏了;
  2. #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.md AC-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.log 20:19:37):http=200 code=200 finalPlanPublished=False reason=PICKUP_DROPOFF_GATE_DISABLED,602202 判定被跳过。
    • 补传 kind=TRANSFER(wf/8429/ac2/run_ac4_pd3b.log 20: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):

  1. batch 提交 [(08-15,车), (08-30,车)]:finalPlanPublished=false, finalPlanNotPublishedReason=GATE_UNSATISFIED;fleet 日志同步打出「接送机未配齐,暂不发布最终方案快照」。
  2. pd 一次性补标 08-15+08-30 两日接机:finalPlanPublished=true, finalPlanNotPublishedReason=null,pickupDropoffGate.satisfied=true;此后日志不再出现「未配齐」提示。
  3. 原样重放 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