13-backend-realtime-sync.md 11 KB

13: 后端执行票 —— 报警/工单实时同步

Type: task Status: resolved Label: ready-for-agent Blocked by: 12 Category: backend-execution

来源

状态

已 resolved(2026-10-09):诊断 + 2 项维护者裁定 + 实施 + 契约测试 + 推送 + 真实环境部署复验 6 项全通过。提交 d8a6b03 已推 origin/master(4ef2159..d8a6b03)。验证:针对性契约测试 21/21(新增 8 例)、全量 8 模块 422/422。基线卡片:报警/工单实时同步(状态 ③ 真实数据联调完成)。复验明细记录在 票 17;上位回写(票 25 / Gogs #63)已按决策 01 执行(Gogs 侧需负责人同步)。

部署复验结果(2026-10-09,实例 PID 20240 @ 13:41:14 加载 d8a6b03)

复验项 实测 判定
新构建加载 PID 20240 启动 13:41:14 > 提交;日志无调度任务异常 通过
WS 直订回执 cockpitSubscribe → cockpitSubscribed(两次实测) 通过
无变化不推送 订阅 75 秒零增量帧;复查库确认 7 张工单零变化 通过
不改状态的操作不触发 13:55:47 真人「处置」284(仅 update_time 变、状态码不变)→ 无帧:证明增量由 orderStatus 驱动、不被 update_time 误导 通过
真人状态变化 → 端到端帧 13:57:54 对 284 提交验收(3→4)→ 13:58:34 帧到达(延迟 40s < scanIntervalMs=60000):workOrderCode=WO202610091014453765、stateCode=4、stateName=待验收、deviceCode=TEST-CBYL5432168;帧内无 orderId/updateTime;items 只含变化那一张 通过
设备帧 eventId 契约与显式 null 序列化已由测试锁定;线上携带值取决于外部发布方(zk-api-service),复验窗口无设备状态发布,如实记录为「契约通过、携带值待外部事件」 契约通过

决策 01 的「真实设备报警闭环真人演示」(报警产生→关联生成工单→推进→驾驶舱呈现全链路)是阶段总收口事项,不在本票验收内——本票已交付其驾驶舱侧全部依赖(REST 快照 + 设备增量 + 工单状态增量)。

实施结果(2026-10-09)

按维护者裁定实施,共 6 个文件(2 新增 + 4 修改):

交付 内容
新增 cockpitWorkOrderStateDelta 帧 CockpitWorkOrderDeltaPushTask:定时扫描 work_order.order_status 逐单比对(60s 可配)——工单状态流转不写 update_time(诊断核实),靠时间过滤会漏掉全部流转,故必须比对状态。只有真实变化才推给驾驶舱全量订阅会话(免鉴权 scope=all,不做按人过滤,与管理端 workOrderReminderPush 语义隔离);首扫只建基线不推送(REST 才是初始快照);帧带 scanIntervalMs 诚实标注延迟上限、不带会误导的 updateTime、不泄漏主键 orderId;stateCode/stateName 出自票 11 字典单源,脏码原样上报名称显式 null
设备帧补 eventId 透传 cockpitDeviceStatusDelta 在 Redis 消息携带 eventId 时透传(前端可精确重取该报警),缺省显式 null 不静默丢;是否携带由发布方(zk-api-service)决定,不臆造
范围 1 REST 快照 复用票 11/12 读模型,无新端点
范围 3 诚实降级/重连 无全量订阅会话时不查库;发送失败的失效会话被清出订阅集合(测试锁定);断线后重订即可,错过的增量由 REST 重取兜底
范围 4 契约测试 新增 8 例(基线不推送/变化推送/新单推送/无变化不推/脏码/无会话不查库/失效会话清理/eventId 透传)
文档 DeviceStatusWebSocketHandler 协议 javadoc + docs/cockpit/README.md(新帧示例 + 「工单状态增量口径」章节)

对抗式自查修正一处:初版帧携带数据库主键 orderId,但驾驶舱 REST 读模型从不暴露它(对外键是 workOrderCode)——已移除并加测试断言「不泄漏 orderId」。

语义隔离(决策 01 红线,已写进文档)

cockpitDeviceStatusDelta(设备在线/报警状态)与 cockpitWorkOrderStateDelta(工单状态)帧类型不同、语义不同,不得互为冒充;REST=快照、WS=增量的分工在两帧上一致。

诊断(2026-10-09)

后端基线(实施前)

项 值
仓库 / 分支 / 提交 origin/master / master / 4ef2159(工作树干净)
联调环境 http://192.168.110.96:8301 实测可达

现状盘点(对照范围 1–4)

能力 状态 证据
范围 1:REST 快照 已具备(票 11/12 交付) /events/current、/equipments/{code}/work-orders、/events/{source}/{code}/work-orders、/work-orders/{code}/timeline;设备/点位快照走 /map-snapshot。无需新端点
WS 连接/直订/重连生命周期 已具备 /ws/deviceStatus + cockpitSubscribe(scope=all 免鉴权,ADR 0005)+ cockpitSubscribed 回执;前端 runtimeSocket.js 退避重连;后端会话清理(afterConnectionClosed/handleTransportError)
设备状态增量帧 已具备 cockpitDeviceStatusDelta:Redis device:status:change 事件驱动(外部 zk-api-service 发布),帧含 deviceCode/monitoringPointCode/data{status,statusTime,serverUpdatedAt,reason},DeviceStatusDeltaTracker 按设备去重只推真变化
工单状态增量帧 不存在 唯一工单推送 workOrderReminderPush(WorkOrderWebSocketPushTask)是管理端提醒:需 authenticate 鉴权、按部门/责任人过滤可见性、内容是待处理/超时清单而非状态增量——驾驶舱(免鉴权、scope=all)收不到,也不该收
报警数据增量帧 无独立通道 报警记录由外部 zk-api-service 写入,唯一信号是设备级 device:status:change;旧版 deviceStatusPush 在 Redis 消息携带 eventId 时透传,驾驶舱 cockpitDeviceStatusDelta 不带 eventId
前端兼容性 新增帧零破坏 前端 handleFrame 只认 cockpitSubscribed/cockpitDeviceStatusDelta/pong,未知帧静默忽略(注释明言「后端将来加帧类型不该让前端崩」)

关键数据事实(决定实现方案)

  1. work_order.update_time 在状态流转时不更新:transition()(派单/接单/处置/验收/取消共用)只 .set(orderStatus),仅创建路径写 updateTime,实体无 FieldFill 自动填充。→ 扫描式增量不能靠 update_time 过滤,必须逐单比对 orderStatus(既有 WorkOrderWebSocketPushTask 的指纹法正是这么做的)。
  2. 可证明的实现路径已在本仓库:WorkOrderWebSocketPushTask(60s 可配扫描 + 指纹比对 + 变化才推 + 会话清理)是被测验证过的基础设施,可复用其模式给驾驶舱全量订阅会话。
  3. 写路径挂钩不可行:工单写逻辑在 zksy-system(WorkOrderServiceImpl),WS 处理器在 zksy-admin,模块依赖方向不允许直连;改写路径还触碰本票非目标(「不修改工单操作逻辑」)。

待裁定问题(已裁定,2026-10-09)

  1. 工单状态增量走哪条路 —— 裁定:实现 cockpitWorkOrderStateDelta 帧(扫描-比对-推送到全量订阅会话;帧带 scanIntervalMs 诚实标注延迟;复用票 11 字典单源)。理由:仓库已有可证明的扫描-指纹-推送基础设施;纯降级会让本票后端侧几乎无交付;前端对未知帧静默忽略,零破坏。
  2. 设备状态 delta 帧是否补 eventId 透传 —— 裁定:补(Redis 消息携带时透传、缺省显式 null)。前端收到 status=ALARM 增量后可精确重取该报警;zk-api-service 是否真发 eventId 未证实,防御性透传不臆造。

范围(做什么)

  1. REST 快照:报警、工单、关联设备/监测点的初始快照走读接口(复用既有读模型)。
  2. WebSocket 增量:在现有驾驶舱 WS 连接与直订生命周期上,消费/扩展真实后端能证明的报警/工单状态增量帧;未具备帧时诚实降级为 REST 重取/轮询并标注更新延迟;不把设备状态增量冒充工单状态增量。
  3. 诚实降级:无增量协议时前端可轮询;断线按既有退避重连。
  4. 契约测试:增量帧解析、降级路径、重连。

非目标

  • 不修改驾驶舱只读边界(驾驶舱不创建/处置工单)。
  • 不改变免鉴权范围(ADR 0005:驾驶舱只读 REST/WS 不鉴权)。

验收

项 状态 证据
编译 + 针对性契约测试 通过 21/21(新增 8 例:帧形状/变化检测/基线/降级纪律/会话清理/eventId)
全量测试 通过 8 模块 422/422(基线 414 + 8)
联调基线登记 已产出 基线卡片 04(状态 ③)
代码提交 + 推送 已完成 d8a6b03 → origin/master(4ef2159..d8a6b03)
部署后实测帧到达 已通过 真人操作 284(3→4)后 40 秒帧到达,字段全对(见上复验表)
上位回写 已完成 票 25 已回写三切片证据;Gogs #63 同文需负责人远端执行
真实设备报警闭环 不归本票 按决策 01 由真人团队执行(阶段总收口)

下一步

  1. 重启加载、部署复验 —— 全部完成(2026-10-09,6 项复验通过)。
  2. 前端消费侧(票 16):消费 cockpitWorkOrderStateDelta(按 workOrderCode 合并、scanIntervalMs 标注延迟)与 cockpitDeviceStatusDelta.eventId;前端对未知帧静默忽略,不升级不会崩。
  3. 设备帧 eventId 线上携带值:待外部发布方(zk-api-service)事件发生时观察,契约已锁定。

关联