17 KiB
17 KiB
全面代码审查报告
总体评价
质量评分:7/10
项目整体架构清晰,模块划分合理,代码风格统一。后端采用 Rust + Axum 技术栈,安全性较好(参数化查询、Argon2 密码哈希、JWT 认证)。前端 React + Arco Design 组件库使用规范,H5 移动端独立实现。但存在若干权限码缺失、数据隔离不完整、前后端接口字段不匹配等严重问题需优先修复。
统计
- 后端文件数:29(Rust)
- 前端文件数:34(TypeScript/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:view、project:view、project:create、project:edit权限。
问题2:设备管理缺少 device:edit 权限码
- 位置:
software/server/src/db/migrate.rs:188+software/server/src/routes/cabinets.rs:166,237 - 描述:
cabinets.rs中update_cabinet和regenerate_auth检查device:edit权限,但种子数据中只有device:view、device:operate、device:create、device:delete,没有device:edit。 - 风险:非总管理员用户无法编辑柜子或重新生成安全码。
- 建议:在种子数据中添加
("device:edit", "编辑设备")。
问题3:H5 接口缺少数据隔离 — 仪表盘返回全局数据
- 位置:
software/server/src/routes/h5.rs:157-245 - 描述:
get_dashboard接口查询的在线柜子数、充电中仓体数、空闲仓体数、故障仓体数、今日充电次数/充电量均为全局统计,未按用户所属组织过滤。企业管理员和普通用户可以看到全平台数据。 - 风险:数据泄露,违反多租户隔离原则。
- 建议:所有统计查询追加
organization_id过滤条件,与后台管理接口保持一致的隔离策略。
问题4:H5 设备详情和仓体详情缺少权限校验
- 位置:
software/server/src/routes/h5.rs:420-488,558-634 - 描述:
get_cabinet_detail、get_cabinet_by_abstract_id、get_compartment_detail三个接口没有进行任何权限检查(只有let _ = &user;),任意已登录用户可通过遍历 ID 访问任意设备和仓体数据。 - 风险:未授权数据访问,任意用户可查看不属于自己组织的设备。
- 建议:添加组织隔离校验,确保用户只能访问本组织下的设备。
问题5:前后端 H5 告警数据结构不匹配
- 位置:
software/server/src/routes/h5.rs:206-224vssoftware/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组件渲染project和cabinet字段,但后端未返回这两个字段,前端会显示undefined。 - 风险:H5 首页告警通知显示异常,用户看到
undefined - undefined 通道X。 - 建议:后端补充
project和cabinet字段(通过 JOIN 查询),或前端改为根据 ID 显示。
问题6:SQL 字符串拼接中直接内联 org_id(h5.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:export或energy:export(任一即可),或新增download:view权限码。
警告(建议修复)
警告1:users.rs 中 list_users 查询分支大量重复
- 位置:
software/server/src/routes/users.rs:143-221 - 描述:
list_users函数中,带关键字/不带关键字 x 有组织过滤/无组织过滤,产生了 6 个几乎相同的 SQL 查询分支,代码约 80 行。 - 建议:使用动态 SQL 构建(类似
charge_records.rs的build_where_clause模式),将条件拼接统一处理。
警告2:energy_stats.rs 组织过滤逻辑重复
- 位置:
software/server/src/routes/energy_stats.rs:90-99,174-183,230-236,289-298 - 描述:
get_summary、get_cabinet_ranking、get_project_stats、get_trend四个函数各自独立实现组织过滤逻辑,代码重复。 - 建议:抽取为通用函数(类似
auth::org_condition),统一复用。
警告3:list_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泛型推导是否正确,修复类型定义使其无需强制转换。
警告6:JWT 密钥常量重复定义
- 位置:
software/server/src/config.rs:4vssoftware/server/src/middleware/auth.rs:33 - 描述:
JWT_SECRET_DEFAULT在config.rs和middleware/auth.rs中各定义一次,值相同但独立维护。 - 建议:只在
config.rs中定义一次,middleware/auth.rs引用crate::config中的常量,或在AppState中传递已解析的密钥。
警告7:401 响应未自动跳转登录页
- 位置:
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约束会自动创建索引,但需确认迁移脚本中已包含。
警告9:DeviceMessage.extra 使用 Option<Value> 与 flatten 冗余
- 位置:
software/server/src/tcp/protocol.rs:29-31 - 描述:
extra: Option<Value>配合#[serde(flatten)],当无额外字段时值为Value::Null而非None,Option包裹无实际意义。 - 建议:改为
extra: Value或去掉Option。
建议
建议1:Protocol::to_json 使用 expect 可改为 unwrap_or_default
- 位置:
software/server/src/tcp/protocol.rs:58,80 - 描述:
ServerResponse::to_json和DeviceCommand::to_json使用expect序列化。虽然理论上不应失败,但为安全起见可改为unwrap_or_default()。 - 建议:改为
serde_json::to_string(self).unwrap_or_default()。
建议2:Dashboard.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 实时数据获取。 - 建议:通过后端接口获取仓体实时数据并渲染。
建议4:energy_stats.rs 中 get_summary 执行了 4 次独立查询
- 位置:
software/server/src/routes/energy_stats.rs:82-162 - 描述:
get_summary分别执行今日能耗、本月能耗、柜子总数、在线柜子数 4 次查询,可合并为 1-2 次查询减少数据库往返。 - 建议:使用
CASE WHEN或子查询合并。
建议5:H5 DownloadCenter 轮询定时器清理不完整
- 位置:
software/web/src/pages/charge-records/DownloadCenter.tsx:79-86 - 描述:
useEffect依赖[visible, tasks, fetchTasks],每次tasks变化都会清除并重建setInterval。当有进行中任务时,每 5 秒触发一次重建。 - 建议:将
tasks从依赖数组中移除,改用useRef跟踪任务状态。
建议6:h5/routes.tsx 中 import 语句位于文件中间
- 位置:
software/web/src/h5/routes.tsx:47-50 - 描述:
import { useEffect, useState }等导入语句出现在文件中间(第 47 行),违反 ES Module 规范(虽然 TypeScript 编译器允许)。 - 建议:将所有 import 语句移至文件顶部。
建议7:config.rs 中 validate 仅打印警告
- 位置:
software/server/src/config.rs:47-51 - 描述:使用默认 JWT 密钥时仅
tracing::warn,生产环境可能被忽略。 - 建议:生产环境应直接 panic 或通过环境变量强制检查(如检查
NODE_ENV=production时拒绝默认密钥)。
建议8:H5 认证恢复未校验 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_KWH、HEARTBEAT_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)
- 配置外部化(环境变量)
优点
- 统一错误处理:
AppError枚举 +IntoResponse实现了优雅的错误处理,所有错误自动转为标准 JSON 格式 - 安全的密码方案:使用 Argon2(当前推荐的密码哈希算法),带随机盐
- TCP 协议设计合理:LF 分隔 JSON、签名验证、频率限制、心跳超时清理,考虑周全
- 连接池管理:
ConnectionPool使用Arc<RwLock<HashMap>>实现线程安全的设备连接管理 - 异步导出架构:
tokio::spawn后台生成 Excel,下载中心轮询查看进度,用户体验良好 - 前端权限组件:
<Permission>组件支持 hidden/disabled 两种模式,按钮级权限控制优雅 - H5 移动端独立实现:与后台管理使用独立认证、独立路由、Tailwind CSS 样式,互不干扰
- 数据库迁移自动化:启动时自动建表 + 种子数据,部署简单
- 操作日志完整:用户管理的增删改查均记录操作日志,便于审计
- 代码注释质量高:中文注释覆盖所有模块、函数和关键逻辑,可读性好
总结
项目代码质量整体良好,架构设计清晰,技术选型合理。优先修复建议如下:
P0(立即修复):
- 补充缺失的权限码种子数据(
org:*、project:*、device:edit)— 否则非总管理员无法管理组织和项目 - H5 接口添加数据隔离 — 否则存在数据泄露风险
P1(尽快修复):
3. 修复前后端 H5 告警数据结构不匹配
4. h5.rs 中 org_project_condition 改为参数化查询
5. 下载中心权限码检查优化
P2(计划修复):
6. 补充数据库索引
7. 消除代码重复(list_users、energy_stats 组织过滤)
8. 前端 401 自动跳转登录页
9. H5 认证恢复时校验 token 有效性