47 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 | 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 侧计划刷新/覆盖查询两个内部端点、既有「整体确认需求」端点新增受控重投分支
⚠️ 关键变化
- 设计稿里的独立
reconfirm端点已取消:早期设计曾计划新增POST /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/reconfirm,该端点从未上线。 受控重开窗口内的「重新确认」动作最终挂在既有的整体确认端点POST /v3/admin/order/group-batch/{groupBatchId}/requirement/confirm上,服务端按需求当前状态自动路由到 受控重开分支——前端不需要调用任何新端点来完成重新确认,仍调那个熟悉的「确认」按钮对应的端点即可。 - 确认成功 ≠ 团期立刻恢复推进:受控重开窗口内的确认只是把「计划刷新」这件事可靠登记下来,
响应新增
planRefreshState/planRefreshOutboxId/advanceUnblocked等六个字段,其中advanceUnblocked恒为false——阻断解除发生在 fleet 侧就绪回调通过两级判定之后,不在本次调用里。 前端必须据此做等待或轮询,不能在确认成功的瞬间就告诉运营「好了」。 - 响应字段名与 AC-22 原文不同:AC-22 原文写的是
planRefreshRequeued(Boolean)+planRefreshReplayCount,但那不是最终落地的字段——2026-09-18 的订正已判定实际字段是planRefreshReplayKind(枚举REQUEUED/WOKEN/NOOP_IN_FLIGHT/NOOP_ALREADY_PICKABLE/NOOP_RACED, 五取值不能合成两档)+planRefreshReplayCount。本文档按代码实际字段名记录。 - 「10 秒内重复提交返回上次结果」这句话不完全准确:
POST /admin/fleet/group-dispatch/batches/{groupBatchId}/reconfigure(fleet 整团逐日配车提交端点,非本文档新增/改造范围,完整契约见补登文档19_7442_团期配车分组写口reconfigure补登-新增接口-管理后台.md;受影响的幂等语义与本次改动强相关)的@Idempotent窗口是 10 秒,但窗口内重复提交是拒绝(返 100502「团期配车重配处理中,请勿重复提交」), 不是「返回上次结果」;「提交同一份计划返回上次结果(idempotentShortCircuit=true)不是失败」这件事 与 10 秒防重窗口是两套独立机制(前者按计划内容摘要判重,没有时间窗限制),详见「四、契约约束」。 - 错误码数量核对:工单 AC-22 原文写「25 个新错误码」(fleet 16 + order-v3 9),本次逐个核对源码后
实际落地 21 个——fleet
GroupDispatchAdminErrorCode602000-602014 共 15 个(不是 16)、 order-v3GroupDispatchReconfigureErrorCode809203/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,本单不新增权限点 - 幂等性:
@Idempotent5 秒窗口 + 同一操作人在生效窗口内重复调用原样返回既有令牌 - 窗口过期后续开是合法路径:
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 都一样。 ⇒ 内部端点一律直连:fleet127.0.0.1:8087、order-v3127.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)
关联 / 联系人
链接
- Issue: #7442
- PR: #7957
- Merge commit: c6aa1224fb46c3a7671da8234886075efde3c4ed
联系人
- 后端负责人: @wx