diff --git a/changelogs-v2/2026-09/21_7988_团期配车需求详情响应新增配车刷新态观测字段-修改接口-管理后台.md b/changelogs-v2/2026-09/21_7988_团期配车需求详情响应新增配车刷新态观测字段-修改接口-管理后台.md new file mode 100644 index 00000000..16fe3ac6 --- /dev/null +++ b/changelogs-v2/2026-09/21_7988_团期配车需求详情响应新增配车刷新态观测字段-修改接口-管理后台.md @@ -0,0 +1,378 @@ +--- +schema: "hl-changelog/v2" +ticket: "7988" +title: "团期配车需求详情响应新增七个配车刷新态观测字段" +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: "接口已在测试环境部署并通过网关验证;七个字段在 origin/dev-v3 已就绪;order-v3 commit d30cd9561 是最新且已部署" +updated_at: "2026-09-21" +base: "dev-v3" +--- + +# 团期配车需求: 查询接口新增七个配车刷新态观测字段 + +> **服务**: hl-order-service-v3(order-v3) +> **PR**: #8130 | **Issue**: #7988 | **验收项**: AC-6 +> **日期**: 2026-09-21 +> **影响范围**: 管理后台「团期详情」配车需求查询接口响应体 + +--- + +## ⚠️ 关键变化 + +**响应体新增七个只读观测字段,描述配车计划刷新的状态与停滞告警。** + +最重要的三件事: + +1. **🔴 判「要不要告警」的主字段是 `planRefreshStalled`,不是 `planRefreshState`。** `planRefreshState` 是原始状态机值(null/PENDING/DONE/FAILED),`PENDING` 在窗口内属于正常在途,不应报警。后端已把「窗口内/窗口外」「重投耗尽」「命令失败」这些判断收敛进了 `planRefreshStalled` 这**一个布尔值**。→ 前端判告警只看 `planRefreshStalled`;`planRefreshState` 仅用于展示原始状态。 + +2. **`FAILED` 是显式告警态。** 它进来时 `planRefreshStalled=true`、`planRefreshStalledReason=STATE_FAILED`。前端不需自己推导「FAILED 算不算停滞」。 + +3. **⚠️ `null` 不等于出错。** `planRefreshState=null` 表示这条需求**从未登记过刷新**(例如免车、或还没确认过),此时 `planRefreshStalled=false`、其余字段为 null / 0。这是安静态,不是异常态。 + +--- + +## 一、背景 + +之前这七个观测字段**只在写口的响应里**(`POST /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/confirm` 与 `POST /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/reopen`),导致「查一下配车刷新死没死」只能调一个会改状态的端点——实测 `confirm` 会先做一次受控重投,把刚才的 `FAILED` 改成 `PENDING`,结果就是 **`FAILED` 态在只读查询端点根本看不到,因为读它的动作会把它改掉**。本次补在只读查询端点上,「查现场」不再改变现场。 + +--- + +## 二、变更接口清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---|------|------|------|----------|------| +| 1 | 团期配车需求详情 | GET | `/v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement` | 响应新增七字段 | 配车刷新态观测;只读投影,不参与提交 | + +--- + +## 三、接口详情 + +### 1. 团期配车需求详情 `GET /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement` + +**VO**: `GroupVehicleRequirementRespVO → Result` + +#### 使用场景 + +管理后台「团期详情」页加载配车需求信息时调用。前端需要据 `planRefreshStalled` 判是否在需求卡片上显示告警;据 `planRefreshStalledReason` 和 `planRefreshTimeoutAt` 展示「为什么停滞」和「倒计时」。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| `groupBatchId` | Path | Long | ✅ | — | 团期聚合主键 | + +#### 出参 `Result` + +| 字段 | 类型 | 说明 | +|------|------|------| +| `planRefreshState` | String / null | 配车刷新状态原值:`null`(从未登记) / `PENDING`(刷新中) / `DONE`(闭环) / `FAILED`(已失败);**单看此字段不足以判停滞,看 `planRefreshStalled`** | +| `planRefreshReplayCount` | Integer | 人工受控重投累计次数(管理员点确认触发的,不含自动重试);上限 5,从未重投过为 0 | +| `blockedStage` | String / null | 团期阻断阶段快照:`null`(未阻断) / `RESOURCE_PREPARING` / `MATERIAL_PREPARING` / `PENDING_DEPARTURE`;非空表示该团正卡在这一步等重新配车 | +| `planRefreshStalled` | Boolean | **行动判据主字段(恒非 null)**:`true`=不会自愈,必须有人处置;`false`=无需动作。判「要不要现在找人」**只看本字段** | +| `planRefreshStalledReason` | String / null | 停滞归因:`STATE_FAILED`(刷新已失败) / `COMMAND_FAILED`(命令行已失败但状态未回写) / `TIMEOUT`(超时);`planRefreshStalled=false` 时恒为 null | +| `planRefreshTimeoutAt` | LocalDateTime / null | 本轮刷新超时时刻(= 发起时刻 + 管理员定的窗口时长,缺省 120 分钟)。PENDING 且已登记发起时刻时有值;其余为 null;过了这个点仍是 PENDING 即判停滞 | +| `planRefreshReplayExhausted` | Boolean | 人工重投额度是否已耗尽(恒非 null):`true`=已达 5 次上限,再点确认报 809210;`false`=停滞时仍可重新确认触发重投 | + +#### 请求示例 + +```http +GET /v3/admin/order/group-batch/2099716954674597889/vehicle-requirement +Authorization: Bearer +``` + +#### 响应示例 + +**场景1:从未登记刷新(多数团期的正常态)** + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "requirementId": "2099716962786373633", + "groupBatchId": "2099716954674597889", + "status": "CONFIRMED", + "version": 1, + "remark": "全程 33 座", + "confirmedBy": "1001", + "confirmedAt": "2026-09-15 12:28:29", + "planRefreshState": null, + "planRefreshReplayCount": 0, + "blockedStage": null, + "planRefreshStalled": false, + "planRefreshStalledReason": null, + "planRefreshTimeoutAt": null, + "planRefreshReplayExhausted": false, + "groups": [] + } +} +``` + +**场景2:刷新已闭环(DONE 态)** + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "requirementId": "2100669178103910402", + "groupBatchId": "2100668723156193282", + "status": "CONFIRMED", + "version": 2, + "remark": "全程 28 座", + "confirmedBy": "2101000047331078146", + "confirmedAt": "2026-09-19 01:43:03", + "planRefreshState": "DONE", + "planRefreshReplayCount": 0, + "blockedStage": null, + "planRefreshStalled": false, + "planRefreshStalledReason": null, + "planRefreshTimeoutAt": null, + "planRefreshReplayExhausted": false, + "groups": [] + } +} +``` + +#### 空数据 / 降级响应 + +接口无空数据场景(群编码、需求 ID 缺失返回 404)。七个新字段恒有值:`planRefreshStalled` 和 `planRefreshReplayExhausted` 恒为 Boolean,其余可为 null / 0。 + +#### 错误响应 + +```json +{ + "code": 404, + "message": "团期不存在或无权限访问", + "success": false, + "data": null +} +``` + +#### 业务边界 + +- **七个字段都是只读投影,不接受回传、不参与任何提交。** 它们由后端从 fleet 数据和受控重开日志投影而出,前端提交时忽略这些字段。 +- **`planRefreshState` 单看分不清「在跑」和「已经死」。** `PENDING` 有三种现实——正常在途、命令已终态但状态未回写、超过时限卡死。后两种必须报警,此时看 `planRefreshStalled`。 +- **`null` 不等于缺失。** `planRefreshState=null` 是「从未登记过刷新」的完全正常形态(绝大多数团期),此时 `planRefreshStalled=false`,前端不要做任何提示。 +- **`FAILED` 在本接口可观测。** 与写口不同,本查询接口不改任何状态,`FAILED` 态会原样返回。 +- **重投次数有上限。** `planRefreshReplayCount >= 5` 时 `planRefreshReplayExhausted=true`;此时再点「重新确认」会收到错误码 809210。 +- **超时时刻可变。** 每次管理员「重新确认」时可重定义窗口时长(缺省 120 分钟),`planRefreshTimeoutAt` 会随之变化。 + +--- + +## 四、契约约束与正确调用方式 + +### 判停滞告警的唯一正确方式 + +| 场景 | planRefreshStalled | planRefreshState | 前端动作 | +|------|-------------------|------------------|---------| +| ✅ 未登记 | false | null | 不告警,正常展示 | +| ✅ 在途中(正常) | false | PENDING | 不告警,展示「配车中」 | +| ✅ 已闭环 | false | DONE | 不告警,展示「已配车」 | +| ❌ 停滞(命令失败) | **true** | FAILED | **告警**,原因为 `STATE_FAILED` | +| ❌ 停滞(超时) | **true** | PENDING | **告警**,原因为 `TIMEOUT`,可读 `planRefreshTimeoutAt` 判是否逾期 | +| ❌ 停滞(未回写) | **true** | PENDING | **告警**,原因为 `COMMAND_FAILED` | + +**核心规则**:`planRefreshStalled == true` 时无条件告警,不要根据 `planRefreshState` 额外判断。`planRefreshState` 只用于展示「当前原始状态」;`planRefreshStalledReason` 用于展示「为什么停滞」。 + +### 错误码与重投限制 + +- **809210**: 人工重投次数已达上限(5 次),不能再点「重新确认」;此时 `planRefreshReplayExhausted=true`,前端应显示「联系后台排查」而非重试按钮。 +- 每次 `planRefreshReplayCount` 自增是在 `POST /v3/admin/order/group-batch/{groupBatchId}/vehicle-requirement/confirm` 触发的受控重投,不含自动重试次数。 + +--- + +## 五、数据库行为 + +接口为只读查询,无数据库写操作。七个新字段映射到既有表: + +| 表 / 字段 | 来源 | 说明 | +|----------|------|------| +| `order_vehicle_plan_refresh.plan_refresh_state` | 直读 | 原始状态机值 | +| `order_vehicle_plan_refresh.replay_count` | 直读 | 人工重投次数 | +| `order_vehicle_plan_refresh.blocked_stage` | 直读 | 阻断阶段快照 | +| `planRefreshStalled` / `planRefreshStalledReason` / `planRefreshTimeoutAt` / `planRefreshReplayExhausted` | 实时计算 | 读取时动态投影,无落库 | + +--- + +## 六、边界行为 + +- **团期不存在** → 404 +- **无权限访问** → 404(鉴权失败的兜底;前置字段、角色判权在 Controller 层) +- **字段为 null 时的序列化** → JSON 中保留 null 值,不省略字段;前端据此判断该字段是否有值 +- **超大团期(>10000 人)查询响应** → 七个新字段不涉及复杂 JOIN,查询耗时无额外增长 + +--- + +## 六.5、枚举 / 数据字典 + +### planRefreshState(配车刷新状态) + +**所属字段**: `planRefreshState` | **类型**: `String / null` + +| 值 | 中文 | 说明 | +|----|------|------| +| `null` | 从未登记 | 团期从未走过受控重开,也未自动触发刷新。这是绝大多数团期的正常状态,**不表示异常** | +| `PENDING` | 刷新中 | 刷新命令已发出,等 fleet 侧回包。窗口内正常,超时则判停滞 | +| `DONE` | 已闭环 | 本轮刷新已有结果,fleet 已回包且后端已处理。可能包含部分成功(部分车型配上、部分待自主响应) | +| `FAILED` | 已失败 | fleet 已明确拒绝或超过自动重试次数,需人工介入。此时 `planRefreshStalled=true` / `planRefreshStalledReason=STATE_FAILED` | + +### planRefreshStalledReason(停滞归因) + +**所属字段**: `planRefreshStalledReason` | **类型**: `String / null` + +| 值 | 中文 | 说明 | +|----|------|------| +| `null` | 未停滞 | `planRefreshStalled=false` 时恒为 null | +| `STATE_FAILED` | 状态失败 | fleet 已判 FAILED(自动重试耗尽或被 fleet 终结)。排障:查 fleet 服务日志为什么拒绝 | +| `COMMAND_FAILED` | 命令行失败 | 命令行已失败但后端未收到状态回写(网络、hook 失败等)。排障:查回写状态的终态钩子 | +| `TIMEOUT` | 超时 | 超过本轮窗口时限仍无结果(可读 `planRefreshTimeoutAt` 判具体何时逾期)。排障:查队列是否积压 | + +### blockedStage(阻断阶段) + +**所属字段**: `blockedStage` | **类型**: `String / null` + +| 值 | 中文 | 说明 | +|----|------|------| +| `null` | 未阻断 | 团期可正常流转 | +| `RESOURCE_PREPARING` | 资源筹备中 | 团期卡在配车需求确认后、资源分配前 | +| `MATERIAL_PREPARING` | 物资筹备中 | 团期卡在资源分配后、物资分配前 | +| `PENDING_DEPARTURE` | 待出团 | 团期卡在物资准备完、出团前。最常见的阻断点 | + +--- + +## 六.6、修改前后对比 + +| 项 | 改前 | 改后 | +|---|---|---| +| 查询需求状态 | 调用 `confirm` / `reopen` 这类**写口**端点 | 调用 GET 只读端点,不改任何状态 | +| 能否观测 `FAILED` 态 | 不能(读的动作会改状态) | 可以(不改状态的纯查询) | +| 响应体新增字段数 | 0 | 7(全是观测字段,只读) | +| 告警判据 | 需要前端自己推导「PENDING + 超时」= 告警 | 后端已收敛为 `planRefreshStalled`,前端一个布尔值判定 | +| 人工重投上限可见 | 无 | 可见:`planRefreshReplayCount` / `planRefreshReplayExhausted` | +| 超时时刻 | 无 | 可见:`planRefreshTimeoutAt`,支持倒计时展示 | + +## 六.7、影响评估 + +| 维度 | 评估 | +|---|---| +| **破坏向后兼容** | 否。只新增字段,不改既有字段;既有消费方可忽略新字段正常工作 | +| **前端是否必须同步** | 是。新字段是告警判据的唯一来源,不同步则无法正确判停滞;`planRefreshStalled` 与 `planRefreshStalledReason` 必须实现 | +| **路径 / HTTP 方法** | 不变 | +| **入参 / 出参结构** | 只新增 7 个字段,不删不改既有字段 | +| **FAILED 态生产可达** | 是。生产环境灰度开关尚未开启(属独立运维动作),但 FAILED 态的业务逻辑已就绪;前端**必须**把 `planRefreshStalled=true + planRefreshStalledReason=STATE_FAILED` 这条分支实现出来,不要因为测试环境造不出来就跳过 | +| **性能影响** | 极小。七个字段由既有表直读或实时计算,无新 JOIN、无新子查询 | +| **下游兼容** | 安全。字段全是观测投影,无外发依赖 | + +--- + +## 七、不影响范围 + +- **零影响**:接口路径、HTTP 方法(GET)、既有响应字段 +- **零影响**:写口端点(`confirm` / `reopen` / `withdraw` / `waive`)的响应、逻辑、校验 +- **零影响**:其他团期查询接口(`list` / `group-batch/{id}` 等) +- **零影响**:前置条件与鉴权(角色、数据权限);新字段投影后的鉴权与既有相同 +- **未新建端点、未删端点** + +--- + +## 八、测试环境已验证 + +✅ **2026-09-21 UTC 14:42:32 ~ 14:42:39 经网关 `https://api.test.1814.love:9443` 真实调用验证** + +部署信息: +- 测试服 order-v3 当前部署 commit `d30cd9561` +- 七字段的祖先提交 `587af48cd` 已在其中(`git merge-base --is-ancestor 587af48cd d30cd9561` 实测为真) +- 接口已在线可调 + +实测用例(两次调用,响应均 `code=200 / success=true`): + +| # | 场景 | 七字段全部到位 | 与数据库一致性 | 备注 | +|---|------|-------------|----------|------| +| 1 | 从未登记刷新 | ✅ | ✅ | planRefreshState=null, planRefreshStalled=false, 其余字段为 null / 0 | +| 2 | 刷新已闭环 | ✅ | ✅ | planRefreshState=DONE, planRefreshStalled=false, blockedStage=null, 其余字段为 null / 0 | + +**原始响应体(两份)**: + +NULL 态: +```json +{ + "code": 200, + "message": "成功", + "data": { + "requirementId": "2099716962786373633", + "groupBatchId": "2099716954674597889", + "status": "CONFIRMED", + "version": 1, + "confirmedBy": "1001", + "confirmedAt": "2026-09-15 12:28:29", + "planRefreshState": null, + "planRefreshReplayCount": 0, + "blockedStage": null, + "planRefreshStalled": false, + "planRefreshStalledReason": null, + "planRefreshTimeoutAt": null, + "planRefreshReplayExhausted": false, + "groups": [] + }, + "success": true +} +``` + +DONE 态: +```json +{ + "code": 200, + "message": "成功", + "data": { + "requirementId": "2100669178103910402", + "groupBatchId": "2100668723156193282", + "status": "CONFIRMED", + "version": 2, + "confirmedBy": "2101000047331078146", + "confirmedAt": "2026-09-19 01:43:03", + "planRefreshState": "DONE", + "planRefreshReplayCount": 0, + "blockedStage": null, + "planRefreshStalled": false, + "planRefreshStalledReason": null, + "planRefreshTimeoutAt": null, + "planRefreshReplayExhausted": false, + "groups": [] + }, + "success": true +} +``` + +--- + +## 十、相关文档 + +- 团期需求文档:`docs/group/`(dev-v3 分支) +- 配车刷新业务设计:工单 #7988 正文与 AC-1 ~ AC-6 +- 受控重开流程:#7996(与本单共线依赖) + +--- + +## 关联 / 联系人 + +### 链接 + +- **Issue**: [#7988](https://git.1814.love:8443/wx/HL/issues/7988) +- **PR**: [#8130](https://git.1814.love:8443/wx/HL/pulls/8130) +- **验收项**: AC-6 + +### 联系人 + +- **后端负责人**: @wx