work-order-realtime-sync.md 10 KB

基线卡片 04:报警/工单实时同步(工单状态增量帧 + 设备帧 eventId 透传)

按决策票 05 真实联调基线与收口证据门槛 的固定骨架产出,对应执行票 13 报警/工单实时同步。状态判定:③ 真实数据联调完成(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)在两帧上一致。

四、实测记录(可复制命令与结果)

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)

# 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)。
  2. 前端消费侧(归 票 16):消费 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)尚未发生。

八、关联