# 后端执行票 —— 预警效率端点缺预警自身处置流程字段(两族回同一套工单口径) Status: resolved Assignees: codex Type: task Blocked by: None Labels: wayfinder:task, ready-for-agent Category: backend-contract ## 来源 [票 65](65-untitled.md)(前端执行票 —— 预警效率面板按预警/报警分流)收口时记录的**契约缺口**。 票 65 的验收第 5 条明写:若需要后端新增预警流程字段,**在票面记录契约缺口并另开后端票,本票不假装已有数据**。 本票就是那张后端票。 ## 缺口(2026-10-21 实测,`192.168.110.96:8301` 只读) `GET /api/cockpit/v1/events/handling-efficiency?eventFamily=WARNING` 与 `=ALARM` 的响应结构**完全相同**, 两族都只给工单口径: | 字段 | 语义 | 预警族实测 | | --- | --- | --- | | `summary.totalEvents` | 窗口内事件条数 | 5 | | `summary.linkedWorkOrderCount` | 回联到工单的事件条数 | **0** | | `summary.missingWorkOrderCount` | 未回联工单的事件条数 | **5**(=全部) | | `summary.handledCount` / `inProgressCount` / `cancelledCount` | **工单**状态派生(不是预警生命周期) | 0 / 1 / 0 | | `summary.handlingEfficiency` | 工单口径处置率(`CLOSED_WORK_ORDERS_OVER_ELIGIBLE_WORK_ORDERS`) | num=0 den=1(分母来自 1 张**与预警无关**的待派单工单) | | `summary.averageDuration` | 关联工单结案时长 | EMPTY / `NO_CLOSED_WORK_ORDERS` | | `workOrderStateCounts` | 窗口内**全部工单**八态(`scopeWorkOrders`,不要求关联事件) | 只有 1 张待派单 | **代码证据**(`CockpitHandlingEfficiencyService.build()`,t30 validation-checkout 同版): - `stateCounts` 由 `scopeWorkOrders(...)` 累加 —— 范围内**全部工单**,注释原文「不要求关联报警/事件」; - 于是 `handledCount` / `inProgressCount` / `cancelledCount` / `handlingEfficiency` / `workOrderStateCounts` 全都是**工单**读数,两族共用同一份(`stateCounts` 只算一次,family 只影响 `facts` 也就是事件集合); - 预警自身的生命周期 `WarningWorkflow`(草稿/待接收/已接收/处置中/已处置/已解除)**不在此端点**, 只存在于另一个端点 `/overview/warning-settings`,且那里的分布是**全量快照、不随 `range` 收缩**, 该端点还不接受 `LAST_90_DAYS`(400)。 ## 为什么必须补 预警**不挂工单**(实测 `linkedWorkOrderCount=0`、`missingWorkOrderCount=总数`)。前端若照字面消费 `summary` 三字段,就会画出「预警处置中 = 1」—— 那个 1 来自一张与预警毫无关系的工单, 正是票 65 票面禁止的「用一个不存在的对象统计预警」。票 65 因此只在预警模式下画 可证实的读数(条数/等级/发生趋势/工单关联实况),前端现在是**如实的空缺口**,不是已解决的问题。 ## 建议契约(形状供后端评审,不以本票为准) `GET /events/handling-efficiency?eventFamily=WARNING` 的 `summary` 增预警自身流程段, 与 `overview/warning-settings` 的预警族语义**同源**(同 `WarningWorkflow` 六态字典,不另造一套): - `warningStateCounts: [{ stateCode, stateName, count }]` —— 窗口内预警的六态全铺(含 0 桶,脏值进 UNKNOWN); - `warningHandlingEfficiency: { numerator, denominator, value, state, reason, basis }` —— 已解除 ÷ 窗口内预警总数(分母 0 → `state=EMPTY` + `reason=NO_WARNINGS`,与票 42 同一纪律); - `warningDuration: { value, unit, sampleCount, state, reason }` —— 预警从发布到已解除的平均时长 (无样本 → `state=EMPTY` + 明确 `reason`,不得回 0 时冒充真实读数); - `warningStatusTrend: [{ timeBucket, total, closed }]` —— 窗口内按桶的发布量/已解除量。 **两族语义分流要求**:`eventFamily=WARNING` 时 `handlingEfficiency`/`workOrderStateCounts`/ `averageDuration` 三个工单口径字段应**显式为 `null`**(而不是照旧回工单读数)—— 与票 41 在 `overview/warning-settings` 已做的裁定完全同一条纪律(预警族工单口径 null 保真, 不用 0 或别的族的数冒充)。报警族保持现状(工单是报警的处置手段,票 41/42/52 既有裁定)。 ## 验收 1. 契约测试覆盖两族:预警族工单口径三字段为 `null`、预警流程段齐备且 `sum(六态)==窗口内预警数`; 报警族工单口径原样保留。 2. 真实环境实测 `eventFamily=WARNING`:`linkedWorkOrderCount=0` 时预警流程段仍给出真实六态分布 (不是全 0 假读数)。 3. 分母 0 / 无样本的回 `state=EMPTY` + 原因码,不回 `"0.0000"` 当真实值。 4. 全量 `Cockpit*Test` 通过;改动推 `origin/master` 并在票面记录提交哈希。 5. 前端消费侧另立前端票(票 65 的预警分支已预留口径:只要新字段到位,替换 `warningEfficiencyKpis` 的 三个计数即可,块结构不用重排)。 ## 关联 - 上游:[票 65 —— 预警效率面板按预警/报警分流](65-untitled.md)(前端已按「诚实分流 + 记缺口」收口)。 - 同族既有裁定:[票 41](41-warning-settings-endpoint-rework.md)(预警族改预警自身生命周期、工单口径 null 保真)、 [票 42](42-42.md)(前端消费两族分叉)、[票 52](52-untitled.md)(报警的单设备工单优先级)、 [票 11](11-backend-work-order-rate.md)(处置率=状态 5 ÷ 状态 0–6 工单数)。 - 参考实现:`CockpitOverviewWarningSettingsService.familyMetrics()` 已有预警族六态/已解除率的 **现成聚合逻辑**,本票是把它搬进带窗口的 efficiency 端点并分族,不是从零写。 - 后端仓库/负责人/权限按本图 Notes:需在仓库、负责人和权限明确后由维护者启动。 ## 进度 - 2026-10-21 由票 65 收口时另立(前端侧已完成诚实分流并记录缺口;本票未动工)。 - 2026-10-23 开工(本会话)。认领;基点=后端 `origin/master` `f36f10dd`(本地 master 与远端一致)。 按本图既有成例(票 29/30)在工作区内建隔离副本 `.scratch/cockpit-real-data-wayfinding/implementation/t74/validation-checkout/`(含 `.git`,`reset --hard origin/master` 得干净树;宿主沙箱不允许写工作区外路径,故不用 `git worktree add E:\...`,共享工作树的并行在制品也未触碰)。 ### 方案(落地口径) **读路径**:`BusinessCockpitEventAdapter.loadHandlingEfficiency()` 的 `SourceWarning` 原先只带编号/等级/发布时间/类型/设备, 预警自身流程在快照里**整条缺失**。新增 `status`(`WarningWorkflow` 六态)与 `updateTime`(解除时刻来源)两个字段, 旧 6 参构造器保留为委托重载(票 41 成例),由适配器从 `EarlyWarning.getStatus()` / `getUpdateTime()` 填。 **响应形状**(按票面建议契约,落点对称于既有工单口径字段): | 位置 | 报警族 | 预警族 | | --- | --- | --- | | `workOrderStateCounts`(8 桶) | 原样 | **`null`** | | `warningStateCounts`(新增,六态) | **`null`** | 六态全铺(缺桶计 0),脏状态补 `UNKNOWN` 桶 | | `summary.handledCount/inProgressCount/cancelledCount` | 原样 | **`null`** | | `summary.handlingEfficiency` | 原样(工单口径) | **`null`** | | `summary.averageDuration` | 原样(工单结案时长) | **`null`** | | `summary.warningHandlingEfficiency`(新增) | **`null`** | 已解除 ÷ 窗口内预警数;分母 0 → `EMPTY`+`NO_WARNINGS` | | `summary.warningDuration`(新增) | **`null`** | 发布→已解除平均时长;无样本 → `EMPTY`+`NO_CLOSED_WARNINGS` | | `durationTrend[]` | 原样 | `eventCount` 保留(真实发布量),`handledCount`/`averageDuration` **`null`**(工单口径,预警族不适用) | 三条与票面建议的**偏离**(都写理由,可被一句话推翻): 1. **票面只点 3 个工单口径字段 null,本票扩到 6 个**:`handledCount/inProgressCount/cancelledCount` 与 `durationTrend` 的 `handledCount/averageDuration` 同样是 `scopeWorkOrders` 派生的工单读数(票面自己的表格已写明「工单状态派生」)—— 留着它们就是同一个谎言换个字段名(「预警处置中 = 1」正是 `inProgressCount` 产生的)。 2. **不做 `warningStatusTrend`**:它的「按桶发布量」与 `durationTrend[].eventCount` 是同一份的数, 同一响应里放两条序列就是第二份口径;已解除量可由 `warningHandlingEfficiency` 与逐条状态得出。 若将来确实要按桶的已解除趋势,应**替换**预警族的 `durationTrend`(改名换语义),而不是并列再加一条。 3. **已解除时刻取 `early_warning.update_time`**:该表没有单独的解除时间列;`clearWarning()` 在置 `CLOSED` 的同一次写入里 显式 `setUpdateTime(now)` 且 `CLOSED` 是终态无后续业务动作,故 CLOSED 行的 `update_time` 即解除时刻。 这是**有前提的读数**,`warningDuration` 的 `basis` 与源码注释都写明来源,不假装是独立采集的时间戳。 4. 处置率分母=**窗口内预警条数**(与 `summary.totalEvents` 同一集合、随 industry/level 收敛),不是全库快照; 与 `overview/warning-settings`(全量快照、不随窗口收缩)**刻意不同**,因为本端点本就是带窗口的。 ### 实施(2026-10-26 完成) **改动(5 文件,+498/−137)**: | 文件 | 改动 | | --- | --- | | `CockpitHandlingEfficiencySnapshot` | `SourceWarning` 增 `status`/`updateTime`,旧 6 参构造器改为委托重载 | | `BusinessCockpitEventAdapter` | `efficiencyWarnings()` 把 `getStatus()`/`getUpdateTime()` 填进快照 | | `CockpitHandlingEfficiencyResponse` | 顶层增 `warningStateCounts`(`WarningStateCount`)、`Summary` 工单口径三计数改 `Integer` 可空并增 `warningHandlingEfficiency`/`warningDuration`、`DurationPoint.handledCount` 改可空 | | `CockpitHandlingEfficiencyService` | `build()` 按族分支聚合;新增 `warningEfficiency`/`warningStateRows`/`warningAverageDuration`/`warningDurationHours`/`normalizeWarningStatus`;`durationTrend` 预警族只给 `eventCount` | | `CockpitHandlingEfficiencyControllerTest` | 契约测试重写:预警组(六态/已解除率/时长/null 纪律)+ 脏状态 UNKNOWN 桶 + 空窗口 EMPTY;原工单口径用例整体迁到 ALARM 族,样本改用报警事件,覆盖不丢 | **验证**: - 编译:`mvn -o -pl pipe-network-service/zksy-system -am compile` 通过。 - 契约测试:`-Dtest=Cockpit*Test,BusinessCockpit*Test` → **540/540**、`BUILD SUCCESS`。 (本机沙箱禁止 surefire fork 子进程,故加 `-DforkCount=0` 在同一 JVM 跑;该模式进程 CWD 是 reactor 根, 导致 `CockpitAnonymousAccessSecurityConfigTest` 的 `..\zksy-framework\...` 相对路径找不到文件, 因此按原路径在 `implementation/t74/zksy-framework/` 放了一份同字节副本作为执行环境垫片——只影响本机跑法,不入库 (收尾已删除;复跑时按 `mkdir -p implementation/t74/zksy-framework/src/main/java/com/zksy/framework/config && cp .../SecurityConfig.java` 重建即可)。 正式 CI 的 forking 跑法不受影响,该测试源码零改动。) - **真实环境复验(第二实例,真实库)**:在本机 8399 起第二实例(拉 nacos 配置落成本地副本以覆盖端口; `-Dfile.encoding=UTF-8` 修 nacos YAML 解码、`-Dcsp.sentinel.log.dir` 指向工作区内以避开沙箱写限制), 连同一联调库(`192.168.110.30:54321/pipi_ner`)实测窗口 `2026-09-23T00:00+08:00 ~ 2026-10-23T00:00+08:00`: | 读数 | 实测值 | 说明 | | --- | --- | --- | | `summary.totalEvents` | 7 | 与改动前构建同值(基线对照见下) | | `summary.linkedWorkOrderCount` | **0** | 预警不挂工单,与本票立票依据一致 | | `warningStateCounts` | PENDING 3 / PROCESSING 1 / CLOSED 3,其余桶 0 | **真实六态分布,和=7=totalEvents** | | `summary.warningHandlingEfficiency` | `3/7 = 0.4286`,`state=AVAILABLE`,`basis=CLOSED_WARNINGS_OVER_WINDOW_WARNINGS` | 预警自身口径 | | `summary.warningDuration` | `0.64` 小时,`sampleCount=3` | 三条已解除的发布→解除时长 | | 工单口径 6 字段 | 全 `null` | `handledCount/inProgressCount/cancelledCount/handlingEfficiency/averageDuration/workOrderStateCounts` | | 报警族同窗口 | `handledCount=4 / inProgressCount=11 / cancelledCount=1`、处置率 `4/15=0.2667`、八态齐、`warning*` 全 `null` | 报警族原样保留 | **基线对照(排除「改动动了事件口径」的怀疑)**:把改动摘掉、用同一份 `origin/master` `f36f10dd` 重新构建并起在 8398, 同窗口预警族 `totalEvents` 同为 **7**、工单读数同为 4/11/1(0.2667),只是没有 `warning*` 段 —— 证明本票是**纯新增 + null 保真**,事件计数与工单口径零漂移。部署中的 `8301` 同窗口回的是 **14**: 那是**更旧的部署构建**(上游 4 个提交之前,设备/行业收敛范围更宽),差额来自部署落后于 `origin/master`,与本票无关。 证据落盘(本图 `verify/artifacts/` 成例):[warning-family-live.json](../verify/artifacts/t74/warning-family-live.json)、 [alarm-family-live.json](../verify/artifacts/t74/alarm-family-live.json)、[baseline-warning-family-live.json](../verify/artifacts/t74/baseline-warning-family-live.json)。 **入库**:rebase 到 `origin/master` `05e9e93d`(上游 4 个提交均未触碰本票文件,无冲突;警告:本机 git 的 nacos/bootstrap 配置差异不影响)后复跑 **540/540**,提交 **`983fc27a`** 已推 `origin/master`(`05e9e93d..983fc27a`), `git ls-remote origin refs/heads/master` 实测=`983fc27a`。 **边界 / 下一步**: - **改动未部署**:`192.168.110.96:8301` 仍是旧构建,新字段上线后才可见。 - 真实库里有 16 条 `CLOSED` 预警的 `publish_time` 为 `null`(历史数据),窗口内只 6 条可算、其中 3 条落在行业范围内, 故 `warningDuration.sampleCount=3`;这是**数据事实**,不是聚合丢样本。 - 前端消费另立[票 78 —— 预警效率面板消费预警自身处置流程](78-kpi.md):只替换 `warningEfficiencyKpis` 的三个计数、补一块六态分布,块结构不动。 ## Comments ## Answer **已实施、测试、真实环境复验并推送关闭(2026-10-26)**。提交 **`983fc27a`** 已推 `origin/master`(`05e9e93d..983fc27a`)。 **做了什么**:`events/handling-efficiency` 按事件族分流。预警族改吃**预警自身处置流程** (`warningStateCounts` 六态全铺、`summary.warningHandlingEfficiency` 已解除÷窗口内预警数、 `summary.warningDuration` 发布→已解除平均时长),工单口径六字段(含 `durationTrend` 的 `handledCount`/`averageDuration`)在预警族**显式为 `null`**;报警族工单口径原样保留,其 `warning*` 段为 `null`。 读路径补 `SourceWarning.status/updateTime`(旧 6 参构造器保留为委托重载)。 **真实数据实测**(本机第二实例连同一联调库,窗口 2026-09-23~2026-10-23):`linkedWorkOrderCount=0`、`totalEvents=7`、 六态 `PENDING 3 / PROCESSING 1 / CLOSED 3`(和=7)、已解除率 `3/7=0.4286`、时长 `0.64h`(3 样本); 报警族同窗口 `4/11/1`、处置率 `4/15=0.2667`、八态齐。同窗口**未改动基线构建**预警族 `totalEvents` 同为 7、 工单读数同为 4/11/1 → 本票是纯新增 + null 保真,事件与工单口径零漂移(部署中的 8301 回 14 属更旧构建,与改动无关)。 **验证**:`Cockpit*Test,BusinessCockpit*Test` **540/540**(本机 `-DforkCount=0` 执行;进程 CWD 垫片只为本机跑法,不入库)。 **边界**:改动**未部署**;真实库 16 条 `CLOSED` 预警缺 `publish_time`,窗口内仅 6 条可算、其中 3 条在行业范围内, 故 `warningDuration.sampleCount=3`(数据事实,非丢样本)。前端消费见[票 78](78-kpi.md)。