36 Commits

Author SHA1 Message Date
12451
8025cd831c fix(db+users): users.name 改 NOT NULL DEFAULT '',并修掉迁移非幂等的两个解析 bug
## 1. users.name 不再允许 NULL(用户要求)

**数据库层加闸**:`name VARCHAR(50) COMMENT '姓名'`
→ `name VARCHAR(50) NOT NULL DEFAULT '' COMMENT '姓名(选填,为空串表示未填写;⛔ 不允许 NULL)'`

同步三处(AGENTS.md 硬约束):
- `SCHEMA_SQL` 建表语句;
- `LEGACY_COLUMN_TARGETS` 加 `("users", "name", "VARCHAR(50) NOT NULL DEFAULT ''")`
  (老库靠它对齐,`CREATE TABLE IF NOT EXISTS` 不改已存在的表);
- Rust `UserRow.name` / `load_user_for_update` / `persist_user_update` /
  `log_user_update` 全部 `Option<String>` → `String`;
  创建用户时 `input.name.unwrap_or_default().trim()`,**不再可能插 NULL**。

前端 `User.name` 类型同步改为 `string`(空串 = 未填写,界面 `v || '-'` 照常)。

### 实测(库层)
```
users.name: varchar(50) nullable=NO default=''      OK
插空串姓名 → name=''                                 OK
插 NULL     → 1048 Column 'name' cannot be null      OK(被拒)
```

## 2. 顺带修掉两个**迁移非幂等**的解析 bug(都是这次上线才暴露的)

`ensure_column_type` 的定位是"只有与目标不一致才 ALTER",但两个归一化缺失让
`users.name` **每次重启都重发一遍 ALTER**(白搭 DDL,大表上还可能触发重建):

| # | 症状 | 根因 | 修法 |
|---|---|---|---|
| 1 | 日志 `default '' -> ''''` 永远不相等 | `target_default_value` 原样返回带引号的 `''`,而 `information_schema.COLUMN_DEFAULT` 返回**去引用后**的空串 | `.trim_matches('\'')` |
| 2 | 日志 `type varchar -> VARCHAR(50)` 永远不相等 | `target_data_type` 返回 `VARCHAR(50)`,而 `information_schema.DATA_TYPE` 只报**裸类型名** `varchar`(长度在 `COLUMN_TYPE`) | 去掉 `(...)` 后缀 |

`users.name` 是对齐清单里**第一条带括号长度**的类型,之前的条目都是单词型
(`BIGINT`/`DATETIME`/`TINYINT`),所以这两个 bug 一直没暴露。

### 幂等已验证
```
测试环境:连续重启两次,ALTER users.name 次数 = 0,迁移完成日志 6 条,ERROR 0
生产环境:重启一次,      ALTER users.name 次数 = 0,ERROR 0
```

## 3. 守门

- `resolve_new_name` 三态单测(未传保持 / 传空清空 / 传非空去空白)—— 已自证能失败;
- `target_default_value` 去引号单测 —— 已自证能失败;
- `target_data_type` 去括号单测 —— 已自证能失败;
- SQL 快照守卫**有意更新**(新增 `users.name` 两条片段)—— 已自证能失败。
  ⚠️ 守卫报警是**对的**:我确实改了 SQL,所以显式更新快照,而不是放宽守卫。

## 4. 数据修复(本次 bug 的实际损害)

"表格内分配组织"只传 `organization_id`,命中姓名三态 bug,把生产
**李四 / 王五** 的姓名抹成 NULL。已按用户此前截图还原(`李四`/`王五`),
组织分配结果保留(org=2)。

## 验收

- `cargo check --all-targets` 两服务:**0 error / 0 warning**
- `cargo test --bin pms-api`:**214 passed**
- 已发布测试 + 生产;生产重启后 0 ERROR、迁移幂等、域名 401/200
2026-09-21 10:42:22 +08:00
12451
f3f841b1f0 feat(users): 用户管理表格内直接分配组织(超管)
## 起因

用户指出「所属组织要可以分配」。核查后发现两件事:

1. **组织没丢** —— 生产 46 个组织、28 个项目、68 个柜子都在;
2. **但 5 个用户里 0 个被分到组织**,所以那一列全是 `-`。
   其中 2 个是总管理员(按规则本就不属组织,正确),
   另 3 个(李四/企业管理员、张三/王五/操作员)**从未分配**,违反
   「操作员必须有企业归属」的规则。

原流程要「点编辑图标 → 弹窗里选组织 → 保存」三步才能改一个用户,
3 个用户要重复 3 次。现改成**表格里直接分配**。

## 改动

`users.tsx` 的「所属组织」列:
- 超管 + 非总管理员 ⇒ 渲染成内联 `Select`,选完即存(`PUT /users/:id` 只传
  `organization_id`,后端 `UpdateUserInput` 本就支持,无需改后端);
- 总管理员行仍显示 `-`(后端 `resolve_update_org_id` 对 `role=2` 强制 `None`);
- 非超管操作者仍只读。

顺带修 `api/user.ts`:`fetchUsers` 的参数类型漏了 `role`(上一轮加了角色过滤
但类型没跟上,靠结构兼容蒙混过关)。

## 实测(测试环境,临时账号用完即删)

```
PUT 只传 organization_id        → HTTP 200,库中 organization_id 已更新   OK
改分配到另一组织                → HTTP 200,库中已更新                  OK
只改名不传组织                  → 组织保持原值,未被清空                  OK
给总管理员分组织                → HTTP 200 但库中仍为 None(被强制忽略)   OK
列表 organization_name          → 正确返回组织名                        OK
```

## ⚠️ 两个我自己出的岔子(都已修正)

1. **断言写错**:我原以为「给总管理员分组织」应返回 400,实测返回 200。
   读库确认 `organization_id` 仍是 `None` —— 后端是**静默忽略**而非拒绝,
   行为符合规则。是测试预期错,不是代码错。
2. **测试脚本留了脏数据**:临时用户删不掉,因为 `operation_logs` 有外键引用
   (`log_operation` 写了日志)。已补删 `operation_logs` → `user_roles` → `users`
   清理干净,测试库恢复 1 用户 / 1 组织 / 1 项目 / 1 柜子。

## 待用户决定(生产数据,我不擅自改)

李四 / 张三 / 王五 三个账号要分到哪个组织?生产有 46 个组织可选。
现在超管可以在表格里直接点选分配,你也可以告诉我分配方案我来执行。

## 验收

- web `npm run typecheck`:exit 0
- `npm run build`:exit 0
- 已发布生产(web 已更新,0 ERROR、域名 401/200)
2026-09-21 05:20:38 +08:00
12451
0441d91d1f feat(users+cabinets): 用户管理加角色过滤、数据范围仅操作员可配、柜子详情透出电表读数
## 1. 用户管理:角色过滤(页面总共只有三种角色,默认罗列全部太吵)

前端加角色下拉 + 后端 `GET /users` 新增 `role` 参数(0/1/2,越界当作不过滤)。

后端把 `build_user_list_where` 从**二元穷举 match 重构为条件累加** ——
加第三个过滤器时穷举会涨到 8 分支且极易漏。同时修一个潜在坑:
**OR 条件必须整体加括号**,否则 `phone LIKE ? OR name LIKE ?` 与
`organization_id = ?` 用 AND 混排时会变成
`(A AND phone LIKE ?) OR name LIKE ?`,把组织/角色过滤整个击穿
(搜个姓名就跨组织串数据)。

新增 6 个守门单测(含"占位符个数必须与 bind 次数一致"的反证),
已自证能失败:去掉 OR 括号 → 2 个用例 FAILED,还原 → 6 passed。

## 2. 数据范围:仅操作员(role=0)可配

按用户口径:
- **总管理员**:豁免范围限制,不需要配;
- **企业管理员**:天然管理本组织全部设备,配项目级范围反而有歧义;
- **操作员**:才需要设置可管理的项目。

前端对该按钮按 `record.role !== 0` 置灰并加 title 说明。
**创建/编辑时操作员必须选组织**(没有企业归属就看不到任何设备),校验文案也分开提示。

## 3. 柜子详情:透出电表读数

接口早已返回 `meter_wh`/`meter_at`,但**前端从没接过**(用户看不到)。
现加到详情页基本信息行(IMEI/ICCID/名称 之后),Wh → kWh 展示并附收到时刻;
`null` = 从未上报过,显示 `-` 而**不是 0 度**。

## ⛔ 表号:系统里没有,做不了

全仓搜不到 `meter_no`/`meter_id`/`meter_sn`/`表号`,协议里 `cabinet.meter`
只是读数(Wh),**没有电表编号字段**。这需要硬件侧在协议里加字段,
平台无法单方面补。

## ⚠️ 我这次犯的流程错误(已修正)

改了 Rust 代码后**只跑了 cargo check/test,没重新 `zigbuild` release 产物**,
`deploy -SkipBuild` 于是把几小时前的旧二进制发上去了 —— 第一次实测角色过滤
三种角色全返回、监控词。重建后才通过。
**教训:改 Rust 后必须重建 release 产物再发。**

## 验收

- `cargo check --all-targets` 两服务:**0 error / 0 warning**
- `cargo test --bin pms-api`:**210 passed**(204 + 6 新)
- web `npm run typecheck`:exit 0
- **角色过滤实测**(测试环境,临时超管+自签 JWT,用完即删):
  ```
  role=0 → 2 人,role 集合 [0]        OK
  role=1 → 1 人,role 集合 [1]        OK
  role=2 → 2 人,role 集合 [2]        OK
  不带 role → [0,1,2]                 OK
  role=0 + keyword → 1 人             OK(keyword 单独查命中 0,证明不是靠关键字通过)
  role=9 越界 → 5 人                  OK(当作不过滤)
  ```
- 已发布测试 + 生产(四服务 active、0 ERROR、消费链探测通过、二进制时间戳已核对)
2026-09-21 05:02:09 +08:00
12451
318501d6e7 feat: 全站统一横版锁标(官网页头 + 管理端登录页改用 logo.svg)
## 背景

上一提交把官网页头换成了**方形**品牌图标(favicon.svg)+ 文字 span,
用户指出仓库里本就有**横版带字样**的锁标 —— 确实有:
`logo/纯色横向.svg`(12.4KB)与 `logo/纯色横向@2x.png`(74.7KB)。

**重要发现**:`logo/纯色横向@2x.png` 与 `software/web/public/logo.png`
**是同一个文件**(SHA256 均为 `8E40298E…`、76523 B、3025×829)——
即管理端登录页早就在用这套设计,只是 PNG 拷贝,且我此前没认出来。

## 改动

新增 `logo.svg`(由 `logo/纯色横向.svg` 机械变换,**渲染零差异**):

1. `<style>` 的 class 上色 → 内联 `fill`(14 处);
2. 中文 id(`图层_2`/`图层_1-2`)→ ASCII;`data-name` 元数据删除;
3. 删 `<style>` 与空掉的 `<defs>`;加 `<title>`。

> 该 SVG 的 `<text>` 元素数为 **0** —— "艾动智能"字样**已转为路径**,
> 无字体依赖,任何环境渲染都一致。

落点:

| 位置 | 改动 |
|---|---|
| `deploy/landing/logo.svg` | 新增;官网页头 `<img src="logo.svg">` |
| `deploy/landing/index.html` | 删掉 `<span class="top-name">`(字样已在锁标里,否则重复)及失效的 `.top-name` CSS |
| `software/web/public/logo.svg` | 新增;`Login.tsx` 由 `logo.png` 改指 `logo.svg` |
| `software/web/public/logo.png` | **删除**(全仓已无引用,74.7KB 死文件) |

## 渲染等价验证

resvg 逐像素比对(600×164):**393600 字节零差异**。

## 实测(发布后打真实 URL)

```
200  21.1KB  app.anzhizhichong.com/favicon.svg       (tab 图标,方形变体)
200  12.3KB  app.anzhizhichong.com/logo.svg          (官网页头)
200  12.3KB  app.anzhizhichong.com/pms/logo.svg      (管理端登录页·生产)
200  12.3KB  test.anzhizhichong.com/pms/logo.svg     (管理端登录页·测试)
200  21.1KB  test.anzhizhichong.com/pms/favicon.svg
```
三处 logo.svg 均确认含 `<title>艾动智能</title>` 与 `fill="#0366e6"` 路径。

## 一个差点误判的"异常"

`/pms/logo.png` 返回 **HTTP 200**,一度以为旧文件没删掉。
实测其 `Content-Type` 是 **`text/html`**、0.47KB(内容是 index.html)——
nginx `location /pms/` 配了 `try_files $uri $uri/ /pms/index.html`(SPA 回退),
**任何**不存在路径都返回 200。对照测试:访问一个肯定不存在的
`/pms/definitely-not-exist-xyz.png` 同样 200 + index.html。
服务器目录实测已无 `logo.png`。**是 404 的预期写错了,不是文件没删。**

## 验收

- `npm run typecheck`:exit 0
- `npm run build`:exit 0,产物仅 `logo.svg`(12.3KB,无 logo.png)
- 已发布:生产(web + 官网)、测试(web)
- 生产 0 ERROR、域名 401/200;nginx `nginx -t` 通过后 reload
2026-09-21 01:24:58 +08:00
12451
2aa002ca02 feat: 全站统一 favicon.svg(取自品牌 logo,替换 Vite 模板紫色闪电)
## 背景

- `software/web/public/favicon.svg` 一直是 **Vite/Arco 模板自带的紫色闪电**
  (`#863bff`),与项目品牌无关;
- 官网 `deploy/landing/index.html` **完全没有 favicon 引用**;
- 仓库里本来就有品牌 logo:`logo/圆切独立LOGO.svg`(蓝色渐变底 + 白色充电柜
  6 仓 + 柜顶青色闪电 + 右侧白色圆弧 + 底部青色弧线)。

## 做法

新增规范文件 `logo/favicon.svg`,由 `圆切独立LOGO.svg` **机械变换**而来
(不改任何路径数据):

1. `<style>` 里的 class 上色 → **内联成 `fill`/`opacity` 属性**(favicon 渲染器
   对 SVG 内 CSS 支持不一,内联更稳);
2. 渐变 ID `未命名的渐变_5`(中文)→ **`lgBrandBlue`**(ASCII)
   (`url(#中文)` 在部分渲染链路上会断);
3. 删掉 `<style>` 块;
4. 加 `<title>艾动智能 · 充电柜</title>`。

变换前后**渲染等价已用 resvg 逐像素验证**:
- 16px / 32px(favicon 真实尺寸):**逐字节一致**;
- 256px:262144 字节中 3 字节不同,且集中在**同一个抗锯齿边界像素** (173,46),
  最大单通道差 9/255 —— 渐变舍入,视觉无差异。

## 落点

| 位置 | 改动 |
|---|---|
| `logo/favicon.svg` | 新增(规范源) |
| `software/web/public/favicon.svg` | 覆盖(管理端;`index.html` L5 已有 `<link rel="icon">`,无需改) |
| `deploy/landing/favicon.svg` | 新增 |
| `deploy/landing/index.html` | 补 `<link rel="icon" type="image/svg+xml" href="/favicon.svg" />` |

**小程序(h5-uniapp)不适用**:微信小程序没有浏览器 favicon 概念
(无 HTML 入口,实测 `src/` 下无 index.html);tabBar 图标是另一套 PNG,不属 favicon。

## 实测(发布后打真实 URL)

```
app.anzhizhichong.com/favicon.svg        → 200 image/svg+xml 21.1KB  新logo ✓
app.anzhizhichong.com/pms/favicon.svg    → 200 image/svg+xml 21.1KB  新logo ✓
test.anzhizhichong.com/pms/favicon.svg   → 200 image/svg+xml 21.1KB  新logo ✓
```
三处均确认**无旧模板紫色残留**(`863bff`)。

`test.anzhizhichong.com/favicon.svg` 返回 404 是**预期**:测试域名 nginx
`root /data/pms_dev`,官网只挂生产域名。

## 验收

- `npm run build`:exit 0,产物含新 favicon.svg(21.1KB)
- 已发布:生产(web + 官网 favicon/index.html)、测试(web)
- nginx `nginx -t` 通过后 reload;生产 0 ERROR、域名 401/200
2026-09-21 00:54:39 +08:00
12451
c1b44b6e23 feat(web): 能耗页新增「整柜日用电量(电表读数口径)」区块
后端 `GET /api/cabinets/{id}/meter-usage` 上一提交已上线,本提交把它接到既有的
能耗管理页(`/energy`)—— 用户指出已有能源统计页面,无需新建页面。

## 区块内容

- 柜子下拉(复用 `/cabinets/options`,支持搜索)+ 表格(日期 / 电表末读数 / 当日用电 / 数据质量)
- 分页 15 条/页,未选柜子时提示"请先选择柜子"(而不是显示空表格)
- 表格下方一行区间合计(kWh),并注明"无基准与电表更换日不计入"

## ⛔ 与页面既有区块的区别(必须让用户看见,不能混)

区块顶部加了一条 Alert 常驻说明:
> 本区块是电表读数口径(整柜实际用电,硬件计量、含非充电损耗);
> 上方卡片与排行是订单电量口径(平台按 V×I×Δt 积分的估算值)。
> 两者数值必然不一致,不可互相替代。

## 三个语义坑在前端的落地(与后端 flags 一一对应)

| flags | 前端渲染 |
|---|---|
| `no_baseline` | 当日用电显示 **`-`**(**不是 0**) |
| `covers_multiple_days` | 数据质量列显示"覆盖多日"(warning 色) |
| `meter_reset_or_replaced` | 当日用电显示 **`-`**,数据质量列显示"电表更换/清零"(danger 色) |

数据质量列正常时显示"正常"。`flags` 与文案的映射收在 `METER_FLAG_META` 常量表里,
**新增 flag 必须同步该表**(否则会渲染成空白标签)。

## 验证

- `npm run typecheck`:**exit 0**
- **自证能失败**:植入未定义变量 `meterFlagNoSuchVar` → `TS2304` / **exit 2**;还原 → exit 0
  > 附:第一次自证我选错了判据(把接口的 `usage_wh?` 改成必填),typecheck 不报错 ——
  > 因为接口数据源是 `any`,类型由我自己声明的接口决定,改必填确实不冲突。
  > 换成编译期必错的未定义变量后才验证成功。**是判据选错,不是工具假绿。**
- `npm run build`:exit 0(产物 572 KB / 6 个文件)
- 后端全量验收仍绿:`cargo check` 0 告警、`cargo test` **204 passed**、
  `device_status_parse` 16/16、`rssi_signal` 9/9、结构漂移无
- **端到端**(临时超管账号 + 自签 JWT,用完即删):造 5 条读数打真实 HTTPS,
  断言前端渲染所需的每个字段都在、`flags` 取值与 `METER_FLAG_META` 的 key 一一对应、
  三个语义坑全部生效、汇总 500 正确 ⇒ **exit 0**
- 已发布:测试 + 生产(web 产物时间戳与本地构建一致 `00:19:40`,
  生产 0 ERROR、域名 401/200;测试环境 nginx reload 后三服务 active)
2026-09-21 00:25:11 +08:00
12451
401f84bda6 chore: 清掉误入库的 agent 工具记忆目录与 Vite 模板 README(内容先按主题归位)
## 起因

用户问"现在确定所有文档都是按分类存放没重复和过期内容了吧" ——
**不能凭感觉答"是"**,于是做了一次针对**已跟踪文件**的体检(重复/断链/过期陈述三维度)。
第一轮体检结果:重复 0 组、断链 0 处,但**又扫出 3 处该处理的东西**。

## 一、`.workbuddy/memory/` —— 另一个 agent 工具的记忆目录被误入库

3 个文件(`MEMORY.md` / `2026-07-02.md` / `2026-07-07.md`),**全仓零引用**,
最后一次"提交"是搭车进 `feat(protocol): TCP v1.1 协议对齐` 这类无关提交 ——
与之前那几张误提交的图片同一模式。

- ⚠️ `2026-07-02.md` 里含一处**生图 API Key 片段**(`sk-ib7i...M2z`);
- `.gitignore` 里 `.codebuddy/` 早有,但 `.workbuddy/` **漏了** ⇒ 才被顺手提交进来。

### 处理:内容先归位,再删目录(不是一删了之)

按 `docs/README.md` 已确立的原则"**过时就删;仍有价值的归到正确的类**",
把 `MEMORY.md` 里仍有价值的三类内容分别落位:

| 内容 | 归到哪 | 为什么 |
|---|---|---|
| UI 颜色系统 / 背景容器 / 36 仓通道卡布局 | **`docs/tech/小程序用户端.md`** 新增「附:UI 设计规范(视觉口径,改动界面前必读)」 | 这是页面视觉口径,属小程序文档 |
| Ardot `batch_edit` 的 `fills`/`strokes`/`cornerRadius` 写法坑 + 设计稿 7 屏 ID | **`docs/compound-notes/2026-07-ardot-batch-edit-属性写法.md`** | 工具特有的单点教训,正是 compound-notes 的定义 |
| "开发前必须确认需求"、"生产环境须授权"、落地页明亮风格偏好 | **`AGENTS.md` 新增「操作规则」小节** | 项目级规则,须常驻加载 |

然后把 `.workbuddy/` 从版本控制移除并**加入 `.gitignore`**(连同 `.opencode/`、`.codegraph/`),
堵住同一个坑再犯。

## 二、`software/web/README.md` —— 建站模板残留

23 行里 9 行是 Vite/plugin-react/Oxlint 的模板话术(英文、与项目无关)。
`api-server` / `device-server` / `h5-uniapp` 三个工程**都没有** README —— 构建方式统一在
`AGENTS.md` 与 `docs/tech/部署手册.md` 里。⇒ **删除**。

## 三、一个我自己的误判(已纠正,记录以免后人重复)

我一度怀疑 `docs/tech/API.md` 接口表"少 12 行"(我数出 68 行,而 `mod.rs` 有 80 个 `.route(`)。
复核发现 **`API.md` 自己写明了计数约定**:"一行 = 一次 `.route(...)` 注册,共 80 行;
展开后 93 对" —— 而我的正则 `^\|\s*(GET|POST)\s*\|` **漏掉了 `| GET / POST |` 这种链式写法**。
用能匹配链式的正则重数 = **正好 80** ⇒ **`API.md` 是准确的,我的怀疑是错的**。

## 校验(只扫 git 已跟踪文件)

- 已跟踪文件 **217**(原 221 → 删 3 个工具记忆 + 1 个模板 README);
- 文档内容重复:**0 组**;
- 已跟踪 md 的本地链接:**0 断链**;
- `.workbuddy` 已跟踪文件数 = 0、工作区已不存在。

## 仍**刻意保留**的(不是遗漏)

- `生产环境.txt`:**唯一凭据清单**且被 `docs/tech/小程序用户端.md` 引用为密钥记录处 ——
  上一提交已把"不入代码仓库"这句与事实矛盾的表述改准;是否改走 `.gitignore`+密码管理器由你定。
- `docs/README.md` 的「旧名→现名映射」表 / 处置记录 / 自证方法:**必须**出现旧名,属预期。
- `docs/compound-notes/TEMPLATE.md`(94 B):新建笔记的模板,给该目录提供格式约束。
- `tasks/002-未结待办-*.md`:两项未结(`energy` 等硬件答复、长函数重构),
  `tasks/` 本身已 gitignore、不入库。
2026-09-20 05:04:53 +08:00
12451
aa02cd4e58 fix(mp): 接入 vue-tsc 真类型检查并修掉它抓出的两个真实缺陷
小程序端此前**没有任何类型门禁**:tsc 不检查 .vue,而 9 个页面全是 .vue,
npm run build:mp-weixin 只验语法不验类型。接入 vue-tsc 后立刻抓到 17 个错误,
其中两个是真 BUG:

1. me/index.vue:写 const user = authStore.user 之后又写 user.value ——
   Pinia setup store 对外已把 ref 解包,user.value 恒为 undefined
   ⇒ **头像上传成功后写回 store 的那行是空操作**(要等冷启动才显示新头像);
   且直接取值是快照、丢掉响应式。改为 computed(() => authStore.user),模板无需改动。
2. api/index.ts:CompartmentDetail 漏了后端确实会返回的 temp
   (routes/h5.rs 的 COMPARTMENT_REALTIME_FIELDS 第 6 项,有单测守门)
   ⇒ compartment-detail 页读 data.temp 报 TS2339。已按后端契约补上。

其余 12 处是宿主全局 wx 未声明(@dcloudio/types 只声明跨端 uni),
在 env.d.ts 里**只声明实际用到的两个 API**(刻意不写 [k:string]:any,
否则等于把这一层检查关掉)。

顺带清理:
- 删除死页面 pages/devices/index.vue(不在 pages.json、无任何跳转引用,grep 确认);
- 删除仓控板「在线/离线」徽标:cabin_boards.status **全后端没有任何写入方**
  (生产库实测取值只有 0),该徽标恒定显示"离线",属对用户的谎报;
  协议层面也没有"仓控板在线"这个概念(TCP 长连接的是柜控板,按 IMEI 标识)。
  按「柜的就显示到柜子」+「不显示推不出来的状态」去掉,并在类型上加禁止说明。

已按仓库铁律自证门禁能失败:植入 export const probe: number = 'x' → exit 2;撤销 → exit 0。
2026-09-20 04:31:42 +08:00
12451
61adcdeaf9 fix(security)+fix(api)+docs: 越权面修复、登录门控与状态语义对齐、文档全面校对
- 越权面(全部 before/after 实测可复现,10 条):cabinets/filter 的 project_id/organization_id
  参数分支越权(客户可拿对方向柜全字段)、organization-tree 返回全量组织树、
  cabin-boards/compartments/cabinets 三个 options 无归属校验、batteries 无归属(改为经
  charge_orders→cabinets→projects 间接归属)、feedback 跨组织读且能改他人状态、
  GET /downloads/{id} 漏 user_id 过滤(跨用户元数据泄露)、
  POST /downloads/cleanup 全局删除所有组织导出(改客户侧只清自己)、
  /api/nodes/* 仅需登录(客户可读内网 IP 并注销节点)→ 收紧 role_level>=2;
  /api/devices/stats 同样收紧;新增共享闸门 auth::require_super_admin
- TCP 登录门控:下行投递改为"登录前白名单"(PRE_LOGIN_ALLOWED_ACTS),与上行同一规则
- 60 秒超时对齐协议:挂起项 TTL 120→60 + 值内嵌 deadline 精确判窗 + 超时 WARN(兼容老格式)
- 下发前在线判据改连接级认证标记 device:authed:{imei}(消除"管理端显示成功、设备收不到")
  ⚠️ device-server 与 api-server 必须同批发布
- 协议对齐:auth_str 登记校验提到限流之前、build_spec Topology 分支 clamp(15/8)、
  行号引用改小节引用、修 3 处错引小节
- 状态语义:compartments.status 只表占用,充满/故障由通道状态派生;柜级 status1/status2
  与开门按钮(柜级独立动作不联动);列注释对齐(cabinets.status 补 2=禁用、
  charge_orders.status 标注 2 保留未使用)
- 假绿单测修复:超时 SQL 抽常量 + 纯函数孪生体双向断言(方向写反必红)
- RSSI 两端统一为同源纯函数 5 档(小程序显示随之变化)+ test/rssi_signal.test.mjs
- nginx:扫码入口 abstract 限流(zone=scan_abstract_zone 30r/m burst=20)+ 主配置仓库镜像
- 文档:AGENTS.md 新增「踩过的坑」7 条教训 + 部署注意事项 11 条 + 文档口径优先级;
  docs/API.md 接口总表 80 条与 mod.rs 逐条一致、Redis 契约按代码重写、9 处事实错误修正;
  权威协议文档 v1.1.1(协议行为未变,仅修订文档自身的错误/矛盾/缺失)
2026-09-20 03:23:00 +08:00
12451
ee92102f3c feat(scope)+fix(db)+feat(security): 数据范围(项目级)生效、库列类型对齐、安全加固、状态位分层展示、能耗实时聚合
- user_scopes 由"假开关"做成生效:粒度到项目级(组织隔离 ∧ 项目∈范围,取交集);未配置=不限制(逐字向后兼容);
  总管理员豁免;脏数据保守忽略不锁死;配置期校验存在性+归属;逐接口叠加(列表/详情/写操作/Excel 导出落 scope_project_ids);
  前端新增"数据范围"弹窗入口。顺带修 GET /api/charge-orders 此前完全没有组织隔离(跨租户泄露)、
  GET /api/projects/{id}/cabinets 的 500
- 数据库列类型系统性对齐:扫描并修复 pms_dev 32 项 / pms 18 项漂移(时间列统一 DATETIME,sqlx NaiveDateTime 只兼容 DATETIME);
  DECIMAL→BIGINT 与列注释口径拉平;修复生产 GET /api/charge-orders、GET /api/device-logs 必然 500 的根因;
  恢复 7 张表丢失的 updated_at ON UPDATE(行为变化,已记入 AGENTS.md);修正 SCHEMA_SQL 前向引用(空库建表必崩);
  新增 tools/db_schema_diff.py(漂移检查,退出码可接部署前置)与 test/schema_alignment_e2e.py
- 安全加固四项:上传去 .svg + 魔数嗅探(堵存储型 XSS);登录失败锁定(5次/15分)+challenge 限流+并发闸门+验证码容差收紧;
  服务降权 root→pms;nginx 安全头 + limit_req + CORS 白名单收敛
- TCP 连接登录门控(上行+下行同一规则):login 前只放行 auth_str/login(PRE_LOGIN_ALLOWED_ACTS)
- 状态位按归属层级展示:柜级 status1/status2 显示在柜子(含柜门 + 开门按钮,独立动作不联动业务)、
  仓体级 batt_status/pow_status 显示在仓体;两端解析函数同源同名
- 能耗报表改实时聚合 charge_records(原 energy_stats 全表只读不写导致报表恒 0)+ 补复合索引
- 组织可见性:新增 GET /api/my-organizations(工厂全量、客户仅本组织)
- 仓体 status 仅表占用,充满/故障由 Redis 通道状态派生
- 订单/占用/队列运维缺口:h5 建单漏单、214 仓体自愈、device:reports 上限、死信 TTL、LimitNOFILE、
  每日 mysqldump + 回滚点保留 5 次
- 文档:AGENTS.md 补层级可见性/数据范围/状态位约定/数据库工具/行为变化;docs/API.md 删除废弃 TCP 章节、接口总表对齐 mod.rs
2026-09-20 02:44:51 +08:00
12451
6b562ae286 chore(web): 修复失效的类型检查命令(tsconfig 是 solution 文件, sc --noEmit 空跑=假绿)→ 新增 typecheck 脚本用 tsc -b;并收尾前端契约修复(路由守卫/分页/查询竞态/类型补全) 2026-09-20 00:44:35 +08:00
12451
f80b3b634c fix: 五角度审查后的安全/正确性/契约修复(含 order_timeout 反条件、连接接管、权限兜底)
## 严重问题
- order_timeout: 超时条件方向写反(`start_time < TIMESTAMPADD(HOUR,10,NOW())` 等价于
  `start_time < NOW()+10h`,条件恒真)→ 活跃订单会被当超时单提前结掉。改为
  `TIMESTAMPADD(HOUR, ?, start_time) < NOW()`,并补"刚建单不命中/超期命中"双向断言
- C1 device-server 连接池可被未验签的报文 dev_id 顶替(覆盖式 insert + 断连无条件 remove):
  改为 conn_id 归属模型 —— 唯一连接标识、remove_if/touch/is_authenticated 均带 conn 归属校验、
  接管时用 oneshot 信号主动驱逐旧连接、每条上行前用 is_owner 复核持有者身份
- C2 POST /api/devices/{imei}/command 零归属校验 + act 自由字符串 + 把 IMEI 塞进协议 id:
  补 scope_guard 归属校验 + act 白名单 + id 从 params 显式取(拒绝 15 位 IMEI)
- 非总管理员权限集恒为空(user_roles 从不写入且无分配入口)→ 全站 403:
  user_roles 为空时按 users.role 回退内置角色权限 + 建/改用户幂等同步映射(仅角色变化时同步)

## 订单生命周期并发正确性
- finalize_order 先取电量累加器后抢结束权 → off/bat_out 成对到达时 energy 被写 0:
  改为先原子抢占(WHERE compartment_id=? AND status=0 ORDER BY id DESC LIMIT 1),
  抢到者才 take_and_reset 取电量;抢不到直接返回且不碰累加器
- 建单"先查后插"无锁无唯一约束 → 同仓体两笔活跃单:
  条件插入(INSERT ... SELECT ... WHERE NOT EXISTS)+ 按仓体进程内锁
  + DB 层 active_flag 生成列 + UNIQUE 唯一索引(幂等迁移,有重复则记 ERROR 跳过,绝不阻断启动)
- handle_bat_out: updated==0 时不再复位占用;通道登记删除改为 battery_id 一致才删;
  新增 should_finalize_bat_out 挡住"迟到 bat_out 结束新单"
- 电压/电流加量程夹取(0–100000 mV/mA 之外按 0 计并 WARN),防位翻转导致积分暴增

## 协议/契约
- spec 绝不可能是 "0000"(生产 42/68 台拓扑为空):新增 build_spec 三级兜底
  (真实拓扑 → cabinets.cell×6 → 6×6),兜底生效记 WARN 报出拓扑数据
- /api/cabinets/realtime-status 改读真实字段(原来读的设备链路从不写的旧字段,恒返回 idle/空),
  用 derive_channel_view 推导旧字段以保持响应字段名不变;修掉"把通道号当板号"的幽灵板问题
- 小程序通道详情/PC 详情改读通道 Hash(弃用无写入方的 compartment:status:{id}),
  取不到返回 null 而非假的 0;补 voltage/current/temp/batt_status/pow_status
- 反馈列表补权限码 operation_log:view + status 筛选真正落到 SQL
- 小程序柜温换算(29800 曾被显示成 29800°C);签名注释 7→8 位;msg_id 序列化省略 null;
  parse_compartment_id 与 status_post 补板号 1–15/通道 1–8 量程校验(不再写 board:0 垃圾 key)

## 文档
- docs/柜控板固件对接文档.md:确立数值口径统一规则(hex 字节流=原始硬件单位·平台换算;
  JSON 数值字段=平台单位·设备换算)、修正 A.5.3 的 id 说明、作废变更记录原位标注(不删历史)、
  spec 表述改为"按拓扑现算+兜底、恒 4 位不会是 0000"、修复文首 7 项表格被引用块切断丢表头、
  修正 §10 误引 channels[].channel、写明无电池兜底阈值与 #16 风险

## 运维加固(服务器侧,非代码)
- 关闭 MySQL/Redis 公网暴露:nginx stream 由 0.0.0.0 改为只监听 127.0.0.1 + ZeroTier
- worker_shutdown_timeout 30s(原先每次 reload 泄漏永不退出的 worker)
- 测试证书改指向 app 证书(原测试证书 9 天后过期且无续期机制);补测试环境 uploads 目录;.env 0600

## 验收
- cargo check 两服务 0 error / 0 warning;cargo test api 88 + device 22 全过
- 路由 79 → 79 逐条一致;web tsc 干净
- 端到端联调(测试环境)13/13,订单结算与重构前逐字段一致;唯一索引已由迁移真实建出
- 生产已部署并验证:服务 active、health 200、ERROR 0、消费链路正常、迁移已建索引
2026-09-20 00:36:37 +08:00
12451
999885c38f feat(protocol): TCP v1.1 协议对齐 + 平台侧兜底 + 生产部署修复
协议(唯一权威文档:docs/柜控板固件对接文档.md)
- 应答必带 act(与请求一致)、msg_id 与请求一致;判"应答 vs 指令"只看有没有 suc
- dev_id 只在 auth_str/login 携带;其余报文用内部字段 _dev_id 路由(下发前由 device-server 摘除)
- id 只表示动作目标(柜级省略 / 一段=单板 / 两段=仓体级),不再含 IMEI
- msg_id 每连接从 1 起自增;60 秒交互超时;status_post 每通道 18 字符 hex 全量上报
- 未登录连接 30 秒内未 login 即断开;未登录只放行 auth_str/login

平台侧实现
- 上行解析按新结构重写(cabinet/boards/battery),删除废弃的 status/parsed_status
- 订单:唯一入口 = on;bat_in 只登记电池台账;4 条结束路径统一走 finalize_order(含充电记录落库)
- 新增设备日志保留策略(90 天,分批删除);未登记 IMEI 静默丢弃;停电标记按 status1 bit0 恢复清除
- 待确认第 13 项兜底:平台侧按 电压 × 电流 × 时间 积分计算订单电量(workers/energy.rs)
- 待确认第 18 项兜底:status_post bit7 连续 2 次无电池 → 判定电池被拔走并提前结单

修复(均为部署后才暴露的问题)
- 队列消费禁用 BRPOP:无限阻塞会因代理掐连接永久卡死(零错误日志);有限超时也只是
  "超时才醒",下行指令延迟 15~45 秒 → 改为 RPOP 非阻塞轮询,延迟降到 0.1~0.2 秒
- sync_iccid 把可空列 iccid 按 Option<String> 解码 → 整条 login 失败(ICCID 未上报的新柜子永远登录不上)
- 迁移读 information_schema.COLUMN_COMMENT 需 CAST(... AS CHAR)(MySQL 8 驱动报 VARBINARY,否则迁移 panic)
- 部署注意事项:.env 不支持行内注释;pscp 上传需 chmod +x;运行中二进制需 .new + mv 原子替换

部署
- 生产迁到 API 9000 / TCP 9001(systemd 单元补 EnvironmentFile、nginx 同步指向)
- 新增 deploy/deploy-prod.ps1(本地一键:构建→校验→备份→原子替换→重启→验收→无副作用探测)
- 废弃 deploy-prod.sh / deploy-test.sh(原脚本会在 target/release 找产物、装重复 systemd 单元、
  并用本地 .env 覆盖线上 .env)

质量
- 单测:api-server 39 个 / device-server 13 个(含协议示例逐值断言、跨模块 Redis key 契约)
- 两服务 cargo check 零错误零告警;web tsc --noEmit 通过
- test/tcp_e2e_smoke.py 端到端联调 13/13 通过
2026-09-19 23:41:09 +08:00
12451
b553c4e519 feat: API 统一 + 设备详情补全 + H5 导航优化
- API 路由统一: 移除 /api/h5/,统一 /api/pms/ 入口
- 二进制改名: api-server -> pms-api(与服务名一致)
- nginx: 合并 /api/ + /pms/api/ 为 /api/pms/
- 新增 docs/API.md 接口文档
- 后端: CabinetDetail 增加 rssi/pow_fail/address/soc 等字段
- Redis 管道批量读取仓体实时数据(SOC/电压/电流/功率)
- DB: 所有 TIMESTAMP 列改为 DATETIME(修复类型不匹配)
- 用户管理: 新增创建时间/修改时间列,状态改用 Switch
- 角色管理/用户管理: 操作按钮改为图标
- PC 设备详情: 基本信息补全 RSSI/ICCID/电源/地址
- PC 卡片: 通道统计改用 Lucide 图标,颜色统一
- PC 二维码: canvas 渲染,含编号文字,支持右键下载
- H5: 按 uni-app 规范修正导航(navigateTo 替代 reLaunch)
- H5: 详情页加返回导航条
- H5: 扫码支持摄像头(jsQR)+ ?id= 参数
- H5: 底部 tabBar 在所有页面常显
- H5: 首页统计改为 6 项(设备/通道/空闲/充电/充满/故障)
- H5: 下拉刷新 / 上拉加载
- 产品首页: 添加业务联系
- 文档: 清理过时文件,补充缺失规范章节
2026-07-04 04:03:02 +08:00
12451
06765f99db feat: H5 用户端 Taro 重写 + 前后端接口对齐修复
- 使用 Taro + React 重写 H5 用户端(替换旧的 React+Vite+H5 实现)
- 6 个页面:登录、首页、设备列表、设备详情、仓体详情、个人中心
- Zustand 状态管理 + API 层封装 11 个接口
- 后端 H5 路由:新增仪表盘/项目/设备/充电控制接口,组织数据隔离
- 扫码入口:/h5/{abstract_id} → 免认证查设备 + 登录后跳转
- 管理端:新增绑定码管理、二维码展示下载、设备绑定/替换 IMEI
- 产品落地页更新(hero-bg + logo 替换)
- 接口对齐修复:修正 action 路径/请求体,DashboardData 和 CabinetItem 字段匹配后端
- 布局兼容性:grid → flex,提升移动浏览器兼容性
- 移除旧 H5 项目 software/h5/
2026-07-03 02:45:00 +08:00
12451
7290da8eae feat: 卡片显示真实通道状态统计(空闲/充电/充满/故障) 2026-07-02 22:09:19 +08:00
12451
8a2e2ffc2b fix: 修复树节点点击选择逻辑,支持多级组织 2026-07-02 21:38:50 +08:00
12451
69faa75310 feat: 支持多级组织机构,组织树可展开显示下级 2026-07-02 21:32:06 +08:00
12451
2453f680ac refactor: 拆分组织树和设备列表为独立接口,支持按组织/项目筛选 2026-07-02 21:20:06 +08:00
12451
a9e720fe02 feat: 后端返回ICCID,卡片显示IMEI/ICCID换行,状态图标 2026-07-02 20:54:37 +08:00
12451
c2c70be3b3 feat: 卡片添加地址字段,中文名字,优化布局 2026-07-02 20:37:24 +08:00
12451
da9296a407 fix: 简化右列布局,使用flex column 2026-07-02 20:00:57 +08:00
12451
0de202d87c fix: 添加width:100%确保右列正确填充 2026-07-02 19:58:43 +08:00
12451
5515cad8f8 fix: 修复右列横向滚动问题,设置minWidth:0和overflow 2026-07-02 19:50:29 +08:00
12451
42dd54a255 fix: 删除CabinetGrid重复的设备列表标题 2026-07-02 19:48:06 +08:00
12451
8d531b34a3 feat: 设备管理页改为左右两列布局 2026-07-02 19:26:23 +08:00
12451
1be7480472 feat: 组织架构树只显示到项目节点,不显示柜子 2026-07-02 19:18:47 +08:00
12451
df44a959f5 fix: BrowserRouter添加basename=/pms,修复无限循环 2026-07-02 18:40:56 +08:00
12451
222ceb4d13 fix: 修复路由路径为相对路径,避免重定向到根目录 2026-07-02 18:34:39 +08:00
12451
9b5789fb75 refactor: H5和WEB前端分离,H5独立项目,扫码路径改为/h5/ 2026-07-02 18:23:13 +08:00
12451
ee330cd149 fix: API_BASE改为/pms/api,修复404问题 2026-07-02 18:10:34 +08:00
12451
d16a0fc545 feat: 重新设计登录页,深色科技风格 2026-07-02 17:56:18 +08:00
12451
b7546ec7d8 fix: 设置vite base为/pms/,修复资源路径问题 2026-07-02 17:53:58 +08:00
12451
c3eed024b6 feat: 柜子详情页添加接触器控制按钮(pow_on/pow_off) 2026-07-02 17:27:32 +08:00
12451
652bde44c4 fix: nginx配置修正Gitea路由 /doc/ 而非 /pms/doc/ 2026-07-02 15:18:45 +08:00
12451
12395ee9e2 init: PMS充电柜管理系统源代码 2026-07-02 05:38:01 +08:00