247 lines
14 KiB
Markdown
247 lines
14 KiB
Markdown
|
|
# 全面代码审查报告
|
|||
|
|
|
|||
|
|
## 总体评价
|
|||
|
|
|
|||
|
|
**质量评分:7 / 10**
|
|||
|
|
|
|||
|
|
项目整体架构清晰,分层合理(接口层/业务层/数据层),代码注释充分,命名语义化。后端 Rust 代码质量较高,错误处理覆盖全面;前端 React 组件拆分得当,H5 移动端体验完整。但存在若干安全、性能和代码规范问题需要修复。
|
|||
|
|
|
|||
|
|
## 统计
|
|||
|
|
|
|||
|
|
- 后端文件数:30(Rust)
|
|||
|
|
- 前端文件数:35(TypeScript/React)
|
|||
|
|
- 总代码行数:约 11,700(后端 6,400 + 前端 5,300)
|
|||
|
|
- 问题总数:28(严重 7 / 警告 12 / 建议 9)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 严重问题(必须修复)
|
|||
|
|
|
|||
|
|
### 问题1:心跳超时与协议上报间隔不匹配
|
|||
|
|
|
|||
|
|
- **位置**:`software/server/src/tcp/server.rs:16`
|
|||
|
|
- **描述**:`HEARTBEAT_TIMEOUT` 设为 5000ms(5秒),但协议规定设备空闲时 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` 字段。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 警告(建议修复)
|
|||
|
|
|
|||
|
|
### 警告1:h5.rs 文件过长(990行)
|
|||
|
|
|
|||
|
|
- **位置**:`software/server/src/routes/h5.rs`
|
|||
|
|
- **描述**:单文件 990 行,远超 300 行限制。包含登录、仪表盘、设备查询、充电控制等多个功能模块。
|
|||
|
|
- **建议**:拆分为 `h5/auth.rs`、`h5/dashboard.rs`、`h5/cabinets.rs`、`h5/control.rs` 等子模块。
|
|||
|
|
|
|||
|
|
### 警告2:users.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` 模式),减少分支和重复代码。
|
|||
|
|
|
|||
|
|
### 警告3:N+1 查询 — 柜子详情
|
|||
|
|
|
|||
|
|
- **位置**:`software/server/src/routes/cabinets.rs:292-308`
|
|||
|
|
- **描述**:`get_cabinet_detail` 先查仓板列表,再逐个查仓体。若柜有 N 块仓板,产生 N+1 次查询。
|
|||
|
|
- **建议**:使用一条 SQL 联表查询所有仓板和仓体,在 Rust 中按 board_id 分组。
|
|||
|
|
|
|||
|
|
### 警告4:N+1 查询 — H5 项目摘要
|
|||
|
|
|
|||
|
|
- **位置**:`software/server/src/routes/h5.rs:436-486`
|
|||
|
|
- **描述**:`fetch_project_summaries` 对每个项目执行 4 次独立查询(柜子数、充电中、空闲、故障),项目数为 N 时产生 4N+1 次查询。
|
|||
|
|
- **建议**:合并为一条 SQL,使用 `LEFT JOIN` + `SUM(CASE WHEN...)` 聚合。
|
|||
|
|
|
|||
|
|
### 警告5:N+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`。
|
|||
|
|
|
|||
|
|
### 警告7:H5 认证恢复不验证 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`。
|
|||
|
|
|
|||
|
|
### 警告9:Dashboard 页面为空
|
|||
|
|
|
|||
|
|
- **位置**:`software/web/src/pages/Dashboard.tsx`
|
|||
|
|
- **描述**:后台管理 Dashboard 仅显示欢迎文字,无任何数据看板。H5 端有完整的 dashboard API,但后台端未使用。
|
|||
|
|
- **建议**:复用 H5 dashboard API 或创建独立的后台 dashboard 接口,展示设备总览、充电统计等。
|
|||
|
|
|
|||
|
|
### 警告10:org_condition 使用字符串拼接 SQL
|
|||
|
|
|
|||
|
|
- **位置**:`software/server/src/middleware/auth.rs:255-288`
|
|||
|
|
- **描述**:`org_condition` 和 `org_condition_for_logs` 通过 `format!` 拼接 SQL 子句,虽然参数使用 `?` 占位符,但表名/列名直接插入字符串。模式脆弱,容易引入 SQL 语法错误。
|
|||
|
|
- **建议**:考虑使用 query builder 库(如 `sea-query`),或至少将子查询模板定义为常量。
|
|||
|
|
|
|||
|
|
### 警告11:compartments 表缺少 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`,体现其通用组件定位。
|
|||
|
|
|
|||
|
|
### 建议4:JWT 密钥在 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)
|
|||
|
|
|
|||
|
|
整体代码质量良好,核心业务逻辑正确,安全基础扎实。上述问题修复后可达到生产就绪水平。
|