# AGENTS.md - 充电柜项目工作手册 ## 项目简介 充电柜矩阵系统(Power Matrix System / PMS) 产品名:安知充(anzhizhichong),已有充电站平台在运行,充电柜是新增产品线。 仓数不固定:1柜控板+N仓控板,每仓控板管6仓,最小6仓。 ## 设备标识 - **平台抽象ID**:`10-00000000`(绑定柜控板,二维码对应此ID) - **设备ID**:4G SIM卡15位IMEI(10进制) - **仓体全局ID**:`平台层ID-仓板ID-仓体ID` - **扫码链接**:`https://app.anzhizhichong.com/h5/{抽象ID}` ## 技术栈 - 后端:Rust + Axum - 前端:React + Arco Design Pro(React版) - H5用户端:uni-app + Vue 3 + Pinia(跨平台,支持 H5 / 微信小程序) - 数据库:MySQL(阿里云RDS),开发/生产共用 - 缓存:Redis(阿里云Redis),开发/生产共用 - 设备通讯:TCP长连接(柜控板4G模块,Air780E Cat.1) - 权限:RBAC,精确到按钮级别(权限码如 device:door:unlock) ## 项目结构 ``` D:\charging-cabinet\ ├── tasks/ # 任务文件(opencode执行) ├── docs/ # 开发文档、协议文档 ├── deploy/ # 部署配置文件 │ ├── nginx/ # Nginx配置 │ ├── *.service # systemd服务单元 │ └── deploy-*.sh # 部署脚本 ├── tools/ # 开发工具 │ └── db.py # 数据库工具 ├── software/ │ ├── api-server/ # Rust API服务 │ ├── device-server/ # Rust TCP设备服务 │ ├── h5-uniapp/ # H5用户端(uni-app + Vue 3) │ └── web/ # React前端(WEB管理端) ├── hardware/ # 硬件资料(原理图/PCB等) └── test/ # 测试用例/报告 ``` ## 部署架构 ### 环境规划 | 环境 | 域名 | API服务 | TCP服务 | Redis DB | 部署路径 | URL路径 | |------|------|---------|---------|----------|----------|---------| | 正式环境 | app.anzhizhichong.com | :9000 | :9001 | db5 | /data/pms | /pms/ | | 测试环境 | test.anzhizhichong.com | :3000 | :3001 | db6 | /data/pms_dev | /pms/ | **服务端口分配:** - 测试环境:API 3000,TCP 3001 - 正式环境:API 9000,TCP 9001 **部署路径:** - 产品介绍页:/data/landing - 正式环境:/data/pms(api-server、device-server、web、h5) - 测试环境:/data/pms_dev(api-server、device-server、web、h5) **Nginx配置:** - 正式: /etc/nginx/conf.d/app.anzhizhichong.com.conf - 测试: /etc/nginx/conf.d/test.anzhizhichong.com.conf ### 服务器信息 - **外网IP**: 118.31.119.206 - **内网IP**: 172.25.42.239 - **系统**: CentOS Stream 9(2核/3.5G/49G) - **ZeroTier**: 网络ID `63e843dd42f54b9c`,服务器节点 `12a91dd47d`,IP `10.8.0.252` - **本地电脑ZeroTier IP**: `10.8.0.248` - **已装软件**: Nginx 1.20.1、ZeroTier 1.16.2、socat(开机自启) - **服务**: mysql-proxy(3306→RDS)、redis-proxy(6379→Redis)、Gitea(3080)、acme.sh - **不装**: Docker/Node/Rust(本地编译传二进制) - **SSH**: plink连接,首次需 `echo y` 接受host key ### 正式环境数据库 - MySQL: `rm-bp1bb7yiv5n8z4uvp.mysql.rds.aliyuncs.com`,user: root,password: Hbhyg731024,database: `pms` - Redis: `r-bp1lsezvgzmgtwhbtg.redis.rds.aliyuncs.com`,password: Hbhyg731024,db: 5 ### 本地开发环境 **数据库连接(通过ZeroTier):** ``` MySQL: 10.8.0.252:3306 user: root password: Hbhyg731024@ database: pms_dev (新建,不动AZZCWeChat*) Redis: 10.8.0.252:6379 password: Hbhyg731024@ db: 6 (测试环境用db6,不动db1-5) ``` **代理说明:** - 服务器nginx stream转发MySQL(3306)和Redis(6379)到阿里云RDS - systemd自启,`Restart=always` 自动恢复 - 本地直连 `10.8.0.252`,无需SSH隧道 ### 数据库工具 本地可直接访问数据库(通过ZeroTier),使用 `tools/db.py`: ```bash # 列出所有表 python tools/db.py tables # 统计各表数据量 python tools/db.py count # 执行查询 python tools/db.py query "SELECT * FROM users" # 执行写入 python tools/db.py exec "UPDATE users SET status=1 WHERE id=1" # 导入SQL文件 python tools/db.py import data.sql ``` ### 证书 - 阿里云免费SSL证书(acme.sh自动续期) - 存放路径: `/etc/nginx/ssl/{域名}.pem` + `/etc/nginx/ssl/{域名}.key` - 已配置: app.anzhizhichong.com、test.anzhizhichong.com ### 内部服务 - **Gitea**: `https://app.anzhizhichong.com/doc/` (端口3080) - 组织: anzhizhichong - 仓库: charging-cabinet - 用户: admin / skyp76 ## 开发约束 ### 禁止操作 - **禁止在服务器上编译Rust** — 2核/3.5G会OOM,本地编译传二进制 - **禁止使用socat fork模式** — 进程泄漏会耗尽内存,用nginx stream代理 - **禁止提交密码/密钥到仓库** — 使用环境变量或本地配置文件(已gitignore) ### 编码任务下发 - 任务需求写到 `tasks/` 目录的md文件 - 通过Agent子代理包裹opencode执行,异步通知 - 质量约束模板固定追加到每个任务文件 - opencode超时必须报告用户,不能fallback手动执行 - 审查用不同模型(不用开发同款) ### 质量约束模板 所有编码任务必须追加以下约束: 1. 完成代码后执行对应栈静态校验(Rust→cargo check,TS→tsc --noEmit),零报错零警告 2. 分层拆分,单函数≤80行,命名语义化,完整注释 3. 所有外部IO/网络请求异常捕获,禁止裸panic 4. 分支逻辑全覆盖,不遗漏兜底分支 5. 常量抽离,不使用废弃API 6. Rust:内存安全,合理管理所有权,禁用unsafe无合理理由 7. React:函数式组件+Hooks,不写Class组件 ### 验收流程 1. 静态校验命令通过 2. 跑起来看效果 3. 抽查关键逻辑(异常处理、权限校验、协议解析) --- ## Rust 编码规范 ### 错误处理 - 使用 `thiserror` 定义业务错误枚举,`AppError` 统一包装 - 所有 `sqlx` / `redis` 调用必须 `.map_err(AppError::Database)?` - 网络请求/外部IO 必须 `match` 或 `if let` 处理,禁止裸 `.unwrap()` - 关键业务路径用 `tracing::error!` 记录错误上下文 ### Async 模式 - 使用 `tokio` 运行时,数据库/Redis 操作用 `.await` - 长时间任务用 `tokio::spawn` 异步处理,不阻塞主循环 - Redis 队列消费:`blpop` 阻塞读取 + `tokio::spawn` 并发处理 ### 模块划分 ``` src/ ├── routes/ # HTTP 路由处理器(按业务拆分) ├── middleware/ # 中间件(认证、日志、CORS) ├── db/ # 数据库连接池、迁移 ├── commands/ # 设备指令下发逻辑 ├── nodes/ # 设备服务节点管理 ├── workers/ # 后台 Worker(report/reply) └── error.rs # 统一错误定义 ``` ### 命名约定 - 路由处理器:`动词 + 名词`(如 `get_cabinets`、`create_organization`) - 数据库查询结构体:`XxxRow`(如 `CabinetBasicRow`) - 请求体:`XxxBody` 或 `XxxQuery`(如 `CreateCabinetBody`) - 常量:`UPPER_SNAKE_CASE`(如 `JWT_SECRET_ENV`) --- ## React 编码规范 ### 组件规范 - 纯函数组件 + Hooks,禁止 Class 组件 - 组件文件与路由页面一一对应,放 `src/pages/` 下 - 复用组件放 `src/components/`,按功能命名 - Props 接口定义在组件文件顶部,用 `interface` 声明 ### 状态管理 - 页面级状态用 `useState` / `useEffect` - 全局状态用 `zustand`(如用户信息、权限) - 服务端数据用 `@tanstack/react-query`,配置缓存策略 ### 样式规范 - 使用 Tailwind CSS,禁止内联 style(除动态计算值) - shadcn/ui 组件优先,不重复造轮子 - 表格列统一 `min-width` + `align="center"`,无 `fixed="right"` - 分页:上下双分页,`page-sizes="[10,20,30,50]"` ### 图标映射 | 操作 | 图标 | |------|------| | 编辑 | Edit | | 删除 | Delete | | 查看 | ViewIcon | | 搜索 | Search | | 重置 | Refresh | | 新增 | Plus(保留文字) | --- ## API 设计规范 ### 响应格式 ```json { "code": 0, // 0=成功,非0=失败 "data": { ... }, // 业务数据 "message": "ok" // 提示信息 } ``` ### 错误码 | code | 含义 | |------|------| | 0 | 成功 | | 400 | 请求参数错误 | | 401 | 未认证(token缺失/无效) | | 403 | 无权限 | | 404 | 资源不存在 | | 500 | 服务器内部错误 | ### 分页约定 ```json { "code": 0, "data": { "list": [...], "total": 100, "page": 1, "page_size": 20 } } ``` ### RESTful 约定 | 方法 | 路径 | 说明 | |------|------|------| | GET | /api/xxx | 列表查询 | | GET | /api/xxx/:id | 详情 | | POST | /api/xxx | 创建 | | PUT | /api/xxx/:id | 更新 | | DELETE | /api/xxx/:id | 删除 | --- ## Git 工作流 ### 分支策略 - `main` — 正式环境,只接受 PR 合并 - `develop` — 开发分支,日常开发在此分支 - `feature/*` — 功能分支,从 develop 拉出,完成后合并回 develop - `fix/*` — 修复分支,从 main 或 develop 拉出 ### Commit 规范 格式:`(): ` | type | 说明 | |------|------| | feat | 新功能 | | fix | 修复 bug | | docs | 文档变更 | | style | 代码格式(不影响逻辑) | | refactor | 重构(不新增功能/修复bug) | | test | 测试相关 | | chore | 构建/工具变更 | 示例: - `feat(cabinets): 添加设备RSSI信号显示` - `fix(h5): 修复登录后token未持久化问题` - `docs(api): 更新充电记录接口文档` --- ## 测试标准 ### 测试类型 | 类型 | 工具 | 要求 | |------|------|------| | 单元测试 | Rust `#[test]` / Vitest | 核心业务逻辑必须覆盖 | | 集成测试 | API 请求验证 | 关键接口(登录/充电/开门)必须验证 | | 静态校验 | `cargo check` / `tsc --noEmit` | 零报错零警告 | ### 测试覆盖率 - 核心业务逻辑(充电控制、指令下发、权限校验):≥80% - 数据库查询函数:≥60% - 工具函数:≥90% ### 测试命名 ``` test_<被测函数>_<场景>_<期望结果> test_verify_password_correct_returns_true test_verify_password_wrong_returns_false test_check_permission_admin_returns_ok test_check_permission_user_returns_forbidden ``` --- ## 安全规范 ### 认证授权 - JWT Token 有效期:7天,过期需重新登录 - 密码存储:argon2 哈希,禁止明文或 MD5/SHA1 - RBAC 权限码:精确到按钮级别(如 `device:door:unlock`) - 敏感操作(删除/禁用/开门)需二次确认 ### 输入校验 - 所有 API 入参必须校验(长度、格式、范围) - SQL 查询使用参数化查询,禁止字符串拼接 - IMEI 校验:必须15位数字 - 手机号校验:11位数字,正则 `^1[3-9]\d{9}$` ### 数据安全 - 生产数据库密码不入代码,使用环境变量 - Redis 密码同上 - 日志禁止输出密码/Token/密钥 - 敏感字段(密码、auth_str)在序列化时跳过 --- ## 日志规范 ### 日志级别 | 级别 | 用途 | 示例 | |------|------|------| | `ERROR` | 系统异常、业务错误 | 数据库连接失败、指令下发超时 | | `WARN` | 潜在问题、可恢复异常 | 设备离线、重试成功 | | `INFO` | 关键业务事件 | 设备登录、充电开始/结束、用户操作 | | `DEBUG` | 调试信息 | API 请求参数、SQL 执行时间 | ### 日志格式 ```rust // 使用 tracing crate tracing::info!(imei = %imei, board_id = board_id, "设备登录成功"); tracing::error!(err = %e, "数据库查询失败"); tracing::warn!(compartment_id = id, "仓体状态异常"); ``` ### 关键操作日志 以下操作必须记录: | 操作 | 日志内容 | |------|----------| | 用户登录/登出 | 用户ID、手机号、IP | | 设备指令下发 | IMEI、指令类型、仓体ID | | 充电开始/结束 | 仓体ID、时长、电量 | | 组织/项目变更 | 操作类型、ID、名称 | | 权限变更 | 用户ID、权限码、操作人 | | 数据删除 | 表名、ID、操作人 | --- ## 性能规范 ### 数据库 - 查询必须走索引,禁止全表扫描(`EXPLAIN` 验证) - 分页查询:避免深分页,用游标分页(`WHERE id > ?`) - 批量操作:用 `INSERT ... VALUES (...), (...)` 而非逐条插入 - 连接池:MySQL 最大连接数 ≤ 50,Redis 连接数 ≤ 20 ### Redis - 缓存 key 规范:`{业务}:{标识}`(如 `device:{imei}`) - TTL 设置:在线状态 300秒,缓存数据按业务设定 - 避免大 Key:单个 Hash ≤ 500 字段,List ≤ 1000 元素 - 批量操作:用 Pipeline 减少往返次数 ### API 响应 - 列表接口响应时间 ≤ 500ms - 详情接口响应时间 ≤ 200ms - 批量操作接口响应时间 ≤ 1s - 响应体大小:列表 ≤ 1MB,详情 ≤ 100KB ### 前端 - 首屏加载 ≤ 3s(3G网络) - 交互响应 ≤ 100ms - 列表滚动:虚拟滚动(≥100条数据) - 图片:WebP 格式,懒加载 ## 部署与维护 ### SSH连接 ```bash # 外网IP(推荐) echo y | plink -ssh root@118.31.119.206 -pw "Hbhyg731024@" # ZeroTier内网IP echo y | plink -ssh root@10.8.0.252 -pw "Hbhyg731024@" ``` ### 数据库连接 所有数据库操作通过 ZeroTier 直接连接 `10.8.0.252`,无需 SSH 隧道到服务器。 **MySQL(通过 ZeroTier):** ```bash # 命令行 mysql -h 10.8.0.252 -u root -p # 密码:Hbhyg731024@ # 测试环境库:pms_dev # 正式环境库:pms ``` ```python # Python(Agent 常用方式) import pymysql conn = pymysql.connect(host='10.8.0.252', user='root', password='Hbhyg731024@', database='pms') # 正式环境 conn = pymysql.connect(host='10.8.0.252', user='root', password='Hbhyg731024@', database='pms_dev') # 测试环境 ``` **Redis(通过 ZeroTier):** ```bash # 命令行 redis-cli -h 10.8.0.252 -a "Hbhyg731024@" # 测试环境:select 6 # 正式环境:select 5 ``` ```python # Python(Agent 常用方式) import redis r = redis.Redis(host='10.8.0.252', password='Hbhyg731024@', db=5) # 正式环境 r = redis.Redis(host='10.8.0.252', password='Hbhyg731024@', db=6) # 测试环境 ``` **数据库工具 `tools/db.py`(默认连接测试库 pms_dev):** ```bash python tools/db.py tables # 列出所有表 python tools/db.py count # 统计各表数据量 python tools/db.py query "SELECT * FROM users" # 查询 python tools/db.py exec "UPDATE users SET ..." # 写入 ``` > 注意:`tools/db.py` 默认连的是 `pms_dev`(测试库),要操作正式库请用上面的 Python 直连方式。 ### Nginx配置 **正式环境** (`/etc/nginx/conf.d/app.anzhizhichong.com.conf`): ```nginx server { listen 80; server_name app.anzhizhichong.com; return 301 https://$server_name$request_uri; } server { listen 443 ssl http2; server_name app.anzhizhichong.com; ssl_certificate /etc/nginx/ssl/app.anzhizhichong.com.pem; ssl_certificate_key /etc/nginx/ssl/app.anzhizhichong.com.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; # 产品介绍页 root /data/landing; index index.html; location /doc/ { proxy_pass http://127.0.0.1:3080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; client_max_body_size 500m; } # PMS API 入口(微服务统一前缀 /api/{服务}) location /api/pms/ { proxy_pass http://127.0.0.1:3000/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # PMS管理前端 location /pms/ { alias /data/pms/web/; try_files $uri $uri/ /pms/index.html; } # H5用户端 location /h5/ { alias /data/pms/h5/; try_files $uri $uri/ /h5/index.html; } } ``` **测试环境** (`/etc/nginx/conf.d/test.anzhizhichong.com.conf`): ```nginx server { listen 80; server_name test.anzhizhichong.com; return 301 https://$server_name$request_uri; } server { listen 443 ssl http2; server_name test.anzhizhichong.com; ssl_certificate /etc/nginx/ssl/test.anzhizhichong.com.pem; ssl_certificate_key /etc/nginx/ssl/test.anzhizhichong.com.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; root /data/pms_dev; index index.html; # PMS API 入口(微服务统一前缀 /api/{服务}) location /api/pms/ { proxy_pass http://127.0.0.1:3000/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } location /pms/ { try_files $uri $uri/ /pms/index.html; } # H5用户端 location /h5/ { alias /data/pms_dev/h5/; try_files $uri $uri/ /h5/index.html; } # H5用户端 location /h5/ { alias /data/pms_dev/h5/; try_files $uri $uri/ /h5/index.html; } } ``` > 测试环境SSL证书与正式环境共用(test是app证书的SAN域名),部署时复制pem+key即可。 ### 部署步骤 **1. 本地编译** ```bash # 后端(Rust) cd software/api-server cargo build --release cd software/device-server cargo build --release # 前端(React) cd software/web npm run build ``` **2. 传输到服务器** ```bash # 传输后端二进制(测试环境) scp software/api-server/target/release/pms-api root@118.31.119.206:/data/pms_dev/api-server/ scp software/device-server/target/release/device-server root@118.31.119.206:/data/pms_dev/device-server/ # 传输后端二进制(正式环境) scp software/api-server/target/release/pms-api root@118.31.119.206:/data/pms/api-server/ scp software/device-server/target/release/device-server root@118.31.119.206:/data/pms/device-server/ # 传输前端静态文件 scp -r software/web/dist/* root@118.31.119.206:/data/pms_dev/web/ # 测试环境 scp -r software/web/dist/* root@118.31.119.206:/data/pms/web/ # 正式环境 # 传输H5用户端(tar.gz打包方式,保留UTF-8编码) cd software/h5-uniapp/dist/build/h5 tar -czf /tmp/h5-build.tar.gz . pscp -pw "Hbhyg731024@" /tmp/h5-build.tar.gz root@118.31.119.206:/tmp/ plink -ssh -pw "Hbhyg731024@" root@118.31.119.206 "rm -rf /data/pms/h5/* && cd /data/pms/h5 && tar -xzf /tmp/h5-build.tar.gz" ``` > ⚠️ H5部署必须用tar.gz打包方式,不要用plink/pscp管道传单文件(会丢失UTF-8编码导致中文变问号)。 **3. 服务器重启服务** ```bash # SSH到服务器 echo y | plink -ssh root@118.31.119.206 -pw "Hbhyg731024@" # 重启测试环境 systemctl restart pms-api-test systemctl restart pms-device-test # 重启正式环境 systemctl restart pms-api-prod systemctl restart pms-device-prod # 重启Nginx systemctl restart nginx ``` ### 服务管理 ```bash # 查看服务状态 systemctl status pms-api-test # 测试环境API systemctl status pms-api-prod # 正式环境API systemctl status nginx # 查看日志 journalctl -u pms-api-test -f journalctl -u pms-api-prod -f tail -f /var/log/nginx/error.log ``` ## 硬件架构 - 柜控板(ID128)×1 + 仓控板(ID1-N)×N,每仓控板管6个仓体 - 仓控板与电源:1TN RS485协议(翌工) - 仓控板与BMS:星恒Modbus / 天能485 / 无协议(平台可配置) - 仓控板与柜控板:自定义485协议(14+功能码) - 柜控板与后台:4G TCP长连接(Air780E模块) ## 开发范围(当前阶段) ### 做 - 后台管理系统(Web端) - 用户端H5页面(`/pms/{id}`,微信扫码可访问) - 设备TCP通讯服务(协议文档已就绪) ### 不做 - 微信小程序(名额暂不可用,先用H5,uni-app可后续转换) - 支付/计费(本系统不涉及) ## 组织架构 ``` 组织(企业) → 项目(放置地点) → 设备(充电柜) ``` 设备卖给哪个企业就划拨给该组织,设备放置的具体地点称为项目,设备必须划拨到项目下。 ## 数据模型 ### 核心关系 ``` organization(组织/企业) ├── project(项目/放置地点) │ └── cabinet(充电柜) │ ├── cabin_board(仓控板×N) │ │ └── compartment(仓体×6) │ ├── charge_record(充电记录) │ └── energy_stat(能耗统计) ├── user(用户) └── device_log(设备日志) operation_log(操作日志) ``` ### 数据库表 | 表名 | 说明 | |------|------| | organizations | 组织表 | | projects | 项目表 | | cabinets | 充电柜表(IMEI、abstract_id、auth_str) | | cabin_boards | 仓控板表 | | compartments | 仓体表 | | users | 用户表(手机号登录) | | charge_records | 充电记录表 | | device_logs | 设备日志表 | | energy_stats | 能耗统计表 | | operation_logs | 操作日志表 | ### Redis 实时状态存储 设备实时状态通过 Redis Hash 层级存储,按 `status_post` 上报内容解析写入。 **层级结构:** ``` device:{imei} # 设备级 Hash ├─ online = "1" # 在线标记(TTL 300秒) ├─ rssi = "85" # 信号强度 ├─ pow_fail_dc = "0/1" # 直流停电(12V掉电) ├─ pow_fail_ac = "0/1" # 交流停电(380V掉电) └─ device:{imei}:board:{board_idx} # 板级 Hash(仓控板1~N) ├─ status_hex = "000100020003000400050006" # 原始hex状态 └─ device:{imei}:board:{board_idx}:ch:{0-5} # 通道级 Hash(6仓) ├─ on = "0/1" # 充电中 ├─ full = "0/1" # 充满 ├─ fault = "0/1" # 故障 └─ hex = "00010002" # 原始hex ``` **写入时机:** | 事件 | 写入位置 | 说明 | |------|----------|------| | `login` | `device:{imei}.online` | 标记在线,设置TTL 300秒 | | `status_post` | `device:{imei}` + board + ch | 解析status字典,按层级写入 | | `pow_fail` | `device:{imei}.pow_fail_dc/ac` | 更新停电状态 | **通道状态映射(hex → 语义):** - 空闲(idle) - 插入(inserted) - 充电中(charging) - 充满(full) - 故障(fault) > ⚠️ hex编码格式待硬件团队最终确认,当前实现预留状态字段,按 bit 位解析。 **API 接口:** | 接口 | 说明 | 查询方式 | |------|------|----------| | `GET /api/cabinets/filter` | 设备列表(设备级状态) | Pipeline 批量 HGET,1次往返 | | `GET /api/cabinets/realtime-status?imei=xxx` | 单设备详情(板级+通道级) | KEYS 扫描 + HMGET | ## TCP通讯协议 详见 [docs/API.md](docs/API.md#tcp-设备通讯协议) — 包含签名机制、指令清单、指令详情、充电结束原因。 ## 数据单位规范 详见 [docs/API.md](docs/API.md#数据单位规范) — 所有数值字段使用整数存储,禁止浮点类型。 **现有浮点问题(需清理):** | 文件 | 位置 | 问题 | 处理 | |------|------|------|------| | `src/routes/charge_records.rs` | L46-47, L77-78 | `Option` 读 DECIMAL | 需 CAST AS DOUBLE [有CAST] | | `src/routes/energy_stats.rs` | L106, L116 | `query_scalar::<_, f64>` 直接读 DECIMAL 无 CAST ❌ | 需加 CAST AS DOUBLE 或改用整数 | | `src/routes/energy_stats.rs` | L43-72 | 多个 `Option` 字段 | 待迁移为 i64 | | `src/db/migrate.rs` | L76-77, L97 | `DECIMAL` 列定义 | 待改为 `BIGINT` | ## H5用户端 ### 信号强度显示(RSSI) 设备卡片右上角使用 Lucide 信号图标显示 RSSI 信号强度,PC 管理端和 H5 移动端共用同一套逻辑。 **RSSI 映射:** | RSSI 范围 | 图标 (Lucide) | 信号格 | 颜色 | |-----------|--------------|--------|------| | ≥ 70 | signal | 5 格(含 M22) | #16a34a 绿色 | | ≥ 55 | signal-high | 4 格(含 M17) | #2563eb 蓝色 | | ≥ 40 | signal-medium | 3 格(含 M12) | #f59e0b 黄色 | | ≥ 30 | signal-low | 2 格(含 M7) | #f97316 橙色 | | < 30 或 null | signal-zero | 1 格(仅 M2) | #9ca3af 灰色(离线) | **实现方式(移动端 H5):** ```vue ``` - `v-if` 按 RSSI 阈值动态显示信号格 - `` 不支持 `display: flex`,统一用 `` 包裹 **实现方式(PC 管理端):** React 组件 `SignalIcon`,同上逻辑用条件渲染 ``。 **后端 RSSI 获取:** 设备列表接口通过 Redis pipeline 批量 HGET 查询 `device:{imei}` 的 `rssi` 字段: ``` HSET device:{imei} online 1 rssi 85 ``` **相关文件:** - PC: `software/web/src/pages/devices/CabinetGrid.tsx` — `SignalIcon` 组件 - H5: `software/h5-uniapp/src/pages/home/index.vue` — signalColor 函数 + 模板 - H5 API: `software/api-server/src/routes/h5.rs` — `get_cabinets` 加 Redis pipeline - 类型: `software/h5-uniapp/src/api/index.ts` — `CabinetItem.rssi` ### 近期 UI 改动 1. **设备卡片头部**:去掉"在线/离线"文字 badge,改为信号图标 + RSSI 数值 2. **通道统计**:图标+数字一行排列,图标用 `` 内联 Lucide(不加容器,flex 居中) 3. **右上角扫码**:scan-line 图标(Lucide),灰色 #6b7280,居中垂直对齐 ### 开发约束(uni-app 特定) 1. **`` 不支持 `display: flex`** — uni-app 的 `` 组件是纯文本组件,不接受 flex 布局。需要 flex 的地方用 `` 2. **SVG 图标居中**:`.stat-chip svg { display: block; flex-shrink: 0 }` 用 CSS 统一控制尺寸,比内联属性更可靠 3. **内联 SVG + v-if**:Vue 3 支持在 `` 上用 `v-if` 条件渲染,适用于动态图标 4. **CSS 覆盖 SVG 尺寸**:用 `.xxx svg { width: Xpx; height: Xpx }` 统一控制图标大小,不在每个 `` 上写死