12451
e8d7442377
feat(meter): 柜级电表读数落库 + 变化留痕(协议 §5.3 要求"平台原样存库",此前只写 Redis)
## 背景:协议一直有上报,是平台没存
`status_post.cabinet.meter`(电表读数,单位 **Wh**,硬件最小刻度 0.1 度电)在协议 §5.3 里
字段表写得很明确:**「平台原样存库、不再 ×100」**(`docs/protocol/柜控板固件对接文档.md` L565),
报文示例(L535)也一直带着它。
但实现侧此前**只写 Redis**(`device:{imei}` 的 `meter`,TTL 300 秒)⇒ 每 60 秒报一次、
**5 分钟后被清掉**,整柜累计电能在平台侧等于不存在、历史无法回溯。
本提交补上落库 —— 属**履约**,不是新增协议内容。
## 改动
1. **新表 `cabinet_meter_readings`**(`cabinet_id` / `reading_wh` / `collected_at`,FK→cabinets,
索引 `(cabinet_id, id)`)+ **`cabinets.meter_wh` / `meter_at`**(最新读数,供列表/详情廉价读取)。
2. **新模块 `workers/meter.rs`**:
- `should_record(prev, current)` —— **纯函数判据**,且**真正承担生产判据**(不是只给单测用);
- `record_if_changed(pool, imei, meter_wh)` —— 读上次值 → 纯函数判定 → 带 `WHERE ... <> ?`
的 UPDATE 做**并发认领** → 认领成功才追加留痕行(2~3 次往返,且读数未变时 0 次写入)。
3. **`status_post` 接线**:`meter` 改为按 `Option` 解析,**区分「字段缺失」与「值为 0」**
(原先 `unwrap_or(0)` 会把缺失当 0 落库,等于在序列里插进凭空的零值)。
Redis 那条路径口径不变(缺失即 0),不影响既有实时状态契约。失败只记 WARN。
## 为什么"只在变化时记一条"
`status_post` 固定 60 秒一次 ⇒ 68 柜约 **9.8 万行/天**;而电表读数**只在有电量消耗时才变**。
只记变化 ⇒ 写入量与真实用电事件成正比,且"变化"本身就是要留的信息
(等价于抄表系统的**抄表事件**)。读数**变小也记**(换表/清零/回绕属真实事件,必须留痕),
但不在此做任何补差推算 —— 口径仍是**宁可少算、不可虚增**。
## 单测(6 条,全过)
`test_missing_meter_is_never_recorded`(⛔ 缺失绝不落库成 0)、
`test_first_reading_is_recorded_as_baseline`、`test_unchanged_reading_is_not_recorded`、
`test_changed_reading_is_recorded_including_decrease`、
`test_absent_and_zero_are_treated_differently`(**反证**:缺失与 0 处置必须不同)、
`test_update_guard_sql_shape`(变化闸门与占位符个数守门)。
## 顺带被两个既有守门拦下(都是它们正常工作,责任在我)
1. `test_schema_sql_energy_columns_comment_unit_is_wh` 的 `!SCHEMA_SQL.contains("kWh")`:
我的新表注释写了"硬件0.1kWh×100"命中了禁词。**守卫意图正确**(防有人把列标成 kWh
而实际存 Wh),故**改自己的措辞**(改述为"最小刻度 0.1 度电=100Wh"),**不放宽守卫**。
2. `EXPECTED_TABLE_COUNT` 21 → 22(断言消息本就写着"新增/删除表时请同步更新本断言")。
## 验收
- `cargo check --all-targets`:**0 error / 0 warning**
- `cargo test --bin pms-api`:**195 passed**(189 + 6 条新单测,未减少)
2026-09-20 05:52:17 +08:00
..
2026-09-20 05:52:17 +08:00
2026-09-19 23:41:09 +08:00
2026-09-19 23:41:09 +08:00
2026-09-19 23:41:09 +08:00