t28-counting-contract.md 29 KB

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。

建议对账公式:

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. 前端兼容性观察(只读,不属于本次修改)

10. 建议实施清单(最小闭环)

  1. distribution 先采集原始关联码并归一有效设备;空点未知类型合法化、可空 Marker 字段,点位类型排除改对象计数;保留当前四正式行业输入。
  2. 两种对象都保留坐标验证结果,去掉“不可上图就 continue”;统计、分页与筛后 mapFilter 同源,坐标诊断在最终纳入时累计。
  3. 独立设备循环排除原始关联码;把点位 deviceCodes 的回显发布移至状态筛选之后。
  4. overview 选择声明点位的函数同时给数量与编码,补入 mapFilter;用原始设备关联集合或显式关联数区分真正空点与未分类非空点;设备在线率算法保留。
  5. 分布信封 freshness 基于对象内容,概览信封与设备在线率的空态分清;更新旧“统计只计可上图”注释,写明 invalidCoordinateCount 是纳入对象的呈现诊断。
  6. 按第 8 节补回归并修订旧坐标排除断言;真实复验、编译/测试与提交推送由主执行代理在授权边界内开展。本研究因用户与项目规则跳过 build/debug、测试执行、HTTP 与浏览器验证。

以上清单分别受本笔记第 2–8 节引用的源码与 T28 范围/验收 支持;不包含坐标校核来源建设、其它模式统一口径、前端展示修复或其它票处理。