17 KiB
schema, ticket, title, consumer, author, change_type, backend_status, gateway_status, frontend_status, frontend_owner, frontend_ref, target_release, verified_at, status_note, updated_at, base
| schema | ticket | title | consumer | author | change_type | backend_status | gateway_status | frontend_status | frontend_owner | frontend_ref | target_release | verified_at | status_note | updated_at | base |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| hl-changelog/v2 | 7988 | 团期配车需求详情响应新增七个配车刷新态观测字段 | admin | wx(GIT) | 修改接口 | deployed | verified | verified | mmg | 3df2c6f8df7902b31886e7ca0fc0ec88807d7032 | v2.1 | 2026-09-21 | 接口已在测试环境部署并通过网关验证;七个字段在 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 交接件零增量代码。 | 2026-09-21 | dev-v3 |
团期配车需求: 查询接口新增七个配车刷新态观测字段
服务: hl-order-service-v3(order-v3) PR: #8130 | Issue: #7988 | 验收项: AC-6 日期: 2026-09-21 影响范围: 管理后台「团期详情」配车需求查询接口响应体
⚠️ 关键变化
响应体新增七个只读观测字段,描述配车计划刷新的状态与停滞告警。
最重要的三件事:
-
🔴 判「要不要告警」的主字段是
planRefreshStalled,不是planRefreshState。planRefreshState是原始状态机值(null/PENDING/DONE/FAILED),PENDING在窗口内属于正常在途,不应报警。后端已把「窗口内/窗口外」「重投耗尽」「命令失败」这些判断收敛进了planRefreshStalled这一个布尔值。→ 前端判告警只看planRefreshStalled;planRefreshState仅用于展示原始状态。 -
FAILED是显式告警态。 它进来时planRefreshStalled=true、planRefreshStalledReason=STATE_FAILED。前端不需自己推导「FAILED 算不算停滞」。 -
⚠️
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<GroupVehicleRequirementRespVO>
使用场景
管理后台「团期详情」页加载配车需求信息时调用。前端需要据 planRefreshStalled 判是否在需求卡片上显示告警;据 planRefreshStalledReason 和 planRefreshTimeoutAt 展示「为什么停滞」和「倒计时」。
入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
groupBatchId |
Path | Long | ✅ | — | 团期聚合主键 |
出参 Result<GroupVehicleRequirementRespVO>
| 字段 | 类型 | 说明 |
|---|---|---|
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=停滞时仍可重新确认触发重投 |
请求示例
GET /v3/admin/order/group-batch/2099716954674597889/vehicle-requirement
Authorization: Bearer <token>
响应示例
场景1:从未登记刷新(多数团期的正常态)
{
"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 态)
{
"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。
错误响应
{
"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 态:
{
"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 态:
{
"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(与本单共线依赖)
关联 / 联系人
链接
联系人
- 后端负责人: @wx