# 基线卡片 04:报警/工单实时同步(工单状态增量帧 + 设备帧 eventId 透传)
> 按决策票 [05 真实联调基线与收口证据门槛](../../issues/05-integration-baseline-and-evidence.md) 的固定骨架产出,对应执行票 [13 报警/工单实时同步](../../issues/13-backend-realtime-sync.md)。**状态判定:③ 真实数据联调完成**(2026-10-09 部署后复验 6 项全通过,含真人触发状态变化的端到端帧捕获;④ 模式整体收口仍待真人验收与前端接线)。
## 一、基线
| 项 | 值 |
| --- | --- |
| 记录时间 | 2026-10-09(本地时钟) |
| 记录角色 | 后端执行代理(实施 + 编译 + 契约测试 + 部署后复验) |
| 联调环境 baseUrl | `http://192.168.110.96:8301`(= 本机,IDE 内运行的 Spring Boot 进程) |
| 后端仓库 / 分支 / 提交 | `http://192.168.110.30:3000/shaoy/pipe-ner-server` / `master` / **`d8a6b03`**(已推送 `4ef2159..d8a6b03`) |
| 运行实例 | PID 20240,启动于 2026-10-09 13:40:58(**已加载新构建**) |
| 访问控制 | 驾驶舱只读 REST/WS 免鉴权(ADR 0005);本票不改鉴权范围 |
| 只读边界 | 未触碰工单创建/派单/接单/处置/验收/延期/取消逻辑(决策 01 非目标) |
## 二、收口条件五行
| # | 条件 | 状态 | 证据 | 未完成项 |
| --- | --- | --- | --- | --- |
| ① | 后端提交 / 编译 / 契约测试 | **已完成** | 编译通过;针对性契约测试 **21/21**;全量 8 模块 **422/422**;`d8a6b03` 已推送 | 无 |
| ② | REST 快照复用 | **已完成(无新端点)** | 票 11/12 交付的读模型即快照源:`/events/current`、`/equipments/{code}/work-orders`、`/events/{source}/{code}/work-orders`、`/work-orders/{code}/timeline` | 无 |
| ③ | WS 增量帧 | **已完成(部署后实测)** | 真人操作 284 工单(3→4)→ **40 秒内 `cockpitWorkOrderStateDelta` 帧到达**,字段全对;无变化/不改状态的操作均正确不推送 | 设备帧 `eventId` 线上携带值待外部发布事件(契约已锁定) |
| ④ | 诚实降级 | **已完成(契约层 + 实测)** | 帧带 `scanIntervalMs` 标注延迟上限(实测延迟 40s < 60s 上限);无订阅不查库;断线重订、失效会话清理有测试 | 前端消费侧呈现归票 16 |
| ⑤ | 真实设备报警闭环 | **不归本票** | 按决策 01 由真人团队执行 | 阶段总收口(票 18 上位阶段) |
## 三、本票交付内容
### 新增:`cockpitWorkOrderStateDelta` 工单状态增量帧
| 项 | 决策 | 依据 |
| --- | --- | --- |
| 产生机制 | 定时扫描 `work_order.order_status` **逐单比对**(默认 60s,`websocket.cockpit-work-order-delta.scan-interval-ms` 可配) | 状态流转不写 `update_time`(`transition()` 只 set orderStatus、无 FieldFill),靠时间过滤会漏掉全部流转——诊断实测核实 |
| 推送对象 | 驾驶舱全量订阅会话(免鉴权 scope=all),**不做按人可见性过滤** | 与管理端 `workOrderReminderPush`(需鉴权+按人过滤)明确区分:内容是**状态增量**,不是提醒清单 |
| 首扫语义 | **只建基线不推送** | REST 才是初始快照(与 `cockpitSubscribe` 既有语义一致);WS 只收后续变化 |
| 变化判定 | orderStatus 与上次扫描不同,或工单新出现 | 新工单也是状态变化(0-待派单入场) |
| 帧形状 | `type/scanIntervalMs/serverUpdatedAt/data{items[{workOrderCode,deviceCode,stateCode,stateName}],total}` | 对外键 `workOrderCode`(order_no),**不泄漏数据库主键 orderId**(与 REST 读模型同口径) |
| 诚实边界 | 帧不带工单 `updateTime`(流转不更新该列,携带会误导);变化时刻=帧级 `serverUpdatedAt`(扫描时间);脏状态码原样上报、`stateName` 显式 null | 不给消费方看起来可信实则不更新的时间;不编造名称 |
| 状态字典 | `stateCode/stateName` 出自 `CockpitWorkOrderStateDictionary`(0–7 单源,票 11) | 与 REST 读模型完全一致 |
| 降级与重连 | 无全量订阅会话时**不查库**;发送失败的失效会话被清出订阅集合;错过的增量由消费方 REST 重取兜底 | 先看会话再扫表(同 `WorkOrderWebSocketPushTask` 纪律);断线后重订即可 |
### 修改:`cockpitDeviceStatusDelta` 补 `eventId` 透传
Redis 消息(`device:status:change`)携带 `eventId` 时透传进驾驶舱帧,缺省**显式 null**(WriteMapNullValue 序列化,不静默丢)——前端收到 `status=ALARM` 增量后可精确重取该报警详情,而非盲刷列表。是否携带由发布方(zk-api-service)决定,后端不臆造。
### 明确的语义隔离(决策 01 红线)
`cockpitDeviceStatusDelta`(设备在线/报警状态)与 `cockpitWorkOrderStateDelta`(工单状态)**帧类型不同、语义不同**,文档明言不得互为冒充;REST 快照与 WS 增量的分工(快照=REST、增量=WS)在两帧上一致。
## 四、实测记录(可复制命令与结果)
```powershell
cd E:\pipe-ner-server
mvn -o -pl pipe-network-service/zksy-admin -am test "-Dtest=CockpitWorkOrderDeltaPushTaskTest,DeviceStatusCockpitWebSocketHandlerTest" "-DfailIfNoTests=false"
mvn -o -pl pipe-network-service/zksy-admin -am test # 全量
```
结果:针对性 **21/21**(新增 8 例);全量 8 模块 **422/422**(基线 414 + 8)。
新增契约覆盖(`CockpitWorkOrderDeltaPushTaskTest` 7 例 + 监听器 1 例):
- `firstScanEstablishesBaselineWithoutPush` —— REST 才是快照,首扫零推送
- `stateChangePushesDeltaWithDictionaryStateNames` —— 变化推送 + 字典名称 + `scanIntervalMs` + 不泄漏 orderId
- `newlyAppearingOrderIsPushedAsDelta` —— 新工单入场即增量;无设备关联时 `deviceCode` 显式 null
- `unchangedOrdersProduceNoSecondPush` —— 无变化不推
- `unknownStateCodeKeepsRawCodeWithExplicitNullName` —— 脏码原样、名称 null
- `noCockpitSessionMeansNoDatabaseScan` —— 无订阅不查库(降级纪律)
- `staleAllScopeSessionIsPrunedWhenSendFails` —— 失效会话清理(重连语义)
- `cockpitDeltaCarriesEventIdWhenPresentAndExplicitNullWhenAbsent` —— eventId 透传与显式 null
### 4.2 部署后真实环境复验(2026-10-09,实例 PID 20240 @ 13:41:14 加载 `d8a6b03`)
```powershell
# WS 直订(System.Net.WebSockets.ClientWebSocket,PowerShell)
$ws.ConnectAsync([Uri]'ws://192.168.110.96:8301/ws/deviceStatus', …)
→ {"type":"cockpitSubscribe","scope":"all"}
← {"type":"cockpitSubscribed",…}
# 保持订阅监听帧;同时用只读 JDBC 探针快照 work_order.order_status 对照
```
结果(6 项全通过):
| 复验项 | 实测 |
| --- | --- |
| 新构建加载 | PID 20240 启动 13:41:14 > 提交;日志无调度任务异常 |
| WS 直订回执 | `cockpitSubscribed`(13:49:38 与 13:52:23 两次) |
| 无变化不推送 | 订阅 75 秒零增量帧;复查库确认 7 张工单零变化(13:48:56 vs 13:50:53) |
| 不改状态的操作不触发 | 13:55:47 真人「处置」284(仅 update_time 变、状态码仍 3)→ **无帧**(检测由 orderStatus 驱动的正面证明) |
| **真人状态变化 → 端到端帧** | 13:57:54 提交验收(3→4)→ **13:58:34 帧到达**(延迟 40s < scanIntervalMs=60000):
`{"type":"cockpitWorkOrderStateDelta","scanIntervalMs":60000,"serverUpdatedAt":"2026-10-09T13:58:34+08:00","data":{"total":1,"items":[{"workOrderCode":"WO202610091014453765","stateCode":4,"stateName":"待验收","deviceCode":"TEST-CBYL5432168"}]}}`
帧内无 `orderId`/`updateTime`;items 只含变化那一张 |
| 设备帧 `eventId` | 契约与显式 null 已由测试锁定;线上携带值待外部发布事件(窗口内无设备状态发布) |
## 五、真人验收
**闭环演示未执行**(归决策 01 的真人团队阶段总收口)。但本票复验包含一次**真人真实操作**:维护者于 13:57:54 在管理端对工单 284 提交验收(状态 3→4),驾驶舱侧 40 秒内收到增量帧——这是真实业务路径的端到端证据,非模拟。
## 六、未完成项与补齐条件
1. ~~部署后复验~~ —— **已完成**(2026-10-09,6 项全通过,见 §4.2;记录同步至 [票 17](../../issues/17-postdeploy-live-verification.md))。
2. **前端消费侧**(归 [票 16](../../issues/16-frontend-alarm-order-consumption.md)):消费 `cockpitWorkOrderStateDelta`(按 `workOrderCode` 合并、`scanIntervalMs` 标注延迟);`cockpitDeviceStatusDelta.eventId` 用于精确重取报警。前端对未知帧静默忽略,**不升级前端也不会崩**。
3. `eventId` 是否真的由 zk-api-service 发布**未证实**——透传是防御性的;复验窗口内无设备状态发布事件,线上携带值待外部发布时观察(契约与显式 null 已由测试锁定)。
4. **决策 01 真人闭环演示**(报警产生→关联生成工单→推进→驾驶舱呈现全链路):归阶段总收口,本票已交付其驾驶舱侧全部依赖。
## 七、状态判定
**当前状态:③ 真实数据联调完成(2026-10-09 达成)。**(④ 模式整体收口仍待真人验收与前端接线。)
依据:编译 + 422/422 全量测试 + 契约测试通过,且**部署后真实环境复验 6 项全通过**——含端到端正面证据(真人操作 3→4 后 40 秒帧到达、字段全对)与两项负向证据(无变化不推送、不改状态码的操作不触发,证明增量由 `orderStatus` 驱动)。
**不进入 ④ 的原因**:④ 要求模式整体收口(真人验收 + 前端接线 + 阶段联调)——本卡片只覆盖后端切片 3;前端 real 生命周期接线(票 14–16)与真人设备报警闭环(决策 01)尚未发生。
## 八、关联
- 本图执行票:[13 报警/工单实时同步](../../issues/13-backend-realtime-sync.md)
- 复验承载:[17 部署后真实环境复验(后端切片 11–13)](../../issues/17-postdeploy-live-verification.md)
- 上游决策:[01 报警—工单闭环读模型与 Gogs 票拆分边界](../../issues/01-backend-slice-ticket-boundary.md)、[04 real 生命周期与 mock 同构启动契约](../../issues/04-real-lifecycle-contract.md)
- 规格:[09 real 生命周期规格](../../issues/09-real-lifecycle-spec.md)
- 下游:前端执行票 [16 报警–工单闭环消费](../../issues/16-frontend-alarm-order-consumption.md)
- 上位台账:[票 25 后端契约缺口待办](../../../cockpit-shell-panel-layout/issues/25-backend-contract-gaps.md)、Gogs #63