## 背景
上一提交把 `status_post.cabinet.meter`(柜级电表读数,Wh)落库并只在读数变化时留痕
(`cabinet_meter_readings`),数据源已就绪。本提交基于它做**整柜日用电量**统计。
## ⛔ 与 energy_stats 的关系:两套口径,严禁混用
| | 数据源 | 口径 |
|---|---|---|
| **本模块** `meter_stats` | `cabinet_meter_readings`(电表读数) | **整柜实际用电**(硬件计量、含非充电损耗) |
| `energy_stats` | `charge_records`(订单结算) | **单订单充电量**(平台 `V×I×Δt` 积分的估算值) |
电表读数天然含待机/风扇/变换器损耗,且是硬件计量;订单电量是估算。两者数值必然不一致,
**谁也不替代谁**。`energy_stats` 的"今日/本月能耗"是订单口径;要"整柜每天用多少度"必须走本接口。
## 计算口径
`当日用电量 = 当日最后一条读数 − 当日之前最近一条读数`
注意是"当日**之前**最近一条",不是"当日第一条"—— 否则同日多次读数会把当天早段的用电漏掉。
## 三个语义坑(都处理了,都有单测)
1. **离线整天 = 无数据,不是 0**:当天一条读数都没有 ⇒ 该日**不出现在结果里**。
渲染成 0 度等于告诉用户"这天没用一度电",而事实是"不知道"。
2. **跨多日 ⇒ 标 `covers_multiple_days`**:前值距当日首读超过 1 天(柜子离线了几天)⇒
差值实际覆盖多天,直接标"当日用电"会高估。
3. **读数变小 ⇒ 标 `meter_reset_or_replaced`**:差值为负,这不是"负用电量",
而是电表被更换或清零 —— 必须标记,绝不当成用电量展示。
另外:**首日无前值 ⇒ `usage_wh` 不出现**(`no_baseline`),也是"无数据"不是 0。
## 汇总口径
`summary.total_wh` 只累加**可信**的日用电 —— `no_baseline`(无对比)与
`meter_reset_or_replaced`(差值非法)都不计入,否则总数会被一个换表日的巨大负值污染。
同时给出 `counted_days`(实际计入)与 `days_with_data`(有读数,含被排除的)。
## 为什么用 Rust 纯函数而不是全 SQL 算
三个语义坑都需要条件分支,SQL 里写会变成一层套一层的 `CASE WHEN`,难读也难单测。
这里 SQL 只负责取数(区间读数 + 区间**之前**最近一条作基准),判定交给
`compute_daily_usage` 纯函数 —— 可完整单测,包括三个坑各自的用例。
## 接口
`GET /api/cabinets/{id}/meter-usage?start=&end=`(闭区间,`YYYY-MM-DD`,缺省最近 30 天,最多 366 天)
权限 `device:view` + `scope_guard::verify_cabinet_ownership`(组织隔离 + 数据范围)。
## 实测(测试环境真实 HTTPS,临时超管账号+自签 JWT,用完即删)
造 5 条读数(day1=1000 / day2=1100,1300 / day3 离线 / day4=1500 / day5=1200):
```
2026-03-01 end=1000 usage=None flags=['no_baseline']
2026-03-02 end=1300 usage=300 flags=[]
2026-03-04 end=1500 usage=200 flags=['covers_multiple_days']
2026-03-05 end=1200 usage=-300 flags=['meter_reset_or_replaced']
summary: total_wh=500 (300+200,换表日不计), counted_days=2, days_with_data=4
⇒ 三个坑全部生效,03-03 缺席,汇总正确
```
## 判据自证能失败
撤掉"跳过区间前基准读数"那段(**我先前真实踩过的 bug**:不跳过则 `idx` 卡在基准上,
首日整天读数取不到)→ 3 个测试 **FAILED**;还原 → 9 passed。
## 验收
- `cargo check --all-targets`:**0 error / 0 warning**(两服务)
- `cargo test --bin pms-api`:**204 passed**(195 + 9 条新单测)
- web `npm run typecheck`:exit 0
- `device_status_parse` 16/16、`rssi_signal` 9/9
- 结构漂移:**无漂移**(两库 159 列 / 0 TIMESTAMP)
- `schema_alignment_e2e --env dev`:通过;`tcp_e2e_smoke`:**18/18**
- 路由:**81** 条 / **94** 对(新增 1 条,`docs/tech/API.md` 计数已同步 80→81、93→94)
- 已发布测试与生产(生产 0 ERROR、域名 401/200、消费链探测通过)
## 文档
- `docs/tech/API.md`:新增接口详情(含 flags 表格、离线≠0、汇总口径),更新路由计数
- `AGENTS.md`:数据模型节补"两套电量口径严禁混用" + 三个语义坑
- `tasks/002`:标记日用电量统计已完成
docs/ 文档地图
本文件是
docs/的分类索引。 新增、移动或改名任何文档后,必须回来更新本文件 并同步全仓引用(见文末「维护约定」)。
一、分类总览
| 目录 | 定位 | 权威性 | 谁该读 |
|---|---|---|---|
protocol/ |
平台设备协议(TCP,冻结,对硬件工程师交付) | ⭐ 唯一权威 | 硬件工程师、固件、平台 TCP 相关开发 |
tech/ |
平台技术文档(开发/运维必读) | ⭐ 权威(各自领域内) | 后端/前端/运维 |
hardware/ |
硬件侧总线协议(RS485,不是平台 TCP 协议) | 硬件事实依据 | 硬件工程师、对接电源/BMS 的人 |
contract/ |
合同、规格、验收标准(商务依据) | 事实依据,但不描述实现 | PM、验收方、新人了解范围 |
compound-notes/ |
单点经验笔记(踩坑/技法) | 参考,非权威 | 遇到同类问题的人 |
二、谁是权威(口径优先级,避免"两份文档谁对"的扯皮)
⚠️ 关于"到底有几份 TCP 协议文档"
全仓只有一份:
docs/protocol/柜控板固件对接文档.md。docs/protocol/目录下有且仅有这一个文件 —— 这是刻意的,便于一眼确认。历史上曾有三份内部件,已在协议 v1.1 前后删除(用户指示:只保留一份权威):
docs/tcp-protocol.md、docs/TCP报文交互示例.md、docs/柜控板-平台TCP接口文档.md。若你看到的"另一份 TCP 协议"是下面这些,它们都不是平台 TCP 协议:
docs/hardware/1TN协议标准_*—— RS485 总线协议(仓控板 ↔ 翌工电源),另一条物理链路;docs/tech/API.md的「TCP 设备通讯协议」章节 —— 只有一句指回权威文档的链接,不重复内容;tasks/*.md—— 任务草稿(tasks/已 gitignore,不入库),文件名可能含"TCP协议" 但那是任务记录,不是规格。
protocol/柜控板固件对接文档.md—— 设备协议(帧格式 /act/ 字段名 / 签名 /id规则) 的唯一权威。协议冻结,设备端固件不可改。- 根目录
AGENTS.md—— 平台侧实现口径的权威:Redis 键与字段、接口行为、库结构、 验收流程、部署注意事项。与tech/API.md的平台存储/行为描述冲突时,以AGENTS.md为准 (它的每一条都按代码契约与库实测核对过)。 tech/API.md—— HTTP 接口清单与响应结构的权威(含数据单位规范)。- 代码与文档冲突时:先确认是不是代码错;确认代码对就改文档并在报告里点出。
三、逐目录说明
protocol/ —— 平台设备协议(只有一份)
| 文件 | 说明 |
|---|---|
柜控板固件对接文档.md |
唯一权威 TCP 文档(v1.1.1)。帧格式、通用规则(act/suc/msg_id/60 秒超时/登录门控)、指令清单、签名算法与自测向量、安全码自愈、带注释报文示例(附 A)、变更记录(附 B)。既是内部文档也可直接对外交付。 |
⛔ 不要在这里放第二份协议文档(哪怕是"内部稿""示例集""补充说明")—— 历史上正是这样分叉出三份,最后对不上。要补充内容就改这一份并升版本号(记入它的「附 B」)。
hardware/ —— 硬件侧总线协议
| 文件 | 说明 |
|---|---|
1TN协议标准_YG20240118-1_V1.0.md |
仓控板 ↔ 电源(苏州翌工)1TN RS485 协议,文本版 |
1TN协议标准_苏州翌工电源_V1.0.pdf |
同上,厂家原始 PDF |
硬件与系统设计文档.md |
硬件与系统设计原文(46 KB):硬件架构、仓控/柜控主板设计、指示灯状态机、充电控制逻辑(握手/双模式/动态调整/无协议模式)、BMS 协议适配、安全防护联动、性能指标、第三方服务配置。硬件侧的主规格文档。 |
⚠️
1TN协议标准_*是另一条物理链路(柜内 485 总线),与平台 TCP 协议无关 —— 别把它当成"第二份 TCP 协议"。硬件侧的其它总线协议(BMS:星恒 Modbus / 天能 485)如需入库,也放这里。⚠️
硬件与系统设计文档.md早于现行实现:其中硬件设计部分仍是权威参考, 但软件平台部分(第 7 章)已被AGENTS.md+docs/tech/取代 —— 两者冲突时,软件口径一律以AGENTS.md为准(它按代码契约与库实测核对过)。
tech/ —— 平台技术文档
| 文件 | 说明 | 入口 |
|---|---|---|
API.md |
HTTP 接口清单与响应结构 + 数据单位规范(整数存储,禁浮点) | PC/小程序前端、后端 |
数据模型.md |
21 张表的逐列语义、外键/唯一键、建表依赖顺序 | 改表结构前必读 |
部署手册.md |
完整部署步骤、nginx 两份 conf.d 全文、全部 11 条部署注意事项 | 运维、发版 |
小程序用户端.md |
小程序页面清单、逐页职责、状态位分层展示、RSSI 分档口径 | 小程序开发 |
这四份是
AGENTS.md的详细参考篇:AGENTS.md只保留"判据 + 链接" (因为AGENTS.md超出工作区指令预算 65,536 B 时会被按头部截断,尾部根本不加载)。
contract/ —— 合同与规格
开发合同 / 附件一-硬件规格 / 附件二-软件规格 / 附件三-交付与验收标准 /
附件-开发需求规格说明书,各含 .md + 原始格式(.docx / .pdf / .html)双份。
另有
营业执照.jpg(主体资质,小程序/备案用)。用途:范围与验收的事实依据。描述"要做什么 / 达到什么标准",不描述当前实现 ——实现现状一律以
AGENTS.md+ 代码为准。
compound-notes/ —— 经验沉淀
单点问题的笔记:2026-08-15-sql-injection-format-param-binding.md(SQL 注入成因与修法)、
2026-07-ardot-batch-edit-属性写法.md(设计稿批量编辑的属性坑)、
TEMPLATE.md(新建笔记的模板)。参考性内容,
遇到同类问题时查;结论若上升为通用规范,应回写进 AGENTS.md 的「踩过的坑」。
archive/ —— 已取消(2026-09-20)
不再设归档目录。 曾经的 archive/ 三份文件已按下述依据处置:
| 原文件 | 处置 | 依据 |
|---|---|---|
开发文档.md |
移出归档 → hardware/硬件与系统设计文档.md |
它不是过时文档而是归错类:含硬件设计(主板、指示灯状态机、充电控制、BMS 适配、防护联动),这些在 AGENTS.md/tech/ 里没有。仅第 7 章软件部分被取代。 |
审查报告-数据单位一致性-2026-07-06.md |
删除 | 一次性审查报告,发现的问题全部已解决(SOC/SOH 单位标注早已重写;该报告以已不存在的 tcp-protocol.md 为基准)。 |
项目深度分析报告-2026-07-06.md |
删除 | 同上。其 4 项"发现的问题"均已失效:明文密码已成明示豁免、"可能用 taro"已确认是 h5-uniapp、test/ 已存在、温度单位已统一。 |
💡 为什么不设归档:被删除的内容在 git 历史里完整可查(
git log -- <原路径>), 归档目录的"留痕"价值因此为零,却会持续制造"这文件还算不算依据"的歧义。 结论:过时就删,历史交给 git;仍有价值的内容应当归到正确的类而不是塞进 archive/。
四、旧名 → 现名映射(查旧链接时用)
| 旧路径(已不存在) | 现路径 |
|---|---|
docs/tcp-protocol.md |
docs/protocol/柜控板固件对接文档.md |
docs/柜控板-平台TCP接口文档.md |
已于协议 v1.1 删除(内部重复文档,内容并入上者) |
docs/柜控板固件对接文档.md |
docs/protocol/柜控板固件对接文档.md |
docs/API.md |
docs/tech/API.md |
docs/数据模型.md · docs/部署手册.md · docs/小程序用户端.md |
同名,移到 docs/tech/ |
docs/开发文档.md |
docs/hardware/硬件与系统设计文档.md(不是过时,是归错类) |
根目录 换电柜用锂电池充电器1TN标准通信协议 - RS485 V1.0(1).pdf |
docs/hardware/1TN协议标准_苏州翌工电源_V1.0.pdf(同一文件,根目录那份是重复,已删) |
docs/审查报告-数据单位一致性-2026-07-06.md · docs/项目深度分析报告-2026-07-06.md |
已删除(问题全部已解决;git 历史可查) |
| 其余合同/规格/验收/协议标准 | 见上表,分别归入 contract/、hardware/ 与 protocol/ |
五、维护约定(新增/移动文档时必做)
- 归位:按"协议 / 技术 / 硬件 / 合同 / 经验"分类放入对应目录,不要再平铺在
docs/根。 - 更新本索引:在对应章节的表格里加一行(文件 + 一句话定位 + 谁该读)。
- 同步全仓引用:文档路径散落在
AGENTS.md、README.md、Rust/TS/Vue 源码注释、test/、tools/、tasks/里;移动后必须全仓替换。 校验方法(应无输出):# 找仍指向旧平铺路径的活引用 # · 排除 docs/README.md —— 本文件的「旧名 → 现名映射」表**必须**出现旧名 grep -rn --exclude-dir={.git,node_modules,target,dist} \ -e 'docs/API\.md' -e 'docs/数据模型\.md' -e 'docs/部署手册\.md' \ -e 'docs/小程序用户端\.md' -e 'docs/开发文档\.md' . \ | grep -v '^\./docs/README\.md:'已实测该命令能抓到问题:往
docs/tech/API.md末尾植入一行<!-- probe: docs/API.md -->→ 命中;删除后 → 无输出。 ⚠️ 注意这些.md多为 CRLF —— 用脚本批量删除/替换时必须按\r?\n处理, 否则会漏改(我第一次就踩了:\n正则配不上\r\n,以为删干净了其实残留)。 - 改
AGENTS.md的链接:AGENTS.md里所有docs/...链接都必须是分类后的路径 (docs/protocol/...、docs/tech/...)。 - 过时就删,不要设归档:被删内容在 git 历史里完整可查,归档只会制造
"这文件还算不算依据"的歧义。仍有价值的归到正确的类,规范要写进
AGENTS.md或tech/。