charging-cabinet/tasks/review-north-mini-report.md
2026-07-02 05:38:01 +08:00

247 lines
14 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.

# 全面代码审查报告
## 总体评价
**质量评分7 / 10**
项目整体架构清晰,分层合理(接口层/业务层/数据层),代码注释充分,命名语义化。后端 Rust 代码质量较高,错误处理覆盖全面;前端 React 组件拆分得当H5 移动端体验完整。但存在若干安全、性能和代码规范问题需要修复。
## 统计
- 后端文件数30Rust
- 前端文件数35TypeScript/React
- 总代码行数:约 11,700后端 6,400 + 前端 5,300
- 问题总数28严重 7 / 警告 12 / 建议 9
---
## 严重问题(必须修复)
### 问题1心跳超时与协议上报间隔不匹配
- **位置**`software/server/src/tcp/server.rs:16`
- **描述**`HEARTBEAT_TIMEOUT` 设为 5000ms5秒但协议规定设备空闲时 1 分钟上报一次。设备在空闲状态下会在两次上报之间被断开连接。
- **风险**:设备频繁断连,无法保持在线状态,影响充电指令下发和状态监控。
- **建议**:将 `HEARTBEAT_TIMEOUT` 改为 `Duration::from_secs(90)`90秒留 1.5 倍余量),或根据设备上报频率动态调整。
### 问题2安全码 auth_str 通过 API 明文返回前端
- **位置**`software/server/src/routes/cabinets.rs:254`
- **描述**`regenerate_auth` 接口将 `auth_str` 直接返回给前端。安全码是设备签名密钥,不应暴露给 Web 端。
- **风险**:安全码泄露后,攻击者可伪造设备签名登录。
- **建议**:接口只返回 `{ "success": true }`,不返回 `auth_str` 值。如需查看,应通过独立的安全审计接口并记录操作日志。
### 问题3登录接口无暴力破解防护
- **位置**`software/server/src/routes/auth.rs:24``software/server/src/routes/h5.rs:105`
- **描述**:登录接口没有频率限制,攻击者可无限次尝试密码。
- **风险**:弱密码账户可被暴力破解。
- **建议**:添加基于 IP 或手机号的登录频率限制(如 5次/分钟),失败过多时临时锁定账户或要求验证码。
### 问题4`device:door:unlock` 权限码未初始化
- **位置**`software/server/src/db/migrate.rs:225-258``software/server/src/routes/h5.rs:879`
- **描述**H5 开门接口使用 `device:door:unlock` 权限码,但 `seed_permissions` 中未插入该权限码。因此没有任何角色拥有此权限,开门功能永远返回 403。
- **风险**:开门功能完全不可用。
- **建议**:在 `seed_permissions` 中添加 `("device:door:unlock", "开门")` 并分配给相应角色。
### 问题5部分接口缺少权限校验
- **位置**`software/server/src/routes/roles.rs:37``software/server/src/routes/roles.rs:232`
- **描述**`list_roles``list_permissions` 接口使用 `_user` 忽略当前用户,未调用 `check_permission`。任何已登录用户都可查看角色和权限配置。
- **风险**:权限信息泄露,攻击者可了解系统权限结构。
- **建议**:添加 `auth::check_permission(&user, "role:view")?;` 校验。
### 问题6下载文件路径无安全校验
- **位置**`software/server/src/routes/downloads.rs:205-213`
- **描述**`download_file` 从数据库读取 `file_path` 后直接读取文件,未验证路径是否在 `./exports` 目录内。如果数据库中的路径被篡改(如 `../../etc/passwd`),可读取服务器任意文件。
- **风险**:路径穿越攻击,可泄露服务器敏感文件。
- **建议**:验证 `file_path` 必须以 `./exports/` 开头,使用 `std::path::Path::canonicalize` 后检查前缀。
### 问题7导出目录使用相对路径
- **位置**`software/server/src/routes/charge_records.rs:380``software/server/src/routes/energy_stats.rs:536`
- **描述**:导出文件保存到 `./exports` 相对路径。如果进程工作目录非预期,文件可能写入敏感位置。
- **风险**:文件写入位置不可控。
- **建议**:使用配置项指定导出根目录的绝对路径,或在 `Config` 中添加 `export_dir` 字段。
---
## 警告(建议修复)
### 警告1h5.rs 文件过长990行
- **位置**`software/server/src/routes/h5.rs`
- **描述**:单文件 990 行,远超 300 行限制。包含登录、仪表盘、设备查询、充电控制等多个功能模块。
- **建议**:拆分为 `h5/auth.rs``h5/dashboard.rs``h5/cabinets.rs``h5/control.rs` 等子模块。
### 警告2users.rs list_users 函数分支过多SQL 重复严重
- **位置**`software/server/src/routes/users.rs:108-268`
- **描述**`list_users` 函数约 160 行,因 keyword 和 org_filter 的组合产生 6 个分支,大量 SQL 重复。
- **建议**:使用动态 SQL 构建(类似 `charge_records.rs``build_where_clause` 模式),减少分支和重复代码。
### 警告3N+1 查询 — 柜子详情
- **位置**`software/server/src/routes/cabinets.rs:292-308`
- **描述**`get_cabinet_detail` 先查仓板列表,再逐个查仓体。若柜有 N 块仓板,产生 N+1 次查询。
- **建议**:使用一条 SQL 联表查询所有仓板和仓体,在 Rust 中按 board_id 分组。
### 警告4N+1 查询 — H5 项目摘要
- **位置**`software/server/src/routes/h5.rs:436-486`
- **描述**`fetch_project_summaries` 对每个项目执行 4 次独立查询(柜子数、充电中、空闲、故障),项目数为 N 时产生 4N+1 次查询。
- **建议**:合并为一条 SQL使用 `LEFT JOIN` + `SUM(CASE WHEN...)` 聚合。
### 警告5N+1 查询 — 角色列表
- **位置**`software/server/src/routes/roles.rs:42-71`
- **描述**`list_roles` 对每个角色单独查询权限码,角色数为 N 时产生 N+1 次查询。
- **建议**:一条 SQL 联表查询所有角色及其权限码,在 Rust 中按 role_id 分组。
### 警告6前端多处使用 `as unknown as` 类型断言
- **位置**`software/web/src/pages/devices/index.tsx:81,103``OrganizationTree.tsx:169,171`
- **描述**:多处使用 `as unknown as` 进行类型转换,等同于 `as any`,绕过 TypeScript 类型检查。
- **建议**:修正 API 返回类型定义,使类型匹配,消除 `as unknown as`
### 警告7H5 认证恢复不验证 token 有效性
- **位置**`software/web/src/h5/auth.ts:48-55`
- **描述**H5 `restore` 仅从 localStorage 读取 token 和用户信息,不调用服务端验证 token 是否过期/有效。若 token 已过期,用户看到已登录状态但请求会失败。
- **建议**restore 时调用 `/api/h5/auth/me` 或类似接口验证 token失败则清除本地状态。对比后台管理端的 `restore` 实现(`stores/auth.ts:72-91`),后者正确调用了 `/auth/me`
### 警告8前端权限分组缺少组织和项目模块
- **位置**`software/web/src/pages/settings/roles.tsx:37-46`
- **描述**`PERM_GROUPS` 缺少 `org``project` 分组,导致组织和项目管理权限在权限树中显示为原始前缀名。
- **建议**:添加 `org: '组织管理'``project: '项目管理'``PERM_GROUPS`
### 警告9Dashboard 页面为空
- **位置**`software/web/src/pages/Dashboard.tsx`
- **描述**:后台管理 Dashboard 仅显示欢迎文字无任何数据看板。H5 端有完整的 dashboard API但后台端未使用。
- **建议**:复用 H5 dashboard API 或创建独立的后台 dashboard 接口,展示设备总览、充电统计等。
### 警告10org_condition 使用字符串拼接 SQL
- **位置**`software/server/src/middleware/auth.rs:255-288`
- **描述**`org_condition``org_condition_for_logs` 通过 `format!` 拼接 SQL 子句,虽然参数使用 `?` 占位符,但表名/列名直接插入字符串。模式脆弱,容易引入 SQL 语法错误。
- **建议**:考虑使用 query builder 库(如 `sea-query`),或至少将子查询模板定义为常量。
### 警告11compartments 表缺少 updated_at 索引
- **位置**`software/server/src/db/migrate.rs:47-55``software/server/src/routes/h5.rs:379-391`
- **描述**`fetch_fault_alerts` 使用 `ORDER BY comp.updated_at DESC`,但 `compartments` 表没有 `updated_at` 索引。
- **建议**:添加 `idx_compartments_updated` 索引,或在 `create_indexes` 中添加。
### 警告12充电记录导出缺少组织数据隔离
- **位置**`software/server/src/routes/charge_records.rs:309-397`
- **描述**`generate_charge_records_excel` 异步导出时未追加组织过滤条件,企业管理员可导出全部组织的充电记录。
- **风险**:数据越权,企业管理员可导出非本组织数据。
- **建议**:在导出查询中追加 `org_condition` 过滤。
---
## 建议
### 建议1统一 API 响应格式
- **位置**:全局
- **描述**:部分接口返回 `{ "code": 0, "data": ..., "message": "ok" }` 格式,部分直接返回数据对象(如 `organizations` 列表接口返回 `json!(rows)`)。前端需要兼容两种格式。
- **建议**:统一所有接口使用 `{ code, data, message }` 包装。
### 建议2常量抽离 — 魔法数字
- **位置**:多处
- **描述**状态码0/1/2/3、角色值0/1/2等魔法数字散落在前后端代码中。
- **建议**:后端使用 Rust 枚举(`enum CompartmentStatus { Idle = 0, Charging = 1, ... }`),前端使用常量对象(`COMPARTMENT_STATUS`)。
### 建议3前端 DownloadCenter 组件复用
- **位置**`software/web/src/pages/charge-records/DownloadCenter.tsx`
- **描述**DownloadCenter 被充电记录和能耗管理共用,但路径为 `charge-records/` 下。
- **建议**:移到 `components/DownloadCenter.tsx`,体现其通用组件定位。
### 建议4JWT 密钥在 middleware/auth.rs 和 config.rs 中重复定义
- **位置**`software/server/src/config.rs:4``software/server/src/middleware/auth.rs:33`
- **描述**`JWT_SECRET_DEFAULT` 在两个文件中重复定义。
- **建议**:统一从 `Config` 结构体读取,避免不一致。
### 建议5`set_role_permissions` 非原子操作
- **位置**`software/server/src/routes/roles.rs:202-228`
- **描述**:先 DELETE 再逐条 INSERT非事务操作。如果中途失败角色权限处于不一致状态。
- **建议**:使用事务包裹,或批量 INSERT。
### 建议6`set_user_roles` 同样非原子
- **位置**`software/server/src/routes/users_perm.rs:144-168`
- **描述**:同上,先 DELETE 再逐条 INSERT。
- **建议**:使用事务。
### 建议7前端 `eslint-disable-next-line` 注释
- **位置**`software/web/src/h5/pages/devices.tsx:62``device-detail.tsx:36``compartment-detail.tsx:42`
- **描述**:多处使用 `eslint-disable-next-line react-hooks/exhaustive-deps` 跳过 hooks 依赖检查。
- **建议**:修正依赖数组,或使用 `useCallback`/`useRef` 解决。
### 建议8充电曲线使用硬编码假数据
- **位置**`software/web/src/h5/pages/compartment-detail.tsx:174`
- **描述**:充电曲线图表使用硬编码数据 `[30, 45, 55, ...]`,非真实数据。
- **建议**:标注为 TODO 或移除该图表,待后端提供充电曲线 API 后再实现。
### 建议9`generate_abstract_id` 算法可预测
- **位置**`software/server/src/routes/organizations.rs:130-135`
- **描述**:抽象 ID 由 IMEI 的 SHA256 前 4 字节生成,仅 8 位十六进制(约 43 亿种组合)。虽然碰撞概率低,但可被预测。
- **建议**:如抽象 ID 不需可预测性,可加入随机盐或使用更长哈希。
---
## 优点
1. **架构分层清晰**:后端严格按 路由/中间件/协议/数据库 分层,前端按 API/Store/Page/Component 分层,职责明确。
2. **统一错误处理**:后端 `AppError` 统一错误类型,自动映射 HTTP 状态码,错误信息对用户友好,内部错误详情仅记录日志。
3. **密码安全**:使用 argon2 哈希(业界推荐),非 MD5/SHA。
4. **参数化查询**:所有数据库查询使用 `?` 占位符,有效防止 SQL 注入。
5. **RBAC 权限模型完整**:角色-权限-范围三层设计,前端 `Permission` 组件支持隐藏/禁用两种模式。
6. **组织数据隔离**:核心查询接口均实现组织级数据隔离,防止跨组织数据泄露。
7. **TCP 协议处理健壮**LF 分隔粘包、JSON 解析容错、签名验证、时间戳窗口校验、auth_str 频率限制。
8. **异步导出流程完整**:任务创建 -> 后台生成 Excel -> 进度更新 -> 下载 -> 过期清理,全链路覆盖。
9. **H5 移动端完整**扫码入口、设备概览、充电控制、BMS 数据展示、确认弹窗,用户体验良好。
10. **代码注释充分**:所有模块和公共函数都有中文文档注释,便于维护。
11. **数据库迁移自动化**:启动时自动建表、建索引、初始化权限码和角色,部署简便。
12. **前端组件拆分合理**:设备管理页拆分为 OrganizationTree、CabinetGrid、AddCabinetModal 等独立组件。
---
## 总结
### 优先修复建议(按紧急程度排序)
1. **P0 — 立即修复**
- 心跳超时改为 90 秒问题1否则设备无法保持在线
- 添加 `device:door:unlock` 权限码问题4否则开门功能不可用
- 下载文件路径安全校验问题6防止路径穿越攻击
2. **P1 — 本迭代修复**
- 登录接口添加频率限制问题3
- auth_str 不返回前端问题2
- 补充缺失的权限校验问题5
- 充电记录导出添加组织隔离警告12
- 导出目录使用绝对路径问题7
3. **P2 — 下迭代优化**
- 拆分 h5.rs警告1
- 修复 N+1 查询警告3/4/5
- H5 认证恢复验证 token警告7
- 消除 `as unknown as` 类型断言警告6
- 补充 compartments.updated_at 索引警告11
整体代码质量良好,核心业务逻辑正确,安全基础扎实。上述问题修复后可达到生产就绪水平。