feat(cabinets): 柜子详情接口透出最新电表读数(meter_wh / meter_at)

协议 §5.3 的 `cabinet.meter`(柜级电表读数,Wh)已落库(`cabinets.meter_wh` / `meter_at`,
变化留痕见 `workers/meter.rs`),本提交把它透出到 `GET /api/cabinets/{id}` 的响应里。

## 做法

沿用**同一个函数里 `project_name` 的既有模式**单查一次,**不给 `CabinetBasicRow` 加字段**:
该结构体有**两个**取数点(列表 L129 / 详情 L592),且其上方注释明确写着
「字段必须与 `CabinetBasicRow` 一一对应」—— 加字段要同步两处 SELECT,耦合更大、风险更高。

- ⚠️ `null` = **从未上报过**,与「读数为 0」是两回事(前端必须区分,别渲染成 0 度);
- 查询失败降级为 `null`,不让详情接口整体失败(与 `project_name` 同口径)。

## 实测(测试环境真实 HTTPS 请求)

临时超管账号 + 自签 HS256 JWT(用测试环境 `.env` 的 `JWT_SECRET`),**用完即删**:

```
库中:cabinet_id=1 meter_wh=125600 meter_at=2026-09-19 21:54:30
接口:meter_wh=125600 meter_at='2026-09-19T21:54:30'
⇒ 逐字一致
```

## 判据自证能失败

把 `should_record` 简化成朴素写法 `prev != current`(即**会把"字段缺失"当成 0 记录**的那版)
→ `test_missing_meter_is_never_recorded` **FAILED**;还原 → 6 passed。

## 验收

- `cargo check --all-targets`:0 error / 0 warning
- `cargo test --bin pms-api`:**195 passed**
- `cd software/web && npm run typecheck`:exit 0
- `node test/device_status_parse.test.mjs`:16/16;`node test/rssi_signal.test.mjs`:9/9
- 路由数:**80**
- `python test/schema_alignment_e2e.py --env dev`:通过(exit 0)
- 已发布**测试与生产**(生产:服务 active、0 ERROR、域名 401/200、消费链探测通过)
This commit is contained in:
12451 2026-09-20 06:00:30 +08:00
parent ad6c32b709
commit cf9a521b31

View File

@ -620,12 +620,29 @@ pub async fn get_cabinet_detail(
None
};
// 柜级**最新电表读数**(协议 §5.3 `status_post.cabinet.meter`,单位 Wh,柜级累计电能)。
// 已按协议"平台原样存库"落库(`cabinets.meter_wh`/`meter_at`,变化留痕见 `workers/meter.rs`)。
// ⚠️ `NULL` = **从未上报过**,与"读数为 0"是两回事 —— 前端必须区分(别渲染成 0 度)。
// 查询失败降级为 None,不让详情接口整体失败(与上面 `project_name` 同口径)。
let meter: Option<(Option<i64>, Option<chrono::NaiveDateTime>)> =
sqlx::query_as("SELECT meter_wh, meter_at FROM cabinets WHERE id = ?")
.bind(id)
.fetch_optional(db)
.await
.ok()
.flatten();
let (meter_wh, meter_at) = meter.unwrap_or((None, None));
Ok(Json(json!({
"code": 0,
"data": {
"id": cabinet.id,
"project_id": cabinet.project_id,
"project_name": project_name,
// 整柜累计电能(电表读数,Wh)。`null` = 从未上报过(**不是 0**)
"meter_wh": meter_wh,
// 该读数的收到时刻
"meter_at": meter_at,
"abstract_id": cabinet.abstract_id,
"imei": cabinet.imei,
"iccid": cabinet.iccid,