From 9f27d9bfb8a7182354900e4e3753f9bee27b2b15 Mon Sep 17 00:00:00 2001 From: API Changelog Bot Date: Mon, 7 Sep 2026 13:39:21 +0800 Subject: [PATCH] =?UTF-8?q?changelog(7203):=20=E8=BD=A6=E5=8A=A1=E6=98=BE?= =?UTF-8?q?=E5=BC=8F=E5=A3=B0=E6=98=8E=E3=80=8C=E6=95=B4=E6=AE=B5=E4=B8=8D?= =?UTF-8?q?=E7=94=A8=E8=BD=A6=E3=80=8D=E2=80=94=E2=80=94=E5=A3=B0=E6=98=8E?= =?UTF-8?q?/=E6=92=A4=E9=94=80/=E5=9B=9E=E6=98=BE=E4=B8=89=E4=B8=AA?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E7=AB=AF=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #7067 去槽位化后占位行退役,「车务决定整段不派车」在车管侧就是零派车行, 与「还没开始排车」在库里逐字相同、无法区分,订单侧的用车需求因此永久卡在处理中。 本单新增需求级显式动作承载这个语义(不按零行推断、不放宽常规快照门禁、不复活占位行)。 三个端点各自自包含:入参/出参/请求示例/响应示例/空数据形态/错误响应/业务边界。 另附枚举字典、正确 vs 错误 payload 对照、数据库行为、边界行为与不影响范围。 前端最要紧的三条已写在开头: - syncStatus 不是订单状态,SYNCED 只表示「送到了」,order-v3 可能按自身规则忽略; - declarationStatus=REVOKED 时 syncStatus 为 null,不要展示同步状态; - 撤销按钮只在 syncStatus=SYNCED 时可点,但「只想继续排车」不必先撤销。 实测证据章节为测试服网关真实往返(HEAD 02d7e59be,fleet 双实例), 逐条核对了订单侧 status / assignment_snapshot_revision / assignment_completion_disposition / assignment_count / assignment_no_vehicle_reason, 不是只看 HTTP 200。 --- ...½¦务显式声明整段不用车-新增接口-管理后台.md | 545 ++++++++++++++++++ 1 file changed, 545 insertions(+) create mode 100644 changelogs-v2/2026-09/07_7203_车务显式声明整段不用车-新增接口-管理后台.md diff --git a/changelogs-v2/2026-09/07_7203_车务显式声明整段不用车-新增接口-管理后台.md b/changelogs-v2/2026-09/07_7203_车务显式声明整段不用车-新增接口-管理后台.md new file mode 100644 index 00000000..c1482aa5 --- /dev/null +++ b/changelogs-v2/2026-09/07_7203_车务显式声明整段不用车-新增接口-管理后台.md @@ -0,0 +1,545 @@ +--- +schema: "hl-changelog/v2" +ticket: "7203" +title: "车务显式声明「整段不用车」:声明 / 撤销 / 回显三个端点" +consumer: "admin" +author: "wx(GIT)" +change_type: "新增接口" +backend_status: "deployed" +gateway_status: "verified" +frontend_status: "pending" +frontend_owner: "mmg" +frontend_ref: "" +target_release: "" +verified_at: "2026-09-07" +status_note: "2026-09-07 PR #7260 已合并 dev-v3 并部署测试服(HEAD 02d7e59be,fleet 双实例)。三个端点在测试服网关真实往返验证,逐条核对了订单侧 status / assignment_snapshot_revision / assignment_completion_disposition / assignment_count / assignment_no_vehicle_reason;五种派车行状态拒绝、接送机两个方向拒绝、撤销同步态门禁(605920/605921)、开关关闭 605035、幂等四种情形(含重启自愈)、并发撤销+排车、迟到旧版本回调不覆盖新方案,全部取到原始响应与 DB 前后对比。前端待做:新增「整段不用车」声明入口、撤销入口与状态回显;务必注意 syncStatus 不等于订单状态、REVOKED 时 syncStatus 为 null 不要展示、撤销按钮只在 syncStatus=SYNCED 时可点但「只想继续排车」不必先撤销。" +updated_at: "2026-09-07" +base: "dev-v3" +--- + +# 车务派单:显式声明「整段不用车」(管理后台) + +> **服务**: hl-fleet-service (8087) +> **PR**: #7260 +> **Issue**: #7203 +> **日期**: 2026-09-07 +> **影响范围**: 管理后台车务派单向导——需要新增「整段不用车」的声明入口、撤销入口与状态回显 + +--- + +## ⚠️ 关键变化 + +**纯新增,不改任何既有接口的请求或响应。** 但有三件事必须在做交互前先定下来,否则会做出错的 UI: + +- **`syncStatus` 不是订单状态**。`SYNCED` 只表示「快照送到 order-v3 了」,**不代表订单车控一定是完成态**—— + order-v3 可能按自己的规则忽略了这次回调(订单已取消、版本落后)。要展示订单状态请读订单侧的字段。 +- **`declarationStatus=REVOKED` 时 `syncStatus` 为 `null`**,此时**不要展示同步状态**。 +- **撤销按钮不是任何时候都能点**:只有 `syncStatus=SYNCED` 才可点。`PENDING` 会返回 605920(提示稍后重试), + `FAILED` 会返回 605921(提示联系运维重投)。**但「想继续排车」不需要先撤销**——见「六、边界行为」。 + +--- + +## 一、背景 + +#7067 去槽位化后,「某天不配车 = 该日没有这条日行」,占位行整体退役。于是「车务看完订单后决定整段 +一辆车都不派」这件事,在车管侧表现为**该需求下零条派车行**——而这个状态与「还没开始排车」在数据上 +完全无法区分。快照生成门禁因此不生成任何方案快照,订单侧收不到回调,用车需求永久卡在「处理中」。 + +| 维度 | 「还没排车」 | 「决定整段不用车」 | +|------|--------------|--------------------| +| 派车行数量 | 0 | 0 | +| 需求是否已被车务处理 | 否 | 是 | +| 订单车控应该流转到 | 保持处理中 | 完成 | + +两者在库里逐字相同,**不可能靠零行推断**——按零行自动推断的话,任何一个刚进车务、还没人碰的订单 +都会被误判成「不用车」并推成完成。所以本单新增一个需求级的显式动作来承载这个语义。 + +--- + +## 二、变更接口清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---|------|------|------|----------|------| +| 1 | 声明整段不用车 | POST | `/admin/fleet/assignments/requirements/{requirementId}/no-vehicle` | 新增 | 车务显式声明本需求整段不派车 | +| 2 | 撤销不用车声明 | DELETE | `/admin/fleet/assignments/requirements/{requirementId}/no-vehicle` | 新增 | 把需求拉回处理中以便重新排车 | +| 3 | 回显不用车声明 | GET | `/admin/fleet/assignments/requirements/{requirementId}/no-vehicle` | 新增 | 刷新页面后回显声明状态与原因 | + +三个端点都在 `/admin/fleet/**` 通配路由下,网关无需新增配置。权限与其它车务写口一致: +`VEHICLE_MANAGER` / `SUPER_ADMIN`。 + +--- + +## 三、接口详情 + +### 1. 声明整段不用车 `POST /admin/fleet/assignments/requirements/{requirementId}/no-vehicle` + +**VO**: `NoVehicleDeclarationReqVO` → `Result` + +#### 使用场景 + +派单向导第②步。车务确认这一单整段都不需要派车(客人自驾、当地已安排车辆、行程改为不含用车), +点「整段不用车」并填写原因。声明成功后订单侧的用车需求流转到完成,订单可以继续走后续流程。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|---|---|---|---|---|---| +| requirementId | path | Long | 是 | 雪花 ID | 当前生效用车需求 ID | +| orderId | body | Long | 是 | 与 `requirementId` 做归属校验 | 订单 ID,不匹配按需求过期拒绝(605905) | +| requestId | body | String | 是 | ≤64 字符,非空 | 幂等键。同键同参重放返回同一结果且只产生一次快照;同键异参返回 605918 | +| expectedRequirementVersion | body | Integer | 是 | 须等于当前需求版本 | 不匹配返回 605905 | +| reason | body | String | 是 | ≤200 字符,非空 | 不用车原因,透传给订单侧作为 `noVehicleReason` | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|---|---|---| +| requirementId | String | 用车需求 ID(雪花,序列化为字符串) | +| declarationStatus | String | 本地声明结果:`DECLARED` / `REVOKED` | +| syncStatus | String | 与订单侧的同步结果:`PENDING` / `SYNCED` / `FAILED`;`REVOKED` 时为 `null` | +| snapshotRevision | Long | 本次声明产生的方案快照版本(小整数,JSON number) | +| reason | String | 不用车原因 | +| revokeReason | String | 撤销原因;未撤销时为 `null` | +| declaredAt | LocalDateTime | 声明时间 | +| revokedAt | LocalDateTime | 撤销时间;未撤销时为 `null` | + +#### 请求示例 + +```json +POST /admin/fleet/assignments/requirements/2096644145371033602/no-vehicle +{ + "orderId": 2096643451159195650, + "requestId": "no-vehicle-20260907-001", + "expectedRequirementVersion": 1, + "reason": "客人自驾,全程不需要用车" +} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "success": true, + "data": { + "requirementId": "2096644145371033602", + "declarationStatus": "DECLARED", + "syncStatus": "SYNCED", + "snapshotRevision": 15, + "reason": "客人自驾,全程不需要用车", + "revokeReason": null, + "declaredAt": "2026-09-07 13:20:11", + "revokedAt": null + } +} +``` + +#### 空数据 / 降级响应 + +本端点不存在空数据形态:要么成功返回声明记录,要么按下表报错。 +订单上下文拉取失败时不降级(fail-closed)——大交通是否声明接送机这件事一旦读成 `null` 就会误放行, +所以拉取失败直接报错而不是当作「无声明」。 + +#### 错误响应 + +```json +{ + "code": 605916, + "success": false, + "message": "该用车需求下存在派车记录,不能声明整段不用车;只是部分日期不用车请走按天取消", + "data": null +} +``` + +| 码 | 触发 | 前端处置 | +|---|---|---| +| 605905 | 需求版本过期 / 订单已取消 / 需求非当前生效版本 | 刷新页面重取需求后重试 | +| 605047 | 行程已结束,只读 | 禁用入口 | +| 605916 | 该需求下存在任一非 canceled 派车行(含 `completed`、`exception`) | 提示「已有派车记录,只是部分日期不用车请走按天取消」 | +| 605917 | 大交通已声明需要接机或送机 | 提示「大交通声明了接送机,请先由定制师清掉接送机要求」 | +| 605918 | 同一 `requestId` 用不同参数重复提交 | 换新 `requestId` 重来 | +| 605922 | 该需求已有生效的不用车声明 | 提示「已有声明,改原因请先撤销再重新声明」 | + +#### 业务边界 + +- **鉴权**:网关对 `/admin/fleet/**` 强制 `VEHICLE_MANAGER` / `SUPER_ADMIN`。 +- **幂等**:持久化请求指纹(`(requirementId, requestId)` 唯一键 + 请求 sha256)。 + 同键同参重放返回同一 `snapshotRevision` 且只产生一次快照;同键异参 605918。 + 并发同键由数据库唯一键仲裁,落败方按指纹判定重放还是冲突——不是先查后插,没有并发窗口。 +- **零写入保证**:任何一条校验不过都在写库之前抛出,不会留下半条声明记录或半条快照事件。 +- **状态**:只允许在「当前生效需求 + 行程未结束 + 零非 canceled 派车行 + 大交通无接送机声明 + + 当前无生效声明」时声明。 +- **原因不可原地改**:要改 `reason` 必须先撤销再重新声明(否则返回 605922)。 + 这条是刻意的——原来的实现是「回显既有声明并回 200」,那会把车务改过的新原因静默丢弃, + 前端却以为保存成功了。 + +--- + +### 2. 撤销不用车声明 `DELETE /admin/fleet/assignments/requirements/{requirementId}/no-vehicle` + +**VO**: `NoVehicleDeclarationRevokeReqVO` → `Result` + +#### 使用场景 + +车务改主意了,要给这一单重新排车。撤销把用车需求从完成拉回处理中,之后正常走第②步排车。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|---|---|---|---|---|---| +| requirementId | path | Long | 是 | 雪花 ID | 当前生效用车需求 ID | +| orderId | body | Long | 是 | 与 `requirementId` 做归属校验 | 订单 ID | +| requestId | body | String | 是 | ≤64 字符,非空 | 撤销侧**独立**的幂等键,与声明侧的 `requestId` 互不干扰 | +| reason | body | String | 否 | ≤200 字符 | 撤销原因 | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|---|---|---| +| requirementId | String | 用车需求 ID(雪花,序列化为字符串) | +| declarationStatus | String | 本地声明结果:`DECLARED` / `REVOKED` | +| syncStatus | String | 与订单侧的同步结果:`PENDING` / `SYNCED` / `FAILED`;`REVOKED` 时为 `null` | +| snapshotRevision | Long | 声明产生的方案快照版本(小整数,JSON number);撤销不产生新版本,回显仍是声明那一版 | +| reason | String | 不用车原因 | +| revokeReason | String | 撤销原因;未撤销时为 `null` | +| declaredAt | LocalDateTime | 声明时间 | +| revokedAt | LocalDateTime | 撤销时间;未撤销时为 `null` | + +撤销成功时固定为 `declarationStatus=REVOKED`、`syncStatus=null`, +`snapshotRevision` 回显的仍是声明那一版(撤销不产生新版本)。 + +#### 请求示例 + +```json +DELETE /admin/fleet/assignments/requirements/2096644145371033602/no-vehicle +{ + "orderId": 2096643451159195650, + "requestId": "no-vehicle-revoke-20260907-001", + "reason": "客人改主意,还是要用车" +} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "success": true, + "data": { + "requirementId": "2096644145371033602", + "declarationStatus": "REVOKED", + "syncStatus": null, + "snapshotRevision": 15, + "reason": "客人自驾,全程不需要用车", + "revokeReason": "客人改主意,还是要用车", + "declaredAt": "2026-09-07 13:20:11", + "revokedAt": "2026-09-07 13:41:02" + } +} +``` + +#### 空数据 / 降级响应 + +该需求当前没有生效声明时不返回空数据,而是报 605919。 + +#### 错误响应 + +```json +{ + "code": 605920, + "success": false, + "message": "不用车声明正在同步到订单,请稍后再撤销", + "data": null +} +``` + +| 码 | 触发 | 前端处置 | +|---|---|---| +| 605035 | 需求级生命周期开关未启用,撤销所依赖的链路不会被消费 | 提示联系运维开启配置 | +| 605047 | 行程已结束 | 禁用入口 | +| 605919 | 该需求当前不存在生效的不用车声明 | 刷新回显后重试 | +| 605920 | 声明快照尚未成功投递(`syncStatus=PENDING`) | 提示「同步中,请稍后再撤销」,可轮询 GET | +| 605921 | 声明快照投递已隔离(`syncStatus=FAILED`) | 提示「同步失败请联系运维重投;如果只是想继续排车,不必先撤销」 | + +#### 业务边界 + +- **必须先同步成功才能撤销**(605920 / 605921)。这条是 fail-closed,不是过度谨慎: + 若允许在快照还在投递时撤销,会出现「接口回了 200 REVOKED,但订单侧随后仍被那条在途快照推成完成」的 + 终态错乱——车管侧显示已撤销、订单侧显示已完成,且此后再撤销会被 605919 拒死。 +- **幂等**:撤销侧有自己的唯一键 `(requirementId, revokeRequestId)` 与自己的请求指纹, + 指纹绑定被撤销声明的身份,因此「撤销 D1 → 重新声明 D2 → 复用同一 `revokeRequestId`」会返回 605918 + 而不是静默假成功。 +- **撤销不产生新的快照版本**:靠需求重开链路把需求拉回处理中,没有可发布的方案形态。 + 迟到的旧版本回调会被订单侧的版本比较判为过期忽略,不会覆盖撤销后重新排出来的方案。 +- **零写入保证**:门禁不过时不写声明状态、不写重开事件。 + +--- + +### 3. 回显不用车声明 `GET /admin/fleet/assignments/requirements/{requirementId}/no-vehicle` + +**VO**: `NoVehicleDeclarationRespVO` → `Result` + +#### 使用场景 + +进入派单向导或刷新页面时回显:这一单是不是已经声明过整段不用车、原因是什么、同步到哪一步了。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|---|---|---|---|---|---| +| requirementId | path | Long | 是 | 雪花 ID | 当前生效用车需求 ID。本端点无查询参数、无请求体 | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|---|---|---| +| requirementId | String | 用车需求 ID(雪花,序列化为字符串) | +| declarationStatus | String | 本地声明结果:`DECLARED` / `REVOKED` | +| syncStatus | String | 与订单侧的同步结果:`PENDING` / `SYNCED` / `FAILED`;`REVOKED` 时为 `null` | +| snapshotRevision | Long | 声明产生的方案快照版本(小整数,JSON number);撤销不产生新版本,回显仍是声明那一版 | +| reason | String | 不用车原因 | +| revokeReason | String | 撤销原因;未撤销时为 `null` | +| declaredAt | LocalDateTime | 声明时间 | +| revokedAt | LocalDateTime | 撤销时间;未撤销时为 `null` | + +从未声明过时 `data` 为 `null`(不是 404、不是错误码)。 + +#### 请求示例 + +```http +GET /admin/fleet/assignments/requirements/2096644145371033602/no-vehicle +Authorization: Bearer {token} +``` + +#### 响应示例 + +已声明: + +```json +{ + "code": 200, + "success": true, + "data": { + "requirementId": "2096644145371033602", + "declarationStatus": "DECLARED", + "syncStatus": "SYNCED", + "snapshotRevision": 15, + "reason": "客人自驾,全程不需要用车", + "revokeReason": null, + "declaredAt": "2026-09-07 13:20:11", + "revokedAt": null + } +} +``` + +#### 空数据 / 降级响应 + +从未声明过: + +```json +{ "code": 200, "success": true, "data": null } +``` + +已撤销(`syncStatus` 为 `null`,两个原因都保留): + +```json +{ + "code": 200, + "success": true, + "data": { + "requirementId": "2096644145371033602", + "declarationStatus": "REVOKED", + "syncStatus": null, + "snapshotRevision": 15, + "reason": "客人自驾,全程不需要用车", + "revokeReason": "客人改主意,还是要用车", + "declaredAt": "2026-09-07 13:20:11", + "revokedAt": "2026-09-07 13:41:02" + } +} +``` + +#### 错误响应 + +本端点不产生任何业务错误码——需求不存在或从未声明一律回 `data=null`。 +只会出现网关层对 `/admin/fleet/**` 的统一鉴权失败响应: + +```json +{ + "code": 401, + "success": false, + "message": "未登录或登录已过期", + "data": null +} +``` + +#### 业务边界 + +- **`syncStatus` 是读时实时重算的**,不是落库快照:投递成功或被隔离都发生在写事务之外, + 直接回落库列会让页面永远显示「同步中」。 +- **已撤销时 `syncStatus` 为 `null`**:该维度对撤销态不适用,前端不要展示同步状态。 +- 只读,不加锁、不产生任何副作用。 + +--- + +## 四、契约约束与正确调用方式 + +### ✅ 正确 / ❌ 错误 payload 对照 + +| 场景 | ❌ 错误 | ✅ 正确 | +|---|---|---| +| 改不用车原因 | 换个 `requestId` 重新 POST(会 605922) | 先 DELETE 撤销,再用新 `requestId` POST | +| 重试超时的声明请求 | 换新 `requestId` 重发(会产生第二条声明或 605922) | 原样重发同一个 `requestId` 和同样的参数,返回同一结果 | +| 只是某几天不用车 | 调本接口(会 605916) | 走按天取消 / 逐日方案里不提交那几天 | +| 判断订单是否已完成 | 看 `syncStatus=SYNCED` | 读订单侧的车控状态字段 | + +### 切换状态时的必要动作 + +- 声明成功 → 需要刷新订单侧状态展示(用车需求会流转到完成)。 +- 撤销成功 → 需求回到处理中、恢复可排车;前端应把第②步排车入口重新放开。 +- 撤销被 605920 / 605921 拒绝 → 不要把按钮置灰后就不管了,应提示可轮询 GET 或联系运维。 + +--- + +## 五、数据库行为 + +新增表 `fleet_no_vehicle_declaration`(迁移 `V20260907_002`): + +| 关注点 | 说明 | +|---|---| +| 主键 | `declaration_id`,雪花 | +| 声明幂等 | 唯一键 `uk_no_vehicle_declaration_requirement_request(requirement_id, request_id)` + `request_sha256` 指纹 | +| 撤销幂等 | 唯一键 `uk_no_vehicle_declaration_revoke_request(requirement_id, revoke_request_id)` + `revoke_request_sha256` 指纹 | +| 状态 | `declaration_status`(DECLARED / REVOKED)、`sync_status`(PENDING / SYNCED / FAILED) | +| 与快照的关联 | `snapshot_event_id`、`snapshot_revision` | + +声明与快照事件在同一个事务内落库;投递走既有的事务外发件箱链路,事务内不做任何远程调用。 + +--- + +## 六、边界行为 + +- **同步失败(`FAILED`)时不阻断重新排车**。声明记录不参与排车门禁——排车只看订单侧状态与派车行。 + 所以「声明快照被隔离了、撤销不了」的情况下,车务直接走第②步排车即可,不需要先撤销成功。 + 这条要写进 UI 提示,否则车务会以为自己被卡死了。 +- **接送机与不用车互斥**。大交通声明了接机或送机时不允许声明整段不用车(605917)——接送机本身就是用车, + 允许的话等于给 #7067 的接送机门禁开后门。要真的整段不用车,先由定制师改大交通声明清掉接送机要求。 +- **已发生的用车事实不可被覆盖**。`completed`(已跑完)与 `exception`(未处理异常)同样触发 605916, + 因为订单侧写入零车方案前会先清掉旧的派车快照,放过去就会覆盖掉已发生的用车与费用记录。 +- **只读端点不产生副作用**,可以放心轮询。 + +--- + +## 六.5、枚举 / 数据字典 + +### `declarationStatus`(`com.hulalv.fleet.assignment.novehicle.enums.NoVehicleDeclarationStatusEnum`) + +| 值 | 中文 | 说明 | +|---|---|---| +| `DECLARED` | 已声明 | 当前生效的「整段不用车」声明 | +| `REVOKED` | 已撤销 | 声明已被撤销,需求已(或正在)回到处理中 | + +### `syncStatus`(`com.hulalv.fleet.assignment.novehicle.enums.NoVehicleSyncStatusEnum`) + +| 值 | 中文 | 说明 | +|---|---|---| +| `PENDING` | 投递中 | 快照事件在途(含首次投递与重试退避) | +| `SYNCED` | 已投递 | 快照已成功投递——订单侧已受理,或已按其规则忽略(订单已取消、版本落后)。不代表订单车控是完成态 | +| `FAILED` | 投递已隔离 | 重试预算耗尽,需运维重投 | +| `null` | 不适用 | 仅出现在 `declarationStatus=REVOKED` 时 | + +--- + +## 七、不影响范围 + +- **订单侧回调契约一个字没改**:本单只是让车管在新场景下也发出那份早已被支持的「无需用车」零单元格形态。 +- 常规排车链路的快照门禁判据未动:零派车行且未声明时,行为与以前逐字相同——不发任何快照、不流转订单状态。 +- #7067 的接送机门禁语义未动。 +- 派单看板、矩阵、候选查询、司机短链均未改。 +- 网关路由未改。 + +--- + +## 八、测试环境已验证 + +全部在测试服网关(`https://api.test.1814.love:9443`)真实往返,逐条核对了订单侧落库值,不是只看 HTTP 200。 +后端 HEAD `02d7e59be`(PR #7260 的 squash 提交),fleet 双实例滚动部署完成。 + +``` +POST /admin/fleet/assignments/requirements/{id}/no-vehicle → 200 DECLARED + snapshotRevision ✓ +DELETE /admin/fleet/assignments/requirements/{id}/no-vehicle → 200 REVOKED + syncStatus=null ✓ +GET /admin/fleet/assignments/requirements/{id}/no-vehicle → 三态(null / DECLARED / REVOKED)✓ +``` + +### 主链路 + +声明成功后订单侧 `order_vehicle_requirement` 逐字核对: +`status=DONE`、`assignment_snapshot_revision` 与响应的 `snapshotRevision` 一致、 +`assignment_completion_disposition=NO_VEHICLE_REQUIRED`、`assignment_count=0`、 +`assignment_no_vehicle_reason` 等于提交的原因。 + +撤销后 `status` 回到 `PROCESSING`,随即走正常的第②步排车 `POST /admin/fleet/assignments/batch` +返回 `finalPlanPublished=true`,证明撤销后确实恢复可排车。 + +### 拒绝分支(每条都带真实响应) + +| 验证点 | 结果 | +|---|---| +| 五种派车行状态各拒一次(`assigned` / `unassigned` / `holding` / `completed` / `exception`) | 全部 605916 ✓ | +| `completed` 那次拒绝前后,订单侧 `status`/`revision`/`disposition`/`update_time` 与车管侧车费三列逐字未变 | ✓ 零写入 | +| 大交通声明 ARRIVAL 接机 → 声明 | 605917 ✓ | +| 大交通声明 DEPARTURE 送机 → 声明 | 605917 ✓ | +| 清掉接送机声明后再声明 | 200 DECLARED ✓(正反两个方向都有证据) | +| 需求版本传旧值 | 605905 ✓ | +| 行程已结束的需求声明 | 605047 ✓ | +| 撤销时快照仍在投递(`syncStatus=PENDING`) | 605920 ✓,声明状态未变、未写出重开事件 | +| 撤销时快照已隔离(`syncStatus=FAILED`) | 605921 ✓ | +| 需求级生命周期开关关闭时撤销 | 605035 ✓,声明状态未变 | + +### 幂等(四种情形) + +- 同 `requestId` 同参重放声明 → 返回同一 `snapshotRevision`,该需求下快照事件**始终只有 1 条** ✓ +- 同 `requestId` 异参(换 `reason`) → 605918 ✓ +- 已有生效声明 + 新 `requestId` → 605922 ✓ +- 撤销侧同样验过:同键同参重放返回完全相同结果(含 `revokedAt`)、异参 605918 ✓ +- **投递未完成时重启服务能自愈**:构造「声明已提交、快照事件仍 PENDING、订单侧仍 PROCESSING」的切片后重启 fleet 实例, + **全程不人工触发重投**,事件在下一拍定时任务被自然消费,订单侧变为 + `status=DONE / revision=3 / disposition=NO_VEHICLE_REQUIRED / count=0`,且该需求下快照事件没有因重启多产出 ✓ + +### 并发与迟到消息 + +- **并发「撤销 + 重新排车」**:同一秒并发发出两个请求,撤销成功(200 REVOKED)、排车被 605906 拒绝, + 车管侧 0 条派车行、订单侧回到 `PROCESSING`——落在合法终态,没有出现「声明已撤销但订单侧仍是零车方案」 + 这类交叉污染。随后再排车成功,两侧 `revision` / `disposition` / `count` 完全对得上 ✓ +- **迟到的旧版本回调**:撤销并重新排车(订单侧已到 `revision=3 / disposition=ASSIGNED`)之后, + 把声明那一版(`revision=1`)的事件重投一次,订单侧 + `status` / `assignment_snapshot_revision` / `disposition` **一个字都没回退** ✓ + +### 单测与门禁 + +- `mvn -pl hl-fleet-service test`:**4073 / 0 failures / 0 errors / 4 skipped**,架构守护门禁 13/13 绿 +- `mvn -pl hl-fleet-service spotless:check` 单独跑绿 +- `mvn -pl hl-order-service-v3 test -Dtest=com.hulalv.order.requirement.**`:605/0/0 + (含两条新增消费侧用例:零单元格回调正确流转、迟到旧版本被判过期忽略) +- 建表迁移用 testcontainers 真 MySQL8 跑过,两个唯一键的行为逐条断言(多个 NULL 可共存、重复键撞唯一约束) + +验证订单:`2096825710311067650` / `2096827371423236098` / `2096828488429293570` / `2096829348983046145` +(本次为验收新建,产品 `2056944461216100353`) + +--- + +## 十、相关文档 + +- 关联 Issue: [wx/HL#7203](https://git.1814.love:8443/wx/HL/issues/7203) +- 前置改造: [wx/HL#7067](https://git.1814.love:8443/wx/HL/issues/7067)(去槽位化,本单的成因) + +## 关联 / 联系人 + +### 链接 + +- **Issue**: [#7203](https://git.1814.love:8443/wx/HL/issues/7203) +- **PR**: [#7260](https://git.1814.love:8443/wx/HL/pulls/7260) + +### 联系人 + +- **后端负责人**: @wx