--- schema: "hl-changelog/v2" ticket: "7442" title: "团期配车受控重开窗口 + 计划刷新状态收口(PR-C2)" consumer: "admin" author: "wx(GIT)" change_type: "新增接口" backend_status: "deployed" gateway_status: "verified" frontend_status: "pending" frontend_owner: "" frontend_ref: "" target_release: "" verified_at: "" status_note: "代码已合入 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 内容。" updated_at: "2026-09-19" base: "dev-v3" --- # fleet/order-v3: 团期配车受控重开窗口 + 计划刷新状态收口(PR-C2) > **存放目录**: changelogs-v2/{YYYY-MM}/ > > **服务**: hl-fleet-service (端口 8082)、hl-order-service-v3 (端口 8083) > **PR**: [#7957](https://git.1814.love:8443/wx/HL/pulls/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\ | 是 | 1-20 个 | 授权可改的乘车分组编码;必须是当前需求已有的组,越界 400 | | scopeDates | Body | List\ | 是 | 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 | 团期当前状态(重开不回退它) | #### 请求示例 ```json POST /v3/admin/order/group-batch/1934567890123456800/vehicle-requirement/reopen { "reason": "客户临时增加 2 人,需要加一辆车", "scopeGroupCodes": ["BUS-A"], "scopeDates": ["2026-10-01"], "windowMinutes": 120 } ``` #### 响应示例 ```json { "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。同一操作人在窗口有效期内重复调用本端点是幂等成功,原样返回既有令牌(不算「空数据」)。 #### 错误响应 ```json { "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 就绪回调通过两级判定之后,绝不在确认这一刻 | #### 请求示例 ```json POST /v3/admin/order/group-batch/1934567890123456800/requirement/confirm ``` (本端点无请求体) #### 响应示例(受控重开窗口内的重新确认,首次调用) ```json { "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 当成异常。 #### 错误响应 ```json { "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\ | 覆盖缺口描述;仅 `COVERAGE_INCOMPLETE` 时非空 | | legacyGroupRowCount | Integer | 本团存活派车行里无分组键的历史行数(非错误,仅留痕) | #### 请求示例 ```json POST /internal/fleet/dispatch/group-batch/1934567890123456800/plan-refresh { "requirementId": "1934567890123456789", "requirementVersion": 4, "sourceRefNo": "1934567890123456999" } ``` #### 响应示例 ```json { "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——在传输层它与「远端暂时不可用」完全同形,而两者的正确处置相反: ```json { "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 侧命令处理器退避重试: ```json { "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\ | 缺口描述(逐条给人看);`satisfied=true` 时为空列表 | | missingGroupCodes | Array\ | 权威清单声明了、但一辆车都没排的组码——**本端点是这个字段唯一有意义的宿主**,重配写口的同名字段在任何路径上都只能是空列表(真的缺组时走抛 602002 的路径,根本不产生响应体) | | legacyGroupRowCount | Integer | 本团存活派车行里无分组键的历史行数,与重配/刷新响应上的同名字段同源同算法(#7442 AC-32) | | planVersion | String(雪花 ID) | fleet 当前计划版本;该团尚无计划时为 null | | planRequirementId | String(雪花 ID) | fleet 当前计划钉住的需求 ID;尚无计划时为 null | | planRequirementVersion | Integer | fleet 当前计划钉住的需求版本;尚无计划时为 null | #### 请求示例 ```json GET /internal/fleet/dispatch/group-batch/1934567890123456800/coverage?requirementId=1934567890123456789&requirementVersion=4 ``` #### 响应示例 ```json { "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` | ```json // ① 无活跃需求 —— 注意这是 200,success=true { "code": 200, "message": "成功", "data": { "satisfied": false, "gaps": ["需求身份已变:请求 …/v3,当前 null/vnull"] }, "success": true } ``` ```json // ② 有需求但零分组(整团免车) { "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\ | 权威服务日集合(`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\ | 本组服务日(可短于全团) | | groups[].dailyHeadcount | Map\ | 该组该日用车人数(乘车人数,非户数) | | groups[].memberOrderIdsByDate | Map\\> | 该组该日实际乘车子订单集合;本单只透传不消费 | | reconfigureWindow | Object | 【**PR-C2 新增**】受控重开窗口;无窗口时为 null;**已过期的窗口也会下发**(不是 bug,理由见下方字段说明与「业务边界」) | | reconfigureWindow.windowToken | String | 一次性窗口令牌 | | reconfigureWindow.openedAt | LocalDateTime | 开窗时刻 | | reconfigureWindow.expiresAt | LocalDateTime | 窗口失效时刻 | | reconfigureWindow.allowedGroupCodes | Array\ | 授权可改的乘车分组编码白名单;空列表/null 都不代表"全都可以改",消费方拿不到明确白名单必须失败关闭 | | reconfigureWindow.allowedDates | Array\ | 授权可改的行程日白名单 | | reconfigureWindow.openedBy | String | 开窗管理员 ID(留痕,关窗不清) | #### 请求示例 ```json GET /v3/internal/group-batch/1934567890123456800/dispatch-baseline ``` #### 响应示例 ```json { "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: ```json { "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 侧收到失败即失败关闭): ```json { "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](https://git.1814.love:8443/wx/HL/issues/7442) - 关联 PR: [wx/HL#7957](https://git.1814.love:8443/wx/HL/pulls/7957) - 前序文档(本文不重复其内容): `changelogs-v2/2026-09/17_7442_团级确认态-需求已发车务回写-新增接口-管理后台.md`(PR-B,已 deployed) ## 关联 / 联系人 ### 链接 - **Issue**: [#7442](https://git.1814.love:8443/wx/HL/issues/7442) - **PR**: [#7957](https://git.1814.love:8443/wx/HL/pulls/7957) - **Merge commit**: [c6aa1224fb46c3a7671da8234886075efde3c4ed](https://git.1814.love:8443/wx/HL/commit/c6aa1224fb46c3a7671da8234886075efde3c4ed) ### 联系人 - **后端负责人**: @wx