文件
hl-api-changelog/changelogs-v2/2026-09/19_7442_团期配车就绪回写带身份两级判定补登-修改接口-管理后台.md
T
API Changelog Bot和Claude Opus 5 cad5dcd021
changelog-filename-gate / validate (push) Failing after 2s
docs(changelog): #7442 三份交接件补真实网关实测,并订正 batchStatus 契约错误
三份文档此前 front matter 写着 gateway_status: "verified",而同一文件的
status_note 与第八节都写着 pending / 「暂无网关实测数据」——头身从第一次提交
起就不一致,那三个 verified 是假状态。

2026-09-19 已完成实测,现在它们是真的:

- reconfigure 补登(1 个端点):正负各一次。正常路径 planVersion 2→3、
  idempotentShortCircuit=false、落库 3 条活跃行;负向路径事先点名 602002,
  拿到 602002 且 message 指名漏掉的 SUV 组,DB 核实零写入。
  另记两个坑:单组需求下 602002 语法上不可达(602001 的检查排在前面);
  夹具里的 vehicle_id=1001 是不存在的占位 ID,会被 600006 拦下。

- 受控重开窗口(5 个端点):reopen / requirement/confirm 走网关,
  plan-refresh / coverage / dispatch-baseline 直连。每个端点都断了至少一个
  本次改动相关字段并做 DB 交叉核实。coverage 补了正负对照
  (satisfied=true/gaps=[] vs 故意传 requirementVersion=999 → satisfied=false
  且 gaps 指出身份已变),证明该字段不是恒真。
  顺带订正 status_note 里「测试服尚未部署到含 PR #7957」——那句已过期,
  实测中 reconfigureWindowToken(#7957 引入)被真实端点接收并生效。

- 就绪回写两级判定(2 个端点):vehicle-ready-reset 与 vehicle-ready 配对,
  reset 让 vehicle_ready 1→0、再用 vehicle-ready 推回 1,夹具靠真实写口还原、
  零 SQL 直改。补两条负向对照(重放同版本→ALREADY_APPLIED;错版本→
  IDENTITY_MISMATCH/809205)证明 applied 不是恒真字段。

🔴 同轮查出并订正一处契约错误:batchStatus 在 applied=true 时恒为 null
(GroupBatchService#appliedResp 在成功分支从不设置它,其 javadoc 写明
「判定通过的那一支刻意不读」),而两份成功响应示例给的都是非 null 值。
照旧稿写的前端会读到一个永远为空的字段。示例与字段说明均已更正,
并写明想拿团期状态要另查团期详情接口。

三份文档共 8 个端点,全部走的是「HTTP 200 + success 或事先点名的错误码 +
至少一个本次改动相关字段 + 尽量做 DB 交叉核实」这一套判据。

Refs #7442

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 14:17:35 +08:00

544 行
28 KiB
Markdown
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
---
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),那部分内容已在 19_7444_团期配车就绪门禁与同团车辆共用关系-新增接口-管理后台.md 交付,本文档不重复。"
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<Void>` | 新增 `GroupBatchVehicleReadyRespDTO`:`applied`/`discardReason`/`discardCode`/`currentRequirementId`/`currentRequirementVersion`/`currentPlanVersion`/`batchStatus` |
### 行为级对比
| 行为 | 改前 | 改后 |
|------|------|------|
| 旧需求晚到的就绪回调 | 会把新需求的未就绪状态覆盖成就绪(污染) | 身份不一致直接丢弃,`applied=false` |
| 旧计划版本晚到的重置回调 | 会把刚配好车的团打回未就绪(污染) | 计划版本落后直接丢弃,`applied=false` |
| 调用方判断回调是否生效 | 只能靠 HTTP 200 推断(不可靠) | 必须读 `applied` 字段 |
---
## 六.7、影响评估
- **是否破坏向后兼容**: 否。请求体新增字段声明 `required=false`,legacy 无身份回调仍走改动前的行为路径;响应体从 `Result<Void>` 扩展为携带结构化结果,属于向后兼容的加字段
- **前端是否必须同步上线**: 否。两个端点均为内部 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 仍按改动前的无身份逻辑处理。**净效果**:
两级判定这项保护在这段过渡期内**还没有生效**(旧漏洞——乱序/旧版回调污染就绪状态——仍然存在),
但不会比改动前更糟,也不会报错或崩溃。
- **结论**:两个方向都不会比"改动前"更差,区别只是"保护何时开始生效"。**仍建议同批滚**,让两级判定
尽快对新回调生效,避免过渡期内继续吃"旧漏洞"的亏。
### 消费方清单不能按 `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;本单不强制
要求特定先后顺序,但**同批滚**仍是最稳妥的做法。
### 🔧 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 推进(见 `19_7444_团期配车就绪门禁与同团车辆共用关系-新增接口-管理后台.md`,本文档不重复该部分)
---
## 八、测试环境已验证
✅ **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`
- `changelogs-v2/2026-09/19_7444_团期配车就绪门禁与同团车辆共用关系-新增接口-管理后台.md`(本端点后续被追加 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