# T28 计数契约边界研究 本笔记只依据本地票面、领域文档、后端源码和已有测试进行静态研究。未修改后端或前端源码,未构建、运行测试、调试、访问 HTTP 或浏览器,未处理或关闭其它票。下文将“现状事实”“票面要求”“建议契约”分开,建议不是现有接口已经承诺的行为。 ## 1. 本票的决定与实现边界 - **已决口径**:无关联设备监测点是合法对象,按 `EMPTY` 计入监测点总数与状态分布;类型/行业不可推导应保留未知,片区不可推导应为未分区。坐标未校验只决定能否上图,不决定统计资格。依据:[执行票:18–24、68–73]()、[领域文档:35–41]()、[领域文档:91–93]()。 - **本次实现应集中在 monitoring/distribution 与 monitoring/overview 的读模型**,保留真实坐标及坐标校验结论。不要为了可统计而把 `coordinateVerified` 改成 true,也不要从 `GS/PS/RQ` 编码、名称或行业猜片区。票面明确坐标来源另属票 24;真实适配器也明确无可信校核字段。依据:[执行票:68–81]()、[分布适配器:37–40]()。 - **“6 个”是票中现网验收基线,不是源码静态读取可以重新确认的实时事实**;本次只确认源码足以解释其消失路径。依据:[执行票:28–40、77]()。 ## 2. 无设备点位怎样表达未知行业/类型 ### 现状与可复用的形状 1. 真实 distribution 适配器遍历全部合法编码点位,关联设备为空仍创建点位;点位 `typeCode` 只在关联设备类型唯一时赋值;无设备时 `district=null`、`typeCode=null`、`devices=[]`,`coordinateVerified=false`。这层没有丢点。依据:[分布适配器:82–115]();已有测试直接断言无设备点 `typeCode/district` 为 null:[适配器测试:78–85]()。 2. `SourceMonitoringPoint` 已含设备事实与可空类型、可空片区,不必为 distribution 改快照构造器。响应的行业是普通 String,`DeviceType` 的 code/name 也不限制非空,因此可保持 `industry=null`、`deviceType={code:null,name:null}`,片区由现有构造器输出 `district=null,districtName="未分区"`。依据:[分布快照:99–125]()、[分布响应:210–220]()、[分布响应:299–338]()。 3. `EMPTY` 应继续由公共状态解析器基于真实设备列表产生,空列表返回 `EMPTY`;无需新造点位状态或借用 freshness 的 `EMPTY`。依据:[状态解析器:12–16]()。 4. 地图服务已经示范“未分类也保留对象”:行业不可解析为 null;无行业/类型筛选时保留,指定正式行业或类型时未知对象不匹配;汇总时仍附坐标校验结果。它是本票可参考的一手先例。依据:[地图服务:198–228]()、[地图服务:248–277]()。 ### 建议契约 - **最小路径**:先构造有效关联设备列表,再允许“真实无设备且类型不可解析”的点位以 null 行业/类型进入统计。其它类型错误仍按既有规则处理,不要只用 `typeCode==null` 判断无设备:有设备但混合类型、字典缺失也会使 typeCode 为 null。依据:[分布适配器:90–111]()、[分布服务:257–270]()。 - 未筛行业/类型时保留未知点;指定 `GAS/WATER_SUPPLY/DRAINAGE/MANHOLE` 或某设备类型时未知点不匹配。`district=UNASSIGNED` 可包含这些点,正式片区不可包含。不要扩展 `industry=UNKNOWN` 请求枚举来完成本票:当前 industry 白名单只接四行业。依据:[分布服务:613–633]()、[地图服务:219–228]()。 - 三种分布建议保留未知行业/类型的 **null 桶**,其中 `pointCount=1,deviceCount=0,statusCounts.empty=1`,从而各分布点位数可汇总对账;不建立伪造的 SourceType/第五行业。当前 String 键桶使用 LinkedHashMap,排序已 `nullsLast`,Accumulator 能保存 null 名称/行业,结构允许实现;这是建议的新契约,应补测试和注释。依据:[分布服务:393–408]()、[分布服务:514–517、540–578]()、[Accumulator:815–838]()。 ## 3. distribution 丢失路径与统计单位 - 当前点位先 `classify(type)`;无类型进入 UNKNOWN_TYPE,而排除量用 `devices.size()`,空列表加 0 后 continue,因此统计、列表、排除计数均看不到对象。非空点位也按设备数记排除,和点位 marker 按 1 计数不一致。独立设备排除则按 1。依据:[分布服务:252–270]()、[分布服务:319–334]()。 - 当前坐标不可绘制即累计 invalidCoordinateCount 并 continue,两侧都不进后续状态、分布、分页与回显。因此只放过空点的类型校验仍不够;真实适配器一律 verified=false,所有这类点仍会掉在坐标分支。依据:[分布服务:277–282、342–347]()、[分布适配器:106–115、126–134]()。 - `pagination.total` 现有单位是 **监测点对象数 + 未关联设备对象数**,不是监测点数,也不是设备数;六态总量同样同时累加这两类对象。分布行的 `pointCount` 只算点位,`deviceCount` 算点位所带设备加独立设备,`statusCounts` 则算对象状态而非所带设备状态。实现时必须保留这些区分。依据:[分布服务:398–424]()、[Accumulator:815–822]()。 - 分页先聚合全量,再切监测点段和独立设备段;统计和 mapFilter 编码应继续是全量筛后范围,不随页面变化。不要改成只统计已上图或当前页。依据:[分布服务:390–441、465–478]()。 ## 4. excluded 与 invalidCoordinateCount 的分离契约 **票面验收“excluded 各计数之和 = 总数 − 纳入数”需要随方案 A 澄清**:如果坐标不可绘制对象仍纳入统计,就不能把 invalidCoordinateCount 和真正类型排除相加后解释统计差额。这是票面两个要求同时执行时的必然结论。依据:[执行票:69–77]()、[领域文档:91–93]()。 建议保持现有 JSON 字段,收紧其语义,不为兼容性而继续丢统计对象: | 字段/集合 | 建议单位与含义 | 与统计集合关系 | | --- | --- | --- | | unknownTypeCount / typeKindMismatchCount / nonFormalIndustryCount | 真正因类型口径不纳入的对象数,点位按点位 1,独立设备按设备 1 | 三者互斥;合法无设备未知点不计入排除 | | invalidCoordinateCount | 已选入统计对象中 `coordinateValidation.renderable=false` 的对象数,点位/独立设备各按 1 | 是纳入集合子集,表示暂不上图,不再参与统计排除差额 | | pagination.total | 全量筛后监测点数 + 全量筛后未关联设备数 | 包含暂不上图对象 | 该表为建议契约,来源是现有响应字段 [Excluded:383–400]()、现有总量 [分布服务:424–431]() 及领域的统计/呈现分离要求 [领域文档:91–93]()。 建议对账公式: ```text P = 筛后纳入监测点(含无设备、不可上图) U = 筛后纳入未关联设备(含不可上图) S = |P| + |U| = pagination.total = sum(pointStatusDistribution.count) C = count(P ∪ U 中 renderable=false) = invalidCoordinateCount renderableObjectCount = S - C 未筛全量的合法身份候选 N = S + unknownTypeCount + typeKindMismatchCount + nonFormalIndustryCount ``` 这套公式需先按编码去重且剔除“全量设备中已关联设备”,否则 N 不会是独立业务对象集合。筛选时还要区分“被用户筛掉”与“口径排除”:当前类型排除在 industry/deviceType 筛选前发生,坐标排除又在 district/status 筛选前发生,所以旧 excluded 并不是统一筛后集合;不能声称任意筛选组合下都直接满足 `原始 N − pagination.total = sum(excluded)`。建议最小保留前三个排除字段的全源诊断范围,把坐标诊断移动到所有业务筛选成功后计数,并明确对账全量前提;若要统一所有字段的筛后范围,需要另行定义未知类型如何匹配行业,不能静默改变。依据:[分布服务:257–299、324–369]()。 坐标字段名称虽叫 invalidCoordinateCount,目前也包含 UNVERIFIED;验证器首先检查 verified=false,再检查缺失/越界,因此“有真实经纬度但未核验”仍返回 UNVERIFIED/UNVERIFIED_COORDINATE,不应改成合法位置,也不应改写为 EMPTY。保留名称可减少破坏,注释必须写成“不可上图诊断(包含未校验)”。依据:[坐标验证器:11–31]()、[坐标响应:3–19]()。 ## 5. overview 必须同时补数量与 pointCodes ### 现状 - 真实 overview 适配器也保留全部点位;无设备、混合类型和未知字典类型均可能产生 `SourceMonitoringPoint(pointCode,null)`。快照点位只有 pointCode/typeCode,无法单凭点位对象辨别上述三者。依据:[概览适配器:75–101]()、[概览快照:73–83]()。 - `countMonitoringPoints` 丢弃 type=null 或非 SMART_DEVICE,造成现网六个无设备点数量 0。它还有一项不对称:未筛行业时并未校验四个正式行业,但设备循环会校验。不能通过“删除 type 校验”把有设备但坏类型的点也全部放进来。依据:[概览服务:266–292]()、[概览服务:142–161]()。 - `mapFilter.pointCodes` 只来自筛后设备的 pointCode。即使只修 countMonitoringPoints,六个空点仍不会回显。响应本身已有 pointCodes,无需增加 JSON 字段。依据:[概览服务:179–186、214–241]()、[概览响应:197–213]()。 ### 最小修复建议与兼容选择 1. 定义“筛后声明点位集合”,同一个选择函数既返回计数用的编码集合,也把这些编码补进 mapFilter.pointCodes;未知空点仅在未筛行业/类型时进入,非空点沿用有效类型范围。不得用筛后设备为空判定真实无设备,因为行业筛选或坏类型设备也会使筛后为空。依据:[概览服务:142–186]()、[概览服务:266–292]()。 2. **不改快照的最小方案**:从整个 snapshot.devices(不是筛后设备)建立有挂接 pointCode 集合,用它判断声明点位是否确实无设备。该方案在真实适配器的全量设备与关联表自洽时够用;若关联设备未进入全量台账,就不能证明真实无设备。真实适配器的设备清单来自 equipmentBaseService.list,关联事实却来自 getEquipmentByPoint,二者是不同读取。依据:[概览适配器:85–100、103–127]()、[概览快照:46–70]()。 3. **事实更完整的方案**:给 SourceMonitoringPoint 附加关联设备数/有无设备事实,由适配器读关联表时记录;保留原两参构造器,新增重载,老构造器的缺失值表示“未知事实”,不要一律当零设备。明确值与从全量设备推导的兼容回退结合,可避免混合类型被误报为空点。依据是当前快照信息不足 [概览快照:73–83]() 与既有两参测试调用 [概览控制器测试:294–295]()。 4. **pointCodes 的兼容陷阱**:现有控制器 fixture 声明 3 个点,但 6 个有效设备反推到 6 个不同点,现有 counts 断言为 3;所以直接把现有设备反推 pointCodes 当作总数会改成 6。最保守修复是计数仍基于声明点位集合,并将其编码与现有设备反推编码合并回显;真实台账自洽时数量与编码一致,旧脏 fixture 的孤儿归属行为保留。若改成只回显声明点位,应明确这属于更强的归属一致性修订并补回归,不宜无意变更。依据:[概览控制器测试:68、251–263]()、[概览服务:179–186]()。 5. 无设备点增加 monitoringPointCount,不增加 equipmentCount、设备状态数量、设备类型分布和在线率分母。在线率仍 `online/(online+offline+stale)`,UNKNOWN 不进分母。只有点位没有设备时在线率 EMPTY/NO_RESOLVED_DEVICES 是正确的设备指标说明,不能为了总点数非零伪造在线率。依据:[概览服务:186–199、227–236]()、[概览适配器测试:103–109]()。 ## 6. freshness 与 NO_RESOLVED_DEVICES 应怎样拆开 - distribution 的 `hasResolved=!deviceCodes.isEmpty()` 使用只在坐标通过后收集的设备集合,因此坐标未校验会被误报 NO_RESOLVED_DEVICES。坐标与统计分离后,这个问题自然消除一部分,但“有合法 EMPTY 点、确实无设备”仍会让整个点位台账 freshness=EMPTY。建议分布信封以纳入对象集合是否非空判有内容,有点位即 CURRENT/READY;没有任何统计对象仍保留 EMPTY/NO_RESOLVED_DEVICES,以减少已有空查询断言变更。依据:[分布服务:277–301、444–460]()、[分布控制器测试:250–260]()。 - overview 已无坐标过滤,NO_RESOLVED_DEVICES 是在线/离线/滞后分母为 0 的结果;它不等价于无设备(全 UNKNOWN 也会出现)。可仅把信封 freshness 与点位/设备内容是否存在分开,保留 onlineRate 的设备分母语义;不得把“没有可判定设备在线样本”改写为坐标原因。是否顺便调整全 UNKNOWN 信封语义应写明确断言,避免扩大成本。依据:[概览服务:164–199]()、[概览适配器:146–151]()。 ## 7. 放开坐标限制时必须防的重复与回显问题 1. **关联设备会重复充当独立设备**:分布适配器按全量设备生成 smartDevices,测试明确关联由服务层判定;但当前分布服务遍历 smartDevices 时没有关联码排除。原本所有设备不可上图掩盖了这个问题,移除坐标 continue 后会暴露重复计数。最小必要护栏:从原始所有点位关联设备中建立 normalized associatedDeviceCodes,独立设备循环先跳过这些码;不能用筛后 deviceCodes,否则点位被筛掉时设备会错误变成“未关联”。依据:[分布适配器:118–140]()、[适配器测试:87]()、[分布服务:319–388]();地图已有原始关联排除实现 [地图服务:280–310]()。 2. **筛掉点位的设备仍可能进入回显**:分布点位循环在计算 selectedStatus 并决定 continue 之前添加 deviceCodes。建议在所有筛选成功后才发布 pointCodes/deviceCodes,状态计算用局部 effective 列表即可。依据:[分布服务:288–315]()。 3. 未知点的 `type=null` 不能继续调用 `type.getIndustry/getCode/getName`;筛选和 Marker 构造要先得到可空局部字段,并在需要明确匹配时排掉未知。这是放开 UNKNOWN_TYPE 分支的直接空指针风险。依据:[分布服务:271–275、302–308]()。 ## 8. 已有测试的覆盖空洞与应调整断言 | 已有测试/来源 | 已证明什么 | T28 还需补什么 | | --- | --- | --- | | [分布控制器测试:424–429、468–474]() | EMPTY fixture 预先带 WATER_SUPPLY_SENSOR 且 verified=true | 真实形状:type=null、district=null、devices=[]、verified=false;必须留列表、EMPTY 桶与分页 | | [分布适配器测试:78–85]() | 适配器保留空点并输出 null 类型/片区 | 再串接 Service 验证对象不被抹掉,不能只测 adapter | | [分布控制器测试:79–151、463–465]() | 旧坐标排除契约:越界 EQ-FREE-005 不计入 total=8 | 新口径应 total=9、独立设备 4、NORMAL 2,其 GAS/片区/类型/设备回显数量同步增加;invalidCoordinateCount 仍 1 | | [分布控制器测试:351–382]() | 设备增量改变统计,旧消息/未知设备被忽略 | 越界 NORMAL 设备纳入后初始 NORMAL 从 1→2、增量后从 2→3;不要把这当成增量回归 | | [概览控制器测试:242–263]() | 所有声明点都有合法类型;没有真实空点 fixture | 6 个无设备点独立构造,断言 monitoringPointCount=6 且 pointCodes 恰含 6 编码、equipmentCount=0、设备分母=0 | | [概览适配器测试:87–109]() | 已有无设备声明点场景,但只断言设备在线率 | 加点位数量与 pointCodes 断言,避免以为已测过空点计数 | | [概览控制器测试:128–140]() | 真正空快照返回 EMPTY、在线率 0.0000 | 保留真空场景,区分“仅有 EMPTY 点”与完全无业务对象 | 建议追加的最小行为矩阵(来自本票验收 [执行票:68–77]() 和上述实现风险): - 无设备未知点:全量和 EMPTY/UNASSIGNED 筛选可见;指定正式行业、指定设备类型、正式片区、非 EMPTY 状态不可见。 - 已关联非空点与未关联设备:分别测试 verified=false、缺坐标、越界,均参与统计并保留验证 reason;上图资格仍 false。 - 点位含 2 台设备且类型未知:排除量按点位 1,不把它伪装成 EMPTY;两台设备出现在全量 smartDevices 时仍不能成为独立设备。 - 同一台关联设备同时出现在 point.devices 与 smartDevices:统计一次;点位被片区/状态筛掉后设备仍不转为独立设备。 - 同一快照跨页:EMPTY 总数与所有分布、mapFilter 编码不随页变化;`total = 所有六态 count 之和`。 - 仅有合法空点:分布有内容、freshness 不因为 deviceCodes=[] 就假报整段无数据;概览点位数与回显正确,设备在线率仍 EMPTY。 这些是应补/修改的测试建议,本次研究没有执行测试,不能声称测试通过。 ## 9. 前端兼容性观察(只读,不属于本次修改) - 当前前端 marker、行业行和类型行用 optionalStr 读取,null 被转换为空串,不会抛异常;类型行空码/空名通过 codeText 显示“未知”。因此后端返回 null 行业/类型比虚构 SourceType 改动小。依据:[前端分布适配:190–206、236–255]()、[可选字段读取:45–49]()、[前端类型行:513–517]()、[空码展示:84–100]()。 - **行业 null 桶显示会叫“未分区”**:行业条调用 `industryLabel(...) || regionText(...)`,regionText 空值用未分区,这是既有展示复用造成的词义问题。不要为了规避 UI 文案就把未知点伪分配到正式行业;可向前端后续票报告,当前只读研究不修改。依据:[前端行业行:507–511]()、[行业翻译:89–92]()、[regionText:103–105]()。 - 前端分布面板把 `pagination.total` 显示成“监测点总数”,但后端单位实际还含独立设备;其坐标说明注释仍写“只统计可上图对象”。修复后不要借 T28 后端改动偷偷改变分页含义,记录前端消费风险即可。依据:[前端总数及坐标行:546–564]()、[后端总量:424]()。 ## 10. 建议实施清单(最小闭环) 1. distribution 先采集原始关联码并归一有效设备;空点未知类型合法化、可空 Marker 字段,点位类型排除改对象计数;保留当前四正式行业输入。 2. 两种对象都保留坐标验证结果,去掉“不可上图就 continue”;统计、分页与筛后 mapFilter 同源,坐标诊断在最终纳入时累计。 3. 独立设备循环排除原始关联码;把点位 deviceCodes 的回显发布移至状态筛选之后。 4. overview 选择声明点位的函数同时给数量与编码,补入 mapFilter;用原始设备关联集合或显式关联数区分真正空点与未分类非空点;设备在线率算法保留。 5. 分布信封 freshness 基于对象内容,概览信封与设备在线率的空态分清;更新旧“统计只计可上图”注释,写明 invalidCoordinateCount 是纳入对象的呈现诊断。 6. 按第 8 节补回归并修订旧坐标排除断言;真实复验、编译/测试与提交推送由主执行代理在授权边界内开展。本研究因用户与项目规则跳过 build/debug、测试执行、HTTP 与浏览器验证。 以上清单分别受本笔记第 2–8 节引用的源码与 [T28 范围/验收]() 支持;不包含坐标校核来源建设、其它模式统一口径、前端展示修复或其它票处理。