# 13: 后端执行票 —— 报警/工单实时同步 Type: task Status: resolved Label: ready-for-agent Blocked by: 12 Category: backend-execution ## 来源 - 决策:[01 报警—工单闭环读模型与 Gogs 票拆分边界](01-backend-slice-ticket-boundary.md)(后端切片 3)、[04 real 生命周期与 mock 同构启动契约](04-real-lifecycle-contract.md) - 规格:[09 real 生命周期规格](09-real-lifecycle-spec.md) ## 状态 **已 resolved(2026-10-09)**:诊断 + 2 项维护者裁定 + 实施 + 契约测试 + 推送 + **真实环境部署复验 6 项全通过**。提交 `d8a6b03` 已推 `origin/master`(`4ef2159..d8a6b03`)。验证:针对性契约测试 **21/21**(新增 8 例)、全量 8 模块 **422/422**。基线卡片:[报警/工单实时同步](../handoff/baselines/work-order-realtime-sync.md)(**状态 ③ 真实数据联调完成**)。复验明细记录在 [票 17](17-postdeploy-live-verification.md);上位回写(票 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](../handoff/baselines/work-order-realtime-sync.md)(**状态 ③**) | | 代码提交 + 推送 | **已完成** | `d8a6b03` → `origin/master`(`4ef2159..d8a6b03`) | | 部署后实测帧到达 | **已通过** | 真人操作 284(3→4)后 **40 秒帧到达**,字段全对(见上复验表) | | 上位回写 | **已完成** | 票 25 已回写三切片证据;Gogs #63 同文需负责人远端执行 | | 真实设备报警闭环 | **不归本票** | 按决策 01 由真人团队执行(阶段总收口) | ## 下一步 1. ~~重启加载、部署复验~~ —— **全部完成**(2026-10-09,6 项复验通过)。 2. **前端消费侧**([票 16](16-frontend-alarm-order-consumption.md)):消费 `cockpitWorkOrderStateDelta`(按 `workOrderCode` 合并、`scanIntervalMs` 标注延迟)与 `cockpitDeviceStatusDelta.eventId`;前端对未知帧静默忽略,不升级不会崩。 3. 设备帧 `eventId` 线上携带值:待外部发布方(zk-api-service)事件发生时观察,契约已锁定。 ## 关联 - 上位:本地票 25、Gogs #63 - 下游:前端执行票 16(报警–工单闭环消费)依赖本票 - 基线:[基线卡片 04 报警/工单实时同步](../handoff/baselines/work-order-realtime-sync.md) - 复验承载:[17 部署后真实环境复验(后端切片 11–13)](17-postdeploy-live-verification.md)