From cf9a521b31b94a361715fcb4f72e3277e6922af3 Mon Sep 17 00:00:00 2001 From: 12451 <12451@example.com> Date: Sun, 20 Sep 2026 06:00:30 +0800 Subject: [PATCH] =?UTF-8?q?feat(cabinets):=20=E6=9F=9C=E5=AD=90=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E6=8E=A5=E5=8F=A3=E9=80=8F=E5=87=BA=E6=9C=80=E6=96=B0?= =?UTF-8?q?=E7=94=B5=E8=A1=A8=E8=AF=BB=E6=95=B0=EF=BC=88meter=5Fwh=20/=20m?= =?UTF-8?q?eter=5Fat=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 协议 §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、消费链探测通过) --- software/api-server/src/routes/cabinets.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/software/api-server/src/routes/cabinets.rs b/software/api-server/src/routes/cabinets.rs index 9e0b50b..4132f37 100644 --- a/software/api-server/src/routes/cabinets.rs +++ b/software/api-server/src/routes/cabinets.rs @@ -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, Option)> = + 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,