charging-cabinet/tasks/review-north-mini-report.md

247 lines
14 KiB
Markdown
Raw Normal View History

# 全面代码审查报告
## 总体评价
**质量评分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
整体代码质量良好,核心业务逻辑正确,安全基础扎实。上述问题修复后可达到生产就绪水平。