charging-cabinet/README.md
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

158 lines
9.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 充电柜矩阵系统 (Power Matrix System / PMS)
产品名:安知充(anzhizhichong),品牌名:艾动智能
## 项目简介
36仓体直流电瓶车充电柜,仓数不固定:1柜控板+N仓控板,每仓控板管6仓,最小6仓。
## 技术栈
- **后端**:Rust + Axum
- **前端**:React + Arco Design Pro(管理端)
- **微信小程序用户端**:uni-app + Vue 3(构建目标 mp-weixin)
- **数据库**:MySQL(阿里云RDS)
- **缓存**:Redis(阿里云Redis)
- **设备通讯**:TCP长连接(柜控板4G模块,Air780E Cat.1)
## 项目结构
```
charging-cabinet/
├── tasks/ # 任务文件(opencode执行)
├── docs/ # 开发文档、协议文档(API.md、柜控板固件对接文档.md …)
├── deploy/ # 部署配置与脚本
│ ├── nginx/ # Nginx 配置(app / test 两份,与线上一致)
│ ├── *.service # systemd 单元(pms-api / pms-device × prod·test)
│ ├── deploy-prod.ps1 # 正式环境一键部署(推荐入口)
│ └── deploy-*.sh # ⛔ 已废弃(执行即报错退出)
├── tools/ # 开发工具
│ ├── db.py # 数据库工具(默认连测试库 pms_dev)
│ └── db_schema_diff.py # 结构漂移巡检(测试库 vs 生产库 / vs 代码权威 DDL)
├── software/
│ ├── api-server/ # Rust API服务(产物 pms-api)
│ ├── device-server/ # Rust TCP设备服务(产物 pms-device-server)
│ ├── web/ # React管理端前端
│ └── h5-uniapp/ # 微信小程序用户端(uni-app + Vue 3,构建目标 mp-weixin)
├── hardware/ # 硬件资料(原理图/PCB等)
└── test/ # 测试/验收脚本
├── tcp_e2e_smoke.py # TCP 协议端到端(模拟柜控板打测试环境 :3001)
├── schema_alignment_e2e.py # 结构对齐端到端(造临时行并自动清理)
├── device_status_parse.test.mjs # PC 与小程序状态位解析一致性
├── rssi_signal.test.mjs # PC 与小程序 RSSI 分档一致性(0–99 逐值)
└── security-hardening/ # 安全加固探针脚本
```
> 本仓库不存放技能库(skills)。开发期技能从全局技能目录加载(`~/.dsh/skills`、`~/.agents/skills`),请勿在仓库内重新 vendor。
## 部署架构
| 环境 | 域名 | API 服务 | TCP 服务 | MySQL | Redis DB | 部署路径 | 前端路径 |
|------|------|----------|----------|-------|----------|----------|----------|
| 正式环境 | app.anzhizhichong.com | `:9000` | `:9001` | `pms` | db5 | `/data/pms` | `/pms/` |
| 测试环境 | test.anzhizhichong.com | `:3000` | `:3001` | `pms_dev` | db6 | `/data/pms_dev` | `/pms/` |
> ⚠️ 两个环境**完全隔离**(独立库 + 独立 Redis DB + 独立端口 + 独立部署目录)。
> ⛔ **绝不要把测试域名的 API 指向生产端口 `:9000`** —— 历史事故:测试 nginx 曾指向当时的生产端口,
> 等于"测试后台在操作生产数据"。判断方法:`test` 域名 `/api/pms/` 返回 **401**(有测试后端)是**对的**;
> 返回 **502** 说明测试后端没起。
> ⚠️ 代码里的默认端口(api `3000` / tcp `3002`)只在**没设环境变量**时生效,线上由 `.env` 的
> `PORT` / `TCP_PORT` 覆盖 —— 以本表为准。
**服务器**:CentOS Stream 9(2核/3.5G/49G),`118.31.119.206`(ZeroTier `10.8.0.252`)
**内部服务**:
- Gitea: https://app.anzhizhichong.com/doc/
- 文档Wiki: https://app.anzhizhichong.com/doc/anzhizhichong/charging-cabinet/wiki/
**部署方式**:本地交叉编译(`cargo zigbuild --release --target x86_64-unknown-linux-gnu`)后传二进制,
**服务器上不装 Rust/Node**(会 OOM)。一键脚本与「部署注意事项」见 `AGENTS.md`「部署与维护」。
## 服务架构
```
┌─────────────────────────────────────────────────────────────┐
│ API服务 (api-server / pms-api) — 主服务 │
│ - HTTP API(后台管理 + 小程序)端口: 正式 9000 / 测试 3000 │
│ - 业务逻辑处理(验签/存DB/建单结单/告警/能耗) │
│ - 节点管理(设备服务注册/心跳) │
│ - 后台 Worker:上报消费(RPOP 200ms 轮询) / 回执 / 订单超时 / │
│ 日志清理 / 占用对账 │
└─────────────────────────────────────────────────────────────┘
↑ Redis LIST device:reports ↑ Redis LIST device:commands
│ (设备上报,device-server LPUSH) │ (平台指令,api-server LPUSH)
│ │ device-server 按 HGET device:{imei} node_id
│ │ 路由到 device:commands:{node_id}
┌─────────────────────────────────────────────────────────────┐
│ 设备服务 (device-server / pms-device-server) — 轻量化 │
│ - TCP监听: 正式 9001 / 测试 3001(设备长连接) │
│ - 只处理通信和指令解析(归属/签名/业务全在 api-server) │
│ - 不存DB、不验证签名、不处理业务 │
│ - 节点注册/心跳:Redis `node:{node_id}`(含 ip/tcp_port/ │
│ status/last_heartbeat),TTL = DEVICE_REGISTER_TTL │
│ - 设备路由:`device:{imei}` 的 `node_id` 字段 │
└─────────────────────────────────────────────────────────────┘
```
**核心设计原则:**
1. **TCP服务轻量化** — 只处理TCP通信和指令解析,不处理业务逻辑
2. **Redis做缓冲** — 服务间通信用Redis LIST,不用HTTP直接调用
3. **全异步处理** — 所有指令异步处理,msg_id匹配响应
4. **协程处理任务** — API服务用tokio::spawn处理Redis队列消息
**通信流程:**
- **设备上报**:设备 → TCP → 设备服务(`LPUSH device:reports`,带 `LTRIM` 上限)→ API服务 worker 消费
- **平台指令**:API服务(`LPUSH device:commands`)→ 设备服务按 `node_id` 路由 → TCP → 设备
- **指令响应**:设备 → TCP → 设备服务 → Redis LIST → API服务按 `msg_id` + `act` 匹配回执
- **死信**:投递失败的指令进 `device:commands:dead`
> ⚠️ **队列消费一律用 `RPOP` 非阻塞轮询(200ms)+ 空队列 `sleep`,禁止 `BRPOP`** ——
> Redis 经 nginx stream 代理 + 多路复用连接时 `BRPOP` 语义不可靠(`BRPOP 0` 会永久卡死且无日志)。
> 依据:`workers/report_worker.rs` 的 `REDIS_POLL_INTERVAL_MS` 注释。
**节点管理:**
- 设备服务启动时注册到 Redis `node:{node_id}`(并调用 API 的注册接口)
- 每 **60 秒**心跳(`HEARTBEAT_INTERVAL=60`),**180 秒**无心跳视为离线(`DEVICE_REGISTER_TTL=180`,
线上 `.env` 显式设置;代码默认值是 300,仅在没有该环境变量时生效)
- 支持多节点扩展(当前单节点 `node-1`)
## TCP 设备通讯协议
> TCP 设备通讯协议见 **[docs/柜控板固件对接文档.md](docs/柜控板固件对接文档.md)**(唯一权威,v1.1)。本文不再重复。
> 全局整数单位(voltage mV、current mA、temp K×100、soc ‰)见 [docs/API.md](docs/API.md#数据单位规范)。
> 平台侧 Redis 实时状态的实际存储结构(key/字段/写入时机)见 [AGENTS.md](AGENTS.md#redis-实时状态存储)。
## 开发任务与质量约束
- 任务需求写到 `tasks/` 目录的md文件(编号递增)
- 通过 Agent 子代理包裹 opencode 执行,异步通知
- 质量约束模板固定追加到每个任务文件(模板见 [AGENTS.md](AGENTS.md#质量约束模板))
- 完成代码后执行对应栈静态校验:**Rust → `cargo check`**,**TS → `npm run typecheck`(即 `tsc -b`)**,零报错零警告
- ⛔ **不要写 `tsc --noEmit`**:`software/web/tsconfig.json` 是 solution 型(`files: []` + `references`),
不带 `-b` 时它**一个文件都不编译、直接 exit 0**,是假绿。实测依据见 [AGENTS.md](AGENTS.md#踩过的坑历史教训改动前必读) 「踩过的坑」第 2 条
## 验收与自检
```bash
# 后端:静态校验 + 单测
cd software/api-server && cargo check --all-targets && cargo test --bin pms-api
cd software/device-server && cargo check --all-targets && cargo test --bin pms-device-server
# 前端:真实类型检查
cd software/web && npm run typecheck
# 结构漂移巡检(改了表结构/迁移必跑)
python tools/db_schema_diff.py
python tools/db_schema_diff.py --vs-ddl
# 端到端
python test/tcp_e2e_smoke.py
python test/schema_alignment_e2e.py --env dev --jwt-secret <JWT_SECRET>
# 两端一致性(PC ↔ 小程序)
node test/device_status_parse.test.mjs
node test/rssi_signal.test.mjs
```
完整验收流程(含自证能失败的要求)见 [AGENTS.md](AGENTS.md#验收流程)。