docs(changelog): #7442 三份交接件补真实网关实测,并订正 batchStatus 契约错误
changelog-filename-gate / validate (push) Failing after 2s

三份文档此前 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>
这个提交包含在:
API Changelog Bot
2026-09-19 14:17:35 +08:00
共同撰写人 Claude Opus 5
父节点 85c8443c73
当前提交 cad5dcd021
共修改 3 个文件,包含 145 行新增和 16 行删除
@@ -12,7 +12,7 @@ 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 先记 pending。frontend_status 记 not_required:本文档涉及的两个端点都是仅限内部 Feign 调用的接口,不面向 hl-ui,前端无需改动。⚠️ 本文档只覆盖 PR-C1(#7442)原始交付的两级判定部分;#7444 PR-1 随后在 vehicle-ready 端点同一事务内又追加了一步(团级正式需求 DISPATCHED→DONE),那部分内容已在 19_7444_团期配车就绪门禁与同团车辆共用关系-新增接口-管理后台.md 交付,本文档不重复。"
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"
---
@@ -94,7 +94,13 @@ fleet-service 整团配车完成后调用本端点回填 `vehicle_ready=true`。
| currentRequirementId | String(雪花 ID) | 提供方当前活跃需求 ID;无活跃需求为 null |
| currentRequirementVersion | Integer | 提供方当前活跃需求版本 |
| currentPlanVersion | Long | 提供方已应用的最高计划版本 |
| batchStatus | String | 回填后的团期状态(可能已被四 ready 闸门推进) |
| 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` 的字段。下方示例已按实测值更正。
> **想拿团期状态,请另查团期详情接口,不要依赖本响应的这个字段。**
#### 请求示例
@@ -121,15 +127,17 @@ POST /v3/internal/group-batch/1934567890123456800/vehicle-ready
"currentRequirementId": "1934567890123456789",
"currentRequirementVersion": 3,
"currentPlanVersion": 7,
"batchStatus": "MATERIAL_PREPARING"
"batchStatus": null
},
"success": true
}
```
> ⚠️ `batchStatus` 在 `applied=true` 时**就是 `null`**(不是示例省略),见上方订正。
#### 空数据 / 降级响应
身份不一致时**一律返 200**,不是错误响应:
身份不一致时**一律返 200**,不是错误响应(下例的 `batchStatus` 有值是对的——丢弃分支才回填):
```json
{
@@ -219,7 +227,7 @@ fleet-service 整团清零或取消成团/流团释放占用后调用本端点
| currentRequirementId | String(雪花 ID) | 提供方当前活跃需求 ID;无活跃需求为 null |
| currentRequirementVersion | Integer | 提供方当前活跃需求版本 |
| currentPlanVersion | Long | 提供方已应用的最高计划版本 |
| batchStatus | String | 重置后的团期状态 |
| batchStatus | String | 🔴 **`applied=true` 时恒为 `null`**(同上方 `vehicle-ready` 的订正,同一份 `appliedResp()` 逻辑),只有丢弃分支才回填 |
#### 请求示例
@@ -246,15 +254,17 @@ POST /v3/internal/group-batch/1934567890123456800/vehicle-ready-reset
"currentRequirementId": "1934567890123456789",
"currentRequirementVersion": 3,
"currentPlanVersion": 10,
"batchStatus": "RESOURCE_PREPARING"
"batchStatus": null
},
"success": true
}
```
> ⚠️ `batchStatus` 在 `applied=true` 时**就是 `null`**(不是示例省略),见上方订正。
#### 空数据 / 降级响应
重放一条落后的 `plan10` reset(当前已推进到 `plan11` 且已就绪)会被丢弃,**不会把就绪状态打回 false**:
重放一条落后的 `plan10` reset(当前已推进到 `plan11` 且已就绪)会被丢弃,**不会把就绪状态打回 false**(下例的 `batchStatus` 有值是对的——丢弃分支才回填):
```json
{
@@ -453,7 +463,62 @@ hl-common/hl-common-core/src/main/java/com/hulalv/common/dto/fleet/GroupBatchVeh
## 八、测试环境已验证
> ⚠️ 本节暂无网关实测数据(两个端点为内部接口,不走网关,网关实测本就不适用);内部调用链的验证归属 fleet↔order-v3 集成测试,本文档撰写时未另行发起手工调用,gateway_status 记 pending 供 #7442 取证车道处理。
✅ **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` 均已还原到与基线一致。**这是真实写口产生的合法状态,不是损坏。**
---