14 KiB
14 KiB
全面代码审查报告
总体评价
质量评分: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 不需可预测性,可加入随机盐或使用更长哈希。
优点
- 架构分层清晰:后端严格按 路由/中间件/协议/数据库 分层,前端按 API/Store/Page/Component 分层,职责明确。
- 统一错误处理:后端
AppError统一错误类型,自动映射 HTTP 状态码,错误信息对用户友好,内部错误详情仅记录日志。 - 密码安全:使用 argon2 哈希(业界推荐),非 MD5/SHA。
- 参数化查询:所有数据库查询使用
?占位符,有效防止 SQL 注入。 - RBAC 权限模型完整:角色-权限-范围三层设计,前端
Permission组件支持隐藏/禁用两种模式。 - 组织数据隔离:核心查询接口均实现组织级数据隔离,防止跨组织数据泄露。
- TCP 协议处理健壮:LF 分隔粘包、JSON 解析容错、签名验证、时间戳窗口校验、auth_str 频率限制。
- 异步导出流程完整:任务创建 -> 后台生成 Excel -> 进度更新 -> 下载 -> 过期清理,全链路覆盖。
- H5 移动端完整:扫码入口、设备概览、充电控制、BMS 数据展示、确认弹窗,用户体验良好。
- 代码注释充分:所有模块和公共函数都有中文文档注释,便于维护。
- 数据库迁移自动化:启动时自动建表、建索引、初始化权限码和角色,部署简便。
- 前端组件拆分合理:设备管理页拆分为 OrganizationTree、CabinetGrid、AddCabinetModal 等独立组件。
总结
优先修复建议(按紧急程度排序)
-
P0 — 立即修复:
- 心跳超时改为 90 秒(问题1),否则设备无法保持在线
- 添加
device:door:unlock权限码(问题4),否则开门功能不可用 - 下载文件路径安全校验(问题6),防止路径穿越攻击
-
P1 — 本迭代修复:
- 登录接口添加频率限制(问题3)
- auth_str 不返回前端(问题2)
- 补充缺失的权限校验(问题5)
- 充电记录导出添加组织隔离(警告12)
- 导出目录使用绝对路径(问题7)
-
P2 — 下迭代优化:
- 拆分 h5.rs(警告1)
- 修复 N+1 查询(警告3/4/5)
- H5 认证恢复验证 token(警告7)
- 消除
as unknown as类型断言(警告6) - 补充 compartments.updated_at 索引(警告11)
整体代码质量良好,核心业务逻辑正确,安全基础扎实。上述问题修复后可达到生产就绪水平。