--- schema: "hl-changelog/v2" ticket: "7442" title: "团期配车就绪回写带身份 + 两级判定(PR-C1 补登)" consumer: "admin" author: "wx(GIT)" change_type: "修改接口" backend_status: "deployed" gateway_status: "verified" frontend_status: "not_required" frontend_owner: "" frontend_ref: "" target_release: "" verified_at: "" status_note: "本文档是补登。PR-C1(#7923,merge commit 6f5b1b679f6b534081ca7b136e338cd3fffc1155,2026-09-18 合入 dev-v3)首次交付本次改动时同样漏写了交接件——这是排查 #7442 交接件缺口时顺带发现的第二处(第一处是 PR-A 的 reconfigure 端点,见 19_7442_团期配车分组写口reconfigure补登-新增接口-管理后台.md)。backend_status=deployed 依据:6f5b1b679 已在 dev-v3,测试服 2026-09-19 01:15 已滚动部署到 4cbccc26b(晚于本提交,链上包含)。gateway_status=verified 依据:2026-09-19 已对本文档覆盖的两个端点各做真实调用并做数据库交叉核实,见第八节;⚠️ 本文档此前 gateway_status 字段写着 verified 而 status_note 与第八节都写着 pending,头身不一致,那时的 verified 是假状态。⚠️ 同轮实测还查出并订正了一处契约错误:batchStatus 在 applied=true 时恒为 null,而本文档原先的两个成功响应示例给的是非 null 值,照旧稿写的前端会读到一个永远为空的字段——已在第三节两处订正。frontend_status 记 not_required:本文档涉及的两个端点都是仅限内部 Feign 调用的接口,不面向 hl-ui,前端无需改动。⚠️ 本文档只覆盖 PR-C1(#7442)原始交付的两级判定部分;#7444 PR-1 随后在 vehicle-ready 端点同一事务内又追加了一步(团级正式需求 DISPATCHED→DONE),那部分内容已在 #7444 的交接件(changelogs-v2/2026-09/ 下按 *_7444_* 检索;本仓文件名带提交日前缀会随提交日变,故不写死文件名。起草期曾以「同团车辆共用关系-新增接口」为名,该稿已并入前者、名字不再存在) 交付,本文档不重复。前端实证维持 not_required(hl-ui v2.1,2026-09-20):本文档覆盖的两个端点仅限服务间 Feign 调用、不面向 hl-ui,全仓 grep 无前端调用方,零改动。" updated_at: "2026-09-19" base: "dev-v3" --- # fleet/order-v3: 团期配车就绪回写带身份 + 两级判定(PR-C1 补登) > **存放目录**: changelogs-v2/{YYYY-MM}/ > > **服务**: hl-fleet-service (端口 8082)、hl-order-service-v3 (端口 8083) > **PR**: [#7923](https://git.1814.love:8443/wx/HL/pulls/7923) > **Issue**: #7442(AC-22) > **日期**: 2026-09-19(补登;改动实际上线于 2026-09-18) > **影响范围**: fleet↔order-v3 内部 Feign 回调——团期配车就绪回填/重置两个端点新增身份判定,杜绝乱序/旧版回调污染就绪状态;**不面向 admin 前端,mmg 无需改动** --- ## ⚠️ 关键变化 1. **这是补登,不是新功能上线通知**:本改动已部署 1 天以上,mmg 不需要做任何事——两个端点都是内部 Feign 接口,从未面向前端开放。补登原因同 `19_7442_团期配车分组写口reconfigure补登-新增接口-管理后台.md`: #7442 AC-22 要求的交接件缺口排查时顺带发现的。 2. **根因**:改前,`vehicle-ready`/`vehicle-ready-reset` 两个内部回调端点**只有路径参数**,没有任何版本信息。 这条回调经 fleet 侧 Outbox 异步投递,到达 order-v3 时活跃需求可能已经换了一版、fleet 的计划也可能已经 又重配了几轮——旧需求产出的就绪意图可能覆盖新需求的未就绪状态,旧计划版本产出的重置意图可能把刚配好车的 团打回未就绪。这两种烂法都**不报错**,只在数据上悄悄错。 3. **两级判定是本次改动的核心**:请求体新增 `requirementId`/`requirementVersion`/`planVersion` 三个身份字段, 提供方(order-v3)先比对需求身份(第一级,逐字相等),再比对同一需求版本内的计划版本大小(第二级), 两级都通过才落库;不通过**一律返回 HTTP 200 + `applied=false` + `discardReason`**,不抛错误码——本端点 由 fleet 侧 Outbox 重试链路驱动,抛错等于让一条已经该丢弃的意图无限重投。 4. **legacy 兼容窗口**:请求体声明为 `required=false`。PR-C1 上线前 fleet 已投出、尚未消费完的在途旧意图 没有 body,提供方对它们走 legacy 路径(行为与改动前逐字一致)——这是滚动上线的兼容窗口,不是校验豁免, body 一旦非空,DTO 上的逐字段约束全部生效。 5. **顺带交付了一条内部可靠性保证("快照顺序不变量"),不影响外部契约**:`AssignmentInsuranceOutboxWriter` 与相关监听器调整了保险快照/Outbox 事件的写入顺序,确保就绪回调触发的下游动作按稳定顺序执行;这部分 纯内部实现细节,不产生任何可观察的接口字段变化,本文档不展开。 --- ## 二、变更接口清单 | # | 接口 | 方法 | 路径 | 变更类型 | 说明 | |---|------|------|------|----------|------| | 1 | 回填配车就绪 | POST | `/v3/internal/group-batch/{groupBatchId}/vehicle-ready` | 修改(请求体由无到有,新增两级判定) | fleet 整团配车完成后回填 vehicle_ready=true,补登 | | 2 | 重置配车就绪 | POST | `/v3/internal/group-batch/{groupBatchId}/vehicle-ready-reset` | 修改(请求体由无到有,新增两级判定) | fleet 清零/释放后重置 vehicle_ready=false,补登 | --- ## 三、接口详情 ### 1. 回填配车就绪 `POST /v3/internal/group-batch/{groupBatchId}/vehicle-ready` ⚠️ **[内部接口,不对前端开放]** 仅限 fleet-service 内部 Feign 调用。 **VO**: `GroupBatchVehicleReadyReqDTO → GroupBatchVehicleReadyRespDTO` #### 使用场景 fleet-service 整团配车完成后调用本端点回填 `vehicle_ready=true`。回填成功后内部自动检测四 ready 闸门, 满足则推进团期状态 `RESOURCE_PREPARING → MATERIAL_PREPARING`。本次改动前该端点只接受路径参数,无法辨别 一条回调到底产自哪个需求版本、哪个计划版本;本次改动后必须携带身份,经两级判定才落库。 #### 入参字段表 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | groupBatchId | Path | Long | 是 | - | 团期主订单 ID | | requirementId | Body | Long | 条件必填(body 整体 `required=false`) | - | 产出本次就绪意图的正式团级用车需求 ID(两级判定第一级) | | requirementVersion | Body | Integer | 同上 | ≥1 | 产出本次就绪意图的需求版本 | | planVersion | Body | Long | 同上 | ≥1 | 产出本次就绪意图的 fleet 团期级计划版本(两级判定第二级,同一需求内部单调递增) | | sourceRefNo | Body | String | 否 | ≤64 | 幂等追溯号(fleet Outbox 记录 ID),仅用于日志对账,不参与判定 | #### 出参字段表 | 字段 | 类型 | 说明 | |------|------|------| | applied | Boolean | 本次是否真的把团期 `vehicle_ready` 改成了本意图的方向 | | discardReason | String | `applied=false` 时的原因常量:`IDENTITY_MISMATCH`/`REQUIREMENT_NOT_CONFIRMED`/`ALREADY_APPLIED`/`PLAN_VERSION_STALE` | | discardCode | Integer | `applied=false` 且该原因有对应错误码时为 809205(身份不一致)/809206(计划版本落后),否则为 null | | currentRequirementId | String(雪花 ID) | 提供方当前活跃需求 ID;无活跃需求为 null | | currentRequirementVersion | Integer | 提供方当前活跃需求版本 | | currentPlanVersion | Long | 提供方已应用的最高计划版本 | | batchStatus | String | 🔴 **`applied=true` 时恒为 `null`**,只有丢弃分支(`applied=false`)才回填团期状态。见下方订正。 | > 🔴 **2026-09-19 契约订正(实测发现,本文档此前写错了)**:`batchStatus` 在**成功分支上永远是 `null`**。 > 源码依据:`GroupBatchService#appliedResp`(`hl-order-service-v3/.../groupbatch/service/GroupBatchService.java:946-954`)在 `applied=true` 分支**从不设置该字段**,其 javadoc(`:974-977`)写明「判定通过的那一支刻意不读」;只有 `discardedResp()` 才回填。 > **实测佐证**:2026-09-19 两次 `applied=true` 的真实调用(`vehicle-ready` 与 `vehicle-ready-reset` 各一次),响应里 `batchStatus` 均为 `null`。 > ⚠️ **本文档原先的两个成功响应示例给的是非 null 值**(`MATERIAL_PREPARING` / `RESOURCE_PREPARING`),照它写的前端会读到一个恒为 `null` 的字段。下方示例已按实测值更正。 > **想拿团期状态,请另查团期详情接口,不要依赖本响应的这个字段。** #### 请求示例 ```json POST /v3/internal/group-batch/1934567890123456800/vehicle-ready { "requirementId": 1934567890123456789, "requirementVersion": 3, "planVersion": 7, "sourceRefNo": "880123" } ``` #### 响应示例 ```json { "code": 200, "message": "成功", "data": { "applied": true, "discardReason": null, "discardCode": null, "currentRequirementId": "1934567890123456789", "currentRequirementVersion": 3, "currentPlanVersion": 7, "batchStatus": null }, "success": true } ``` > ⚠️ `batchStatus` 在 `applied=true` 时**就是 `null`**(不是示例省略),见上方订正。 #### 空数据 / 降级响应 身份不一致时**一律返 200**,不是错误响应(下例的 `batchStatus` 有值是对的——丢弃分支才回填): ```json { "code": 200, "message": "成功", "data": { "applied": false, "discardReason": "IDENTITY_MISMATCH", "discardCode": 809205, "currentRequirementId": "1934567890123456789", "currentRequirementVersion": 4, "currentPlanVersion": 8, "batchStatus": "MATERIAL_PREPARING" }, "success": true } ``` 请求体缺省(`required=false`,legacy 兼容路径)时行为与改动前逐字一致,不做两级判定: ```json POST /v3/internal/group-batch/1934567890123456800/vehicle-ready ``` #### 错误响应 真正可重试的故障(DB 不可用、CAS 被并发抢跑)仍以异常形式返回失败 Result,由 Outbox 退避重试: ```json { "code": 500, "message": "数据库异常", "data": null, "success": false } ``` #### 业务边界 - **一律返 200**:本端点由 fleet 侧 Outbox 重试链路驱动,「需求已换版」「计划版本落后」这些结论再投多少次 都一样,用错误码表达会让这条意图无限重投;是否真的落库看 `applied`,没落库的原因看 `discardReason` - **幂等性**:重投同一条 `sourceRefNo`,结果保持一致 - **legacy 兼容窗口是过渡态,不是长期行为**:请求体缺省时的 legacy 路径服务的是 PR-C1 上线前已投出的 在途旧意图,不建议新代码依赖这条路径 - **`discardReason=REQUIREMENT_NOT_CONFIRMED`/`ALREADY_APPLIED` 没有对应错误码**:前者是受控重开窗口期间 fleet 重配发出的意图正常会落在的分支(预期路径不是异常);后者是 Outbox 重试的正常幂等形态 --- ### 2. 重置配车就绪 `POST /v3/internal/group-batch/{groupBatchId}/vehicle-ready-reset` ⚠️ **[内部接口,不对前端开放]** 仅限 fleet-service 内部 Feign 调用。 **VO**: `GroupBatchVehicleReadyReqDTO → GroupBatchVehicleReadyRespDTO` #### 使用场景 fleet-service 整团清零或取消成团/流团释放占用后调用本端点重置 `vehicle_ready=false`。判定与「1. 回填配车就绪」 **完全相同、无任何例外**:先判 `requirementId + requirementVersion` 与当前活跃需求完全一致(不一致丢弃, `discardReason=IDENTITY_MISMATCH`),再判该需求版本下的 `planVersion` 不落后(落后即丢弃, `discardReason=PLAN_VERSION_STALE`)。本次改动前该端点同样没有版本信息,旧的 reset 晚到会无条件把 `vehicle_ready` 打回 false(例如:"清零 plan10 → 重配出 plan11 → plan11 已就绪 → 重放 plan10 的 reset" 会错误地把就绪状态打回 false)。 #### 入参字段表 字段结构与「1. 回填配车就绪」完全一致(唯一区别是本端点的置位方向固定为 `ready=false`,体现在服务端内部 处理逻辑上,不是一个显式的请求字段): | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | groupBatchId | Path | Long | 是 | - | 团期主订单 ID | | requirementId | Body | Long | 条件必填(body 整体 `required=false`) | - | 产出本次重置意图的正式团级用车需求 ID(两级判定第一级) | | requirementVersion | Body | Integer | 同上 | ≥1 | 产出本次重置意图的需求版本 | | planVersion | Body | Long | 同上 | ≥1 | 产出本次重置意图的 fleet 团期级计划版本(两级判定第二级) | | sourceRefNo | Body | String | 否 | ≤64 | 幂等追溯号(fleet Outbox 记录 ID),仅用于日志对账 | #### 出参字段表 字段结构与「1. 回填配车就绪」完全一致: | 字段 | 类型 | 说明 | |------|------|------| | applied | Boolean | 本次是否真的把团期 `vehicle_ready` 改成了 false | | discardReason | String | `applied=false` 时的原因常量:`IDENTITY_MISMATCH`/`REQUIREMENT_NOT_CONFIRMED`/`ALREADY_APPLIED`/`PLAN_VERSION_STALE` | | discardCode | Integer | `applied=false` 且该原因有对应错误码时为 809205/809206,否则为 null | | currentRequirementId | String(雪花 ID) | 提供方当前活跃需求 ID;无活跃需求为 null | | currentRequirementVersion | Integer | 提供方当前活跃需求版本 | | currentPlanVersion | Long | 提供方已应用的最高计划版本 | | batchStatus | String | 🔴 **`applied=true` 时恒为 `null`**(同上方 `vehicle-ready` 的订正,同一份 `appliedResp()` 逻辑),只有丢弃分支才回填 | #### 请求示例 ```json POST /v3/internal/group-batch/1934567890123456800/vehicle-ready-reset { "requirementId": 1934567890123456789, "requirementVersion": 3, "planVersion": 10, "sourceRefNo": "880130" } ``` #### 响应示例 ```json { "code": 200, "message": "成功", "data": { "applied": true, "discardReason": null, "discardCode": null, "currentRequirementId": "1934567890123456789", "currentRequirementVersion": 3, "currentPlanVersion": 10, "batchStatus": null }, "success": true } ``` > ⚠️ `batchStatus` 在 `applied=true` 时**就是 `null`**(不是示例省略),见上方订正。 #### 空数据 / 降级响应 重放一条落后的 `plan10` reset(当前已推进到 `plan11` 且已就绪)会被丢弃,**不会把就绪状态打回 false**(下例的 `batchStatus` 有值是对的——丢弃分支才回填): ```json { "code": 200, "message": "成功", "data": { "applied": false, "discardReason": "PLAN_VERSION_STALE", "discardCode": 809206, "currentRequirementId": "1934567890123456789", "currentRequirementVersion": 3, "currentPlanVersion": 11, "batchStatus": "MATERIAL_PREPARING" }, "success": true } ``` #### 错误响应 同「1. 回填配车就绪」: ```json { "code": 500, "message": "数据库异常", "data": null, "success": false } ``` #### 业务边界 - **判定与置位方向完全一致、无任何例外**——起草阶段曾给本端点开过"`planVersion` 落后仍执行"的例外,最终 版本删除了这条例外:新增释放/清零本来就会产生一个更高的计划版本,合法的释放意图永远带着新版本到达, 不需要豁免;反过来,带着落后版本到达的 reset 只可能是旧事件重放 - **该团已无活跃需求时清零仍照常应用**:这是本方向唯一的口子——流团已经把需求失活,而释放回调仍必须能把 标志清掉 - 其余边界(一律返 200、幂等性、legacy 兼容窗口)同「1. 回填配车就绪」 --- ## 四、契约约束与正确调用方式 > 本节只写后端接受/拒绝 payload 的规则,不写 UI 渲染建议(本单两个端点均为内部接口,无 UI 直接对接)。 | 场景 | 做法 | |------|------| | fleet 侧发起就绪回调 | 必须带上产生这条意图那一刻的 `requirementId`/`requirementVersion`/`planVersion`,不能只传 `groupBatchId` | | 判断回调是否真的生效 | 看响应 `applied`,不要用 HTTP 状态码判断——不通过的判定同样返回 200 | | 需要排查一条回调为什么没生效 | 看 `discardReason`,四种原因分别指向不同的处置方向(换版 / 窗口期正常分支 / 幂等重放 / 乱序过期),不要合并处理 | --- ## 五、数据库行为 | 操作 | 数据库影响 | |------|----------| | 两级判定通过、`ready` 方向 | `order_group_batch.vehicle_ready → true`,可能连带推进 `batch_status` | | 两级判定通过、`reset` 方向 | `order_group_batch.vehicle_ready → false` | | 两级判定未通过(任一方向) | 零写入 | | `GroupVehicleRequirementDO` 新增列 | 新增 `plan_version`/相关身份列,供两级判定读取当前已应用的最高计划版本(Flyway `V20260918_*__add_group_vehicle_requirement_add_vehicle_plan_version.sql`) | --- ## 六、边界行为 - **legacy 无身份回调**:`req == null` 或 `req.requirementId == null` 时走改动前的行为,不做两级判定 - **同需求版本内计划版本必须单调不落后**:落后判定只在同一需求版本内部比较,换了需求版本后 fleet 的计划 版本并不重置,不会拿跨需求的两个版本比大小 --- ## 六.5 枚举 ### discardReason(GroupBatchVehicleReadyRespDTO.discardReason) **所属字段**: `discardReason` | **类型**: `String` | 值 | 中文 | 对应错误码 | 说明 | |----|------|------------|------| | `IDENTITY_MISMATCH` | 身份不一致 | 809205 | 回调携带的需求身份与当前活跃需求不一致 | | `REQUIREMENT_NOT_CONFIRMED` | 需求未在已确认档 | 无 | 受控重开窗口期间的正常路径,不是异常 | | `ALREADY_APPLIED` | 已应用过 | 无 | Outbox 重试的正常幂等形态 | | `PLAN_VERSION_STALE` | 计划版本落后 | 809206 | 同一需求版本内计划版本比已应用的旧,乱序到达的过期意图 | --- ## 六.6、修改前后对比 ### 字段级对比 | 字段 | 改前 | 改后 | |------|------|------| | `vehicle-ready`/`vehicle-ready-reset` 请求体 | 无(仅路径参数) | 新增可选请求体 `requirementId`/`requirementVersion`/`planVersion`/`sourceRefNo`(legacy 兼容,非必填) | | `vehicle-ready`/`vehicle-ready-reset` 响应体 | 仅 `Result` | 新增 `GroupBatchVehicleReadyRespDTO`:`applied`/`discardReason`/`discardCode`/`currentRequirementId`/`currentRequirementVersion`/`currentPlanVersion`/`batchStatus` | ### 行为级对比 | 行为 | 改前 | 改后 | |------|------|------| | 旧需求晚到的就绪回调 | 会把新需求的未就绪状态覆盖成就绪(污染) | 身份不一致直接丢弃,`applied=false` | | 旧计划版本晚到的重置回调 | 会把刚配好车的团打回未就绪(污染) | 计划版本落后直接丢弃,`applied=false` | | 调用方判断回调是否生效 | 只能靠 HTTP 200 推断(不可靠) | 必须读 `applied` 字段 | --- ## 六.7、影响评估 - **是否破坏向后兼容**: 否。请求体新增字段声明 `required=false`,legacy 无身份回调仍走改动前的行为路径;响应体从 `Result` 扩展为携带结构化结果,属于向后兼容的加字段 - **前端是否必须同步上线**: 否。两个端点均为内部 Feign 接口,不面向 hl-ui - **前端 workaround 清理点**: 无 --- ## 部署清单(本单改了 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 仍按改动前的无身份逻辑处理。**净效果**: 两级判定这项保护在这段过渡期内**还没有生效**(旧漏洞——乱序/旧版回调污染就绪状态——仍然存在), 但不会比改动前更糟,也不会报错或崩溃。 - **结论(⚠️ 只对本单这一次改动成立,不要拿它指导今天的部署)**:**单看本单**,两个方向都不会比 "改动前"更差,区别只是"保护何时开始生效"。 > 🔴 **2026-09-19 订正:不要据此认为"顺序无所谓"。**本单自身确实不挑顺序,但**同期还有别的改动** > 落在同一批部署单位上——PR-C2(#7957)在 `GroupBatchDispatchBaselineDTO` 上加了 `reconfigureWindow`, > 它**强制 `fleet` 先于 `order-v3`**(order-v3 先滚 ⇒ 旧 fleet 不校验窗口令牌,「受控重开」静默退化成 > 「不受控重开」,两端日志都正常)。今天部署的是这些改动的**并集**。 > ⚠️ **按单写的"顺序无所谓",在并集里就是错的**,而且它不会报错、没有人会回来改它。 > ⇒ 写部署顺序结论一律带限定:「**截至 <日期/commit>,这批部署单位上还有哪些已合入的改动**」。 **⇒ 今天的实操答案:同批滚;必须分批时 `fleet` 先、`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` 后**(本单自身不挑顺序,但同期 PR-C2 挑, 今天部署的是并集——见上方 2026-09-19 订正框)。 ### 🔧 2026-09-19 补充取证:PR 正文与本节结论的矛盾判定依据 PR #7923 正文写的是「只需滚 fleet + order-v3」,与本节「实际部署单位共 7 个」的结论字面矛盾。**判定不采信 两份文档中的任何一份,只认那次合并的真实 diff**: ``` git merge-base a18569162cb97e54b63f6b442e75292db4ad88d7 6f5b1b679f6b534081ca7b136e338cd3fffc1155 # => a18569162cb97e54b63f6b442e75292db4ad88d7(即 6f5b1b679 的直接父提交;squash 合并单亲提交, # merge-base 与父提交重合) git diff --name-only a18569162cb97e54b63f6b442e75292db4ad88d7 6f5b1b679f6b534081ca7b136e338cd3fffc1155 \ | sed -E 's#^([^/]+)/.*#\1#' | sort -u ``` 输出: ``` hl-common hl-fleet-service hl-order-service-v3 ``` `hl-common` 命中后逐文件展开,确认落在 `hl-common-core`(不是文档/测试目录误判): ``` git diff --name-only a18569162cb97e54b63f6b442e75292db4ad88d7 6f5b1b679f6b534081ca7b136e338cd3fffc1155 | grep '^hl-common' ``` 输出: ``` hl-common/hl-common-core/src/main/java/com/hulalv/common/dto/fleet/GroupBatchVehicleReadyReqDTO.java hl-common/hl-common-core/src/main/java/com/hulalv/common/dto/fleet/GroupBatchVehicleReadyRespDTO.java ``` ⇒ **命中 CODE_RULES §16.6**:本 PR 在 `hl-common-core` 新增了两个全新 DTO(`GroupBatchVehicleReadyReqDTO`/ `GroupBatchVehicleReadyRespDTO`),按规则消费方须同批滚,本节「实际部署单位共 7 个」的结论成立、无需改动。 **PR #7923 正文「只需滚 fleet + order-v3」是错的,需要订正**——它遗漏了 hl-common-core 新增 DTO 触发的 §16.6 全量同批滚要求。PR 正文改写由 wx 另行处理,本文档不代为改写 PR。 --- ## 七、不影响范围 - **仅影响**: fleet→order-v3 的就绪回调链路(两个内部端点) - **零影响**: - admin/mp 前端可见的任何端点 - #7442 PR-A 的 `reconfigure` 写口(见 `19_7442_团期配车分组写口reconfigure补登-新增接口-管理后台.md`) - #7442 PR-B 的确认/回写链路(见 `17_7442_团级确认态-需求已发车务回写-新增接口-管理后台.md`) - #7442 PR-C2 的受控重开窗口流程(见 `19_7442_团期配车受控重开窗口计划刷新状态收口-新增接口-管理后台.md`) - #7444 PR-1 在本端点追加的 DISPATCHED→DONE 推进(见 #7444 的交接件(`changelogs-v2/2026-09/` 下按 `*_7444_*` 检索;本仓文件名带**提交日前缀**,会随提交日变,所以这里不写死文件名。⚠️ 起草期曾以 `…同团车辆共用关系-新增接口…` 为名,该稿已并入前者、名字不再存在),本文档不重复该部分) --- ## 八、测试环境已验证 ✅ **2026-09-19 已完成实测,本文档覆盖的两个端点各验一次,并各配负向对照。** > 🔴 **走位**:两个端点都是 `/v3/internal/**`,**不走网关也不能走** —— `JwtAuthFilter.isInternalPath()`(`hl-gateway/.../filter/JwtAuthFilter.java:290-294, :326-329`)对任何匹配 `/internal/*` 或 `/v3/internal/*` 的请求**无条件 403**(路由是配了的,但请求到不了后端),带不带 admin token 都一样。⇒ **直连 order-v3 `127.0.0.1:8086`,带 `X-Internal-Token`**。 夹具 `groupBatchId=2099951310525673474` / `requirementId=2099951397100302337`。**全程零 SQL 直改业务状态**,唯一的 SQL 是 SELECT 读数。 **基线读数**:`vehicle_ready=1`、`vehicle_plan_version=5`、`status=CONFIRMED`、`version=5`、`blocked_stage=NULL`、`plan_refresh_state=DONE`。 ### 1. `POST /v3/internal/group-batch/{id}/vehicle-ready-reset` ```json // 请求 {"requirementId":2099951397100302337,"requirementVersion":5,"planVersion":6,"sourceRefNo":"ac21-verify-reset"} // 响应 HTTP 200 {"code":200,"message":"成功","success":true, "data":{"applied":true,"discardReason":null,"discardCode":null, "currentRequirementId":"2099951397100302337","currentRequirementVersion":5, "currentPlanVersion":"6","batchStatus":null}} ``` **DB 交叉核实**:`vehicle_ready` **1 → 0**、`vehicle_plan_version` **5 → 6**;`status`/`blocked_stage`/`plan_refresh_state` 不变。 (`batchStatus` 为 `null` —— 这正是本文档第三节订正的那条契约,此处是它的实测读数。) ### 2. `POST /v3/internal/group-batch/{id}/vehicle-ready`(还原) ```json // 请求 {"requirementId":2099951397100302337,"requirementVersion":5,"planVersion":7,"sourceRefNo":"ac21-verify-restore"} // 响应 HTTP 200,applied=true,currentPlanVersion="7",batchStatus=null ``` **DB 交叉核实**:`vehicle_ready` **0 → 1**、`vehicle_plan_version` **6 → 7**;其余与基线一致。 ### 3. 两条负向对照(证明 `applied` 不是恒真字段) 在 reset 之后、还原之前的中间态上做: | 构造 | 响应 | DB 复读 | |---|---|---| | 重放同一 `planVersion=6`(身份不变) | `applied=false`、`discardReason=ALREADY_APPLIED` | `vehicle_ready` 仍 0、`vehicle_plan_version` 仍 6,**未再变化** | | 故意传 `requirementVersion=99` | `applied=false`、`discardReason=IDENTITY_MISMATCH`、**`discardCode=809205`** | **零写入** | > 🔴 **为什么必须做这两条**:只看成功那一次,`applied=true` 与「这个端点无论如何都返回 true」观测完全相同。两条负向让它翻成 `false` 并给出不同的丢弃原因和错误码,才证明两级判定在**判**而不是在**报**。 ### 4. 契约逐字段核对 入参 4 字段 + 出参 7 字段共 **11 个字段**逐条对源码核过(`GroupBatchVehicleReadyReqDTO` / `GroupBatchVehicleReadyRespDTO` / `GroupBatchResourceController` / `GroupBatchService#applyReadyCallback`、`#appliedResp`、`#discardedResp` / `GroupVehicleRequirementService#decideVehicleReadyCallback`): - **10/11 一致**(类型、必填性、`required=false` 的 legacy 兼容窗口、两级判定顺序、`discardReason` 四常量、`discardCode` 只在 809205/809206 有值、丢弃场景一律 HTTP 200); - **1 处不一致已订正**:`batchStatus`(见第三节)。 ### 5. 残留 `vehicle_plan_version` 由基线 5 前进到 **7**(reset 用 6、mark 用 7)。这是两级判定 CAS 语义的必然结果 —— 该列**只能单调递增**,且**不允许用 SQL 回改**(回改会造出系统不可能自然产生的状态,下一个人会把它当真实缺陷去追)。`vehicle_ready`/`status`/`blocked_stage`/`plan_refresh_state` 均已还原到与基线一致。**这是真实写口产生的合法状态,不是损坏。** --- ## 十、相关文档 - 关联 Issue: [wx/HL#7442](https://git.1814.love:8443/wx/HL/issues/7442) - 关联 PR: [wx/HL#7923](https://git.1814.love:8443/wx/HL/pulls/7923) - 相关文档: - `changelogs-v2/2026-09/19_7442_团期配车分组写口reconfigure补登-新增接口-管理后台.md` - #7444 的交接件(`changelogs-v2/2026-09/` 下按 `*_7444_*` 检索;本仓文件名带**提交日前缀**,会随提交日变,所以这里不写死文件名。⚠️ 起草期曾以 `…同团车辆共用关系-新增接口…` 为名,该稿已并入前者、名字不再存在)(本端点后续被追加 DISPATCHED→DONE 推进的部分) ## 关联 / 联系人 ### 链接 - **Issue**: [#7442](https://git.1814.love:8443/wx/HL/issues/7442) - **PR**: [#7923](https://git.1814.love:8443/wx/HL/pulls/7923) - **Merge commit**: [6f5b1b679f6b534081ca7b136e338cd3fffc1155](https://git.1814.love:8443/wx/HL/commit/6f5b1b679f6b534081ca7b136e338cd3fffc1155) ### 联系人 - **后端负责人**: @wx