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

14 KiB
Raw Blame 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:24software/server/src/routes/h5.rs:105
  • 描述:登录接口没有频率限制,攻击者可无限次尝试密码。
  • 风险:弱密码账户可被暴力破解。
  • 建议:添加基于 IP 或手机号的登录频率限制(如 5次/分钟),失败过多时临时锁定账户或要求验证码。

问题4device:door:unlock 权限码未初始化

  • 位置software/server/src/db/migrate.rs:225-258software/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:37software/server/src/routes/roles.rs:232
  • 描述list_roleslist_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:380software/server/src/routes/energy_stats.rs:536
  • 描述:导出文件保存到 ./exports 相对路径。如果进程工作目录非预期,文件可能写入敏感位置。
  • 风险:文件写入位置不可控。
  • 建议:使用配置项指定导出根目录的绝对路径,或在 Config 中添加 export_dir 字段。

警告(建议修复)

警告1h5.rs 文件过长990行

  • 位置software/server/src/routes/h5.rs
  • 描述:单文件 990 行,远超 300 行限制。包含登录、仪表盘、设备查询、充电控制等多个功能模块。
  • 建议:拆分为 h5/auth.rsh5/dashboard.rsh5/cabinets.rsh5/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.rsbuild_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,103OrganizationTree.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 缺少 orgproject 分组,导致组织和项目管理权限在权限树中显示为原始前缀名。
  • 建议:添加 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_conditionorg_condition_for_logs 通过 format! 拼接 SQL 子句,虽然参数使用 ? 占位符,但表名/列名直接插入字符串。模式脆弱,容易引入 SQL 语法错误。
  • 建议:考虑使用 query builder 库(如 sea-query),或至少将子查询模板定义为常量。

警告11compartments 表缺少 updated_at 索引

  • 位置software/server/src/db/migrate.rs:47-55software/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:4software/server/src/middleware/auth.rs:33
  • 描述JWT_SECRET_DEFAULT 在两个文件中重复定义。
  • 建议:统一从 Config 结构体读取,避免不一致。

建议5set_role_permissions 非原子操作

  • 位置software/server/src/routes/roles.rs:202-228
  • 描述:先 DELETE 再逐条 INSERT非事务操作。如果中途失败角色权限处于不一致状态。
  • 建议:使用事务包裹,或批量 INSERT。

建议6set_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:62device-detail.tsx:36compartment-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 后再实现。

建议9generate_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

整体代码质量良好,核心业务逻辑正确,安全基础扎实。上述问题修复后可达到生产就绪水平。