# 12: 后端执行票 —— 报警/工单读模型、设备–监测点回联 + district 片区契约 Type: task Status: resolved Label: ready-for-agent Blocked by: 11 Category: backend-execution ## 来源 - 决策:[01 报警—工单闭环读模型与 Gogs 票拆分边界](01-backend-slice-ticket-boundary.md)(后端切片 2)、[06 异常统计、工单处置率与管理字段词汇](06-cross-mode-business-semantics.md) - 规格:[10 业务语义规格](10-business-semantics-spec.md) ## 状态 **已完成并推送**(2026-10-08/09)。诊断 + 六项裁定 + 范围 1–4 实施 + 提交推送 + **真实环境复验通过**。提交:`ee2e1b6`(主实施)+ `905f593`(被撤回)+ `18db910`(撤回)+ 合并 `818a6d7` → 已推送 `origin/master`(`89ca989..818a6d7`)。验证:组合测试 **372/372 通过**。基线卡片:[报警/工单读模型 + district 片区契约](../handoff/baselines/work-order-read-model-district.md)。 **本票按其自身交付物 close**(原因、证据、下一步均已入正文与基线卡片);真实环境复验记录追加到 [票 17](17-postdeploy-live-verification.md)(滚动复验票)。 ### 真实环境复验结果(2026-10-08/09,实例 PID 15244 已加载新构建) | 复验项 | 实测 | 判定 | | --- | --- | --- | | 六族端点健康 | 7/7 全部 HTTP 200(此前 5 端点 500 的故障已由上游修复) | **通过** | | `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=true`;报警维度按真实 UUID 回 `READY`+1 张 | **通过** | ### 一处测量错误与撤回(如实登记) 复验中我一度依据「`alarm_data.id` 全部是同一个全零占位值」提交了 `905f593`(改为按 `alarm_no` 解析)。**该观测是错的** —— 它是控制台把 32 位十六进制串统一渲染成 `0` 的**显示假象**,`md5(id)` 也被同样渲染。 用字符直方图与 base64 复核后确认:6 行 `id` 是**互不相同的真实 UUID**(非零字符数 30/29/32/29/32/30,`count(DISTINCT id)=6`;base64 解出 `fca8fd6b…`、`8b5c81fc…`,与端点回显一致)。实测 `events/ALARM/{真UUID}/work-orders` 回 `READY`+1 张工单,证明**原按 id 解析本就是对的**。 故以 `18db910` 撤回该改动:`findAlarmByReference` 恢复先按主键 `getById`、查不到时才回退 `alarm_no`;`alarmReference` 恢复优先回 `id`。保留 `905f593` 中仍正确的部分(`lambdaQuery()` 链改 `list(wrapper)` 便于测试 + 新增适配器测试)。 **教训**:控制台对长十六进制串的渲染不可信;判定「数据是否相同/是否为零」必须用 `count(DISTINCT)`、字符直方图、base64 等**不可被渲染扭曲**的方式,不能凭肉眼读控制台。 ### 全量测试中的一个既有失败(非本票引入) `zksy-system` 全量跑有 **5 个失败**,全部在报警/供水域、**与本票无关**: `WaterSupplyDeviceServiceImplTest`(2)、`AlarmClearServiceTest`(1)、`AlarmReviewAttachmentServiceTest`(2)。 根因是上游 `7df9a87` 把 `setAbnormalReason(alarm.getWarningType())` 硬编码为 `"设备报警"`(删除了 `warningType` 用法),但测试仍断言旧值 `"供水压力超限"`。已核实在基点 `89ca989` 上实现与测试断言**本就冲突**,且本票 **0 处**改动这些文件。该失败需另开票修(不在本票范围)。 ## 实施结果(2026-10-08) 按维护者六项裁定实施,共 **48 个文件改动 + 1 个新增**(`cockpit/foundation/CockpitDistrict.java` 片区口径单源)。六个响应族统一改造: | 族 | 改动 | | --- | --- | | 地图摘要 | `map-snapshot?district=0\|1\|2\|UNASSIGNED`;点位/设备 `district`+`districtName`;`mapFilter` 加 `districtFilter` | | 运行监测 | `monitoring/distribution`、`monitoring/situation` 的 `regionDistribution`→`districtDistribution`,marker 双字段 | | 基础设施 | `base-archives` 同上;**管理信息删字段** `organizations`/`ownershipDistribution`/`unmaintainedCount`/`organizationCount`,改 `districtIndustry[]`(片区×行业) | | 事件多维分析 | 维度 `REGION/区域`→`DISTRICT/片区`,桶码 `0/1/2/UNASSIGNED` | | 工单读模型 | 每张工单新增显式 `associatedWithCurrentEvent` + 顶层 `unassociatedWorkOrderCount` | **点位与设施无片区来源**(`monitor_point` 无列、`PipeNetworkBase` 无字段、`equipment_point_rel` 实测 0 行)→ 点位按「经其上挂设备归纳」、设施一律**未分区**,如实呈现,未加数据库列、未用行业/管理单位冒充。 ## 维护者裁定(记录) 1. district 来源=设备级权威 + 点位经其上挂设备归纳;不改 DDL。 2. 形态=编码 + 中文名双字段;入参 `0/1/2/UNASSIGNED`,脏值 400。 3. `district` 取代 `region`,`region` 入参不再接受。 4. 管理信息字段**本票直接删**。 5. 多工单:保留全部明细,**不**声明规则缺失。 6. 未关联标注:关联键以数据库事实为准 —— `alarm_data.work_order_id → work_order.order_id`(维护者原述 `alarm_no = order_no` 经实测命中 0 条且两列均不存在,已裁定采用 `work_order_id`)。 ## 诊断(2026-10-08) ### 后端基线(实施前) | 项 | 值 | | --- | --- | | 仓库 / 分支 / 提交 | `http://192.168.110.30:3000/shaoy/pipe-ner-server` / `master` / `89ca989` | | 远端 | `origin/master = 89ca989`(与本地一致,工作树干净) | | 联调环境 | `http://192.168.110.96:8301` 实测可达,只读接口免鉴权 HTTP 200 | ### 范围 1 —— 报警/工单读模型与设备–监测点回联:**已存在实现,但有缺口** 已有端点与实现(`cockpit/workorder/`): | 端点 | 实测 | | --- | --- | | `GET /equipments/{deviceCode}/work-orders` | 真实设备 `21119740` 回 2 张工单(状态 1 待接单、5 已结案),`state=READY` | | `GET /events/{source}/{code}/work-orders` | 可用;`ALARM` 走 `alarm_data.work_order_id`,`WARNING` 走 `alarm_warning_rel → alarm_data` | | `GET /work-orders/{orderNo}/timeline` | 可用,状态码经 `CockpitWorkOrderStateDictionary` 0–7 解析 | 缺口(对照本票「范围 1」逐句): 1. **「未关联当前报警的工单明确提示」未满足**:`CockpitWorkOrderSummary.eventAssociation` 在无关联时为 `null`(**隐式**)。实测两张工单均 `eventAssociation=null`,但响应没有任何**显式**字段说明「该工单与当前报警无关」,前端只能靠 null 猜。 2. **设备–监测点回联在真实库上不可证明**:`app_user.equipment_point_rel` 实测 **0 行**;`monitor_point`(6 行)**没有 district 列**。故「按后端现有关系输出可证明的逐条关系」在当前数据下只能如实输出「无关系 + 原因」,不能编造。 3. **多工单「当前/有效/最新」规则**:后端既有实现是**全部返回 + 按 create_time 倒序**,未定义「当前工单」规则。按决策 01 应「保留明细并显示规则缺失,不由前端猜选」——当前响应**未**声明这一点。 ### 范围 2 —— district 片区契约:**完全未接入(cockpit 代码零命中 `district`)** | 事实 | 证据 | | --- | --- | | 权威来源已存在 | `EquipmentBase.district`(Integer,注释「0-城南 1-城北 2-太常,见 DistrictEnum」);`DistrictEnum`(`base/enums/`)含 `SOUTH_CITY(0,"城南")/NORTH_CITY(1,"城北")/TAI_CHANG(2,"太常")` 与 `getByCode/getDescByCode/isValid` | | 真实数据齐备 | `equipment_base` 7 行,**district 全部非空**:`0→4、1→2、2→1` | | cockpit 代码**零**使用 district | 对 `cockpit/**` 全量 grep `district` **0 命中**;现有实现只用旧自由串 `region` | | region 实际恒为 null | `BusinessCockpitMapSnapshotAdapter` 第 234/251 行**硬编码传 `null`**,故服务层 `regionOf()` 一律归一为 `未分区`。实测地图首 3 个点位 `region=未分区` | | 设施与点位**没有** district | `PipeNetworkBase`(管网设施)无 district 字段;`monitor_point` 无 district 列。**只有设备(`EquipmentBase`)有** | | `district` 入参未被接受 | 实测 `map-snapshot?district=0` 被**忽略**(`mapFilter` 无该字段,入参不在控制器签名里);`region=城南` 被接受(自由文本、无校验、无白名单) | 涉及 `region`/`regionDistribution` 的响应族实测(4 族): | 端点 | region 类字段 | 现状 | | --- | --- | --- | | `/map-snapshot` | `mapFilter.region`、点位/设备 `region` | 入参可为自由串;值恒 `未分区` | | `/monitoring/distribution` | `regionDistribution[]`、marker `region` | 有;适配器注释明写「无可靠来源 → null → 未分区」 | | `/monitoring/situation` | 对象内 `region` | 有(同 null 归一) | | `/infrastructure/base-archives` | `regionDistribution[]`、`selectedFilters.region`、`mapFilter.region` | 有(设施无 district 来源) | | `/events/multidimensional-analysis` | `dimensions` 内 `REGION` 桶 | 有(`UNASSIGNED_REGION`/`未分区` 双常量) | ### 范围 3 —— 管理信息响应字段删除:**未删除,且前端仍在消费** `CockpitInfrastructureManagementInformationResponse` 仍含 `organizations`、`ownershipDistribution`、`Totals.unmaintainedCount`、`Organization.unmaintained/ownershipDistribution`;实测 `management-information` 顶层字段含 `organizations`、`ownershipDistribution`。 **耦合风险(实施时序需要裁定)**:前端 `src/modules/cockpit/domain/infrastructurePanels.js` 第 213–232 行**正在读** `organizations`、`ownershipDistribution`、`totals.unmaintainedCount`,并用于面板渲染(第 433、441–446 行)。按决策 06「管理信息面板改片区×行业视图、后端删这三个字段」,**后端先删会让前端面板读到空**,需与前端票 15(管理面板片区化)对齐落地时序。 ### 判定 **本票不能直接按字面实施**:范围 2 的「统一按 district 过滤与回显」在**点位与设施缺 district 来源**的现实下无法字面达成(只有设备有);范围 3 的删除与前端存在时序耦合;范围 1 有 3 处语义缺口。故先请维护者裁定,不猜。 ## 维护者裁定(2026-10-08) 1. **district 来源**:设备级权威(`equipment_base.district`)+ **点位经其上挂设备归纳**;多设备不一致/无设备 → `未分区` 并计数。**不改 DDL、不给点位/设施加列**(另开迁移票)。 2. **形态**:**编码 + 中文名双字段** —— 回 `district`(Integer,null=未分区)+ `districtName`(未分区时 `"未分区"`);筛选入参 `district=0|1|2`,不传=全部,`UNASSIGNED`=只看未分区。符合决策 06「统计保留数字、展示层映射中文」。 3. **region 去留**:**district 取代 region**,`region` 入参不再接受;响应中 `region`/`regionDistribution` 改为 `district`/`districtDistribution`。 4. **管理信息删字段**:**本票直接删** `organizations` / `ownershipDistribution` / `unmaintained`(含 `Totals.unmaintainedCount`);需回写前端票 15 同步改面板。 5. **多工单**:**保留全部明细即可,不需要声明规则缺失**(维护者明确不要「规则缺失」声明)。 6. **未关联标注**:**关联键以数据库事实为准** —— `alarm_data.work_order_id → work_order.order_id`。维护者原述 `alarm_data.alarm_no = work_order.order_no` 经实测**命中 0 条**(`work_order` 无 `alarm_no` 列、`alarm_data` 无 `order_no` 列,`order_no` 为建单时独立生成),维护者已裁定采用 `work_order_id`。据此实现每张工单的显式关联标记。 ## 待裁定问题(已全部裁定,见上) 1. **district 的来源边界**:只有 `equipment_base` 有 district;`monitor_point` 无该列、`PipeNetworkBase` 无该字段、`equipment_point_rel` 0 行。点位与设施的片区如何取? 2. **district 的参数与回显形态**:入参/回显用编码(`0/1/2` + 「未分区」哨兵)还是中文(城南/城北/太常)? 3. **旧 `region` 入参的去留**:直接以 `district` 取代(`region` 不再接受),还是并存过渡? 4. **管理信息字段删除的时序**:后端立即删(前端票 15 落地前面板会空),还是等 15 一并落地? 5. **`work_order` 多张工单的「当前工单」规则**:后端无此规则,是否按决策 01「保留明细并声明规则缺失」实现(不引入新规则)? 6. **设备–监测点回联在 0 关系数据下的输出**:如实回「无关系 + 原因」,还是本票只交付契约与测试、真实关系待数据补齐? ## 范围(做什么) 1. **报警/工单读模型与设备–监测点回联**:按后端现有报警、工单、设备和监测点关系输出可证明的逐条关系;设备详情展示该设备已有全部工单(未关联当前报警的工单明确提示);报警详情只展示与该报警实际关联的工单;多工单遵循后端既有规则或如实说明规则缺失。 2. **district 片区契约**: - 相关响应族(地图摘要、运行监测、基础设施、事件/预警分析)统一按 `district`(`0/1/2 → 城南/城北/太常`)过滤与回显;空值/乱码/未识别 → `未分区`。 - 旧自由串 `region` 不再作为正式范围契约(`regionMap` 过渡失效)。 - 无独立「责任区域」字段;不用管理单位、权属单位、行业或其它字段冒充(责任区域=片区别名)。 3. **管理信息响应字段删除**:删除 `organizations / ownershipDistribution / unmaintained`(按 #06 决策)。 4. **契约测试与文档**:回联逐条关系、district 枚举与未分区归一、删除字段后的响应形状。 ## 非目标 - 不修改工单操作逻辑;不新增工单写接口。 - 不实现实时推送帧(归执行票 13)。 ## 验收 代码提交 + 编译 + 针对性契约测试 + 联调基线登记;本地联调通过后提交推送 Gogs。 | 项 | 状态 | 证据 | | --- | --- | --- | | 编译 | **通过** | `mvn -o -pl pipe-network-service/zksy-admin -am compile` | | 组合契约测试 | **通过** | `-Dtest=Cockpit*Test,Business*AdapterTest` → **372/372** | | 联调基线登记 | **已产出** | [基线卡片 03](../handoff/baselines/work-order-read-model-district.md) | | 代码提交 + 推送 | **已完成** | `818a6d7` → `origin/master`(`89ca989..818a6d7`) | | 真实环境复验 | **通过** | 见「状态」节复验表(8 项全通过) | ## 下一步(本票已完成;以下为遗留项与下游) 1. ~~六项裁定 / 实施 / 契约测试 / 基线卡片 / 提交推送 / 真实环境复验~~ —— **全部完成**。 2. **前端消费侧同步**(**未完成,阻塞前端票**):已通知 [执行票 15](15-frontend-time-dimension-panel.md) —— `regionMap` 配置映射失效、管理信息三字段消失、`NO_ORGANIZATIONS` 改名 `NO_RELATIONS`。 3. **两个 demo html** 仍是 `region:'城区A'` fixtures(离线样例,不阻塞契约,低优先)。 4. **下游 [执行票 13](13-backend-realtime-sync.md)**:本票已 close,其阻塞解除,可开工。 5. ~~既有测试失败需另开票~~ —— **已直接修复**(2026-10-09,维护者裁定:过期断言改当前契约、无法表达现行契约的测试类删除)。提交 `4ef2159` 已推 `origin/master`;全量 8 模块 **414/414** 通过。明细:`WaterSupplyDeviceServiceImplTest` 两处断言更新为「设备报警」、`AlarmClearServiceTest` 摘要断言更新为「监测报警」、`AlarmReviewAttachmentServiceTest` 整类删除(其断言的内容嗅探/伪造扩展名拦截为生产从未实现的策略,且测试环境必然 NPE)。 6. 票 13 落地部署后,其复验项追加到 [票 17](17-postdeploy-live-verification.md) 的复验进度表。 ## 关联 - 上位:本地票 25、Gogs #63 - 下游:执行票 13(实时同步)依赖本票 - 复验承载:[17 部署后真实环境复验(后端切片 11–13)](17-postdeploy-live-verification.md) - 基线:[基线卡片 03 报警/工单读模型 + district 片区契约](../handoff/baselines/work-order-read-model-district.md)