--- schema: "hl-changelog/v2" ticket: "7988" title: "团期配车需求详情响应新增七个配车刷新态观测字段" consumer: "admin" author: "wx(GIT)" change_type: "修改接口" backend_status: "deployed" gateway_status: "verified" frontend_status: "verified" frontend_owner: "mmg" frontend_ref: "3df2c6f8df7902b31886e7ca0fc0ec88807d7032" target_release: "v2.1" verified_at: "2026-09-21" status_note: "接口已在测试环境部署并通过网关验证;七个字段在 origin/dev-v3 已就绪;order-v3 commit d30cd9561 是最新且已部署 前端 2026-09-21 核销:与 20_7988 同一观测块,已随 #7442 PR-C2 增量1 交付(hl-admin v2.1 3df2c6f8,GroupVehicleRequirementSection 观测块——planRefreshStalled 判据/replayExhausted 分叉/PENDING 短轮询),本份 AC-6 交接件零增量代码。" 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