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

17 KiB
Raw Blame History

全面代码审查报告

总体评价

质量评分7/10

项目整体架构清晰,模块划分合理,代码风格统一。后端采用 Rust + Axum 技术栈安全性较好参数化查询、Argon2 密码哈希、JWT 认证)。前端 React + Arco Design 组件库使用规范H5 移动端独立实现。但存在若干权限码缺失、数据隔离不完整、前后端接口字段不匹配等严重问题需优先修复。

统计

  • 后端文件数29Rust
  • 前端文件数34TypeScript/React
  • 后端代码行数6,114
  • 前端代码行数5,300
  • 总代码行数11,414
  • 问题总数24严重 7 / 警告 9 / 建议 8

严重问题(必须修复)

问题1权限码种子数据缺失组织/项目管理权限

  • 位置software/server/src/db/migrate.rs:187-209 + software/server/src/routes/organizations.rs:154,172,198,227 + software/server/src/routes/projects.rs:20,40,78,107
  • 描述seed_permissions 函数只初始化了 device:*charge:*charge_record:*device_log:*energy:*user:*role:*operation_log:* 等权限码,但组织管理 API 检查的是 org:view/org:create/org:edit/org:delete,项目管理 API 检查的是 project:view/project:create/project:edit/project:delete。这些权限码从未在数据库中创建。
  • 风险非总管理员用户role_level < 2永远无法执行组织和项目的增删改查操作因为 check_permission 在权限码集合中找不到这些 code 会返回 Forbidden。
  • 建议:在 seed_permissions 中补充以下权限码:
    ("org:view", "查看组织"), ("org:create", "创建组织"),
    ("org:edit", "编辑组织"), ("org:delete", "删除组织"),
    ("project:view", "查看项目"), ("project:create", "创建项目"),
    ("project:edit", "编辑项目"), ("project:delete", "删除项目"),
    
    并为 org_admin 角色分配 org:viewproject:viewproject:createproject:edit 权限。

问题2设备管理缺少 device:edit 权限码

  • 位置software/server/src/db/migrate.rs:188 + software/server/src/routes/cabinets.rs:166,237
  • 描述cabinets.rsupdate_cabinetregenerate_auth 检查 device:edit 权限,但种子数据中只有 device:viewdevice:operatedevice:createdevice:delete,没有 device:edit
  • 风险:非总管理员用户无法编辑柜子或重新生成安全码。
  • 建议:在种子数据中添加 ("device:edit", "编辑设备")

问题3H5 接口缺少数据隔离 — 仪表盘返回全局数据

  • 位置software/server/src/routes/h5.rs:157-245
  • 描述get_dashboard 接口查询的在线柜子数、充电中仓体数、空闲仓体数、故障仓体数、今日充电次数/充电量均为全局统计,未按用户所属组织过滤。企业管理员和普通用户可以看到全平台数据。
  • 风险:数据泄露,违反多租户隔离原则。
  • 建议:所有统计查询追加 organization_id 过滤条件,与后台管理接口保持一致的隔离策略。

问题4H5 设备详情和仓体详情缺少权限校验

  • 位置software/server/src/routes/h5.rs:420-488,558-634
  • 描述get_cabinet_detailget_cabinet_by_abstract_idget_compartment_detail 三个接口没有进行任何权限检查(只有 let _ = &user;),任意已登录用户可通过遍历 ID 访问任意设备和仓体数据。
  • 风险:未授权数据访问,任意用户可查看不属于自己组织的设备。
  • 建议:添加组织隔离校验,确保用户只能访问本组织下的设备。

问题5前后端 H5 告警数据结构不匹配

  • 位置software/server/src/routes/h5.rs:206-224 vs software/web/src/h5/api.ts:21-26 + software/web/src/h5/components.tsx:96-113
  • 描述:后端返回告警字段为 { compartment_id, channel, cabinet_id, alert_type },前端 AlertItem 接口定义为 { project: string, cabinet: string, channel: number, alert_type: string }AlertCard 组件渲染 projectcabinet 字段,但后端未返回这两个字段,前端会显示 undefined
  • 风险H5 首页告警通知显示异常,用户看到 undefined - undefined 通道X
  • 建议:后端补充 projectcabinet 字段(通过 JOIN 查询),或前端改为根据 ID 显示。

问题6SQL 字符串拼接中直接内联 org_idh5.rs

  • 位置software/server/src/routes/h5.rs:744-756
  • 描述org_project_condition 函数使用 format!org_id 直接拼入 SQL 字符串("...WHERE organization_id = {}"),而非使用参数化查询的 ? 占位符。虽然 org_id 来自 JWT 解析的 i64 类型(理论上安全),但这种模式违反了参数化查询的最佳实践。
  • 风险:代码模式不安全,若未来重构改变了 org_id 的来源类型,可能引入 SQL 注入。
  • 建议:改为返回参数化条件,使用 ? 占位符并通过 .bind() 传值,与 auth::org_condition 保持一致。

问题7下载中心权限码检查不精确

  • 位置software/server/src/routes/downloads.rs:96,141,184,241
  • 描述:下载中心所有接口(列表、详情、文件下载、清理)统一检查 charge_record:export 权限。但能耗统计导出也使用下载中心,应同时接受 energy:export 权限。当前设计导致只有充电记录导出权限的用户才能看到所有下载任务。
  • 风险:权限模型不够灵活,能耗导出权限的用户无法使用下载中心。
  • 建议:下载中心列表接口改为检查 charge_record:exportenergy:export(任一即可),或新增 download:view 权限码。

警告(建议修复)

警告1users.rslist_users 查询分支大量重复

  • 位置software/server/src/routes/users.rs:143-221
  • 描述list_users 函数中,带关键字/不带关键字 x 有组织过滤/无组织过滤,产生了 6 个几乎相同的 SQL 查询分支,代码约 80 行。
  • 建议:使用动态 SQL 构建(类似 charge_records.rsbuild_where_clause 模式),将条件拼接统一处理。

警告2energy_stats.rs 组织过滤逻辑重复

  • 位置software/server/src/routes/energy_stats.rs:90-99,174-183,230-236,289-298
  • 描述get_summaryget_cabinet_rankingget_project_statsget_trend 四个函数各自独立实现组织过滤逻辑,代码重复。
  • 建议:抽取为通用函数(类似 auth::org_condition),统一复用。

警告3list_cabinet_options 无组织过滤

  • 位置software/server/src/routes/cabinets.rs:366-389
  • 描述:当 project_id 为空时,list_cabinet_options 返回所有柜子,未按用户组织过滤。企业管理员和普通用户可看到全部柜子下拉选项。
  • 建议:追加 auth::org_condition 过滤。

警告4设备重连时旧连接未显式关闭

  • 位置software/server/src/tcp/connection.rs:49-52 + software/server/src/tcp/server.rs:98-104
  • 描述ConnectionPool::register 直接 insert 覆盖旧连接,但旧连接的 mpsc::UnboundedSender 克隆体在 handle_connection 中仍被持有,旧 TCP 连接的任务不会立即终止。
  • 建议:注册时检查是否已存在旧连接,若有则通过通道发送关闭信号或记录日志。

警告5前端 as unknown as 类型断言绕过类型安全

  • 位置software/web/src/pages/devices/index.tsx:81,103 + software/web/src/pages/devices/AddCabinetModal.tsx:44
  • 描述:多处使用 (res as unknown as TreeNode[]) 双重类型断言,绕过了 TypeScript 类型检查。说明 api.get<T> 的返回类型 ApiResponse<T> 与实际使用不匹配。
  • 建议:检查 api.get 泛型推导是否正确,修复类型定义使其无需强制转换。

警告6JWT 密钥常量重复定义

  • 位置software/server/src/config.rs:4 vs software/server/src/middleware/auth.rs:33
  • 描述JWT_SECRET_DEFAULTconfig.rsmiddleware/auth.rs 中各定义一次,值相同但独立维护。
  • 建议:只在 config.rs 中定义一次,middleware/auth.rs 引用 crate::config 中的常量,或在 AppState 中传递已解析的密钥。

警告7401 响应未自动跳转登录页

  • 位置software/web/src/api/request.ts:44-59
  • 描述request 函数在收到 401 响应时抛出 ApiError,但未自动清除本地 token 或跳转到登录页。当 JWT 过期后,用户操作会报错但不会自动跳转到登录页。
  • 建议:在 request 函数中拦截 401 状态码,自动清除 localStorage 并跳转到 /login

警告8数据库表缺少关键索引

  • 位置software/server/src/db/migrate.rs
  • 描述:多个高频查询涉及的列缺少索引:
    • cabin_boards.cabinet_id(设备详情查询)
    • compartments.cabin_board_id(仓体列表查询)
    • charge_records.compartment_id(充电记录查询)
    • charge_records.start_time(时间范围过滤)
    • device_logs.cabinet_id + device_logs.created_at(日志查询)
    • download_tasks.user_id(下载列表查询)
    • users.organization_id(用户组织过滤)
    • cabinets.project_id(项目下柜子查询)
  • 建议为上述列添加索引。MySQL 的 FOREIGN KEY 约束会自动创建索引,但需确认迁移脚本中已包含。

警告9DeviceMessage.extra 使用 Option<Value>flatten 冗余

  • 位置software/server/src/tcp/protocol.rs:29-31
  • 描述extra: Option<Value> 配合 #[serde(flatten)],当无额外字段时值为 Value::Null 而非 NoneOption 包裹无实际意义。
  • 建议:改为 extra: Value 或去掉 Option

建议

建议1Protocol::to_json 使用 expect 可改为 unwrap_or_default

  • 位置software/server/src/tcp/protocol.rs:58,80
  • 描述ServerResponse::to_jsonDeviceCommand::to_json 使用 expect 序列化。虽然理论上不应失败,但为安全起见可改为 unwrap_or_default()
  • 建议:改为 serde_json::to_string(self).unwrap_or_default()

建议2Dashboard.tsx 为空白占位页

  • 位置software/web/src/pages/Dashboard.tsx
  • 描述:后台管理 Dashboard 页面仅为静态文本 "欢迎使用充电柜管理系统",未展示任何统计数据。
  • 建议:参考 H5 首页实现,展示设备在线率、今日充电量、告警数量等关键指标。

建议3柜子详情页显示硬编码零值

  • 位置software/web/src/pages/devices/cabinet.tsx:144-147
  • 描述:柜子详情的仓体卡片中,电压/电流/功率显示为硬编码的 0.0V/0.0A/0W,未从 Redis 实时数据获取。
  • 建议:通过后端接口获取仓体实时数据并渲染。

建议4energy_stats.rsget_summary 执行了 4 次独立查询

  • 位置software/server/src/routes/energy_stats.rs:82-162
  • 描述get_summary 分别执行今日能耗、本月能耗、柜子总数、在线柜子数 4 次查询,可合并为 1-2 次查询减少数据库往返。
  • 建议:使用 CASE WHEN 或子查询合并。

建议5H5 DownloadCenter 轮询定时器清理不完整

  • 位置software/web/src/pages/charge-records/DownloadCenter.tsx:79-86
  • 描述useEffect 依赖 [visible, tasks, fetchTasks],每次 tasks 变化都会清除并重建 setInterval。当有进行中任务时,每 5 秒触发一次重建。
  • 建议:将 tasks 从依赖数组中移除,改用 useRef 跟踪任务状态。

建议6h5/routes.tsx 中 import 语句位于文件中间

  • 位置software/web/src/h5/routes.tsx:47-50
  • 描述import { useEffect, useState } 等导入语句出现在文件中间(第 47 行),违反 ES Module 规范(虽然 TypeScript 编译器允许)。
  • 建议:将所有 import 语句移至文件顶部。

建议7config.rsvalidate 仅打印警告

  • 位置software/server/src/config.rs:47-51
  • 描述:使用默认 JWT 密钥时仅 tracing::warn,生产环境可能被忽略。
  • 建议:生产环境应直接 panic 或通过环境变量强制检查(如检查 NODE_ENV=production 时拒绝默认密钥)。

建议8H5 认证恢复未校验 token 有效性

  • 位置software/web/src/h5/auth.ts:48-55
  • 描述restore 函数从 localStorage 恢复 token 后直接设置 isAuthenticated: true,未向后端验证 token 是否仍然有效。后台管理的 auth.ts 会调用 /auth/me 验证H5 端缺少此步骤。
  • 建议H5 恢复时也调用 /h5/dashboard 或类似接口验证 token 有效性。

审查清单完成情况

1. 编译与类型检查

  • Rust 代码结构完整,模块引用正确(无法在此环境运行 cargo check
  • TypeScript 代码类型定义完整,接口匹配(无法在此环境运行 tsc --noEmit
  • 前端构建配置正常Vite + React

2. 代码规范

  • 单函数基本 <=80 行(最长函数约 70 行)
  • 命名语义化,中文注释完整
  • 常量已抽离(COST_PER_KWHHEARTBEAT_TIMEOUT 等)
  • 无废弃 API 使用

3. 错误处理

  • 所有外部 IO 有异常捕获
  • 无裸 panic!expect 仅用于不可能失败的序列化)
  • 分支逻辑基本全覆盖
  • 错误信息有意义(中文描述)

4. Rust 专项

  • unsafe
  • 所有权管理合理(Clone 用于共享状态,Arc<RwLock> 用于连接池)
  • 异步代码无阻塞操作(使用 tokio::fs 而非 std::fs
  • 内存安全,无冗余拷贝

5. React 专项

  • 全部使用函数式组件 + Hooks
  • 状态管理使用 Zustand轻量级
  • useEffect 清理正确(定时器清理)
  • 存在 as unknown as 类型断言见警告5

6. 安全

  • SQL 全部使用参数化查询(? 占位符)
  • 密码使用 Argon2 哈希
  • JWT 密钥支持环境变量覆盖
  • 输入参数有基本校验手机号、IMEI 格式)
  • 权限码种子数据不完整见严重问题1、2
  • H5 接口缺少数据隔离见严重问题3、4
  • 敏感参数未打印到日志(密码未出现在 tracing 中)

7. 业务逻辑

  • TCP 协议解析正确LF 分隔、签名验证、超时清理)
  • 权限模型正确(总管理员/企业管理员/普通用户三级)
  • 异步导出流程完整(任务创建 -> tokio::spawn 后台处理 -> 下载中心查看)
  • 前端权限组件正确隐藏/显示按钮

8. 架构

  • 接口层、业务逻辑层、数据模型层分离清晰
  • 无循环依赖
  • 模块职责清晰routes 按功能拆分tcp 按职责拆分)
  • 路由注册完整

9. 性能

  • 数据库索引需补充见警告8
  • 无 N+1 查询问题(组织树使用批量查询后内存组装)
  • 前端列表有分页
  • 大数据量导出使用异步处理

10. 可维护性

  • 文件长度合理(大部分 <=300 行,最长 h5.rs 约 757 行但含多个独立接口)
  • 组件拆分合理
  • 部分代码重复见警告1、2
  • 配置外部化(环境变量)

优点

  1. 统一错误处理AppError 枚举 + IntoResponse 实现了优雅的错误处理,所有错误自动转为标准 JSON 格式
  2. 安全的密码方案:使用 Argon2当前推荐的密码哈希算法带随机盐
  3. TCP 协议设计合理LF 分隔 JSON、签名验证、频率限制、心跳超时清理考虑周全
  4. 连接池管理ConnectionPool 使用 Arc<RwLock<HashMap>> 实现线程安全的设备连接管理
  5. 异步导出架构tokio::spawn 后台生成 Excel下载中心轮询查看进度用户体验良好
  6. 前端权限组件<Permission> 组件支持 hidden/disabled 两种模式,按钮级权限控制优雅
  7. H5 移动端独立实现与后台管理使用独立认证、独立路由、Tailwind CSS 样式,互不干扰
  8. 数据库迁移自动化:启动时自动建表 + 种子数据,部署简单
  9. 操作日志完整:用户管理的增删改查均记录操作日志,便于审计
  10. 代码注释质量高:中文注释覆盖所有模块、函数和关键逻辑,可读性好

总结

项目代码质量整体良好,架构设计清晰,技术选型合理。优先修复建议如下:

P0立即修复

  1. 补充缺失的权限码种子数据(org:*project:*device:edit)— 否则非总管理员无法管理组织和项目
  2. H5 接口添加数据隔离 — 否则存在数据泄露风险

P1尽快修复 3. 修复前后端 H5 告警数据结构不匹配 4. h5.rsorg_project_condition 改为参数化查询 5. 下载中心权限码检查优化

P2计划修复 6. 补充数据库索引 7. 消除代码重复(list_usersenergy_stats 组织过滤) 8. 前端 401 自动跳转登录页 9. H5 认证恢复时校验 token 有效性