# 17: 部署后真实环境复验(后端切片 11–13) Type: task Status: resolved Label: ready-for-human Blocked by: 11 Category: backend-execution > **本票为滚动复验票,贯穿票 11–13**:每张后端切片落地并部署后,其新契约复验追加到本票(同一部署窗口、同一环境),**不另开复验票**。当前进度:**票 11 复验已通过**(2026-10-08);**票 12 复验已通过**(2026-10-09,8 项全通过);**票 13 复验已通过**(2026-10-09,6 项全通过,含真人触发状态变化的端到端帧捕获,见下)。**三切片全部复验通过 → 滚动使命完成,本票 resolved**;决策 01 的真人闭环演示归阶段总收口(票 18 上位阶段),不在本票范围。 ## 复验进度 | 切片 | 复验项 | 状态 | | --- | --- | --- | | **票 11**(工单状态与处置率) | `handling-efficiency` 新 basis + 工单分母;`history-analysis` 汇总;`overview/warning-settings` 两 family | **已通过**(2026-10-08,见下) | | **票 12**(报警/工单读模型 + district) | 六族 district 字段出现、`region` 入参被忽略、脏值 `district=9`→400、管理信息无 `organizations`、工单显式关联标记 | **已通过**(2026-10-09,8 项全通过,见下) | | **票 13**(报警/工单实时同步) | WS 直订回执;扫描任务健康;无变化不推送;真人触发状态变化后收到 `cockpitWorkOrderStateDelta` 帧;设备帧 `eventId` 字段 | **已通过**(2026-10-09,6 项全通过,见下) | ## 票 13 复验结论(2026-10-09,已通过) **六项复验全部符合票 13 的新契约。** 部署依据:`origin/master = d8a6b03`;运行进程 PID 20240,启动于 13:40:58(13:41:14 Started),晚于提交。 | 复验项 | 实测 | 判定 | | --- | --- | --- | | 新构建加载 | PID 20240 启动 13:41:14 > 提交 `d8a6b03`;日志无调度任务异常 | **通过** | | WS 直订回执 | 连接 `/ws/deviceStatus` → `cockpitSubscribe` → `cockpitSubscribed`(13:49:38 与 13:52:23 两次) | **通过** | | 无变化不推送 | 订阅保持 75 秒(一个完整扫描周期):仅收回执 1 帧、零增量帧;复查库确认 7 张工单状态零变化(13:48:56 vs 13:50:53) | **通过** | | **不改状态的操作不触发**(意外强化) | 13:55:47 真人「处置」284(只改 `update_time`、`orderStatus` 仍 3)→ **无帧**——证明增量检测由 `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` 只含变化那一张(283/285 未入帧) | **通过** | | 设备帧 `eventId` 字段 | 契约与序列化(显式 null)已由单元测试锁定;线上是否携带由发布方(zk-api-service)决定——复验窗口内无设备状态发布,无法实测携带值,如实记录 | **契约通过;携带值待外部发布事件** | > **基线卡片 04 的阶梯已由 ② 升 ③(真实数据联调完成)** —— 该判定针对票 13 切片。决策 01 的「真实设备报警闭环真人演示」是**阶段总收口**事项(报警产生→关联生成工单→推进→驾驶舱呈现全链路),不在本票复验范围。 ### 本次复验方法论(供后续切片借鉴) - 负向观察必须闭环:说「没收到帧」必须同时复查库证明「确实没变化」,否则「没帧」与「链路坏」不可区分。 - 临时探针一律放 `%TEMP%`(不进 git 跟踪目录),用完即删——本图早前曾把含明文口令的探针留在 `.scratch`,已纠正为纪律。 ## 票 12 复验结论(2026-10-09,已通过) **八项复验全部符合票 12 的新契约。** 部署依据:`origin/master = 818a6d7`(含票 12 的 `ee2e1b6`+`18db910`);运行进程 PID 15244(重启后加载新字节码)。 | 复验项 | 实测 | 判定 | | --- | --- | --- | | 六族端点健康 | 7/7 全部 HTTP 200 | **通过** | | `district` 字段出现 | `mapFilter` 含 `district`/`districtName`/`districtFilter`;`region` 已消失 | **通过** | | 筛选生效 | `district=0`→4、`1`→2、`2`→1、`UNASSIGNED`→6;合计 7+6=13 == 全量(自洽) | **通过** | | 脏值 400 | `district=9` / `-1` / `城区B` 全部 400 | **通过** | | 旧 `region` 被忽略 | `region=城南` → 200 且 `districtFilter=NONE`(不生效、不报错) | **通过** | | 管理信息删字段 | 顶层无 `organizations`/`ownershipDistribution`;`districtIndustry[]` 7 条(含「未分区」桶) | **通过** | | 点位 district 归纳 | `equipment_point_rel` 0 行 → 点位全部「未分区」(如实,非缺陷) | **通过** | | 工单显式关联标记 | 设备维度逐张 `associatedWithCurrentEvent`;报警维度按真实 UUID 回 `READY`+1 张;`unassociatedWorkOrderCount` 同源 | **通过** | > **基线卡片 03 的阶梯已到 ③(真实数据联调完成)** —— 该判定针对票 12 切片。④「模式整体收口」仍待真人验收(决策票 01)与前端接线(票 14–16)。 ### 复验中的一处测量错误(如实登记,供后续复验借鉴) 复验时我一度依据「`alarm_data.id` 全部是同一个全零占位值」提交了 `905f593`,把报警定位从「按主键 id」改为「按 alarm_no」。**该观测是错的**:它是控制台把 32 位十六进制串统一渲染成 `0` 的**显示假象**,连 `md5(id)` 都被同样渲染(PowerShell 输出为 `000...0`)。 用**不可被渲染扭曲**的方式复核后确认 6 行 `id` 是互不相同的真实 UUID:`count(DISTINCT id)=6`、字符直方图显示非零字符数 30/29/32/29/32/30、base64 解出 `fca8fd6b…`/`8b5c81fc…`(与端点回显一致)。实测 `events/ALARM/{真UUID}/work-orders` 回 `READY`+1 张工单,证明**原按 id 解析本就是对的**。 已以 `18db910` 撤回。**复验纪律**:判「两个值是否相同/是否为零」时,禁止凭肉眼读控制台长十六进制串,必须用 `count(DISTINCT)`、直方图或 base64。 ## 票 11 复验结论(2026-10-08,已通过) **三项复验全部符合票 11 的新契约。** 部署依据:`origin/master = 89ca989`(含票 11 的 `3de679c` 与上游 `7df9a87`);运行进程 PID 26956,启动于 18:39:11(重启后加载新字节码)。实测命令与结果见下方「实测记录」。 > **基线卡片 02 的阶梯已由 ② 升到 ③(真实数据联调完成)** —— 该判定针对票 11 切片。④「模式整体收口」仍待真人验收(决策票 01)与前端接线(票 14–16)。 | 复验项 | 判据 | 实测 | 判定 | | --- | --- | --- | --- | | `events/handling-efficiency` | `basis` 改名;`sum(状态0–6)==denominator`;分母≠事件数 | ALARM:`basis=CLOSED_WORK_ORDERS_OVER_ELIGIBLE_WORK_ORDERS`、`num/den=2/3`、`value=0.6667`、`sum(0..6)=3`==分母、`totalEvents=6`≠分母 | **通过** | | `events/history-analysis` | 汇总为新口径;小时桶仍事件桶(预期不变) | 汇总 `num/den=2/3`、`value=0.6667`;行级 `reason=NO_EVENTS`(事件桶) | **通过** | | `overview/warning-settings` | 两 family 共用同一份全范围工单读数 | warning 与 alarm 均 `num/den=2/3`;事件计数各算(10 / 6) | **通过** | **同时确认**:票 11 的口径单源 `CockpitWorkOrderStateDictionary`(`HANDLING_RATE_BASIS` / `CLOSED_RATE_CODE=5` / `isRateEligible`)与三处服务改造(各命中 5 处 `isRateEligible`/`scopeWorkOrders`/`scopeEfficiency`)在部署版本中完整存活 —— 上游 `7df9a87` 未触及口径相关代码。 **数据稀疏事实(如实记录,不当作缺陷)**:`work_order` 表仅 3 行(状态 1×1、5×2),故分母为 3。分母 ≠ 2(早先勘误已说明:那 2 是旧「事件回联」计数,与新口径的「全部工单」基数不同)。样本极小,本票只验证**口径与自洽性**,不宣称统计意义。 ## 部署过程中的一次真实环境故障(已解决,如实登记) 重启窗口内曾出现 **5 个只读端点 500**(`events/*` 与 `overview/warning-settings`),根因是**数据库表结构重构与 Java 层不匹配**: - `app_user.equipment_warning_type` 已重构为**关联表**(`equipment_type_id` + `warning_type_id`),指标元数据移到新的 `app_user.warning_type` 表; - `app_user.alarm_data.warning_type` 列已**删除**(`warning_code` 仍在)。 旧基点 `9380c1b` 的代码仍按重构前的单表形状映射,`BaseMapper` 生成的 `SELECT id,...,warning_type,... FROM equipment_warning_type` 与 `alarm_data` 查询因此报「字段不存在」。 **该重构已由上游 `7df9a87「优化报警管理」` 完整修复**(新增 `EquipmentWarningType` 关联表实体、`AlarmData` 删除 `warning_type` 并新增非持久化 `warningName`、`WarningThresholdServiceImpl` 与 4 个 XML mapper 一并改到新结构)。本会话曾基于旧基点尝试局部修复(分支 `fix/cockpit-warning-type-split`),发现上游已有完整解后**已废弃该分支**(未推送、无损失),跟随 master。 教训(对后续复验有用):**复验前必须先 `git fetch` 确认远端基线**,不要在落后于远端的基点上诊断「环境坏了」。当时把上游的新结构误读为「改了一半的迁移」,方向判断反了。 ## 为什么有这张票 [执行票 11](11-backend-work-order-rate.md) 的实施已交付(代码提交 + 编译 + 契约测试 + 基线卡片),但「新契约在**真实部署环境**生效」需要**重新部署**才能观察。把这件事留在票 11 里,会让「已完成的实现」与「尚未发生的部署」共用一张票,进而阻塞下游 [执行票 12](12-backend-read-model-district.md) / [13](13-backend-realtime-sync.md) 的启动。 本票把该复验**单独承载**:票 11 按其自身交付物收口,本票负责把真实环境证据补齐、把基线阶梯从 ② 推上 ③。**本票不阻塞 12/13** —— 12 只依赖票 11。 ## 前置(人工) 1. **部署新构建**到联调环境 `http://192.168.110.96:8301`。部署需维护者显式确认(本仓库规则:部署不自动执行)。 2. 确认部署所依据的提交包含票 11 的口径修订 `3de679c`(及其后的 `9380c1b`),即远端 `master ≥ 9380c1b`。 ## 复验内容(可复制命令) 按[基线卡片 02 工单状态与处置率读模型](../handoff/baselines/work-order-rate.md) §4.1 复跑: ```powershell $base='http://192.168.110.96:8301/api/cockpit/v1' $a = Invoke-WebRequest "$base/events/handling-efficiency?eventFamily=ALARM&startTime=2026-09-08T00:00:00%2B08:00&endTime=2026-10-08T00:00:00%2B08:00" -UseBasicParsing ($a.Content | ConvertFrom-Json).data.summary ``` **期望(票 11 的新契约)**: | 字段 | 期望值 | | --- | --- | | `handlingEfficiency.basis` | `CLOSED_WORK_ORDERS_OVER_ELIGIBLE_WORK_ORDERS` | | `handlingEfficiency.denominator` | 工单数(状态 0–6),**不再等于 `totalEvents`** | | `handlingEfficiency.state` | `AVAILABLE`(有分母)/`EMPTY` + `reason=NO_ELIGIBLE_WORK_ORDERS`(分母 0) | | `workOrderStateCounts` | `sum(状态 0–6) == denominator`,`stateCounts[7]` 不计入分母 | **部署前实测对照(旧口径,供比对)**:同请求回 `basis=CLOSED_WORK_ORDERS_OVER_TOTAL_EVENTS`、`denominator=6`(=`totalEvents`)、`value=0.1667`;当次 `workOrderStateCounts` 为 `0=1, 5=1`(**事件回联**计数)。 > **勘误(2026-10-08,来自票 11 的更正)**:本行原写「新口径应为 `denominator=2`、`value=0.5000`」,**该预期错误**。那个 2 是从旧响应的 `stateCounts`(`0=1, 5=1`)反推的,但旧 `stateCounts` 只统计**已关联报警**的工单(未关联者 `continue` 跳过)且按**事件**累加(同一工单多事件重复计);新口径分母取自 `scopeWorkOrders`,含范围内**全部**工单(包括未关联报警者),且按**工单**计数。两者基数不同,不可互推。 > > 因此上表「`denominator` = 工单数(状态 0–6),不再等于 `totalEvents`」与「`sum(状态 0–6) == denominator`」是**正确判据**;**不得**把「分母等于某个具体数值」当作通过条件 —— 部署后分母随真实工单数据变化,通常**大于** 2。请以自洽性(`sum(0–6) == denominator` 且分母 ≠ 事件数)判定。 同时复核票 11 另两处同族改动未被部署遗漏: - `GET /events/history-analysis?eventFamily=WARNING&startTime=…&endTime=…` → `summary.handlingEfficiency` 为新 basis/工单分母;小时桶仍为事件桶(**预期不变**)。 - `GET /overview/warning-settings?mode=WARNING` → `handlingEfficiency` 两 family 共用同一份全范围工单读数。 ## 判定与回写 **本票为滚动复验票,判定按切片记录,不因单一切片通过而关闭本票。** - **票 11 切片:全部符合期望**(2026-10-08)→ 已回写:基线卡片 02 §4.3 改写为实测结果、§2 收口条件⑤ 置为完成、§7 阶梯升至 **③ 真实数据联调完成**;票 11 的「部署后复验」缺口标记为已补齐。**本票保持开放**,等待票 12/13 追加。 - 后续切片(12/13)落地并部署后:在本票「复验进度」表追加行、在文末追加各自的实测记录;票 12 的复验项另起基线卡片(按决策票 05 模板)。 - **不符合期望时** → **不改**基线卡片结论、不升阶梯;把实测值与原期望的差异写进本票并回写对应执行票。不在本票内改后端代码:发现问题按正常流程另开修复或重开该执行票。 ## 实测记录(票 11 切片,2026-10-08,可复制命令) ```powershell $base='http://192.168.110.96:8301/api/cockpit/v1' $end=(Get-Date).ToString('yyyy-MM-ddTHH:mm:sszzz'); $start=(Get-Date).AddDays(-31).ToString('yyyy-MM-ddTHH:mm:sszzz') $qs="startTime=$([uri]::EscapeDataString($start))&endTime=$([uri]::EscapeDataString($end))" # ① 效率端点 Invoke-RestMethod "$base/events/handling-efficiency?eventFamily=ALARM&$qs" | % { $_.data.summary } # ② 历史分析(汇总 vs 行级) Invoke-RestMethod "$base/events/history-analysis?eventFamily=WARNING&$qs" | % { $_.data.summary } # ③ 预警设置(两 family) Invoke-RestMethod "$base/overview/warning-settings" | % { $_.data.warning.handlingEfficiency; $_.data.alarm.handlingEfficiency } ``` 结果: | 端点 | 关键实测值 | | --- | --- | | `events/handling-efficiency?eventFamily=ALARM` | `basis=CLOSED_WORK_ORDERS_OVER_ELIGIBLE_WORK_ORDERS`;`num/den=2/3`;`value=0.6667`;`state=AVAILABLE`;`stateCounts: 1=1 5=2`;`sum(0..6)=3`==分母;`totalEvents=6` | | `events/handling-efficiency?eventFamily=WARNING` | 同上(工单范围与 family 无关,符合决策 06) | | `events/history-analysis?eventFamily=WARNING` | 汇总 `num/den=2/3`、`value=0.6667`;行级 `effState=EMPTY`、`reason=NO_EVENTS`(事件桶未变) | | `overview/warning-settings` | warning 与 alarm 均 `num/den=2/3`;`warning.total=10` / `alarm.total=6`(事件各算) | | `events/current`、`events/multidimensional-analysis` | 均 HTTP 200(500 故障已随上游 `7df9a87` 解决) | ## 非目标 - 不实施 [执行票 12](12-backend-read-model-district.md) / [13](13-backend-realtime-sync.md) 的功能(read model、district、实时同步)。 - 不把 mock 走查或本机证据当作真实环境结论。 - 不做真人设备报警闭环验收(按[决策票 01](01-backend-slice-ticket-boundary.md),那是阶段总收口)。 ## 后续扩展 票 12 / 13 落地并部署后,各自的新契约复验**追加到本票**(同一部署窗口、同一环境),不另开复验票,避免票据碎片化。票 12 的复验项按 [决策票 05](05-integration-baseline-and-evidence.md) 的基线卡片模板另起卡片。 ## 关联 - 前置:[11 后端执行票 —— 工单状态与处置率读模型对齐](11-backend-work-order-rate.md) - 基线:[基线卡片 02 工单状态与处置率读模型](../handoff/baselines/work-order-rate.md) - 决策:[05 真实联调基线与收口证据门槛](05-integration-baseline-and-evidence.md) - 上位:本地票 25、Gogs #63