文件
hl-api-changelog/changelogs-v2/2026-09/19_7442_团期配车受控重开窗口计划刷新状态收口-新增接口-管理后台.md
T

47 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 7442 团期配车受控重开窗口 + 计划刷新状态收口(PR-C2) admin wx(GIT) 新增接口 deployed verified verified mmg 3df2c6f8df7902b31886e7ca0fc0ec88807d7032 2026-09-21 代码已合入 dev-v3(PR #7957,merge commit c6aa1224fb46c3a7671da8234886075efde3c4ed)。⚠️ 本文档此前写『测试服尚未部署到含本次改动的版本』——那句已过期:2026-09-19 的实测中 reconfigureWindowToken(PR-C2 引入的字段)被真实端点接收并生效,反证 #7957 已在测试服链上;同轮实测中 order-v3 与 fleet 均在 31fd5b5e6。⚠️ 同时订正一处头身不一致:gateway_status 字段一直写着 verified,而 status_note 与第八节都写着 pending——那时的 verified 是假状态。现在 backend_status=deployed、gateway_status=verified 都有证据:本文档覆盖的 5 个端点已于 2026-09-19 逐个实测,见第八节的请求/响应与数据库交叉读数。本文档是 #7442 PR-C(团级确认态与配车恢复流程)的第三份交接件:PR-B(fleet 确认整团配车 + order-v3 回写已发车务)已由 17_7442_团级确认态-需求已发车务回写-新增接口-管理后台.md 交付并 deployed;本文档只覆盖 PR-C2(受控重开窗口 + 计划刷新状态收口)新增/改造的端点,不重复 PR-B 内容。前端 2026-09-21 已交付(增量1):① reopen 入口落在 GroupVehicleRequirementSection「受控重开配车窗口」(DISPATCHED/PENDING_RECONFIRM 可开,reason/scopeGroupCodes/scopeDates/windowMinutes 四必填走 NForm rules,成功结果视图展示 windowToken+expiresAt+复制按钮供转达车务,重开后重拉同步 PENDING_RECONFIRM 标签);② requirement/confirm 六字段分支:planRefreshState!=null 即受控重开链路,只提示「已受理重新确认,配车计划刷新中」不走户级放行文案(advanceUnblocked 恒 false 不宣称恢复),并联动需求区 reload+短轮询;809204/809210 拦截器透 message。plan-refresh/coverage/dispatch-baseline 三内部端点前端不对接。PR-A reconfigure 编辑器为增量2 待建。 2026-09-19 dev-v3

fleet/order-v3: 团期配车受控重开窗口 + 计划刷新状态收口(PR-C2)

存放目录: changelogs-v2/{YYYY-MM}/

服务: hl-fleet-service (端口 8082)、hl-order-service-v3 (端口 8083) PR: #7957 Issue: #7442 PR-C2(AC-22) 日期: 2026-09-19 影响范围: 团期已过资源准备阶段后的「受控重开配车」恢复流程——新增受控重开窗口开启端点、新增 fleet 侧计划刷新/覆盖查询两个内部端点、既有「整体确认需求」端点新增受控重投分支


⚠️ 关键变化

  1. 设计稿里的独立 reconfirm 端点已取消:早期设计曾计划新增 POST /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/reconfirm,该端点从未上线。 受控重开窗口内的「重新确认」动作最终挂在既有的整体确认端点 POST /v3/admin/order/group-batch/{groupBatchId}/requirement/confirm 上,服务端按需求当前状态自动路由到 受控重开分支——前端不需要调用任何新端点来完成重新确认,仍调那个熟悉的「确认」按钮对应的端点即可。
  2. 确认成功 ≠ 团期立刻恢复推进:受控重开窗口内的确认只是把「计划刷新」这件事可靠登记下来, 响应新增 planRefreshState/planRefreshOutboxId/advanceUnblocked 等六个字段,其中 advanceUnblocked 恒为 false——阻断解除发生在 fleet 侧就绪回调通过两级判定之后,不在本次调用里。 前端必须据此做等待或轮询,不能在确认成功的瞬间就告诉运营「好了」。
  3. 响应字段名与 AC-22 原文不同:AC-22 原文写的是 planRefreshRequeued(Boolean)+ planRefreshReplayCount,但那不是最终落地的字段——2026-09-18 的订正已判定实际字段是 planRefreshReplayKind(枚举 REQUEUED/WOKEN/NOOP_IN_FLIGHT/NOOP_ALREADY_PICKABLE/NOOP_RACED, 五取值不能合成两档)+ planRefreshReplayCount。本文档按代码实际字段名记录。
  4. 「10 秒内重复提交返回上次结果」这句话不完全准确:POST /admin/fleet/group-dispatch/batches/{groupBatchId}/reconfigure (fleet 整团逐日配车提交端点,非本文档新增/改造范围,完整契约见补登文档 19_7442_团期配车分组写口reconfigure补登-新增接口-管理后台.md;受影响的幂等语义与本次改动强相关)的 @Idempotent 窗口是 10 秒,但窗口内重复提交是拒绝(返 100502「团期配车重配处理中,请勿重复提交」), 不是「返回上次结果」;「提交同一份计划返回上次结果(idempotentShortCircuit=true)不是失败」这件事 与 10 秒防重窗口是两套独立机制(前者按计划内容摘要判重,没有时间窗限制),详见「四、契约约束」。
  5. 错误码数量核对:工单 AC-22 原文写「25 个新错误码」(fleet 16 + order-v3 9),本次逐个核对源码后 实际落地 21 个——fleet GroupDispatchAdminErrorCode 602000-602014 共 15 个(不是 16)、 order-v3 GroupDispatchReconfigureErrorCode 809203/809204/809205/809206/809209/809210 共 6 个 (不是 9)。差额 4 个(fleet 602015、order-v3 809202/809207/809208)是登记但明确不建的号 (类注释里有完整的「0 调用即删」红线不可达论证/裁定记录,不是本单遗漏或漏抛),按 AC-22 数字去核对会 误以为少了 4 个。

二、变更接口清单

# 接口 方法 路径 变更类型 说明
1 受控重开正式用车需求 POST /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/reopen 新增 团期管理员开一个带令牌/范围/有效期的窗口,让车务在窗口内改车
2 整体确认需求(放行房务) POST /v3/admin/order/group-batch/{groupBatchId}/requirement/confirm 改造(入参不变,出参新增 6 字段) 受控重开窗口内的重新确认改走 confirmWithCoveragePrecheck,新增受控重投分支
3 按需求身份刷新团期配车计划 POST /internal/fleet/dispatch/group-batch/{groupBatchId}/plan-refresh 新增(内部) order-v3 的耐久命令执行面,让 fleet 按新需求身份重刷计划并重发就绪意图
4 查询团期配车覆盖现状 GET /internal/fleet/dispatch/group-batch/{groupBatchId}/coverage 新增(内部) 重新确认前的快速失败预检,不是权威判定
5 团期配车权威基线 GET /v3/internal/group-batch/{groupBatchId}/dispatch-baseline 改造(内部,补记;PR-A 首次扩充 + 本次 PR-C2 追加 reconfigureWindow) 新增受控重开窗口字段,dispatchable 语义放宽

本单不改动 POST /admin/fleet/group-dispatch/batches/{groupBatchId}/reconfigure(整团逐日配车提交)、 POST /admin/fleet/group-dispatch/batches/{groupBatchId}/confirm(确认整团配车)、 POST /v3/internal/group-batch/{groupBatchId}/vehicle-requirement/dispatched(回写已发车务) 三个端点的契约——它们分别属于 #7442 更早的 PR-A、PR-B,PR-B 两个端点已由 17_7442_团级确认态-需求已发车务回写-新增接口-管理后台.md 交付;PR-A 的 reconfigure 端点至今没有任何 changelog 记录,本文档不越权补写,已在交付报告中单独提示。


三、接口详情

1. 受控重开正式用车需求 POST /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/reopen

VO: GroupVehicleRequirementReopenReqVO → GroupVehicleReopenRespVO

使用场景

团期已经过了「资源准备」这个允许配车的阶段(进入 MATERIAL_PREPARING/PENDING_DEPARTURE),但客观情况变了 (临时加人、换车),团期管理员在团期详情页点「受控重开」,显式开一个带令牌、带范围(哪几个乘车分组、哪几天)、 带有效期的窗口,把已配车的正式需求打回 PENDING_RECONFIRM,同时清 vehicle_ready(团期推进随即被出团门②挡住)。 车务拿到 windowToken 后在配车页重配时必须原样带回。

入参字段表

字段 位置 类型 必填 约束 说明
groupBatchId Path Long 是 - 团期主订单 ID
reason Body String 是 ≤200 重开原因,写进需求时间线留痕
scopeGroupCodes Body List<String> 是 1-20 个 授权可改的乘车分组编码;必须是当前需求已有的组,越界 400
scopeDates Body List<LocalDate> 是 1-60 个 授权可改的行程日;必须落在 scopeGroupCodes 各组服务日并集内
windowMinutes Body Integer 否 10-1440,默认 120 窗口有效分钟数;同时是本轮计划刷新的超时阈值

出参字段表

字段 类型 说明
requirementId String(雪花 ID) 被重开的正式需求 ID
requirementVersion Integer 重开后的需求版本,与重开前相同(重开不递增版本)
requirementStatus String 恒为 PENDING_RECONFIRM
windowToken String 一次性窗口令牌,车务重配时必须原样带回,否则 fleet 报 602012
expiresAt LocalDateTime 窗口失效时刻
blockedStage String 重开时的团期阶段快照
batchStatus String 团期当前状态(重开不回退它)

请求示例

POST /v3/admin/order/group-batch/1934567890123456800/vehicle-requirement/reopen
{
  "reason": "客户临时增加 2 人,需要加一辆车",
  "scopeGroupCodes": ["BUS-A"],
  "scopeDates": ["2026-10-01"],
  "windowMinutes": 120
}

响应示例

{
  "code": 200,
  "message": "成功",
  "data": {
    "requirementId": "1934567890123456789",
    "requirementVersion": 3,
    "requirementStatus": "PENDING_RECONFIRM",
    "windowToken": "a1b2c3d4e5f6...",
    "expiresAt": "2026-09-19 14:00:00",
    "blockedStage": "PENDING_DEPARTURE",
    "batchStatus": "PENDING_DEPARTURE"
  },
  "success": true
}

空数据 / 降级响应

N/A。同一操作人在窗口有效期内重复调用本端点是幂等成功,原样返回既有令牌(不算「空数据」)。

错误响应

{
  "code": 809203,
  "message": "该团已有生效中的重开窗口(开窗人 1001,2026-09-19 14:00:00 失效),请等其结束或由本人操作",
  "data": null,
  "success": false
}

可能的错误码:

  • 809100 - 该团期无活跃正式需求
  • 809101 - 需求状态/分组形态不允许重开(整团免车的团不许重开;CONFIRMED 只在刷新已失败时可重开)
  • 809111 - 团期阶段不允许重开(不在 RESOURCE_PREPARING/MATERIAL_PREPARING/PENDING_DEPARTURE)
  • 809203 - 已有他人开的生效中窗口
  • 809209 - 上一轮计划刷新尚未结束

业务边界

  • 鉴权: 沿用团期需求既有权限点 group-batch:demand:confirm,本单不新增权限点
  • 幂等性: @Idempotent 5 秒窗口 + 同一操作人在生效窗口内重复调用原样返回既有令牌
  • 窗口过期后续开是合法路径: PENDING_RECONFIRM 且窗口已过期时可以再开一次窗口(不算异常状态),这是本端点唯一能让「到点车还没排完」的团解开的入口——不放行的话这个团会永久锁死在阻断态
  • 整团免车(零分组)的需求不能重开:这类团在 fleet 侧压根没有配车行,「重新配车」对它没有意义,要改成需要车须先撤回免车声明

2. 整体确认需求(放行房务) POST /v3/admin/order/group-batch/{groupBatchId}/requirement/confirm

VO: 无请求体(Path only) → GroupBatchRequirementConfirmRespVO

使用场景

本端点是 #7210 起就有的既有端点,团期管理员在团期详情页点「确认」——本次改造前该端点只负责住宿+车侧的整体确认; 本次改造后,Controller 改调 confirmWithCoveragePrecheck:受控重开窗口内的重新确认会先向车务问一次覆盖现状 (不满足抛 809204),然后才进入既有的幂等/锁/事务流程。普通整团确认(未受控重开的绝大多数场景)行为一字未变, 只有当活跃需求处于 PENDING_RECONFIRM 且有阻断留痕(即经上面「reopen」端点重开过)时才会走新增分支。

入参字段表

字段 位置 类型 必填 约束 说明
groupBatchId Path Long 是 - 团期主订单 ID

本端点无请求体,入参与改造前完全一致(零变化)。

出参字段表

出参在既有字段基础上新增 6 个受控重开专用字段(其余既有字段——groupBatchId/requirementConfirmed/ dispatchedOrderIds/skippedOrderIds/dispatchedCount/vehicleDispatchedOrderIds 等——本次零改动,不在此重复列出):

字段 类型 说明
planRefreshState String PENDING(刷新命令已登记、尚未成立,首次调用几乎恒为此值)/ DONE(罕见:回调已在响应前先落库)/ FAILED(重入路径读到的上次失败态);非受控重开链路为 null
planRefreshOutboxId String(雪花 ID) 本轮刷新命令的 Outbox ID,排障用;非受控重开链路为 null
planRefreshReplayKind String 本次受控重投的落点:REQUEUED(终态命令已重新入队)/ WOKEN(退避已清,立即可领)/ NOOP_IN_FLIGHT(命令正在跑)/ NOOP_ALREADY_PICKABLE(本就待领)/ NOOP_RACED(并发抢跑);首次确认(非重投)与非受控链路一律为 null
planRefreshReplayCount Integer 人工受控重投累计次数;达到 5 次后再点确认会收到 809210,此时提示应改为「请联系后台排查」
blockedStage String 团期阻断阶段快照(重开时记下的团期状态);非阻断为 null
advanceUnblocked Boolean 恒为 false——解除阻断只发生在 fleet 就绪回调通过两级判定之后,绝不在确认这一刻

请求示例

POST /v3/admin/order/group-batch/1934567890123456800/requirement/confirm

(本端点无请求体)

响应示例(受控重开窗口内的重新确认,首次调用)

{
  "code": 200,
  "message": "成功",
  "data": {
    "groupBatchId": "1934567890123456800",
    "requirementConfirmed": true,
    "dispatchedOrderIds": [],
    "skippedOrderIds": [],
    "dispatchedCount": 0,
    "vehicleDispatchedOrderIds": [],
    "transferDispatchedOrderIds": [],
    "vehicleSkippedOrderIds": [],
    "vehicleDispatchedCount": 0,
    "groupVehicleRequirementId": "1934567890123456789",
    "groupVehicleRequirementStatus": "CONFIRMED",
    "groupVehicleRequirementVersion": 4,
    "planRefreshState": "PENDING",
    "planRefreshOutboxId": "1934567890123456999",
    "planRefreshReplayKind": null,
    "planRefreshReplayCount": 0,
    "blockedStage": "PENDING_DEPARTURE",
    "advanceUnblocked": false
  },
  "success": true
}

空数据 / 降级响应

普通整团确认(非受控重开链路)时,上述 6 个受控重开字段整块为 null/false——这是合法形态,前端据此判断 「这次确认要不要等刷新结果」;不要把整块 null 当成异常。

错误响应

{
  "code": 809204,
  "message": "车务配车尚未覆盖完整,不能重新确认: 乘车分组 BUS-A 缺失 2026-10-01",
  "data": null,
  "success": false
}

可能的错误码(受控重开分支新增,普通确认分支错误码零变化):

  • 809204 - 受控重开链路且车务配车仍未覆盖完整(fail-closed:Feign 降级/异常同样抛此码,零写入)
  • 809210 - 人工受控重投次数已达上限(达 5 次)

业务边界

  • 预检只在受控重开链路上发 Feign:活跃需求不在 PENDING_RECONFIRM、或没有阻断留痕时,confirmWithCoveragePrecheck 直接跳过预检——普通整团确认因此一次远端调用都不多,不受本次改造影响
  • 受控重投是零写入的三种情形不扣重投预算:命令正在跑(NOOP_IN_FLIGHT)/ 本就待领(NOOP_ALREADY_PICKABLE)/ 并发抢跑(NOOP_RACED)——只有 REQUEUED/WOKEN 才真的推进了一次,才计入 planRefreshReplayCount
  • planRefreshState=DONE 时重复确认是幂等成功:原样返回、零写入,不会对已闭环的刷新再发一次
  • 本端点没有单独的防重提交窗口:@Idempotent 挂在既有的 confirm 方法上(5 秒),预检 Feign 调用被移到幂等窗口与锁窗口之外执行,避免一次 Feign 最坏墙钟穿透幂等窗口与 30 秒锁租约

3. 按需求身份刷新团期配车计划 POST /internal/fleet/dispatch/group-batch/{groupBatchId}/plan-refresh

⚠️ [内部接口,不对前端开放] order-v3 的 GROUP_DISPATCH_PLAN_REFRESH 耐久命令执行面,Feign 直连调用。

VO: GroupDispatchPlanRefreshReqDTO → GroupDispatchPlanRefreshRespDTO

使用场景

团期管理员在受控重开窗口内点「重新确认」后,order-v3 在同一事务里登记一条计划刷新耐久命令;命令处理器随后(异步、 无事务上下文)调用本端点,让 fleet 把计划行上钉住的需求身份推进到新版本,并重新发出就绪意图。按 (requirementId, requirementVersion) 幂等:身份已对上时不递增计划版本,只把就绪意图重新放回投递链路。

入参字段表

字段 位置 类型 必填 约束 说明
groupBatchId Path Long 是 - 团期主订单 ID
requirementId Body Long 是 - 刷新后计划应当钉住的正式团级用车需求 ID
requirementVersion Body Integer 是 ≥1 刷新后计划应当钉住的需求版本
sourceRefNo Body String 否 ≤64 幂等追溯号(order-v3 侧 Outbox 命令 ID),仅用于日志对账

出参字段表

字段 类型 说明
groupBatchId String(雪花 ID) 团期主订单 ID
planVersion Long 刷新后的计划版本;已按同一身份刷过时返回上次的值且不递增
planDigest String 刷新后的计划摘要
versionBumped Boolean 本次是否真的递增了计划版本(幂等命中时为 false)
readyIntentEmitted Boolean 就绪意图是否处于可投递态(幂等命中时仍为 true,但需靠受控重投真的推回 PENDING 才算数)
readyIntentRequeued Boolean 本次是否把一条已终态(SUCCESS/QUARANTINED)的就绪意图推回了 PENDING;意图本就在途时为 false
aliveCount Integer 参与本次计划的活跃配车行数
definitiveFailure String 判定性失败原因;IDENTITY_STALE(需求身份已陈旧)/ COVERAGE_INCOMPLETE(库里配车仍有覆盖缺口);null=成功
gaps Array<String> 覆盖缺口描述;仅 COVERAGE_INCOMPLETE 时非空
legacyGroupRowCount Integer 本团存活派车行里无分组键的历史行数(非错误,仅留痕)

请求示例

POST /internal/fleet/dispatch/group-batch/1934567890123456800/plan-refresh
{
  "requirementId": "1934567890123456789",
  "requirementVersion": 4,
  "sourceRefNo": "1934567890123456999"
}

响应示例

{
  "code": 200,
  "message": "成功",
  "data": {
    "groupBatchId": "1934567890123456800",
    "planVersion": 8,
    "planDigest": "a1b2c3d4...",
    "versionBumped": true,
    "readyIntentEmitted": true,
    "readyIntentRequeued": false,
    "aliveCount": 9,
    "definitiveFailure": null,
    "gaps": [],
    "legacyGroupRowCount": 0
  },
  "success": true
}

空数据 / 降级响应

判定性失败以 HTTP 200 表达,不是 4xx/5xx——在传输层它与「远端暂时不可用」完全同形,而两者的正确处置相反:

{
  "code": 200,
  "message": "成功",
  "data": {
    "groupBatchId": "1934567890123456800",
    "planVersion": 7,
    "planDigest": "旧摘要...",
    "versionBumped": false,
    "readyIntentEmitted": false,
    "readyIntentRequeued": false,
    "aliveCount": 9,
    "definitiveFailure": "COVERAGE_INCOMPLETE",
    "gaps": ["乘车分组 BUS-A 缺失 2026-10-01"],
    "legacyGroupRowCount": 0
  },
  "success": true
}

错误响应

真正可重试的故障(DB 不可用、CAS 并发冲突、该团零活跃配车行)仍以异常形式返回失败 Result,由 order-v3 侧命令处理器退避重试:

{
  "code": 602014,
  "message": "本团没有活跃配车行,无法刷新配车计划",
  "data": null,
  "success": false
}

可能的错误码:

  • 602014 - 该团零活跃配车行(不产出空计划)
  • 600008 - 计划版本 CAS 被并发推进
  • 600009 - 基线不可用

业务边界

  • 判定性失败 vs 可重试故障必须分开:IDENTITY_STALE/COVERAGE_INCOMPLETE 由调用方立刻转终态 FAILED、不再指数退避空转;数据库不可用等由既有重试机制处理
  • 幂等命中时仍会把就绪意图推回可投递态(受控重投):不是只回一个旧 eventId 就宣称重发过——readyIntentRequeued=true 才说明「回调丢失」这个场景真的被修复了
  • 本端点无 admin 入口,仅供内部 Feign 调用

4. 查询团期配车覆盖现状 GET /internal/fleet/dispatch/group-batch/{groupBatchId}/coverage

⚠️ [内部接口,不对前端开放] order-v3 消费方在受控重开窗口内「重新确认」之前的快速失败预检调用。

VO: GroupDispatchCoverageQueryReqVO → GroupBatchDispatchCoverageDTO

使用场景

对应「三、接口详情」第 2 条里 confirmWithCoveragePrecheck 调用的正是本端点。order-v3 先问一句「库里这套车现在够不够」, 够就放行确认,不够就当场报 809204 并把缺口念给管理员听——否则确认会成功、刷新命令随后被 fleet 判定性终结,管理员要等到 刷新失败才知道车没排满。它是快速失败的 UX 门,不是权威判定,权威判定在 fleet 刷新事务内部(同一把团级锁、同一次当前读)。

入参字段表

字段 位置 类型 必填 约束 说明
groupBatchId Path Long 是 - 团期主订单 ID
requirementId Query Long 是 - 按哪一份正式团级用车需求判覆盖
requirementVersion Query Integer 是 ≥1 按需求的哪一版判覆盖;与基线当前版本不一致时返回 satisfied=false 而不是抛错

出参字段表

字段 类型 说明
groupBatchId String(雪花 ID) 团期主订单 ID
satisfied Boolean 库里现存配车是否已完整覆盖权威需求;需求身份与入参不一致时同样返回 false
gaps Array<String> 缺口描述(逐条给人看);satisfied=true 时为空列表
missingGroupCodes Array<String> 权威清单声明了、但一辆车都没排的组码——本端点是这个字段唯一有意义的宿主,重配写口的同名字段在任何路径上都只能是空列表(真的缺组时走抛 602002 的路径,根本不产生响应体)
legacyGroupRowCount Integer 本团存活派车行里无分组键的历史行数,与重配/刷新响应上的同名字段同源同算法(#7442 AC-32)
planVersion String(雪花 ID) fleet 当前计划版本;该团尚无计划时为 null
planRequirementId String(雪花 ID) fleet 当前计划钉住的需求 ID;尚无计划时为 null
planRequirementVersion Integer fleet 当前计划钉住的需求版本;尚无计划时为 null

请求示例

GET /internal/fleet/dispatch/group-batch/1934567890123456800/coverage?requirementId=1934567890123456789&requirementVersion=4

响应示例

{
  "code": 200,
  "message": "成功",
  "data": {
    "groupBatchId": "1934567890123456800",
    "satisfied": false,
    "gaps": ["乘车分组 BUS-A 缺失 2026-10-01"],
    "missingGroupCodes": [],
    "legacyGroupRowCount": 0,
    "planVersion": "7",
    "planRequirementId": "1934567890123456789",
    "planRequirementVersion": 3
  },
  "success": true
}

空数据 / 降级响应

satisfied=false 本身就是本端点的正常「不满足」答案,不是错误;只读接口不产出空 data。

错误响应

🔴 2026-09-19 订正:本节此前整段写错了。原文写成「本团正式需求零分组(整团免车)或无需求时统一失败关闭、返 602009」,并引了一句并不存在的 message。逐行核 GroupDispatchService.java:1182-1201 queryCoverage 后,实际是三条互不相同的路径——把它们压成一条,会让调用方给一个根本返 200 的场景写错误处理分支。

情形 实际响应 依据
无活跃需求(baseline.getRequirementId() 为 null,与入参不等) 🔴 HTTP 200,不是错误:satisfied=false,gaps=["需求身份已变:请求 x/vy,当前 null/vnull"] GroupDispatchService.java:1182-1201,走 identity-mismatch 分支
有匹配需求但零分组(整团免车) 602009,message 实为 "正式用车需求未声明任何乘车分组(整团免车的团期不应配车)" 抛出点在 GroupDispatchCoverageCalculator.java:60
基线拉取不可达 600009(基线不可用),不是 602009 fetchBaselineOrFail
// ① 无活跃需求 —— 注意这是 200,success=true
{
  "code": 200,
  "message": "成功",
  "data": { "satisfied": false, "gaps": ["需求身份已变:请求 …/v3,当前 null/vnull"] },
  "success": true
}
// ② 有需求但零分组(整团免车)
{
  "code": 602009,
  "message": "正式用车需求未声明任何乘车分组(整团免车的团期不应配车)",
  "data": null,
  "success": false
}

⚠️ 调用方要点:「拿不到覆盖结论」和「覆盖结论是不满足」在本端点不是同一件事——前者才是错误码,后者是 200 + satisfied=false。把 ① 当错误处理,会把一个正常的「需求身份已变、请重新确认」渲染成系统故障。

业务边界

  • missingGroupCodes 在本端点才有非空的可能,其余端点(重配写口)的同名字段在任何路径上都只能是空列表——两处含义不同,前端渲染缺口清单只能信本端点
  • 只读接口,无副作用、无锁
  • 仅供内部 Feign 调用,无 admin 入口

5. 团期配车权威基线 GET /v3/internal/group-batch/{groupBatchId}/dispatch-baseline

⚠️ [内部接口,不对前端开放] fleet-service 重配前拉取权威基线,仅限内部 Feign 调用。本节是补记—— 该端点的响应契约实际分两步被 #7442 改造(PR-A 首次扩充、本次 PR-C2 再追加一个字段),此前三份 #7442 changelog(含本文档早前版本)都漏记了它,排查 AC-19 ③ 部署约束缺口时一并发现并补齐。

VO: Long(Path) → GroupBatchDispatchBaselineDTO

使用场景

fleet 侧提交整团逐日配车计划(POST .../reconfigure)、以及受控重开窗口内重新确认(本文档第 2 条)之前, 都要先调本端点拉一次团期权威基线,据此做完整覆盖/越界/重复校验,并判断团期当前是否可配、有没有生效中的受控 重开窗口。

入参字段表

字段 位置 类型 必填 约束 说明
groupBatchId Path Long 是 - 团期主订单 ID

出参字段表

字段 类型 说明
groupBatchId String(雪花 ID) 团期主订单 ID
batchStatus String 团期生命周期状态(透传权威值,fleet 仅记录不自行判定)
departDate LocalDate 出团日(团期权威服务日首日);null=团期未设置日期,不可配
endDate LocalDate 返团日(团期权威服务日末日);null=团期未设置日期,不可配
serviceDates Array<LocalDate> 权威服务日集合(departDate..endDate 连续日期,含首尾)
dispatchable Boolean 团期是否可配;【PR-C2 语义扩展,字段名/类型未变】 改前 = 仅 RESOURCE_PREPARING;改后 = RESOURCE_PREPARING 或「受控重开窗口有效 且 团期 ∈ {MATERIAL_PREPARING, PENDING_DEPARTURE}」,见「⚠️ 关键变化」相关说明
requirementId String(雪花 ID) 【PR-A 新增】当前活跃正式团级需求 ID;无活跃需求为 null
requirementVersion Integer 【PR-A 新增】当前活跃需求版本
requirementStatus String 【PR-A 新增】当前活跃需求状态:DRAFT/CONFIRMED/DISPATCHED/DONE/PENDING_RECONFIRM/CANCELLED
groups Array 【PR-A 新增】权威乘车分组清单;空数组触发 fleet 侧 602009 fail-closed
groups[].groupCode String 分组键(=需求侧 group_code,车费分摊 alloc_group 来源)
groups[].vehicleType String 车型文本/字典值(需求侧所报)
groups[].serviceDates Array<LocalDate> 本组服务日(可短于全团)
groups[].dailyHeadcount Map<LocalDate,Integer> 该组该日用车人数(乘车人数,非户数)
groups[].memberOrderIdsByDate Map<LocalDate,Array<Long>> 该组该日实际乘车子订单集合;本单只透传不消费
reconfigureWindow Object 【PR-C2 新增】受控重开窗口;无窗口时为 null;已过期的窗口也会下发(不是 bug,理由见下方字段说明与「业务边界」)
reconfigureWindow.windowToken String 一次性窗口令牌
reconfigureWindow.openedAt LocalDateTime 开窗时刻
reconfigureWindow.expiresAt LocalDateTime 窗口失效时刻
reconfigureWindow.allowedGroupCodes Array<String> 授权可改的乘车分组编码白名单;空列表/null 都不代表"全都可以改",消费方拿不到明确白名单必须失败关闭
reconfigureWindow.allowedDates Array<LocalDate> 授权可改的行程日白名单
reconfigureWindow.openedBy String 开窗管理员 ID(留痕,关窗不清)

请求示例

GET /v3/internal/group-batch/1934567890123456800/dispatch-baseline

响应示例

{
  "code": 200,
  "message": "成功",
  "data": {
    "groupBatchId": "1934567890123456800",
    "batchStatus": "PENDING_DEPARTURE",
    "departDate": "2026-10-01",
    "endDate": "2026-10-06",
    "serviceDates": ["2026-10-01", "2026-10-02", "2026-10-03", "2026-10-04", "2026-10-05", "2026-10-06"],
    "dispatchable": true,
    "requirementId": "1934567890123456789",
    "requirementVersion": 4,
    "requirementStatus": "PENDING_RECONFIRM",
    "groups": [
      {
        "groupCode": "BUS-A",
        "vehicleType": "宇通33座大巴",
        "serviceDates": ["2026-10-01", "2026-10-02"],
        "dailyHeadcount": {"2026-10-01": 30, "2026-10-02": 30},
        "memberOrderIdsByDate": {"2026-10-01": [70123, 70124]}
      }
    ],
    "reconfigureWindow": {
      "windowToken": "a1b2c3d4e5f6...",
      "openedAt": "2026-09-19 10:00:00",
      "expiresAt": "2026-09-19 12:00:00",
      "allowedGroupCodes": ["BUS-A"],
      "allowedDates": ["2026-10-01"],
      "openedBy": "1001"
    }
  },
  "success": true
}

空数据 / 降级响应

无活跃需求或整团免车(零分组)时,requirementId/requirementVersion/requirementStatus 为 null、 groups 为空数组;reconfigureWindow 无生效/曾开过窗口时恒为 null:

{
  "code": 200,
  "message": "成功",
  "data": {
    "groupBatchId": "1934567890123456800",
    "batchStatus": "RESOURCE_PREPARING",
    "departDate": "2026-10-01",
    "endDate": "2026-10-06",
    "serviceDates": ["2026-10-01", "2026-10-02", "2026-10-03", "2026-10-04", "2026-10-05", "2026-10-06"],
    "dispatchable": true,
    "requirementId": null,
    "requirementVersion": null,
    "requirementStatus": null,
    "groups": [],
    "reconfigureWindow": null
  },
  "success": true
}

错误响应

团期不存在时返回失败 Result(fleet 侧收到失败即失败关闭):

{
  "code": 589500,
  "message": "团期不存在",
  "data": null,
  "success": false
}

业务边界

  • dispatchable 字段名与类型没有变,语义变了:这类变更编译、契约测试、既有单测全部看不见——消费方 若在 PR-C2 之前就把 dispatchable=true 简单等同于"可以无条件重配",PR-C2 之后这个等价关系仍然成立, 但多出了一种"受控窗口内才可配"的子情形,必须配合 reconfigureWindow 一起判断,不能只看这一个布尔值
  • 已过期的窗口同样下发,不是缺陷:只有下发了,fleet 才报得出 602012「窗口已过期,请重新开窗」,而不是 一句会把人引错方向的 600010「团期不可配」
  • groups 空数组时消费方必须失败关闭:不能因为拿不到分组就放行一份没有分母的计划(对应 fleet 侧 602009)
  • 本端点是 hl-common-core 跨服务契约变更的核心承载点:响应结构由 order-v3 提供、fleet 消费,两端 必须同批部署,详见「部署清单」

部署清单(本单改了 hl-common-core,CODE_RULES §16.6)

本单在 hl-common-core 新增/追加了字段:GroupBatchDispatchBaselineDTO 新增 reconfigureWindow 字段 (PR-C2,dispatchable 取值域同时放宽,字段名/类型未变,属契约语义变更);GroupDispatchPlanRefreshReqDTO/ GroupDispatchPlanRefreshRespDTO/GroupBatchDispatchCoverageDTO/GroupBatchReconfigureWindowDTO 四个 全新 DTO。这类"字段名没变、类型没变、DTO 类没变,只是语义变了"的改动,编译、契约测试、单测全部看不见, 只有真跑起来、两端版本不一致时才会暴露,而且暴露方式还是静默的(见下)。

🔴 滚动顺序:fleet 必须先于 order-v3,顺序反了不会报错,会静默把"受控重开"变成"不受控重开"

  • fleet 先滚(正确顺序):新版 fleet 部署时,order-v3 还是旧版、不下发 reconfigureWindow 字段 ⇒ Jackson 反序列化该字段为 null ⇒ fleet 走既有的"无窗口"分支,行为与本单改动前逐字一致,安全。
  • order-v3 先滚(错误顺序):order-v3 已按新语义把 dispatchable 放宽到"受控窗口有效且团期在 MATERIAL_PREPARING/PENDING_DEPARTURE"也返回 true;但旧版 fleet 不认识"受控窗口"这个概念, 它的判定逻辑仍是"看到 dispatchable=true 就直接放行重配"——旧 fleet 根本不读 reconfigureWindow 字段,也就不会校验令牌、范围、有效期。⇒ "受控重开"这层门禁在这个窗口期内整个失效,退化成"只要 dispatchable=true 就能任意重配",而且 fleet 端与 order-v3 端的日志都是正常的、没有任何报错—— 两边都认为自己做的事情是对的,只有拿两版代码对照才能看出问题。

消费方清单不能按 pom.xml 直接依赖关系查

⚠️ grep -rl 'hl-common-core' */pom.xml 的结果里只命中 hl-gateway 与 hl-finance——其余六个服务 (hl-user-service/hl-resource-service/hl-product-service-v2/hl-order-service-v3/hl-mp-service/ hl-fleet-service)全部经 hl-common-web/hl-starter-* 传递引入,直接查 pom.xml 会把本单真正改了 代码、也真正需要重启的 order-v3 与 fleet 恰好漏掉。

部署单位 与 hl-common-core 的依赖关系 是否需要本次一起滚
hl-gateway 直接依赖(pom.xml 命中) 是
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 传递依赖 是(本单直接改了这个服务的生产代码,且必须先于 order-v3 启动)
hl-finance 直接依赖(pom.xml 命中) 不单独部署——packaging=jar,无 spring-boot-maven-plugin,是 order-v3 的库依赖,随 order-v3 一起滚

⇒ 实际部署单位共 7 个:gateway / user / resource / product-v2 / order-v3 / mp / fleet;fleet 必须先于 order-v3 启动。


四、契约约束与正确调用方式

本节只写后端接受/拒绝 payload 的规则,不写 UI 渲染建议。

场景 做法
受控重开窗口内重新确认 不需要调新端点,仍调既有「整体确认」POST /v3/admin/order/group-batch/{groupBatchId}/requirement/confirm
确认成功后判断能不能继续推进团期 看响应 advanceUnblocked(恒 false)与 planRefreshState——DONE 才代表刷新已闭环;PENDING/FAILED 都要等待或引导重投
重投次数已到 5 次 响应 planRefreshReplayCount=5 且再点确认拿到 809210,按钮文案改「请联系后台排查」
区分「团期配车重配 10 秒内防重拒绝」与「重复提交同一份计划不是失败」 前者是 @Idempotent(timeout=10) 拒绝(100502),后者是 idempotentShortCircuit=true 幂等成功——两者触发条件不同,见「⚠️ 关键变化」第 4 条

五、数据库行为

操作 数据库影响
受控重开(reopen) order_group_vehicle_requirement 该行 status → PENDING_RECONFIRM,落 reconfigure_window_token/reconfigure_opened_by/reconfigure_expires_at/reconfigure_scope 等窗口列;version 不变
受控重开窗口内重新确认 同一条 CAS 内 status → CONFIRMED、version +1、关窗(窗口列清空)、登记刷新五列(plan_refresh_state='PENDING'、plan_refresh_outbox_id 等)
受控重投 plan_refresh_state 保持/转 PENDING,plan_refresh_replay_count +1(仅 REQUEUED/WOKEN 两种落点)
fleet 计划刷新成功 fleet_group_dispatch_plan.requirement_id/requirement_version 推进到新身份,plan_version 按幂等规则递增或维持

六、边界行为

  • 窗口过期后仍可续开:PENDING_RECONFIRM 且窗口已过期不算异常状态,是唯一能解开「到点车还没排完」的团的入口
  • 零分组(整团免车)的需求不能重开:这类团没有配车行,重新配车没有意义
  • 预检 Feign 降级/异常一律 fail-closed:assertCoverageComplete 捕获 RuntimeException、响应为空、satisfied 非 true 三种情形都抛 809204,绝不放行
  • 重投预算不因「无实际动作」的三种 NOOP 情形被消耗:NOOP_IN_FLIGHT/NOOP_ALREADY_PICKABLE/NOOP_RACED 不扣 planRefreshReplayCount
  • plan-refresh 判定性失败零写入:IDENTITY_STALE/COVERAGE_INCOMPLETE 两种情形在 fleet 事务内零写入之后返回,不留半态

六.5、枚举

planRefreshState(GroupBatchRequirementConfirmRespVO.planRefreshState)

所属字段: planRefreshState | 类型: String

值 中文 说明
PENDING 刷新中 刷新命令已登记、尚未成立;首次调用几乎恒为此值
DONE 已闭环 罕见:回调已在响应前先落库
FAILED 已失败 重入路径读到的上次失败态,需人工重投或重开窗口

planRefreshReplayKind(GroupBatchRequirementConfirmRespVO.planRefreshReplayKind)

所属字段: planRefreshReplayKind | 类型: String

值 中文 说明
REQUEUED 已重新入队 终态命令(FAILED/SUCCEEDED)已放回 PENDING,process_version +1
WOKEN 退避已清 PENDING 且退避未到期,已清 next_retry_at 立即可领,process_version 不变
NOOP_IN_FLIGHT 在途未动 命令正被 worker 处理中,本次不动它
NOOP_ALREADY_PICKABLE 本就待领 命令本就 PENDING 且已到期,下一轮取件自然会跑到
NOOP_RACED 并发抢跑 读写之间被并发推进,不报错也不回滚调用方事务

definitiveFailure(GroupDispatchPlanRefreshRespDTO.definitiveFailure)

所属字段: definitiveFailure | 类型: String

值 中文 说明
IDENTITY_STALE 需求身份已陈旧 本次要钉的需求身份不是基线此刻的活跃需求,重试不会变
COVERAGE_INCOMPLETE 覆盖缺口未补齐 库里这套车按此刻的权威分组清单还有缺口,重试不会变

六.6、修改前后对比

字段级对比

字段 改前 改后
GroupBatchRequirementConfirmRespVO(POST .../requirement/confirm 出参) 无受控重开相关字段 新增 planRefreshState/planRefreshOutboxId/planRefreshReplayKind/planRefreshReplayCount/blockedStage/advanceUnblocked 六个字段,非受控重开链路为 null/false

行为级对比

行为 改前 改后
团期过了资源准备阶段后需要改车 无解——写口直接抛 GROUP_BATCH_NOT_DISPATCHABLE(600010) 团期管理员可通过 reopen 端点开一个受控窗口
受控重开窗口内重新确认 端点不存在 复用既有确认端点,自动路由到受控重投分支
「车辆一字未改但需要重新确认」 会被 planDigest 幂等短路,不产生新意图、不发就绪事件 通过 plan-refresh 内部端点显式刷新,幂等命中时仍受控重投一次就绪意图

六.7、影响评估

  • 是否破坏向后兼容: 否。requirement/confirm 端点入参零变化,出参只新增字段(非受控重开链路全为 null/false);两个内部端点均为新增,不影响既有调用方
  • 前端是否必须同步上线: 是——前端在受控重开链路下必须依据 advanceUnblocked 恒 false 做等待/轮询设计,不能在确认成功时直接提示「已恢复」;若要支持「团期管理员发起受控重开」这个新功能点,还需接入 reopen 端点
  • 前端 workaround 清理点: 无

七、不影响范围

  • 仅影响: 团期已过资源准备阶段后的受控重开配车恢复链路(reopen、受控重投确认分支、刷新与覆盖两个内部端点)
  • 零影响:
    • 团期在 RESOURCE_PREPARING 阶段内的正常配车提交/确认流程(reconfigure/confirm 两个端点契约零变化)
    • 普通(非受控重开)整团确认的既有字段、既有错误码语义
    • POST /v3/internal/group-batch/{groupBatchId}/vehicle-ready/vehicle-ready-reset 两个内部端点(属 #7442 PR-C1/#7923,不在本单范围)
    • #7443 接送机 TRANSFER 派车行相关端点

八、测试环境已验证

✅ 2026-09-19 已完成实测,本文档覆盖的 5 个端点全部验过。 部署基线:order-v3 与 fleet 均在 31fd5b5e6。夹具 groupBatchId=2099951310525673474 / requirementId=2099951397100302337,全链路经真实写口驱动、零 SQL 直改。

🔴 先说一条走位,否则下一个人会白跑:本文档的 5 个端点里只有 2 个走网关,另外 3 个必须直连服务实例。 网关路由表里 /v3/internal/** 与 /internal/fleet/** 路由确实存在,但 JwtAuthFilter.isInternalPath()(hl-gateway/.../filter/JwtAuthFilter.java:290-294, :326-329)对任何匹配 /internal/* 或 /v3/internal/* 的请求无条件 403,请求到不了后端 —— 带不带 admin token 都一样。 ⇒ 内部端点一律直连:fleet 127.0.0.1:8087、order-v3 127.0.0.1:8086,带 X-Internal-Token。

判据:每个端点 HTTP 200 + success=true(或调用前即写明的错误码)+ 至少一个本次改动相关字段,并尽量做数据库交叉核实。

# 端点 走向 结果 断言到的字段(含 DB 交叉核实)
1 POST /v3/admin/order/group-batch/{id}/vehicle-requirement/reopen 网关 200 / success=true requirementStatus DONE → PENDING_RECONFIRM、blockedStage=RESOURCE_PREPARING;DB:blocked_stage 同步置位、order_group_batch.vehicle_ready 1 → 0
2 POST /v3/admin/order/group-batch/{id}/requirement/confirm(受控重投分支) 网关 200 / success=true version 4 → 5、planRefreshState=PENDING、planRefreshOutboxId 非空;blockedStage 仍为 RESOURCE_PREPARING(正确:成功 ≠ 恢复推进)
3 POST /internal/fleet/dispatch/group-batch/{id}/plan-refresh 直连 200 / success=true planVersion 4 → 5(DB 核实同步)、readyIntentEmitted=true、readyIntentRequeued=true、gaps=[]
4 GET /internal/fleet/dispatch/group-batch/{id}/coverage 直连 200 / success=true 见下方正负对照
5 GET /v3/internal/group-batch/{id}/dispatch-baseline 直连 200 / success=true groups[] 非空、requirementVersion=5 —— 实时反映刚确认的新版本,不是陈旧缓存

端点 4 的正负对照(证明 satisfied/gaps 不是恒真字段)

GET /internal/fleet/dispatch/group-batch/2099951310525673474/coverage?requirementVersion=<当前版本>
→ 200, satisfied=true, gaps=[]

GET /internal/fleet/dispatch/group-batch/2099951310525673474/coverage?requirementVersion=999   ← 故意传错
→ 200, satisfied=false,
     gaps=["需求身份已变:请求 …/v999,当前 …/v6"]

🔴 为什么要做这一对:只看成功路径那一次,satisfied=true 与「这个字段永远返回 true」观测完全相同。负向那次让它翻成 false 并给出具体原因,才证明它在算而不是在报。 ⚠️ 同理,不要用 coverage.missingGroupCodes 当验证依据 —— 该字段在成功路径上恒空,零分辨力。

端点 1 的时序提示(前端排查用)

requirement/confirm 触发刷新意图后,异步 worker 约 4 秒就会把 blocked_stage 清空、plan_refresh_state 置 DONE、vehicle_ready 置 1。这个窗口比一次人工 HTTP 往返还短 —— 如果前端在确认后立刻查详情看到 blockedStage 非空,那多半是正常的过渡态,隔几秒再查即可,不要据此判失败。

副作用:主夹具终态与起始态结构等价(CONFIRMED / blocked_stage=NULL / plan_refresh_state=DONE / vehicle_ready=1),outbox 命令已 SUCCEEDED、无悬挂,无需复位。


十、相关文档

  • 关联 Issue: wx/HL#7442
  • 关联 PR: wx/HL#7957
  • 前序文档(本文不重复其内容): changelogs-v2/2026-09/17_7442_团级确认态-需求已发车务回写-新增接口-管理后台.md(PR-B,已 deployed)

关联 / 联系人

链接

联系人

  • 后端负责人: @wx