From 1c0439c44a10ce4f157bf74c7f2d6157235c2972 Mon Sep 17 00:00:00 2001 From: API Changelog Bot Date: Sat, 19 Sep 2026 02:42:19 +0800 Subject: [PATCH] =?UTF-8?q?docs(changelog):=20#7442=20PR-C2=20=E5=8F=97?= =?UTF-8?q?=E6=8E=A7=E9=87=8D=E5=BC=80=E7=AA=97=E5=8F=A3=20+=20=E4=B8=89?= =?UTF-8?q?=E4=BB=BD=E8=A1=A5=E9=83=A8=E7=BD=B2=E6=B8=85=E5=8D=95=E4=B8=8E?= =?UTF-8?q?=20dispatch-baseline=20=E6=9D=A1=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 PR-C2 那份(受控重开窗口 + 计划刷新状态收口),并给三份都补了此前缺的 「部署清单」一节 —— AC-19 ③ 逐字要求部署约束写进 PR 正文与 changelog, 实测 PR 正文有、三份 changelog 全文零命中。 滚动顺序逐 PR 分别分析,没有照抄同一份危险描述: - PR-C2:🔴 逆序会静默退化 —— order-v3 先滚,旧 fleet 收到 dispatchable=true 直接放行重配,而它不读 reconfigureWindow、不校验令牌与范围,两端日志都正常、 没有任何报错。所以 fleet 必须先于 order-v3。 - PR-A / PR-C1:两个方向都不静默出错(fleet 先滚会整体报 602009,是明确错误码 不是静默放行),如实写清与 PR-C2 的区别。 同时补记 GET /v3/internal/group-batch/{id}/dispatch-baseline —— 对 origin/dev-v3 查证,该端点响应体被 #7442 改过两次(8eb8e13cd 加四字段、c6aa1224f 加 reconfigureWindow),三份 changelog 此前都漏记。 三份都带上了「别按直接 pom 依赖查消费方」的警告:grep -rl 'hl-common-core' */pom.xml 只命中 gateway 与 hl-finance,其余六个服务经 hl-common-web / hl-starter-* 传递引入, 照那份清单部署漏掉的恰恰是本单真正改了的 order-v3 与 fleet。 Refs #7442 Co-Authored-By: Claude Opus 5 (1M context) --- ...½¦分组写口reconfigure补登-新增接口-管理后台.md | 30 + ...控重开窗口计划刷新状态收口-新增接口-管理后台.md | 802 ++++++++++++++++++ ...°±绪回写带身份两级判定补登-修改接口-管理后台.md | 30 + 3 files changed, 862 insertions(+) create mode 100644 changelogs-v2/2026-09/19_7442_团期配车受控重开窗口计划刷新状态收口-新增接口-管理后台.md diff --git a/changelogs-v2/2026-09/19_7442_团期配车分组写口reconfigure补登-新增接口-管理后台.md b/changelogs-v2/2026-09/19_7442_团期配车分组写口reconfigure补登-新增接口-管理后台.md index be6eeeac..3c802d53 100644 --- a/changelogs-v2/2026-09/19_7442_团期配车分组写口reconfigure补登-新增接口-管理后台.md +++ b/changelogs-v2/2026-09/19_7442_团期配车分组写口reconfigure补登-新增接口-管理后台.md @@ -284,6 +284,36 @@ POST /admin/fleet/group-dispatch/batches/1934567890123456800/reconfigure --- +## 部署清单(本单改了 hl-common-core,CODE_RULES §16.6) + +本单在 `hl-common-core` 的 `GroupBatchDispatchBaselineDTO`(团期配车权威基线)新增 `requirementId`/ +`requirementVersion`/`requirementStatus`/`groups` 四个字段,并新增 `GroupBatchVehicleGroupBaselineDTO` +(乘车分组基线)全新类;order-v3 是这份基线的提供方,fleet 是消费方。 + +### 滚动顺序分析:本单与 #7957(PR-C2)的"fleet 必须先滚"不同——本单两个方向都不会静默出错 + +- **order-v3 先滚**:order-v3 开始下发新字段(`groups[]` 等),此时 fleet 还是旧版——**旧版 fleet 压根没有 + 本单新增的 `POST /admin/fleet/group-dispatch/batches/{groupBatchId}/reconfigure` 端点**(该端点是本单 + 才新增的),不存在任何消费方读取这些新字段,**零风险**。 +- **fleet 先滚**:fleet 的新端点已经存在,但调用旧版 order-v3 的基线接口时**拿不到 `groups` 字段** + (Jackson 反序列化为 `null`)——fleet 侧的按组覆盖校验属于**失败关闭**设计(602009「无法取得本团的权威 + 乘车分组清单」),**这是本单与 PR-C2 最大的不同**:PR-C2 的 `dispatchable` 放宽会被旧 fleet 静默忽略、 + 放行了本不该放行的请求;本单缺 `groups[]` 则是**整个新端点在这段时间内全部请求都报 602009**——响应是 + **明确的错误码,不是静默放行**,运营/车务会立刻发现"这功能用不了"而不是"这功能用了但结果不对"。 +- **结论**:两个方向都不会产生数据错乱,区别只是"功能完全不可用一段时间"(fleet 先滚)还是"零影响" + (order-v3 先滚)。**仍然建议同批滚**(消除过渡期报错),但万一要分批,**order-v3 先滚风险更低**。 + +### 消费方清单不能按 `pom.xml` 直接依赖关系查 + +理由与判据同 `19_7442_团期配车受控重开窗口计划刷新状态收口-新增接口-管理后台.md`「部署清单」节—— +`grep -rl 'hl-common-core' */pom.xml` 只命中 `hl-gateway`/`hl-finance`,order-v3 与 fleet 都经 +`hl-common-web` 传递引入,不在直接依赖清单里,但正是本单真正改了代码的两个服务。 + +⇒ **实际部署单位共 7 个**:gateway / user / resource / product-v2 / order-v3 / mp / fleet; +本单不强制要求特定先后顺序,但**同批滚**仍是最稳妥的做法。 + +--- + ## 七、不影响范围 - **仅影响**: 团期配车页的整团逐日提交动作 diff --git a/changelogs-v2/2026-09/19_7442_团期配车受控重开窗口计划刷新状态收口-新增接口-管理后台.md b/changelogs-v2/2026-09/19_7442_团期配车受控重开窗口计划刷新状态收口-新增接口-管理后台.md new file mode 100644 index 00000000..09f3f0b5 --- /dev/null +++ b/changelogs-v2/2026-09/19_7442_团期配车受控重开窗口计划刷新状态收口-新增接口-管理后台.md @@ -0,0 +1,802 @@ +--- +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),测试服尚未部署到含本次改动的版本,backend_status/gateway_status 暂记 pending,不满足发布门禁。本文档是 #7442 PR-C(团级确认态与配车恢复流程)的第三份交接件:PR-B(fleet 确认整团配车 + order-v3 回写已发车务)已由 17_7442_团级确认态-需求已发车务回写-新增接口-管理后台.md 交付并 deployed;本文档只覆盖 PR-C2(受控重开窗口 + 计划刷新状态收口)新增/改造的 4 个端点,不重复 PR-B 内容。部署完成并网关实测后需回填 backend_status=deployed、gateway_status=verified。" +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。 + +#### 错误响应 + +本团正式需求零分组(整团免车)或无需求时失败关闭: + +```json +{ + "code": 602009, + "message": "无法取得本团的权威乘车分组清单: 该团正式需求整团免车", + "data": null, + "success": 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 派车行相关端点 + +--- + +## 八、测试环境已验证 + +> ⚠️ 测试服尚未部署到含 PR #7957 改动的版本,本节暂无网关实测数据。部署完成后按以下清单补验: + +``` +POST /v3/admin/order/group-batch/{id}/vehicle-requirement/reopen → 待验证 +POST /v3/admin/order/group-batch/{id}/requirement/confirm(受控重投分支) → 待验证 +POST /internal/fleet/dispatch/group-batch/{id}/plan-refresh → 待验证(内部接口) +GET /internal/fleet/dispatch/group-batch/{id}/coverage → 待验证(内部接口) +``` + +--- + +## 十、相关文档 + +- 关联 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 diff --git a/changelogs-v2/2026-09/19_7442_团期配车就绪回写带身份两级判定补登-修改接口-管理后台.md b/changelogs-v2/2026-09/19_7442_团期配车就绪回写带身份两级判定补登-修改接口-管理后台.md index 937ac085..9f97435f 100644 --- a/changelogs-v2/2026-09/19_7442_团期配车就绪回写带身份两级判定补登-修改接口-管理后台.md +++ b/changelogs-v2/2026-09/19_7442_团期配车就绪回写带身份两级判定补登-修改接口-管理后台.md @@ -370,6 +370,36 @@ POST /v3/internal/group-batch/1934567890123456800/vehicle-ready-reset --- +## 部署清单(本单改了 hl-common-core,CODE_RULES §16.6) + +本单在 `hl-common-core` 新增两个全新 DTO:`GroupBatchVehicleReadyReqDTO`(就绪回调两级判定身份)、 +`GroupBatchVehicleReadyRespDTO`(两级判定结果)。order-v3 是这两个端点(`vehicle-ready`/`vehicle-ready-reset`) +的提供方,fleet 是发起方(调用方)。 + +### 滚动顺序分析:本单两个方向都不静默出错,但"order-v3 先滚"能让保护提前生效 + +- **order-v3 先滚**:order-v3 的端点方法签名新增了一个 `@RequestBody(required = false)` 参数,此时 fleet + 还是旧版、调用时**不带任何请求体**(旧代码逻辑本就没有这个概念)——`req == null` 命中 legacy 路径, + 行为与改动前逐字一致,**安全**;等 fleet 也滚上去开始携带身份后,两级判定立刻对新产生的回调生效。 +- **fleet 先滚**:fleet 开始在请求体里携带 `requirementId`/`requirementVersion`/`planVersion`,但旧版 + order-v3 的端点方法签名**根本没有声明接收请求体的参数**——Spring 对声明之外的请求体内容视而不见, + 不会报错也不会解析失败,等价于"这个请求体被忽略了",order-v3 仍按改动前的无身份逻辑处理。**净效果**: + 两级判定这项保护在这段过渡期内**还没有生效**(旧漏洞——乱序/旧版回调污染就绪状态——仍然存在), + 但不会比改动前更糟,也不会报错或崩溃。 +- **结论**:两个方向都不会比"改动前"更差,区别只是"保护何时开始生效"。**仍建议同批滚**,让两级判定 + 尽快对新回调生效,避免过渡期内继续吃"旧漏洞"的亏。 + +### 消费方清单不能按 `pom.xml` 直接依赖关系查 + +理由与判据同 `19_7442_团期配车受控重开窗口计划刷新状态收口-新增接口-管理后台.md`「部署清单」节—— +`grep -rl 'hl-common-core' */pom.xml` 只命中 `hl-gateway`/`hl-finance`,order-v3 与 fleet 都经 +`hl-common-web` 传递引入,不在直接依赖清单里,但正是本单真正改了代码的两个服务。 + +⇒ **实际部署单位共 7 个**:gateway / user / resource / product-v2 / order-v3 / mp / fleet;本单不强制 +要求特定先后顺序,但**同批滚**仍是最稳妥的做法。 + +--- + ## 七、不影响范围 - **仅影响**: fleet→order-v3 的就绪回调链路(两个内部端点)