From 3cc21b621fb83bd95574ff064644713d8b382f7b Mon Sep 17 00:00:00 2001 From: 12451 <12451@example.com> Date: Thu, 2 Jul 2026 05:40:44 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E4=BB=BB=E5=8A=A1=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E4=B8=8D=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + tasks/001-server-skeleton.md | 221 --------------- tasks/002-web-scaffold.md | 92 ------ tasks/003-tcp-server.md | 85 ------ tasks/004-organization-management.md | 119 -------- tasks/005-permission-system.md | 171 ------------ tasks/006-charge-records-and-logs.md | 113 -------- tasks/007-energy-management.md | 81 ------ tasks/008-h5-user-interface.md | 118 -------- tasks/009-system-settings.md | 71 ----- tasks/010-security-fixes.md | 155 ----------- tasks/011-code-quality-improvements.md | 100 ------- tasks/012-permission-checks-supplement.md | 172 ------------ tasks/013-critical-fixes.md | 153 ---------- tasks/014-mimo-review-fixes.md | 284 ------------------- tasks/015-nemotron-review-fixes.md | 219 --------------- tasks/016-tcp-service-refactor.md | 324 ---------------------- tasks/017-fix-review-issues.md | 95 ------- tasks/018-code-optimizations.md | 63 ----- tasks/review-code-quality.md | 122 -------- tasks/review-fix-verification-report.md | 81 ------ tasks/review-fix-verification.md | 67 ----- tasks/review-full-codebase-report.md | 221 --------------- tasks/review-full-codebase.md | 184 ------------ tasks/review-mimo-report.md | 262 ----------------- tasks/review-nemotron-report.md | 296 -------------------- tasks/review-north-mini-report.md | 246 ---------------- tasks/review-report.md | 206 -------------- tasks/review-round-2-report.md | 85 ------ tasks/review-round-2.md | 128 --------- tasks/review-tcp-refactor-report.md | 196 ------------- tasks/review-tcp-refactor.md | 91 ------ 32 files changed, 3 insertions(+), 4821 deletions(-) delete mode 100644 tasks/001-server-skeleton.md delete mode 100644 tasks/002-web-scaffold.md delete mode 100644 tasks/003-tcp-server.md delete mode 100644 tasks/004-organization-management.md delete mode 100644 tasks/005-permission-system.md delete mode 100644 tasks/006-charge-records-and-logs.md delete mode 100644 tasks/007-energy-management.md delete mode 100644 tasks/008-h5-user-interface.md delete mode 100644 tasks/009-system-settings.md delete mode 100644 tasks/010-security-fixes.md delete mode 100644 tasks/011-code-quality-improvements.md delete mode 100644 tasks/012-permission-checks-supplement.md delete mode 100644 tasks/013-critical-fixes.md delete mode 100644 tasks/014-mimo-review-fixes.md delete mode 100644 tasks/015-nemotron-review-fixes.md delete mode 100644 tasks/016-tcp-service-refactor.md delete mode 100644 tasks/017-fix-review-issues.md delete mode 100644 tasks/018-code-optimizations.md delete mode 100644 tasks/review-code-quality.md delete mode 100644 tasks/review-fix-verification-report.md delete mode 100644 tasks/review-fix-verification.md delete mode 100644 tasks/review-full-codebase-report.md delete mode 100644 tasks/review-full-codebase.md delete mode 100644 tasks/review-mimo-report.md delete mode 100644 tasks/review-nemotron-report.md delete mode 100644 tasks/review-north-mini-report.md delete mode 100644 tasks/review-report.md delete mode 100644 tasks/review-round-2-report.md delete mode 100644 tasks/review-round-2.md delete mode 100644 tasks/review-tcp-refactor-report.md delete mode 100644 tasks/review-tcp-refactor.md diff --git a/.gitignore b/.gitignore index 73df920..f5d5f94 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,6 @@ Thumbs.db # Temporary *.tmp *.log + +# Tasks +tasks/ diff --git a/tasks/001-server-skeleton.md b/tasks/001-server-skeleton.md deleted file mode 100644 index b06fe62..0000000 --- a/tasks/001-server-skeleton.md +++ /dev/null @@ -1,221 +0,0 @@ -# 任务001:后端骨架 - -## 目标 - -搭建Rust + Axum后端项目骨架,连接MySQL和Redis,建立基础路由框架。 - -## 技术栈 - -- Rust + Axum + Tokio -- MySQL (sqlx) -- Redis (redis crate) - -## 数据库连接 - -``` -MySQL: 10.8.0.252:3306 (root / Hbhyg731024@) -Redis: 10.8.0.252:6379 (password: Hbhyg731024@, db: 5) -``` - -**注意:** 不要动现有的 `AZZCWeChat*` 数据库,新建 `pms_dev` 库。 - -## 项目结构 - -``` -software/server/ -├── Cargo.toml -├── src/ -│ ├── main.rs # 启动入口 -│ ├── config.rs # 配置管理 -│ ├── db/ -│ │ ├── mod.rs # MySQL连接池 -│ │ └── migrate.rs # 数据库迁移(建表) -│ ├── redis.rs # Redis连接 -│ ├── routes/ -│ │ ├── mod.rs # 路由注册 -│ │ └── health.rs # 健康检查接口 -│ ── error.rs # 统一错误处理 -``` - -## 数据库表 - -创建 `pms_dev` 库,执行以下建表SQL: - -```sql --- 组织表 -CREATE TABLE organizations ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(100) NOT NULL COMMENT '组织名称', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- 项目表 -CREATE TABLE projects ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - organization_id BIGINT NOT NULL, - name VARCHAR(100) NOT NULL COMMENT '项目名称', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (organization_id) REFERENCES organizations(id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- 充电柜表 -CREATE TABLE cabinets ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - project_id BIGINT, - abstract_id VARCHAR(20) NOT NULL UNIQUE COMMENT '平台抽象ID,如10-00000000', - imei VARCHAR(15) NOT NULL UNIQUE COMMENT '4G模块IMEI', - auth_str VARCHAR(8) COMMENT '签名安全码', - name VARCHAR(100) COMMENT '柜子名称', - status TINYINT DEFAULT 0 COMMENT '0离线 1在线', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (project_id) REFERENCES projects(id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- 仓控板表 -CREATE TABLE cabin_boards ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - cabinet_id BIGINT NOT NULL, - board_id TINYINT NOT NULL COMMENT '仓控板ID 1-6', - status TINYINT DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (cabinet_id) REFERENCES cabinets(id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- 仓体表 -CREATE TABLE compartments ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - cabin_board_id BIGINT NOT NULL, - channel_id TINYINT NOT NULL COMMENT '通道ID 1-6', - status TINYINT DEFAULT 0 COMMENT '0空闲 1充电中 2故障 3禁用', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (cabin_board_id) REFERENCES cabin_boards(id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- 用户表 -CREATE TABLE users ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - phone VARCHAR(11) NOT NULL UNIQUE COMMENT '手机号', - name VARCHAR(50) COMMENT '姓名', - password VARCHAR(100) NOT NULL, - role TINYINT DEFAULT 0 COMMENT '0普通 1企业管理员 2总管理员', - organization_id BIGINT, - status TINYINT DEFAULT 1 COMMENT '0禁用 1启用', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (organization_id) REFERENCES organizations(id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- 充电记录表 -CREATE TABLE charge_records ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - compartment_id BIGINT NOT NULL, - cabinet_id BIGINT NOT NULL, - start_time TIMESTAMP, - end_time TIMESTAMP, - energy DECIMAL(10,2) COMMENT '充电量kWh', - max_temperature DECIMAL(5,1) COMMENT '最高温度', - status TINYINT DEFAULT 0 COMMENT '0正常 1异常 2手动停止', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (compartment_id) REFERENCES compartments(id), - FOREIGN KEY (cabinet_id) REFERENCES cabinets(id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- 设备日志表 -CREATE TABLE device_logs ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - cabinet_id BIGINT NOT NULL, - log_type TINYINT NOT NULL COMMENT '1状态上报 2故障 3告警 4停电', - content TEXT COMMENT '日志内容JSON', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (cabinet_id) REFERENCES cabinets(id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- 能耗统计表 -CREATE TABLE energy_stats ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - cabinet_id BIGINT NOT NULL, - stat_date DATE NOT NULL, - total_energy DECIMAL(10,2) COMMENT '总能耗kWh', - charge_count INT DEFAULT 0 COMMENT '充电次数', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (cabinet_id) REFERENCES cabinets(id), - UNIQUE KEY uk_cabinet_date (cabinet_id, stat_date) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- 操作日志表 -CREATE TABLE operation_logs ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - user_id BIGINT NOT NULL, - action VARCHAR(100) NOT NULL COMMENT '操作动作', - target_type VARCHAR(50) COMMENT '操作对象类型', - target_id BIGINT COMMENT '操作对象ID', - detail TEXT COMMENT '操作详情', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - -## API路由 - -注册以下基础路由: - -```rust -// 健康检查 -GET /api/health → 返回 {"status": "ok"} - -// 认证 -POST /api/auth/login → 登录 -POST /api/auth/logout → 登出 - -// 组织管理 -GET /api/organizations → 组织列表 -POST /api/organizations → 创建组织 -PUT /api/organizations/:id → 更新组织 -DELETE /api/organizations/:id → 删除组织 - -// 项目管理 -GET /api/projects → 项目列表 -POST /api/projects → 创建项目 -PUT /api/projects/:id → 更新项目 -DELETE /api/projects/:id → 删除项目 - -// 设备管理 -GET /api/cabinets → 柜子列表 -POST /api/cabinets → 添加柜子(支持批量IMEI) -PUT /api/cabinets/:id → 更新柜子 -DELETE /api/cabinets/:id → 删除柜子 -POST /api/cabinets/:id/regenerate-auth → 重新生成auth_str - -// 充电记录 -GET /api/charge-records → 充电记录列表 -GET /api/charge-records/export → 导出充电记录 - -// 设备日志 -GET /api/device-logs → 设备日志列表 - -// 能耗管理 -GET /api/energy-stats → 能耗统计 - -// 用户管理 -GET /api/users → 用户列表 -POST /api/users → 创建用户 -PUT /api/users/:id → 更新用户 -PUT /api/users/:id/reset-password → 重置密码 - -// 操作日志 -GET /api/operation-logs → 操作日志列表 -``` - -## 质量约束 - -1. 完成代码后执行 `cargo check`,必须零报错、零警告 -2. 分层拆分,单函数≤80行,命名语义化,完整注释 -3. 所有外部IO/网络请求异常捕获,禁止裸panic -4. 分支逻辑全覆盖,不遗漏兜底分支 -5. 常量抽离,不使用废弃API -6. Rust内存安全,合理管理所有权,禁用unsafe无合理理由 diff --git a/tasks/002-web-scaffold.md b/tasks/002-web-scaffold.md deleted file mode 100644 index 7df8167..0000000 --- a/tasks/002-web-scaffold.md +++ /dev/null @@ -1,92 +0,0 @@ -# 任务002:前端骨架 - -## 目标 - -搭建React + Arco Design Pro前端项目,配置菜单和路由,实现登录页。 - -## 技术栈 - -- React + TypeScript -- Arco Design Pro (React版) -- Vite -- Tailwind CSS - -## 项目结构 - -``` -software/web/ -├── package.json -├── vite.config.ts -├── src/ -│ ├── App.tsx -│ ├── main.tsx -│ ├── layouts/ -│ │ └── AdminLayout.tsx # 后台布局(侧栏+顶栏+内容区) -│ ├── pages/ -│ │ ├── Login.tsx # 登录页 -│ │ ├── Dashboard.tsx # 首页概览 -│ │ ├── devices/ # 设备管理 -│ │ ├── charge-records/ # 充电记录 -│ │ ├── device-logs/ # 设备日志 -│ │ ├── energy/ # 能耗管理 -│ │ └── settings/ # 系统设置 -│ ├── api/ -│ │ └── request.ts # HTTP请求封装 -│ ├── stores/ -│ │ └── auth.ts # 认证状态 -│ └── utils/ -│ └── permission.ts # 权限工具 -``` - -## 菜单配置 - -```typescript -const menuConfig = [ - { key: 'dashboard', label: 'Dashboard', icon: 'Dashboard' }, - { key: 'devices', label: '设备管理', icon: 'Device' }, - { key: 'charge-records', label: '充电记录', icon: 'File' }, - { key: 'device-logs', label: '设备日志', icon: 'Bug' }, - { key: 'energy', label: '能耗管理', icon: 'Thunderbolt' }, - { key: 'settings', label: '系统设置', icon: 'Settings', children: [ - { key: 'users', label: '用户管理' }, - { key: 'roles', label: '角色权限' }, - { key: 'operation-logs', label: '操作日志' } - ]} -]; -``` - -## 登录页 - -- 手机号 + 密码登录 -- 登录后存储token到localStorage -- 跳转到Dashboard - -## API基础配置 - -```typescript -// 开发环境 -const API_BASE = 'http://localhost:3000/api'; - -// 请求封装 -const request = async (url, options) => { - const token = localStorage.getItem('token'); - const res = await fetch(`${API_BASE}${url}`, { - ...options, - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}` - } - }); - if (!res.ok) throw new Error(res.statusText); - return res.json(); -}; -``` - -## 质量约束 - -1. 完成代码后执行 `tsc --noEmit`,必须零报错、零警告 -2. 分层拆分,单函数≤80行,命名语义化,完整注释 -3. 所有外部IO/网络请求异常捕获 -4. 分支逻辑全覆盖,不遗漏兜底分支 -5. 常量抽离,不使用废弃API -6. React函数式组件+Hooks,不写Class组件 diff --git a/tasks/003-tcp-server.md b/tasks/003-tcp-server.md deleted file mode 100644 index 4257058..0000000 --- a/tasks/003-tcp-server.md +++ /dev/null @@ -1,85 +0,0 @@ -# 任务003:TCP通讯服务 - -## 目标 - -实现TCP长连接服务,处理设备登录、签名验证、数据上报、指令下发。 - -## 技术栈 - -- Rust + Tokio -- TCP长连接 - -## 协议规范 - -### 通信规则 -- 长连接,无需PING包 -- 空闲1分钟上报,充电15秒上报 -- 超时5000ms -- JSON去空格回车省流量,LF处理粘包 -- `dev_id`:IMEI(10进制) -- `sub_device_id`:485 ID -- `msg_id`:自增会话ID - -### 签名机制 -- 设备首次连接调 `auth_str` 接口获取8位安全码(仅一次) -- `sign = SHA256(dev_id + auth_str + timestamp).substring(56, 63)` -- 时间戳秒级,平台校验±60秒 - -### 接口清单 - -| 动作 | 方向 | 说明 | -|------|------|------| -| `login` | 设备→平台 | 登录(需sign签名+timestamp) | -| `auth_str` | 设备→平台 | 获取安全码(仅首次) | -| `off` | 双向 | 停止供电 | -| `status_post` | 设备→平台 | 定时状态上报(rssi/status) | -| `on` | 平台→设备 | 开始供电 | -| `open` | 平台→设备 | 开门(错误码63=门锁故障) | -| `status_get` | 平台→设备 | 读取仓板状态 | -| `reset` | 平台→设备 | 设备重启 | -| `pow_fail` | 设备→平台 | 停电上报 | - -## 实现要求 - -### TCP Server -- 监听端口:3002(可配置) -- 连接管理:维护设备连接池 -- 心跳检测:5000ms超时断开 -- 粘包处理:LF分隔 - -### 登录流程 -1. 设备发送 `{"act":"login","dev_id":"IMEI","msg_id":1,"timestamp":xxx,"sign":"xxx"}` -2. 平台验证签名 -3. 验证通过 → 返回 `{"suc":1,"msg_id":1}` -4. 验证失败 → 返回 `{"suc":0,"err":"签名错误"}` - -### 数据上报处理 -- `status_post`:解析status HEX数据,存入Redis(实时)+ MySQL(持久化) -- `pow_fail`:记录停电日志 - -### 指令下发 -- 提供API接口供HTTP服务调用 -- 通过设备连接池下发指令 -- 设备不在线时返回错误 - -## 项目结构 - -``` -software/server/src/ -├── tcp/ -│ ├── mod.rs # TCP服务入口 -│ ├── server.rs # TCP Server实现 -│ ├── connection.rs # 连接管理 -│ ├── protocol.rs # 协议解析 -│ ├── handler.rs # 消息处理 -│ └── commands.rs # 指令下发 -``` - -## 质量约束 - -1. 完成代码后执行 `cargo check`,必须零报错、零警告 -2. 分层拆分,单函数≤80行,命名语义化,完整注释 -3. 所有外部IO/网络请求异常捕获,禁止裸panic -4. 分支逻辑全覆盖,不遗漏兜底分支 -5. 常量抽离,不使用废弃API -6. Rust内存安全,合理管理所有权,禁用unsafe无合理理由 diff --git a/tasks/004-organization-management.md b/tasks/004-organization-management.md deleted file mode 100644 index d44d039..0000000 --- a/tasks/004-organization-management.md +++ /dev/null @@ -1,119 +0,0 @@ -# 任务004:组织架构管理 - -## 目标 - -实现组织、项目、设备的增删改查,树状菜单展示,设备划拨功能。 - -## 后端API - -### 组织管理 -``` -GET /api/organizations # 组织列表 -POST /api/organizations # 创建组织 -PUT /api/organizations/:id # 更新组织 -DELETE /api/organizations/:id # 删除组织 -``` - -### 项目管理 -``` -GET /api/organizations/:org_id/projects # 项目列表(按组织) -POST /api/organizations/:org_id/projects # 创建项目 -PUT /api/projects/:id # 更新项目 -DELETE /api/projects/:id # 删除项目 -``` - -### 设备管理 -``` -GET /api/projects/:project_id/cabinets # 柜子列表(按项目) -POST /api/cabinets # 添加柜子(支持批量IMEI) -PUT /api/cabinets/:id # 更新柜子(划拨项目) -DELETE /api/cabinets/:id # 删除柜子 -POST /api/cabinets/:id/regenerate-auth # 重新生成auth_str -``` - -### 树状结构 -``` -GET /api/organization-tree # 返回完整树:组织→项目→设备 -``` - -返回格式: -```json -{ - "id": 1, - "name": "组织A", - "children": [ - { - "id": 1, - "name": "项目1", - "children": [ - {"id": 1, "abstract_id": "10-00000001", "imei": "123456789012345", "status": 1} - ] - } - ] -} -``` - -## 前端页面 - -### 设备管理页 - -**布局:** -``` -┌─────────────────┬────────────────────────────────── -│ 组织架构树 │ 卡片网格 │ -│ ├─ 全部 │ ┌─────┐ ┌─────┐ ┌─────┐ │ -│ ├─ 组织A │ │柜子1 │ │柜子2 │ │柜子3 │ │ -│ │ ├─ 项目1 │ │ID │ │ID │ │ID │ │ -│ │ └─ 项目2 │ │状态 │ │状态 │ │状态 │ │ -│ ─ 组织B │ └─────┘ └─────┘ └─────┘ │ -│ │ │ -└─────────────────┴──────────────────────────────────┘ -``` - -**左侧树:** -- 显示组织→项目→设备树状结构 -- 右键菜单:添加组织、添加项目、编辑、删除 -- 点击节点过滤右侧卡片 - -**右侧卡片:** -- 显示柜子ID、状态(在线/离线/充电中/故障) -- 点击卡片进入柜子详情页 - -### 柜子详情页 - -**布局:** -``` -┌──────────────────────────────────────────────┐ -│ ← 返回 柜子10-00000000 状态: 在线 │ -├──────────────────────────────────────────────┤ -│ 仓板1 (ID:1) │ -│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐│ -│ │通道1 │ │通道2 │ │通道3 │ │通道4 │ │通道5 │ │通道6 ││ -│ │状态 │ │状态 │ │状态 │ │状态 │ │状态 │ │状态 ││ -│ │电压 │ │电压 │ │电压 │ │电压 │ │电压 │ │电压 ││ -│ │电流 │ │电流 │ │电流 │ │电流 │ │电流 │ │电流 ││ -│ │功率 │ │功率 │ │功率 │ │功率 │ │功率 │ │功率 ││ -│ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘│ -├──────────────────────────────────────────────┤ -│ 仓板2 (ID:2) ... │ -└────────────────────────────────────────────── -``` - -**通道卡片显示:** 工作状态、电压、电流、功率 - -**点击通道 → 模态框显示充电记录** - -### 添加柜子弹窗 - -- 文本框输入IMEI,一行一个(支持批量) -- 选择所属组织/项目 -- 提交后后端生成auth_str - -## 质量约束 - -1. 完成代码后执行 `cargo check` + `tsc --noEmit`,零报错零警告 -2. 分层拆分,单函数≤80行,命名语义化,完整注释 -3. 所有外部IO/网络请求异常捕获,禁止裸panic -4. 分支逻辑全覆盖,不遗漏兜底分支 -5. 常量抽离,不使用废弃API -6. Rust内存安全,React函数组件+Hooks diff --git a/tasks/005-permission-system.md b/tasks/005-permission-system.md deleted file mode 100644 index 74fc11a..0000000 --- a/tasks/005-permission-system.md +++ /dev/null @@ -1,171 +0,0 @@ -# 任务005:权限系统 - -## 目标 - -实现RBAC权限控制,精确到按钮级别。 - -## 权限模型 - -### 角色 -- **总管理员** — 全部权限,管理所有组织/项目/设备 -- **企业管理员** — 只能管理自己组织下的项目/设备/人员 -- **普通用户** — 根据配置查看/操作特定组织/项目/设备 - -### 权限粒度 -- **组织级** — 用户可看/操作整个组织下所有设备 -- **项目级** — 用户只能看/操作某项目下的设备 -- **设备级** — 用户只能看/操作某一台柜子 - -### 权限类型 -- `view` — 查看 -- `operate` — 操作(控制设备) - -## 数据库表 - -```sql --- 角色表 -CREATE TABLE roles ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(50) NOT NULL UNIQUE, - description VARCHAR(200), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- 权限表 -CREATE TABLE permissions ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - code VARCHAR(50) NOT NULL UNIQUE COMMENT '权限码,如device:view', - name VARCHAR(100) NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- 角色权限关联表 -CREATE TABLE role_permissions ( - role_id BIGINT NOT NULL, - permission_id BIGINT NOT NULL, - PRIMARY KEY (role_id, permission_id), - FOREIGN KEY (role_id) REFERENCES roles(id), - FOREIGN KEY (permission_id) REFERENCES permissions(id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- 用户角色关联表 -CREATE TABLE user_roles ( - user_id BIGINT NOT NULL, - role_id BIGINT NOT NULL, - PRIMARY KEY (user_id, role_id), - FOREIGN KEY (user_id) REFERENCES users(id), - FOREIGN KEY (role_id) REFERENCES roles(id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - --- 用户权限范围表(组织/项目/设备级) -CREATE TABLE user_scopes ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - user_id BIGINT NOT NULL, - scope_type TINYINT NOT NULL COMMENT '1组织 2项目 3设备', - scope_id BIGINT NOT NULL, - permission_type TINYINT NOT NULL COMMENT '1查看 2操作', - FOREIGN KEY (user_id) REFERENCES users(id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - -## 后端API - -### 角色管理 -``` -GET /api/roles # 角色列表 -POST /api/roles # 创建角色 -PUT /api/roles/:id # 更新角色 -DELETE /api/roles/:id # 删除角色 -PUT /api/roles/:id/permissions # 设置角色权限 -``` - -### 用户权限 -``` -GET /api/users/:id/permissions # 获取用户权限 -PUT /api/users/:id/roles # 设置用户角色 -PUT /api/users/:id/scopes # 设置用户权限范围 -``` - -### 权限校验中间件 -```rust -// 路由级别权限校验 -#[middleware] -fn require_permission(permission: &str) { - // 1. 从token获取用户ID - // 2. 查询用户角色和权限 - // 3. 校验权限范围(组织/项目/设备) - // 4. 无权限返回403 -} -``` - -## 前端组件 - -### 权限组件 -```tsx -// 按钮级权限控制 - - - - -// 页面级权限控制 - - - -``` - -### 角色管理页 -- 角色列表 -- 创建/编辑角色 -- 权限树勾选(按模块分组) - -### 用户权限配置 -- 选择角色 -- 配置权限范围(组织/项目/设备) -- 选择权限类型(查看/操作) - -## 权限码列表 - -``` -# 设备管理 -device:view # 查看设备 -device:operate # 操作设备 -device:create # 添加设备 -device:delete # 删除设备 - -# 充电控制 -charge:start # 开始充电 -charge:stop # 停止充电 -charge:open_door # 开门 - -# 充电记录 -charge_record:view # 查看充电记录 -charge_record:export # 导出充电记录 - -# 设备日志 -device_log:view # 查看设备日志 - -# 能耗管理 -energy:view # 查看能耗 -energy:export # 导出能耗 - -# 系统设置 -user:view # 查看用户 -user:create # 创建用户 -user:edit # 编辑用户 -user:delete # 删除用户 -role:view # 查看角色 -role:create # 创建角色 -role:edit # 编辑角色 -role:delete # 删除角色 -operation_log:view # 查看操作日志 -``` - -## 质量约束 - -1. 完成代码后执行 `cargo check` + `tsc --noEmit`,零报错零警告 -2. 分层拆分,单函数≤80行,命名语义化,完整注释 -3. 所有外部IO/网络请求异常捕获,禁止裸panic -4. 分支逻辑全覆盖,不遗漏兜底分支 -5. 常量抽离,不使用废弃API -6. Rust内存安全,React函数组件+Hooks -7. 权限校验必须覆盖所有API接口 diff --git a/tasks/006-charge-records-and-logs.md b/tasks/006-charge-records-and-logs.md deleted file mode 100644 index 044cc50..0000000 --- a/tasks/006-charge-records-and-logs.md +++ /dev/null @@ -1,113 +0,0 @@ -# 任务006:充电记录与设备日志 - -## 目标 - -实现充电记录查询/导出、设备日志查看、异步导出下载中心。 - -## 后端API - -### 充电记录 -``` -GET /api/charge-records # 充电记录列表 - ?cabinet_id=xxx&cabin_board_id=xxx&compartment_id=xxx - &start_time=xxx&end_time=xxx&page=1&page_size=20 - -GET /api/charge-records/export # 导出充电记录(异步) - ?cabinet_id=xxx&start_time=xxx&end_time=xxx -``` - -### 设备日志 -``` -GET /api/device-logs # 设备日志列表 - ?cabinet_id=xxx&log_type=xxx - &start_time=xxx&end_time=xxx&page=1&page_size=20 -``` - -### 下载中心 -``` -GET /api/downloads # 下载任务列表 -GET /api/downloads/:id # 获取下载链接 -POST /api/downloads/cleanup # 清理过期文件 -``` - -## 异步导出流程 - -``` -1. 用户点击"导出" → POST /api/charge-records/export -2. 后端创建下载任务,返回任务ID -3. 后台异步生成Excel文件 -4. 用户到"下载中心"查看进度 -5. 完成后点击下载 -``` - -### 下载任务表 -```sql -CREATE TABLE download_tasks ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - user_id BIGINT NOT NULL, - task_type VARCHAR(50) NOT NULL COMMENT 'charge_records/energy_stats', - status TINYINT DEFAULT 0 COMMENT '0排队 1处理中 2完成 3失败', - file_path VARCHAR(500) COMMENT '文件路径', - file_name VARCHAR(200) COMMENT '文件名', - progress INT DEFAULT 0 COMMENT '进度百分比', - error_message TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - completed_at TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - -### 文件名智能生成 -``` -充电记录_项目A_20260701-20260731.xlsx -能耗统计_组织B_2026年7月.xlsx -设备日志_柜子10-00000001_20260701.xlsx -``` - -## 前端页面 - -### 充电记录页 - -**顶部过滤:** -- 柜子下拉(可选) -- 仓板下拉(可选,依赖柜子) -- 通道下拉(可选,依赖仓板) -- 时间范围选择器 -- 查询按钮 + 导出按钮 - -**列表显示:** -| 柜子ID | 仓板 | 通道 | 开始时间 | 结束时间 | 充电量 | 最高温度 | 状态 | -|--------|------|------|----------|----------|--------|----------|------| - -**分页:** 每页20条 - -### 设备日志页 - -**顶部过滤:** -- 柜子下拉 -- 日志类型(状态上报/故障/告警/停电) -- 时间范围 - -**列表显示:** -| 柜子ID | 类型 | 内容摘要 | 时间 | -|--------|------|----------|------| - -### 下载中心 - -**位置:** 充电记录页/能耗管理页的"导出"按钮旁 - -**弹窗显示:** -| 任务名称 | 状态 | 进度 | 操作 | -|----------|------|------|------| -| 充电记录_项目A... | 处理中 | 65% | 等待... | -| 能耗统计_组织B... | 完成 | 100% | [下载] | - -## 质量约束 - -1. 完成代码后执行 `cargo check` + `tsc --noEmit`,零报错零警告 -2. 分层拆分,单函数≤80行,命名语义化,完整注释 -3. 所有外部IO/网络请求异常捕获,禁止裸panic -4. 分支逻辑全覆盖,不遗漏兜底分支 -5. 常量抽离,不使用废弃API -6. Rust内存安全,React函数组件+Hooks -7. 导出任务异步执行,不阻塞请求 diff --git a/tasks/007-energy-management.md b/tasks/007-energy-management.md deleted file mode 100644 index aae9ff6..0000000 --- a/tasks/007-energy-management.md +++ /dev/null @@ -1,81 +0,0 @@ -# 任务007:能耗管理 - -## 目标 - -实现电表数据统计,柜子/项目维度的能耗分析,趋势图表。 - -## 后端API - -``` -GET /api/energy-stats/summary # 总览(今日/本月总能耗、电费估算) -GET /api/energy-stats/cabinets # 柜子能耗排行 -GET /api/energy-stats/projects # 项目能耗统计 -GET /api/energy-stats/trend # 趋势数据(日/周/月) -GET /api/energy-stats/export # 导出能耗报表(异步) -``` - -### 响应示例 - -**总览:** -```json -{ - "today_energy": 3750.50, - "month_energy": 85200.00, - "estimated_cost": 68160.00, - "cabinet_count": 128, - "online_count": 125 -} -``` - -**柜子排行:** -```json -[ - {"cabinet_id": 1, "abstract_id": "10-00000001", "today_energy": 320.5}, - {"cabinet_id": 2, "abstract_id": "10-00000002", "today_energy": 285.0} -] -``` - -**趋势(日):** -```json -[ - {"date": "2026-07-01", "energy": 3750.50, "charge_count": 423}, - {"date": "2026-06-30", "energy": 3680.20, "charge_count": 415} -] -``` - -## 前端页面 - -### 能耗管理页 - -**顶部数据卡片:** -``` -┌─────────┐ ┌───────── ┌─────────┐ ┌───────── -│今日能耗 │ │本月能耗 │ │电费估算 │ │在线柜子 │ -│3,750 kWh│ │85,200 kWh│ │¥68,160 │ │ 125 │ -└───────── └─────────┘ └───────── └─────────┘ -``` - -**柜子能耗排行:** -- 表格显示柜子ID、今日能耗、本月能耗 -- 支持排序 - -**项目能耗统计:** -- 表格显示项目名、柜子数、今日能耗、本月能耗 - -**趋势图:** -- 日/周/月切换 -- 折线图显示能耗趋势 -- 柱状图显示充电次数 - -**导出按钮:** -- 异步导出Excel -- 文件名:`能耗统计_2026年7月.xlsx` - -## 质量约束 - -1. 完成代码后执行 `cargo check` + `tsc --noEmit`,零报错零警告 -2. 分层拆分,单函数≤80行,命名语义化,完整注释 -3. 所有外部IO/网络请求异常捕获,禁止裸panic -4. 分支逻辑全覆盖,不遗漏兜底分支 -5. 常量抽离,不使用废弃API -6. Rust内存安全,React函数组件+Hooks diff --git a/tasks/008-h5-user-interface.md b/tasks/008-h5-user-interface.md deleted file mode 100644 index aa19e38..0000000 --- a/tasks/008-h5-user-interface.md +++ /dev/null @@ -1,118 +0,0 @@ -# 任务008:H5用户端 - -## 目标 - -实现H5用户端,底部导航(首页/设备/我的),扫码进入设备详情,充电控制。 - -## 技术栈 - -- React + TypeScript(复用前端项目) -- 移动端适配(viewport + rem/vw) -- Arco Design Mobile(或自定义移动端组件) - -## 路由结构 - -``` -/pms # H5首页(Dashboard) -/pms/devices # 设备列表 -/pms/devices/:id # 设备详情(柜子视图) -/pms/me # 我的 -/pms/login # 登录 -``` - -**扫码入口:** `/pms/{abstract_id}` 直接跳到设备详情 - -## 页面设计 - -### 底部导航 - -``` -┌─────────────────┐ -│ │ -│ 页面内容 │ -│ │ -─────────────────┤ -│ 首页 | 设备 | 我的 │ -└─────────────────┘ -``` - -### 首页(Dashboard) - -**数据卡片:** -``` -┌─────┐ ┌───── ┌─────┐ ┌───── -│在线 │ │充电中│ │空闲 │ │故障 │ -│ 125 │ │ 45 │ │ 82 │ │ 3 │ -│柜子 │ │柜子 │ │通道 │ │通道 │ -└───── └───── └───── └─────┘ -``` - -**今日统计:** -- 充电次数:423次 -- 充电时长:628小时 -- 总能耗:3,750 kWh - -**告警通知:** -- ⚠ 项目A-柜子3 通道12 过温 -- ⚠ 项目B-柜子7 离线 - -**项目列表:** -- 项目A | 3柜 | 充电28 空闲10 故障2 -- 项目B | 2柜 | 充电17 空闲15 故障0 - -### 设备页 - -**项目列表 → 设备列表 → 设备详情** - -**设备详情(柜子视图):** -``` -┌──────────────────────────────────────────────┐ -│ 柜子ID: 10-00000000 状态: 在线 │ -├──────────────────────────────────────────────┤ -│ 仓板1 (ID:1) │ -│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐│ -│ │通道1 │ │通道2 │ │通道3 │ │通道4 │ │通道5 │ │通道6 ││ -│ │状态 │ │状态 │ │状态 │ │状态 │ │状态 │ │状态 ││ -│ │电压 │ │电压 │ │电压 │ │电压 │ │电压 │ │电压 ││ -│ │电流 │ │电流 │ │电流 │ │电流 │ │电流 │ │电流 ││ -│ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘│ -──────────────────────────────────────────────┤ -│ 仓板2 (ID:2) ... │ -└──────────────────────────────────────────────┘ -``` - -**通道详情页:** -- 实时状态(SOC、SOH) -- 电气参数(电压、电流、功率、能耗) -- BMS数据(单体电压、温度、告警) -- 充电曲线 -- 操作按钮(停止充电、开门)— 需确认弹窗 - -### 我的页 - -- 个人信息(姓名、手机号) -- 修改密码 -- 退出登录 - -## API接口 - -``` -GET /api/h5/dashboard # 首页数据 -GET /api/h5/projects # 项目列表 -GET /api/h5/cabinets # 设备列表(按项目) -GET /api/h5/cabinets/:id # 设备详情 -GET /api/h5/compartments/:id # 通道详情 -POST /api/h5/charge/start # 开始充电 -POST /api/h5/charge/stop # 停止充电 -POST /api/h5/door/open # 开门 -``` - -## 质量约束 - -1. 完成代码后执行 `tsc --noEmit`,零报错零警告 -2. 分层拆分,单函数≤80行,命名语义化,完整注释 -3. 所有外部IO/网络请求异常捕获 -4. 分支逻辑全覆盖,不遗漏兜底分支 -5. 常量抽离,不使用废弃API -6. React函数组件+Hooks -7. 移动端适配,触摸友好 diff --git a/tasks/009-system-settings.md b/tasks/009-system-settings.md deleted file mode 100644 index 00491c4..0000000 --- a/tasks/009-system-settings.md +++ /dev/null @@ -1,71 +0,0 @@ -# 任务009:系统设置 - -## 目标 - -实现用户管理、操作日志查看。 - -## 后端API - -### 用户管理 -``` -GET /api/users # 用户列表(分页) -POST /api/users # 创建用户(手机号+姓名+角色) -PUT /api/users/:id # 编辑用户 -PUT /api/users/:id/reset-password # 重置密码 -PUT /api/users/:id/status # 启用/禁用 -DELETE /api/users/:id # 删除用户 -``` - -### 操作日志 -``` -GET /api/operation-logs # 操作日志列表 - ?user_id=xxx&action=xxx - &start_time=xxx&end_time=xxx&page=1&page_size=20 -``` - -## 前端页面 - -### 用户管理页 - -**列表显示:** -| 手机号 | 姓名 | 角色 | 所属组织 | 状态 | 操作 | -|--------|------|------|----------|------|------| -| 138... | 张三 | 企业管理员 | 组织A | 启用 | 编辑/重置密码/禁用 | - -**创建用户弹窗:** -- 手机号(必填,11位) -- 姓名(选填) -- 角色选择(总管理员/企业管理员/普通用户) -- 所属组织(企业管理员必填) -- 初始密码(默认123456,可修改) - -**编辑用户弹窗:** -- 姓名 -- 角色 -- 所属组织 - -**重置密码:** -- 确认弹窗 -- 新密码(默认123456) - -### 操作日志页 - -**顶部过滤:** -- 用户下拉 -- 操作类型(登录/创建/编辑/删除/控制设备) -- 时间范围 - -**列表显示:** -| 用户 | 操作 | 对象类型 | 对象 | 详情 | 时间 | -|------|------|----------|------|------|------| -| 张三 | 创建柜子 | 设备 | 10-00000001 | 批量添加3台 | 2026-07-01 10:30 | -| 李四 | 开始充电 | 通道 | 通道1 | 柜子10-00000001 | 2026-07-01 10:35 | - -## 质量约束 - -1. 完成代码后执行 `cargo check` + `tsc --noEmit`,零报错零警告 -2. 分层拆分,单函数≤80行,命名语义化,完整注释 -3. 所有外部IO/网络请求异常捕获,禁止裸panic -4. 分支逻辑全覆盖,不遗漏兜底分支 -5. 常量抽离,不使用废弃API -6. Rust内存安全,React函数组件+Hooks diff --git a/tasks/010-security-fixes.md b/tasks/010-security-fixes.md deleted file mode 100644 index 2443a30..0000000 --- a/tasks/010-security-fixes.md +++ /dev/null @@ -1,155 +0,0 @@ -# 任务010:安全修复 - -## 目标 - -修复代码审查发现的4个严重安全问题。 - -## 修复清单 - -### 1. SQL注入修复( 高优先级) - -**问题位置:** -- `src/routes/operation_logs.rs:49-63` -- `src/routes/charge_records.rs:130` -- `src/routes/energy_stats.rs:341` - -**修复方案:** -将所有 `format!` 拼接的SQL改为参数化查询。 - -```rust -// 错误示例(operation_logs.rs) -let sql = format!( - "SELECT * FROM operation_logs WHERE action = '{}' AND created_at >= '{}'", - action.replace("'", "''"), start_time -); - -// 正确示例 -let sql = "SELECT * FROM operation_logs WHERE action = $1 AND created_at >= $2"; -sqlx::query_as(sql) - .bind(&action) - .bind(&start_time) -``` - -**涉及文件:** -- `src/routes/operation_logs.rs` -- `src/routes/charge_records.rs` -- `src/routes/energy_stats.rs` -- `src/routes/downloads.rs` - -### 2. 密码哈希(🔴 高优先级) - -**问题位置:** `src/routes/auth.rs:51` - -**修复方案:** -集成 `argon2` crate,密码存储和验证使用哈希。 - -```rust -// Cargo.toml 添加 -argon2 = "0.5" - -// 密码哈希(创建/重置用户时) -use argon2::{ - password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, SaltString}, - Argon2, -}; - -fn hash_password(password: &str) -> Result { - let salt = SaltString::generate(&mut OsRng); - let argon2 = Argon2::default(); - let hash = argon2.hash_password(password.as_bytes(), &salt)?; - Ok(hash.to_string()) -} - -// 密码验证(登录时) -fn verify_password(password: &str, hash: &str) -> Result { - let parsed_hash = PasswordHash::new(hash)?; - let argon2 = Argon2::default(); - Ok(argon2.verify_password(password.as_bytes(), &parsed_hash).is_ok()) -} -``` - -**涉及文件:** -- `src/routes/auth.rs` — 登录验证改用 `verify_password` -- `src/routes/users.rs` — 创建/重置密码改用 `hash_password` -- `src/db/migrate.rs` — users表password字段长度改为255(argon2哈希较长) - -### 3. 组织级数据隔离(🔴 高优先级) - -**问题位置:** -- `src/routes/organizations.rs` 全部路由 -- `src/routes/charge_records.rs` -- `src/routes/device_logs.rs` -- `src/routes/energy_stats.rs` - -**修复方案:** -在 `CurrentUser` 中携带 `organization_id`,查询时自动过滤。 - -```rust -// middleware/auth.rs 修改 -pub struct CurrentUser { - pub user_id: i64, - pub role_level: i32, // 0普通 1企业管理员 2总管理员 - pub organization_id: Option, // 新增 - pub permissions: HashSet, -} - -// 查询时根据角色自动过滤 -pub fn apply_org_filter(query: &str, user: &CurrentUser) -> String { - if user.role_level >= 2 { - // 总管理员看全部 - query.to_string() - } else if let Some(org_id) = user.organization_id { - // 企业管理员只看本组织 - format!("{} WHERE organization_id = {}", query, org_id) - } else { - format!("{} WHERE 1=0", query) // 无组织用户看不了 - } -} -``` - -**涉及文件:** -- `src/middleware/auth.rs` — CurrentUser增加organization_id -- `src/routes/organizations.rs` — 所有查询加组织过滤 -- `src/routes/charge_records.rs` — 加组织过滤 -- `src/routes/device_logs.rs` — 加组织过滤 -- `src/routes/energy_stats.rs` — 加组织过滤 - -### 4. 补充权限校验(🟡 中优先级) - -**问题位置:** -- `src/routes/charge_records.rs:158` -- `src/routes/device_logs.rs:62` -- `src/routes/energy_stats.rs:81-283` -- `src/routes/downloads.rs:87` - -**修复方案:** -为每个路由添加对应的 `check_permission` 调用。 - -```rust -// 示例 -#[get("/api/charge-records")] -async fn list_charge_records( - user: CurrentUser, - pool: State, - query: Query, -) -> Result>> { - auth::check_permission(&user, "charge_record:view")?; // 新增 - // ... 原有逻辑 -} -``` - -**权限码对应:** -- 充电记录 → `charge_record:view` -- 设备日志 → `device_log:view` -- 能耗统计 → `energy:view` -- 下载中心 → `charge_record:export` 或 `energy:export` - -## 质量约束 - -1. 完成代码后执行 `cargo check` + `cargo clippy`,零报错零警告 -2. 分层拆分,单函数≤80行,命名语义化,完整注释 -3. 所有外部IO/网络请求异常捕获,禁止裸panic -4. 分支逻辑全覆盖,不遗漏兜底分支 -5. 常量抽离,不使用废弃API -6. Rust内存安全,合理管理所有权,禁用unsafe无合理理由 -7. 修复后重新运行审查清单中的安全项目,确保全部通过 diff --git a/tasks/011-code-quality-improvements.md b/tasks/011-code-quality-improvements.md deleted file mode 100644 index de1d007..0000000 --- a/tasks/011-code-quality-improvements.md +++ /dev/null @@ -1,100 +0,0 @@ -# 任务011:代码质量优化 - -## 目标 - -修复代码审查发现的警告问题,提升代码质量。 - -## 修复清单 - -### 1. 拆分大文件( 中优先级) - -**organizations.rs(693行)** -拆分为: -- `src/routes/organizations.rs` — 组织CRUD(~150行) -- `src/routes/projects.rs` — 项目CRUD(~150行) -- `src/routes/cabinets.rs` — 设备CRUD+详情(~200行) -- `src/routes/cabinet_tree.rs` — 树状结构(~100行) - -**users.rs(495行)** -拆分为: -- `src/routes/users.rs` — 用户CRUD(~200行) -- `src/routes/user_operations.rs` — 操作日志辅助函数(~100行) - -**devices/index.tsx(631行)** -拆分为: -- `src/pages/devices/index.tsx` — 主页面(~150行) -- `src/pages/devices/OrganizationTree.tsx` — 组织树组件(~150行) -- `src/pages/devices/CabinetGrid.tsx` — 柜子卡片网格(~150行) -- `src/pages/devices/AddCabinetModal.tsx` — 添加柜子弹窗(~100行) - -### 2. JWT密钥安全检查(🟡 中优先级) - -**位置:** `src/middleware/auth.rs:28` - -**修复方案:** -启动时检测默认密钥,打印WARN日志。 - -```rust -// config.rs 添加 -impl Config { - pub fn validate(&self) -> Result<()> { - if self.jwt_secret == JWT_SECRET_DEFAULT { - tracing::warn!("⚠️ 使用默认JWT密钥,生产环境请设置JWT_SECRET环境变量"); - } - Ok(()) - } -} - -// main.rs 启动时调用 -let config = Config::from_env()?; -config.validate()?; // 新增 -``` - -### 3. 修复IconFolderOpen构建错误(🟡 中优先级) - -**位置:** `src/pages/devices/index.tsx:33` - -**修复方案:** -替换为可用图标。 - -```tsx -// 错误 -import { IconFolderOpen } from '@arco-design/web-react/icon'; - -// 修复(选择存在的图标) -import { IconFolder } from '@arco-design/web-react/icon'; -// 或 -import { IconApps } from '@arco-design/web-react/icon'; -``` - -### 4. 清理clippy警告(🟢 低优先级) - -**修复方案:** -```bash -cd software/server -cargo clippy --fix --allow-dirty -``` - -自动修复大部分clippy警告(collapsible_if、manual_unwrap_or_default等)。 - -### 5. H5充电曲线数据(🟢 低优先级) - -**位置:** `src/h5/pages/compartment-detail.tsx:156` - -**修复方案:** -添加TODO注释,标记为待后端提供数据后替换。 - -```tsx -// TODO: 待后端提供充电曲线API后替换为真实数据 -const mockChartData = [30, 45, 55, ...]; -``` - -## 质量约束 - -1. 完成代码后执行 `cargo check` + `cargo clippy` + `tsc --noEmit`,零报错零警告 -2. 分层拆分,单函数≤80行,命名语义化,完整注释 -3. 所有外部IO/网络请求异常捕获,禁止裸panic -4. 分支逻辑全覆盖,不遗漏兜底分支 -5. 常量抽离,不使用废弃API -6. Rust内存安全,React函数组件+Hooks -7. 拆分后的文件职责单一,无循环依赖 diff --git a/tasks/012-permission-checks-supplement.md b/tasks/012-permission-checks-supplement.md deleted file mode 100644 index 58bb356..0000000 --- a/tasks/012-permission-checks-supplement.md +++ /dev/null @@ -1,172 +0,0 @@ -# 任务012:补充权限校验 - -## 目标 - -为 cabinets.rs、projects.rs、organizations.rs 三个模块补充完整的权限校验。 - -## 问题描述 - -第二轮代码审查发现这3个模块的handler函数签名中缺少 `CurrentUser` 参数,导致无法进行权限校验。任何登录用户都可以访问这些敏感接口。 - -## 修复清单 - -### 1. 权限码定义 - -首先在权限表中添加缺失的权限码: - -```sql --- 组织管理 -INSERT INTO permissions (code, name) VALUES ('org:view', '查看组织'); -INSERT INTO permissions (code, name) VALUES ('org:create', '创建组织'); -INSERT INTO permissions (code, name) VALUES ('org:edit', '编辑组织'); -INSERT INTO permissions (code, name) VALUES ('org:delete', '删除组织'); - --- 项目管理 -INSERT INTO permissions (code, name) VALUES ('project:view', '查看项目'); -INSERT INTO permissions (code, name) VALUES ('project:create', '创建项目'); -INSERT INTO permissions (code, name) VALUES ('project:edit', '编辑项目'); -INSERT INTO permissions (code, name) VALUES ('project:delete', '删除项目'); - --- 设备管理(补充) -INSERT INTO permissions (code, name) VALUES ('device:edit', '编辑设备'); -``` - -### 2. organizations.rs 修复 - -**文件:** `src/routes/organizations.rs` - -**修改内容:** -- 所有handler函数签名添加 `user: CurrentUser` 参数 -- 每个handler开头调用 `check_permission` - -```rust -// 示例:获取组织列表 -#[get("/api/organizations")] -async fn list_organizations( - user: CurrentUser, // 新增 - pool: State, -) -> Result>> { - auth::check_permission(&user, "org:view")?; // 新增 - // ... 原有逻辑 -} - -// 示例:创建组织 -#[post("/api/organizations")] -async fn create_organization( - user: CurrentUser, // 新增 - pool: State, - json: Json, -) -> Result> { - auth::check_permission(&user, "org:create")?; // 新增 - // ... 原有逻辑 -} -``` - -**需要修复的端点:** -- `GET /api/organizations` → `org:view` -- `POST /api/organizations` → `org:create` -- `PUT /api/organizations/:id` → `org:edit` -- `DELETE /api/organizations/:id` → `org:delete` - -### 3. projects.rs 修复 - -**文件:** `src/routes/projects.rs` - -**修改内容:** -- 所有handler函数签名添加 `user: CurrentUser` 参数 -- 每个handler开头调用 `check_permission` - -**需要修复的端点:** -- `GET /api/organizations/:org_id/projects` → `project:view` -- `POST /api/organizations/:org_id/projects` → `project:create` -- `PUT /api/projects/:id` → `project:edit` -- `DELETE /api/projects/:id` → `project:delete` - -### 4. cabinets.rs 修复 - -**文件:** `src/routes/cabinets.rs` - -**修改内容:** -- 所有handler函数签名添加 `user: CurrentUser` 参数 -- 每个handler开头调用 `check_permission` -- `list_cabinets` 和 `get_cabinet` 需要额外验证用户是否有权访问该项目 - -**需要修复的端点:** -- `GET /api/projects/:project_id/cabinets` → `device:view` + 项目权限验证 -- `POST /api/cabinets` → `device:create` -- `PUT /api/cabinets/:id` → `device:edit` -- `DELETE /api/cabinets/:id` → `device:delete` -- `POST /api/cabinets/:id/regenerate-auth` → `device:edit` -- `GET /api/cabinets/:id` → `device:view` + 项目权限验证 -- `GET /api/cabinets/options` → `device:view` -- `GET /api/cabin-boards/options` → `device:view` -- `GET /api/compartments/options` → `device:view` - -**项目权限验证逻辑:** -```rust -// 验证用户是否有权访问指定项目 -async fn check_project_access( - user: &CurrentUser, - pool: &MySqlPool, - project_id: i64, -) -> Result<()> { - if user.role_level >= 2 { - // 总管理员可访问所有项目 - return Ok(()); - } - - if let Some(org_id) = user.organization_id { - // 验证项目是否属于用户的组织 - let project: Option<(i64,)> = sqlx::query_as( - "SELECT id FROM projects WHERE id = ? AND organization_id = ?" - ) - .bind(project_id) - .bind(org_id) - .fetch_optional(pool) - .await?; - - if project.is_none() { - return Err(AppError::Forbidden("无权访问该项目".into())); - } - } else { - return Err(AppError::Forbidden("无组织关联".into())); - } - - Ok(()) -} -``` - -### 5. users.rs 修复(额外) - -**文件:** `src/routes/users.rs` - -**修改内容:** -- `list_users` 添加组织隔离,企业管理员只能看到本组织的用户 - -```rust -// 修改 list_users 查询 -let users = if user.role_level >= 2 { - // 总管理员看所有用户 - sqlx::query_as("SELECT * FROM users") - .fetch_all(&pool) - .await? -} else if let Some(org_id) = user.organization_id { - // 企业管理员只看本组织用户 - sqlx::query_as("SELECT * FROM users WHERE organization_id = ?") - .bind(org_id) - .fetch_all(&pool) - .await? -} else { - vec![] // 无组织用户看不到任何人 -}; -``` - -## 质量约束 - -1. 完成代码后执行 `cargo check` + `cargo clippy`,零报错零警告 -2. 分层拆分,单函数≤80行,命名语义化,完整注释 -3. 所有外部IO/网络请求异常捕获,禁止裸panic -4. 分支逻辑全覆盖,不遗漏兜底分支 -5. 常量抽离,不使用废弃API -6. Rust内存安全,合理管理所有权,禁用unsafe无合理理由 -7. 修复后重新运行 `cargo test`(如有测试)确保无回归 diff --git a/tasks/013-critical-fixes.md b/tasks/013-critical-fixes.md deleted file mode 100644 index 4b42ee5..0000000 --- a/tasks/013-critical-fixes.md +++ /dev/null @@ -1,153 +0,0 @@ -# 任务013:严重问题修复 - -## 目标 - -修复全面代码审查发现的5个严重问题。 - -## 修复清单 - -### 1. 设备指令接口权限校验( 高优先级) - -**位置:** `src/tcp/commands.rs` - -**问题:** HTTP API接口缺少权限校验,任何登录用户都能控制设备。 - -**修复方案:** -```rust -// 修改 send_command handler -pub async fn send_command( - user: CurrentUser, // 新增 - State(state): State, - Path(dev_id): Path, - Json(body): Json, -) -> Result, AppError> { - auth::check_permission(&user, "device:operate")?; // 新增 - // ... 原有逻辑 -} - -// 修改 get_online_devices handler -pub async fn get_online_devices( - user: CurrentUser, // 新增 - State(state): State, -) -> Result, AppError> { - auth::check_permission(&user, "device:view")?; // 新增 - // ... 原有逻辑 -} -``` - -### 2. 组织树接口权限校验和数据隔离(🔴 高优先级) - -**位置:** `src/routes/cabinet_tree.rs` - -**问题:** 组织树接口无权限校验,且未做数据隔离,用户能看到所有组织的数据。 - -**修复方案:** -```rust -pub async fn get_organization_tree( - user: CurrentUser, // 新增 - State(state): State, -) -> Result, AppError> { - auth::check_permission(&user, "device:view")?; // 新增 - - let db = &state.mysql; - - // 根据角色过滤数据 - let orgs = if user.role_level >= 2 { - // 总管理员看所有组织 - sqlx::query_as::<_, OrganizationRow>("SELECT id, name FROM organizations ORDER BY id") - .fetch_all(db) - .await? - } else if let Some(org_id) = user.organization_id { - // 企业管理员只看本组织 - sqlx::query_as::<_, OrganizationRow>( - "SELECT id, name FROM organizations WHERE id = ? ORDER BY id" - ) - .bind(org_id) - .fetch_all(db) - .await? - } else { - vec![] // 无组织用户看不到数据 - }; - - // ... 构建树状结构 -} -``` - -### 3. 默认密码硬编码(🔴 中优先级) - -**位置:** `src/routes/users.rs` - -**问题:** 默认密码 "123456" 硬编码在代码中。 - -**修复方案:** -```rust -// config.rs 添加默认密码配置 -pub const DEFAULT_PASSWORD: &str = "123456"; // 或从环境变量读取 - -// users.rs 使用配置 -use crate::config::DEFAULT_PASSWORD; - -// 创建用户时 -let password = body.password.unwrap_or_else(|| DEFAULT_PASSWORD.to_string()); -let hashed = auth::hash_password(&password)?; -``` - -### 4. auth_str安全码明文打印到日志(🔴 中优先级) - -**位置:** `src/routes/organizations.rs` 或 `src/tcp/handler.rs` - -**问题:** auth_str安全码明文打印到日志,存在泄露风险。 - -**修复方案:** -```rust -// 修改日志输出,只打印前2位+*** -tracing::info!( - "生成安全码 id={} auth_str={}***", - id, - &auth_str[..2] -); - -// 或完全不打印auth_str -tracing::info!("生成安全码 id={}", id); -``` - -### 5. 前端刷新后用户信息丢失(🔴 中优先级) - -**位置:** `src/stores/auth.ts` 或 `src/App.tsx` - -**问题:** 页面刷新后,虽然token在localStorage,但用户信息(permissions等)未恢复,导致权限按钮消失。 - -**修复方案:** -```tsx -// stores/auth.ts 添加 restore 方法 -const restoreAuth = async () => { - const token = localStorage.getItem('token'); - if (!token) return false; - - try { - const user = await api.get('/auth/me'); - setToken(token); - setUser(user); - setPermissions(user.permissions); - return true; - } catch { - localStorage.removeItem('token'); - return false; - } -}; - -// App.tsx 或 AdminLayout.tsx 中调用 -useEffect(() => { - restoreAuth(); -}, []); -``` - -## 质量约束 - -1. 完成代码后执行 `cargo check` + `cargo clippy` + `tsc --noEmit`,零报错零警告 -2. 分层拆分,单函数≤80行,命名语义化,完整注释 -3. 所有外部IO/网络请求异常捕获,禁止裸panic -4. 分支逻辑全覆盖,不遗漏兜底分支 -5. 常量抽离,不使用废弃API -6. Rust内存安全,React函数组件+Hooks -7. 修复后重新运行审查清单中的安全项目,确保全部通过 diff --git a/tasks/014-mimo-review-fixes.md b/tasks/014-mimo-review-fixes.md deleted file mode 100644 index 88e2521..0000000 --- a/tasks/014-mimo-review-fixes.md +++ /dev/null @@ -1,284 +0,0 @@ -# 任务014:MIMO审查问题修复 - -## 目标 - -修复MIMO审查发现的5个严重问题和部分警告。 - -## 修复清单 - -### 1. list_users元组编译问题( 高优先级) - -**位置:** `src/routes/users.rs` - -**问题:** `list_users` 使用7元素元组可能导致sqlx编译错误。 - -**修复方案:** -```rust -// 定义结构体替代元组 -#[derive(sqlx::FromRow)] -struct UserRow { - id: i64, - phone: String, - name: Option, - role: i32, - organization_id: Option, - status: i32, - created_at: chrono::NaiveDateTime, -} - -// 查询时使用结构体 -let users = sqlx::query_as::<_, UserRow>( - "SELECT id, phone, name, role, organization_id, status, created_at FROM users ..." -) -.fetch_all(pool) -.await?; -``` - -### 2. 充电记录COUNT查询缺少JOIN( 高优先级) - -**位置:** `src/routes/charge_records.rs` - -**问题:** COUNT查询缺少`compartments`表JOIN,分页总数计算错误。 - -**修复方案:** -```rust -// 修改COUNT查询,添加JOIN -let total: (i64,) = sqlx::query_as( - r#"SELECT COUNT(*) FROM charge_records cr - INNER JOIN compartments c ON cr.compartment_id = c.id - INNER JOIN cabin_boards cb ON c.cabin_board_id = cb.id - INNER JOIN cabinets cab ON cb.cabinet_id = cab.id - WHERE cab.project_id = ?"# // 添加组织过滤 -) -.bind(project_id) -.fetch_one(pool) -.await?; -``` - -### 3. H5后端接口实现(🔴 高优先级) - -**位置:** 新建 `src/routes/h5.rs` - -**问题:** H5前端10+个接口均会404,后端未实现。 - -**修复方案:** -```rust -// 新建 h5.rs 模块 -use axum::extract::{Path, State}; -use axum::Json; -use serde_json::{json, Value}; - -use crate::error::AppError; -use crate::middleware::auth::{self, CurrentUser}; -use crate::tcp::commands::AppState; - -/// GET /api/h5/dashboard — H5首页数据 -pub async fn get_dashboard( - user: CurrentUser, - State(state): State, -) -> Result, AppError> { - // 查询柜子统计、今日充电、告警等 - // ... -} - -/// GET /api/h5/projects — H5项目列表 -pub async fn get_projects( - user: CurrentUser, - State(state): State, -) -> Result, AppError> { - // 查询用户有权访问的项目 - // ... -} - -/// GET /api/h5/cabinets — H5设备列表 -pub async fn get_cabinets( - user: CurrentUser, - State(state): State, - Path(project_id): Path, -) -> Result, AppError> { - // 查询项目下的柜子 - // ... -} - -/// GET /api/h5/cabinets/:id — H5设备详情 -pub async fn get_cabinet_detail( - user: CurrentUser, - State(state): State, - Path(id): Path, -) -> Result, AppError> { - // 查询柜子详情(仓控板+仓体) - // ... -} - -/// POST /api/h5/charge/start — H5开始充电 -pub async fn start_charge( - user: CurrentUser, - State(state): State, - Json(body): Json, -) -> Result, AppError> { - auth::check_permission(&user, "charge:start")?; - // 下发充电指令到设备 - // ... -} - -/// POST /api/h5/charge/stop — H5停止充电 -pub async fn stop_charge( - user: CurrentUser, - State(state): State, - Json(body): Json, -) -> Result, AppError> { - auth::check_permission(&user, "charge:stop")?; - // 下发停止指令到设备 - // ... -} - -/// POST /api/h5/door/open — H5开门 -pub async fn open_door( - user: CurrentUser, - State(state): State, - Json(body): Json, -) -> Result, AppError> { - auth::check_permission(&user, "charge:open_door")?; - // 下发开门指令到设备 - // ... -} -``` - -**路由注册:** 在 `src/routes/mod.rs` 中添加: -```rust -mod h5; - -// 在 build 函数中添加 -router = router - .route("/api/h5/dashboard", get(h5::get_dashboard)) - .route("/api/h5/projects", get(h5::get_projects)) - .route("/api/h5/cabinets", get(h5::get_cabinets)) - .route("/api/h5/cabinets/:id", get(h5::get_cabinet_detail)) - .route("/api/h5/charge/start", post(h5::start_charge)) - .route("/api/h5/charge/stop", post(h5::stop_charge)) - .route("/api/h5/door/open", post(h5::open_door)); -``` - -### 4. 文件下载端点实现(🔴 高优先级) - -**位置:** `src/routes/downloads.rs` - -**问题:** `/api/downloads/:id/file` 未实现,导出功能不完整。 - -**修复方案:** -```rust -use axum::body::Body; -use axum::http::header; -use axum::response::Response; -use tokio::fs; - -/// GET /api/downloads/:id/file — 下载文件 -pub async fn download_file( - user: CurrentUser, - State(state): State, - Path(id): Path, -) -> Result, AppError> { - auth::check_permission(&user, "charge_record:export")?; - - let db = &state.mysql; - - // 查询下载任务 - let task: Option = sqlx::query_as( - "SELECT * FROM download_tasks WHERE id = ? AND user_id = ?" - ) - .bind(id) - .bind(user.user_id) - .fetch_optional(db) - .await?; - - let task = task.ok_or(AppError::NotFound("下载任务不存在".into()))?; - - if task.status != 2 { - return Err(AppError::BadRequest("文件未生成完成".into())); - } - - // 读取文件 - let file_path = task.file_path.ok_or(AppError::NotFound("文件路径不存在".into()))?; - let file_content = fs::read(&file_path).await.map_err(|e| { - AppError::Internal(format!("读取文件失败: {}", e)) - })?; - - // 返回文件 - let response = Response::builder() - .status(200) - .header(header::CONTENT_TYPE, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") - .header(header::CONTENT_DISPOSITION, format!("attachment; filename=\"{}\"", task.file_name)) - .body(Body::from(file_content)) - .map_err(|e| AppError::Internal(format!("构建响应失败: {}", e)))?; - - Ok(response) -} -``` - -### 5. TCP auth_str频率限制(🟡 中优先级) - -**位置:** `src/tcp/handler.rs` - -**问题:** `auth_str`接口无频率限制,可能被暴力调用。 - -**修复方案:** -```rust -use std::collections::HashMap; -use std::sync::Arc; -use tokio::sync::RwLock; -use std::time::{Duration, Instant}; - -// 频率限制器 -pub struct RateLimiter { - attempts: Arc>>>, - max_attempts: usize, - window: Duration, -} - -impl RateLimiter { - pub fn new(max_attempts: usize, window_secs: u64) -> Self { - Self { - attempts: Arc::new(RwLock::new(HashMap::new())), - max_attempts, - window: Duration::from_secs(window_secs), - } - } - - pub async fn check(&self, key: &str) -> bool { - let mut attempts = self.attempts.write().await; - let now = Instant::now(); - let window_start = now - self.window; - - // 清理过期记录 - let entry = attempts.entry(key.to_string()).or_insert_with(Vec::new); - entry.retain(|t| *t > window_start); - - if entry.len() >= self.max_attempts { - return false; // 超限 - } - - entry.push(now); - true - } -} - -// 在 handler 中使用 -lazy_static::lazy_static! { - static ref AUTH_STR_LIMITER: RateLimiter = RateLimiter::new(5, 300); // 5次/5分钟 -} - -// 在 auth_str handler 中 -if !AUTH_STR_LIMITER.check(&dev_id).await { - return Err(AppError::TooManyRequests("请求过于频繁".into())); -} -``` - -## 质量约束 - -1. 完成代码后执行 `cargo check` + `cargo clippy` + `tsc --noEmit`,零报错零警告 -2. 分层拆分,单函数≤80行,命名语义化,完整注释 -3. 所有外部IO/网络请求异常捕获,禁止裸panic -4. 分支逻辑全覆盖,不遗漏兜底分支 -5. 常量抽离,不使用废弃API -6. Rust内存安全,React函数组件+Hooks -7. H5接口必须添加权限校验 diff --git a/tasks/015-nemotron-review-fixes.md b/tasks/015-nemotron-review-fixes.md deleted file mode 100644 index 1bf72ed..0000000 --- a/tasks/015-nemotron-review-fixes.md +++ /dev/null @@ -1,219 +0,0 @@ -# 任务015:Nemotron审查问题修复 - -## 目标 - -修复Nemotron审查发现的7个严重问题。 - -## 修复清单 - -### 1. 权限码种子数据缺失( 高优先级) - -**位置:** `src/db/migrate.rs` - -**问题:** `org:*`、`project:*`、`device:edit` 等权限码未在`seed_permissions`中初始化。 - -**修复方案:** -```rust -// 在 seed_permissions 函数中添加缺失的权限码 -let permissions = vec![ - // ... 已有的权限码 - - // 组织管理 - ("org:view", "查看组织"), - ("org:create", "创建组织"), - ("org:edit", "编辑组织"), - ("org:delete", "删除组织"), - - // 项目管理 - ("project:view", "查看项目"), - ("project:create", "创建项目"), - ("project:edit", "编辑项目"), - ("project:delete", "删除项目"), - - // 设备管理(补充) - ("device:edit", "编辑设备"), -]; - -// 插入数据库(使用 INSERT IGNORE 避免重复) -for (code, name) in permissions { - sqlx::query( - "INSERT IGNORE INTO permissions (code, name) VALUES (?, ?)" - ) - .bind(code) - .bind(name) - .execute(pool) - .await?; -} -``` - -**角色权限分配:** -```rust -// 总管理员 — 全部权限 -// 企业管理员 — 添加 org:*, project:*, device:edit 权限 -// 普通用户 — 只保留 view 权限 -``` - -### 2. H5接口数据隔离(🔴 高优先级) - -**位置:** `src/routes/h5.rs` - -**问题:** `get_dashboard`返回全局统计数据,`get_cabinet_detail`/`get_compartment_detail`无权限校验。 - -**修复方案:** -```rust -/// GET /api/h5/dashboard — H5首页数据(带组织隔离) -pub async fn get_dashboard( - user: CurrentUser, - State(state): State, -) -> Result, AppError> { - auth::check_permission(&user, "device:view")?; - - let db = &state.mysql; - - // 根据角色过滤数据 - let org_filter = if user.role_level >= 2 { - None // 总管理员看全部 - } else { - user.organization_id // 企业管理员只看本组织 - }; - - // 查询柜子统计(带组织过滤) - let cabinet_stats = if let Some(org_id) = org_filter { - sqlx::query_as::<_, (i64, i64, i64, i64)>( - r#"SELECT - COUNT(*) as total, - SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as online, - SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as charging, - SUM(CASE WHEN status = 3 THEN 1 ELSE 0 END) as fault - FROM cabinets c - INNER JOIN projects p ON c.project_id = p.id - WHERE p.organization_id = ?"# - ) - .bind(org_id) - .fetch_one(db) - .await? - } else { - // 总管理员查询全部 - // ... - }; - - Ok(Json(json!({ - "total": cabinet_stats.0, - "online": cabinet_stats.1, - "charging": cabinet_stats.2, - "fault": cabinet_stats.3 - }))) -} - -/// GET /api/h5/cabinets/:id — H5设备详情(带权限校验) -pub async fn get_cabinet_detail( - user: CurrentUser, - State(state): State, - Path(id): Path, -) -> Result, AppError> { - auth::check_permission(&user, "device:view")?; - - let db = &state.mysql; - - // 验证用户是否有权访问该设备 - if user.role_level < 2 { - if let Some(org_id) = user.organization_id { - let access: Option<(i64,)> = sqlx::query_as( - r#"SELECT c.id FROM cabinets c - INNER JOIN projects p ON c.project_id = p.id - WHERE c.id = ? AND p.organization_id = ?"# - ) - .bind(id) - .bind(org_id) - .fetch_optional(db) - .await?; - - if access.is_none() { - return Err(AppError::Forbidden("无权访问该设备".into())); - } - } else { - return Err(AppError::Forbidden("无组织关联".into())); - } - } - - // ... 原有查询逻辑 -} -``` - -### 3. 前后端H5告警字段匹配(🔴 中优先级) - -**位置:** `src/routes/h5.rs` 和 `src/h5/pages/home.tsx` - -**问题:** 后端返回`{compartment_id, cabinet_id}`,前端期望`{project, cabinet}`。 - -**修复方案(后端):** -```rust -// 修改告警查询,返回前端期望的字段 -let alerts = sqlx::query_as::<_, (String, String, String)>( - r#"SELECT - p.name as project, - cab.name as cabinet, - dl.content as message - FROM device_logs dl - INNER JOIN cabinets cab ON dl.cabinet_id = cab.id - INNER JOIN projects p ON cab.project_id = p.id - WHERE dl.log_type = 3 -- 告警 - ORDER BY dl.created_at DESC - LIMIT 10"# -) -.fetch_all(db) -.await?; - -Ok(Json(json!({ - "alerts": alerts.iter().map(|(project, cabinet, message)| { - json!({ - "project": project, - "cabinet": cabinet, - "message": message - }) - }).collect::>() -}))) -``` - -### 4. 其他严重问题修复 - -**4.1 SQL参数化(charge_records.rs)** -```rust -// 确保所有SQL查询使用参数化绑定 -// 检查并修复任何 format! 拼接SQL的地方 -``` - -**4.2 数据库索引(migrate.rs)** -```rust -// 添加关键索引 -sqlx::query("CREATE INDEX idx_cabinets_project ON cabinets(project_id)") - .execute(pool).await?; -sqlx::query("CREATE INDEX idx_charge_records_cabinet ON charge_records(cabinet_id)") - .execute(pool).await?; -sqlx::query("CREATE INDEX idx_device_logs_cabinet ON device_logs(cabinet_id)") - .execute(pool).await?; -``` - -**4.3 消除重复代码** -```rust -// 提取公共的组织过滤逻辑到 middleware/auth.rs -pub fn org_condition(user: &CurrentUser) -> String { - if user.role_level >= 2 { - String::new() - } else if let Some(org_id) = user.organization_id { - format!("AND organization_id = {}", org_id) - } else { - "AND 1=0".to_string() - } -} -``` - -## 质量约束 - -1. 完成代码后执行 `cargo check` + `cargo clippy` + `tsc --noEmit`,零报错零警告 -2. 分层拆分,单函数≤80行,命名语义化,完整注释 -3. 所有外部IO/网络请求异常捕获,禁止裸panic -4. 分支逻辑全覆盖,不遗漏兜底分支 -5. 常量抽离,不使用废弃API -6. Rust内存安全,React函数组件+Hooks -7. H5接口必须添加权限校验和组织数据隔离 diff --git a/tasks/016-tcp-service-refactor.md b/tasks/016-tcp-service-refactor.md deleted file mode 100644 index 7753e31..0000000 --- a/tasks/016-tcp-service-refactor.md +++ /dev/null @@ -1,324 +0,0 @@ -# 任务016:TCP服务重构 - -## 目标 - -重构TCP服务,拆分为API服务和设备服务,使用Redis做服务发现和消息队列。 - -## 架构设计 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ API服务 (api-server) — 主服务 │ -│ - HTTP API(后台管理 + H5)端口: 3000 │ -│ - 前端轮询刷新(每5秒) │ -│ - 业务逻辑处理(验证/存DB/告警) │ -│ - 节点管理(设备服务注册/心跳) │ -│ - 后台协程处理Redis队列 │ -└─────────────────────────────────────────────────────────────┘ - ↑ Redis LIST ↑ Redis LIST - │ device:report:{imei} │ device:cmd:{imei} - │ (设备上报) │ (平台指令) - │ │ -┌─────────────────────────────────────────────────────────────┐ -│ 设备服务 (device-server) — 轻量化 │ -│ - TCP监听: 3002(设备长连接) │ -│ - 只处理通信和指令解析 │ -│ - 不存DB、不验证签名、不处理业务 │ -│ - 注册到API服务(启动/心跳/注销) │ -│ - Redis连接注册: device:{imei} → {node_id, ip, port} │ -└─────────────────────────────────────────────────────────────┘ -``` - -## 核心设计原则 - -1. **TCP服务轻量化** — 只处理TCP通信和指令解析,不处理业务逻辑 -2. **Redis做缓冲** — 服务间通信用Redis LIST,不用HTTP直接调用 -3. **全异步处理** — 所有指令异步处理,msg_id匹配响应 -4. **协程处理任务** — API服务用tokio::spawn处理Redis队列消息 - -## 重构清单 - -### 1. 项目结构拆分 - -**当前:** 单服务 `software/server/` - -**重构后:** -``` -software/ -├── api-server/ # API服务(主服务) -│ ├── Cargo.toml -│ ├── config.toml -│ └── src/ -│ ├── main.rs -│ ├── config.rs -│ ├── routes/ -│ │ ├── devices.rs # 设备管理API -│ │ ├── nodes.rs # 节点管理API -│ │ └── ... -│ ├── workers/ # 后台协程 -│ │ ├── report_worker.rs # 处理设备上报 -│ │ └── reply_worker.rs # 处理设备响应 -│ ├── middleware/ -│ ── h5/ -├── device-server/ # 设备服务(轻量化) -│ ├── Cargo.toml -│ ├── config.toml -│ └── src/ -│ ├── main.rs -│ ├── config.rs -│ ├── tcp/ -│ │ ├── server.rs # TCP监听 -│ │ ├── connection.rs # 连接管理(内存HashMap) -│ │ ├── protocol.rs # 协议解析 -│ │ └── handler.rs # 消息处理(解析+转发Redis) -│ └── redis.rs # Redis连接注册 -``` - -**说明:** 暂不提取shared crate,协议定义等代码先复制到两个服务。 - -### 2. 配置文件(config.toml) - -**api-server/config.toml:** -```toml -[server] -host = "0.0.0.0" -port = 3000 - -[database] -url = "mysql://root:password@10.8.0.252:3306/pms_dev" - -[redis] -url = "redis://:password@10.8.0.252:6379/5" - -[jwt] -secret = "pms-dev-secret-change-me-in-production" - -[export] -dir = "./exports" - -[node] -heartbeat_timeout = 180 -``` - -**device-server/config.toml:** -```toml -[server] -host = "0.0.0.0" -tcp_port = 3002 -node_id = "node-1" - -[redis] -url = "redis://:password@10.8.0.252:6379/5" - -[api_server] -url = "http://127.0.0.1:3000" -heartbeat_interval = 60 - -[device] -register_ttl = 180 -``` - -### 3. 服务间通信(Redis LIST) - -**设备上报(设备→API):** -```rust -// 设备服务:收到设备消息后推送到Redis -async fn forward_to_api(redis: &Redis, imei: &str, msg: &DeviceMessage) { - redis.lpush(&format!("device:report:{}", imei), serde_json::to_string(msg)).await; -} - -// API服务:后台协程处理上报 -async fn process_reports(redis: Redis, mysql: MySqlPool) { - loop { - let (_, msg_json) = redis.brpop(&["device:report:*"], 0).await; - let msg: DeviceMessage = serde_json::from_str(&msg_json).unwrap(); - tokio::spawn(handle_report(msg, mysql.clone())); // 异步处理 - } -} -``` - -**平台指令(API→设备):** -```rust -// API服务:下发指令推送到Redis -async fn send_command(redis: &Redis, imei: &str, cmd: DeviceCommand) { - redis.lpush(&format!("device:cmd:{}", imei), serde_json::to_string(&cmd)).await; -} - -// 设备服务:阻塞弹出指令,发送到设备 -async fn process_commands(redis: Redis, pool: ConnectionPool) { - loop { - let (key, cmd_json) = redis.brpop(&["device:cmd:*"], 0).await; - let imei = extract_imei(&key); - let cmd: DeviceCommand = serde_json::from_str(&cmd_json).unwrap(); - - // 发送到设备(异步等待响应) - if let Some(conn) = pool.get(&imei) { - conn.send(cmd).await; - } - } -} -``` - -**指令响应(设备→API):** -```rust -// 设备服务:收到设备响应后推送到Redis -async fn forward_reply(redis: &Redis, imei: &str, reply: DeviceResponse) { - redis.lpush(&format!("device:reply:{}", imei), serde_json::to_string(&reply)).await; -} - -// API服务:后台协程匹配msg_id -async fn process_replies(redis: Redis, pending: PendingCommands) { - loop { - let (_, reply_json) = redis.brpop(&["device:reply:*"], 0).await; - let reply: DeviceResponse = serde_json::from_str(&reply_json).unwrap(); - if let Some(tx) = pending.remove(&reply.msg_id) { - tx.send(reply).ok(); - } - } -} -``` - -### 4. 节点管理(API服务) - -**节点注册:** -```rust -// POST /api/nodes/register -async fn register_node(Json(req): Json) -> Result> { - // 记录节点信息到内存/DB - nodes.insert(req.node_id.clone(), NodeInfo { - id: req.node_id, - ip: req.ip, - tcp_port: req.tcp_port, - status: "online", - last_heartbeat: Instant::now(), - }); - Ok(Json(json!({"suc": 1}))) -} -``` - -**节点心跳:** -```rust -// POST /api/nodes/:node_id/heartbeat -async fn node_heartbeat(Path(node_id): Path) -> Result> { - if let Some(node) = nodes.get_mut(&node_id) { - node.last_heartbeat = Instant::now(); - } - Ok(Json(json!({"suc": 1}))) -} -``` - -**节点注销:** -```rust -// POST /api/nodes/:node_id/deregister -async fn deregister_node(Path(node_id): Path) -> Result> { - nodes.remove(&node_id); - Ok(Json(json!({"suc": 1}))) -} -``` - -**节点列表:** -```rust -// GET /api/nodes -async fn list_nodes() -> Result>> { - Ok(Json(json!(nodes.values().collect::>()))) -} -``` - -### 5. 设备服务注册流程 - -```rust -// 设备服务启动时 -async fn startup(api_url: &str, node_id: &str, ip: &str, tcp_port: u16) { - // 1. 注册到API服务 - let client = reqwest::Client::new(); - client.post(&format!("{}/api/nodes/register", api_url)) - .json(&json!({ "node_id": node_id, "ip": ip, "tcp_port": tcp_port })) - .send().await; - - // 2. 启动心跳协程(每60秒) - let api_url = api_url.to_string(); - let node_id = node_id.to_string(); - tokio::spawn(async move { - let mut interval = tokio::time::interval(Duration::from_secs(60)); - loop { - interval.tick().await; - client.post(&format!("{}/api/nodes/{}/heartbeat", api_url, node_id)) - .send().await; - } - }); -} - -// 设备服务关闭时 -async fn shutdown(api_url: &str, node_id: &str) { - let client = reqwest::Client::new(); - client.post(&format!("{}/api/nodes/{}/deregister", api_url, node_id)) - .send().await; -} -``` - -### 6. 异步指令处理(msg_id匹配) - -```rust -// API服务下发指令 -async fn send_command_to_device(imei: &str, cmd: DeviceCommand) -> Result { - let (tx, rx) = oneshot::channel(); - pending_commands.insert(cmd.msg_id, tx); - - // 推送到Redis队列 - redis.lpush(&format!("device:cmd:{}", imei), serde_json::to_string(&cmd)).await; - - // 等待响应(超时60秒) - match tokio::time::timeout(Duration::from_secs(60), rx).await { - Ok(Ok(response)) => Ok(response), - Ok(Err(_)) => Err(AppError::Internal("通道关闭".into())), - Err(_) => { - pending_commands.remove(&cmd.msg_id); - Err(AppError::Timeout("设备响应超时".into())) - } - } -} -``` - -### 7. 自动充电流程 - -```rust -// API服务收到 bat_in 上报 -async fn handle_bat_in(imei: &str, channel: &str, bms: &BmsData) { - // 1. 检查通道是否已有电池 - if let Some(old_battery) = battery_map.get(channel) { - // 旧电池强制结束 - record_charge_complete(channel, old_battery, ChargeEndReason::Forced); - } - - // 2. 记录新电池 - battery_map.insert(channel.to_string(), battery.clone()); - - // 3. 验证电池合规性 - match validate_battery(bms) { - Ok(()) => { - // 4. 自动下发充电指令 - send_command_to_device(imei, DeviceCommand { - act: "on".to_string(), - id: channel.to_string(), - msg_id: generate_msg_id(), - }).await; - } - Err(e) => { - // 验证失败,记录日志 - tracing::warn!("电池验证失败: {}", e); - } - } -} -``` - -## 质量约束 - -1. 完成代码后执行 `cargo check` + `cargo clippy`,零报错零警告 -2. 分层拆分,单函数≤80行,命名语义化,完整注释 -3. 所有外部IO/网络请求异常捕获,禁止裸panic -4. 分支逻辑全覆盖,不遗漏兜底分支 -5. 常量抽离,不使用废弃API -6. Rust内存安全,合理管理所有权,禁用unsafe无合理理由 -7. 异步代码无阻塞操作 -8. 指令处理必须异步,msg_id匹配响应 -9. TCP服务不处理业务逻辑,只负责通信和解析 diff --git a/tasks/017-fix-review-issues.md b/tasks/017-fix-review-issues.md deleted file mode 100644 index 0fa6605..0000000 --- a/tasks/017-fix-review-issues.md +++ /dev/null @@ -1,95 +0,0 @@ -# 任务017:重构审查问题修复 - -## 目标 - -修复TCP服务重构审查发现的P0/P1问题。 - -## 修复清单 - -### P0:Redis类型冲突 - -**位置:** `device-server/src/main.rs` - -**问题:** `register_node`用`SET`(string类型),`heartbeat`用`HSET`(hash类型),同一key操作触发`WRONGTYPE`错误。 - -**修复:** 统一用HSET: -```rust -// 节点注册 -async fn register_node(redis: &Redis, node_id: &str, ip: &str, tcp_port: u16) { - redis.hset(&format!("node:{}", node_id), &[ - ("ip", ip), - ("tcp_port", &tcp_port.to_string()), - ("status", "online"), - ("last_heartbeat", &Instant::now().elapsed().as_secs().to_string()), - ]).await; - redis.expire(&format!("node:{}", node_id), 300).await; -} - -// 节点心跳 -async fn heartbeat(redis: &Redis, node_id: &str) { - redis.hset(&format!("node:{}", node_id), &[ - ("last_heartbeat", &Instant::now().elapsed().as_secs().to_string()), - ]).await; - redis.expire(&format!("node:{}", node_id), 300).await; -} -``` - -### P1:节点管理双轨制 - -**问题:** API服务维护内存`NodeRegistry`,设备服务直接写Redis,两者不关联。 - -**修复方案A(推荐):** API服务从Redis读取节点信息 -```rust -// API服务节点列表 -async fn list_nodes(redis: &Redis) -> Result>> { - let keys = redis.keys("node:*").await?; - let mut nodes = Vec::new(); - for key in keys { - let node_id = key.strip_prefix("node:").unwrap(); - let info: HashMap = redis.hgetall(&key).await?; - nodes.push(NodeInfo { - id: node_id.to_string(), - ip: info.get("ip").cloned().unwrap_or_default(), - tcp_port: info.get("tcp_port").and_then(|p| p.parse().ok()).unwrap_or(0), - status: info.get("status").cloned().unwrap_or_default(), - last_heartbeat: info.get("last_heartbeat").cloned().unwrap_or_default(), - }); - } - Ok(Json(nodes)) -} -``` - -**修复方案B:** 设备服务通过HTTP注册到API服务 -- 设备服务启动时POST到`/api/nodes/register` -- API服务存内存 -- 心跳POST到`/api/nodes/:id/heartbeat` -- 注销POST到`/api/nodes/:id/deregister` - -### P2:配置系统 - -**问题:** 代码用`dotenvy`读环境变量,config.toml形同虚设。 - -**修复:** 二选一 -- 方案A:用`toml` crate读config.toml -- 方案B:删除config.toml,只用.env - -### P3:协议结构体重复 - -**问题:** `DeviceMessage`等结构体在两个服务中重复定义。 - -**修复:** 暂不处理,后续提取shared crate时统一。 - -### P4:限流器同步Mutex - -**问题:** 限流器用`std::sync::Mutex`,不符合异步规范。 - -**修复:** 改用`tokio::sync::Mutex`或`RwLock`。 - -## 质量约束 - -1. 完成代码后执行 `cargo check` + `cargo clippy`,零报错零警告 -2. 分层拆分,单函数≤80行,命名语义化,完整注释 -3. 所有外部IO/网络请求异常捕获,禁止裸panic -4. 分支逻辑全覆盖,不遗漏兜底分支 -5. 常量抽离,不使用废弃API -6. Rust内存安全,合理管理所有权,禁用unsafe无合理理由 diff --git a/tasks/018-code-optimizations.md b/tasks/018-code-optimizations.md deleted file mode 100644 index 912301e..0000000 --- a/tasks/018-code-optimizations.md +++ /dev/null @@ -1,63 +0,0 @@ -# 任务018:代码优化 - -## 目标 - -完成审查提出的3个改进建议。 - -## 优化清单 - -### 1. KEYS → SCAN - -**位置:** `api-server/src/routes/nodes.rs` 或 `api-server/src/commands.rs` - -**当前:** -```rust -let keys = redis.keys("node:*").await?; -``` - -**改进:** -```rust -async fn scan_nodes(redis: &redis::aio::ConnectionManager) -> Result> { - let mut nodes = Vec::new(); - let mut cursor = 0i64; - loop { - let (new_cursor, keys): (i64, Vec) = redis - .scan(cursor) - .await?; - nodes.extend(keys); - if new_cursor == 0 { break; } - cursor = new_cursor; - } - Ok(nodes) -} -``` - -### 2. JWT常量去重 - -**位置:** `api-server/src/middleware/auth.rs` 和 `api-server/src/config.rs` - -**当前:** `JWT_SECRET_DEFAULT` 在两个文件重复定义 - -**改进:** 只在 `config.rs` 定义,`auth.rs` 从 `Config` 读取 - -### 3. rand API更新 - -**位置:** `device-server/src/main.rs` 或 `api-server/src/routes/organizations.rs` - -**当前:** 可能用了旧版 `rand::thread_rng()` 或 `gen_range` - -**改进:** 确认使用 rand 0.8+ API: -```rust -use rand::Rng; -let mut rng = rand::thread_rng(); -let value = rng.gen_range(0..CHARSET.len()); -``` - -## 质量约束 - -1. 完成代码后执行 `cargo check` + `cargo clippy`,零报错零警告 -2. 分层拆分,单函数≤80行,命名语义化,完整注释 -3. 所有外部IO/网络请求异常捕获,禁止裸panic -4. 分支逻辑全覆盖,不遗漏兜底分支 -5. 常量抽离,不使用废弃API -6. Rust内存安全,合理管理所有权,禁用unsafe无合理理由 diff --git a/tasks/review-code-quality.md b/tasks/review-code-quality.md deleted file mode 100644 index 79e30b9..0000000 --- a/tasks/review-code-quality.md +++ /dev/null @@ -1,122 +0,0 @@ -# 代码审查报告 - -**审查日期**:2026-07-01 -**审查范围**:`software/server/src/`(Rust + Axum)、`software/web/src/`(React + TypeScript) -**审查工具**:`cargo check`、`cargo clippy`、`tsc --noEmit`、人工审计 - ---- - -## 总体评价:7.5 / 10 - -项目整体架构清晰,分层合理,错误处理较为完善。Rust 端无裸 `panic!` / `.unwrap()`,前端无 `as any`。主要问题集中在:**SQL 注入风险**、**数据隔离缺失**、**权限校验不完整**、**部分文件过长**。 - ---- - -## 严重问题(🔴 必须修复) - -### 问题 1:操作日志查询存在 SQL 注入风险 -- **位置**:`src/routes/operation_logs.rs:49-63` -- **描述**:`action`、`start_time`、`end_time` 参数通过 `format!` 直接拼接到 SQL WHERE 子句中,虽然做了 `'` 替换,但仍可被绕过(如 `1; DROP TABLE users--`)。 -- **风险**:攻击者可注入任意 SQL 语句,导致数据泄露或破坏。 -- **建议**:改用 `sqlx::query_as` 的参数化绑定,或将条件构建逻辑统一为 `build_where_clause` 模式(如 `charge_records.rs` 已实现的那样)。 - -### 问题 2:充电记录 LIMIT/OFFSET 通过 format! 拼接 -- **位置**:`src/routes/charge_records.rs:130` -- **描述**:`LIMIT {} OFFSET {}` 直接使用 `format!` 拼接 `i64` 参数。虽然参数类型是 `i64`,但若未来参数类型变更或被误传字符串,将产生注入。 -- **风险**:中等(当前类型安全,但模式脆弱)。 -- **建议**:将 `limit` / `offset` 也通过 `.bind()` 传入。 - -### 问题 3:无组织/项目级数据隔离 -- **位置**:`src/routes/organizations.rs` 全部路由、`src/routes/charge_records.rs`、`src/routes/device_logs.rs`、`src/routes/energy_stats.rs` -- **描述**:所有查询均无 `WHERE organization_id = ?` 或 `WHERE project_id = ?` 过滤。企业管理员可以查看所有组织的数据,而非仅其管辖范围。 -- **风险**:高。多租户场景下,企业 A 的管理员可以看到企业 B 的所有设备和数据。 -- **建议**:在 `CurrentUser` 中携带 `organization_id`,在所有查询中自动附加 `WHERE organization_id = ?` 过滤。 - -### 问题 4:部分路由缺少权限校验 -- **位置**:`src/routes/charge_records.rs:158`、`src/routes/device_logs.rs:62`、`src/routes/energy_stats.rs:81-283`、`src/routes/downloads.rs:87`、`src/routes/organizations.rs:144-551` -- **描述**:这些路由处理器未调用 `auth::check_permission()`,仅依赖 JWT 认证中间件。任何登录用户均可访问充电记录、设备日志、能耗统计、下载中心等敏感数据。 -- **风险**:中。越权访问非管理类功能。 -- **建议**:为每个路由添加对应的 `check_permission` 调用,如 `user:view`、`energy:view` 等。 - -### 问题 5:密码明文存储 -- **位置**:`src/routes/auth.rs:51` -- **描述**:`stored_password != input.password` 直接比较明文。代码注释也明确写道"后续应改为 bcrypt/argon2 哈希"。 -- **风险**:高。数据库泄露即所有用户密码暴露。 -- **建议**:集成 `argon2` 或 `bcrypt` crate,在登录时 `argon2::verify`,在创建/重置时 `argon2::hash`。 - ---- - -## 警告(🟡 建议修复) - -### 警告 1:organizations.rs 文件过长(693 行) -- **位置**:`src/routes/organizations.rs` -- **描述**:单个文件 693 行,包含组织/项目/柜子 CRUD + 树状结构 + 柜子详情,职责过多。 -- **建议**:拆分为 `organizations.rs`、`projects.rs`、`cabinets.rs` 三个文件。 - -### 警告 2:users.rs 文件过长(495 行) -- **位置**:`src/routes/users.rs` -- **描述**:包含用户 CRUD、密码重置、状态管理、操作日志记录,超过 80 行/函数约束(部分辅助函数虽短,但整体文件过大)。 -- **建议**:将 `log_operation` 辅助函数提取到独立模块。 - -### 警告 3:JWT 默认密钥在生产环境可能泄露 -- **位置**:`src/middleware/auth.rs:28` -- **描述**:`JWT_SECRET_DEFAULT = "pms-dev-secret-change-me-in-production"` 如果环境变量未设置,将使用此默认值。 -- **建议**:启动时若检测到默认密钥,打印 WARN 日志;或在 `config.rs` 中将 JWT_SECRET 设为必填。 - -### 警告 4:energy_stats 导出 LIMIT/OFFSET 同样存在 format! 拼接 -- **位置**:`src/routes/energy_stats.rs:341` -- **描述**:与 charge_records.rs 相同的问题,LIMIT/OFFSET 通过 `format!` 拼接。 -- **建议**:统一使用参数化绑定。 - -### 警告 5:devices/index.tsx 文件过长(631 行) -- **位置**:`src/pages/devices/index.tsx` -- **描述**:单个 TSX 文件 631 行,包含组织树、柜子列表、CRUD 弹窗、右键菜单、批量添加等全部逻辑。 -- **建议**:提取 `OrganizationalTree.tsx`、`CabinetGrid.tsx`、`AddCabinetModal.tsx` 为独立组件。 - -### 警告 6:Pre-existing 构建错误(IconFolderOpen) -- **位置**:`src/pages/devices/index.tsx:33` -- **描述**:`IconFolderOpen` 从 `@arco-design/web-react/icon` 导入,但该版本未导出此图标。导致 `vite build` 失败。 -- **风险**:前端无法构建生产包。 -- **建议**:替换为 `IconFolder` 或其他可用图标。 - -### 警告 7:clippy 警告 15 条 -- **位置**:多处 -- **描述**:主要包括 `collapsible_if`(可折叠嵌套 if)、`type_complexity`(复杂类型未抽取 type alias)、`manual_unwrap_or_default`、`obfuscated_if_else` 等。 -- **建议**:运行 `cargo clippy --fix` 自动修复大部分问题。 - -### 警告 8:前端 H5 页面中充电曲线为静态 Mock 数据 -- **位置**:`src/h5/pages/compartment-detail.tsx:156` -- **描述**:充电曲线使用硬编码的模拟数据 `[30, 45, 55, ...]`,后端未提供实时充电曲线数据。 -- **建议**:待后端提供数据后替换为真实折线图。 - ---- - -## 优点 - -1. **Rust 错误处理规范**:无裸 `panic!` / `.unwrap()`,使用 `Result` 和 `?` 运算符,业务逻辑层错误处理完善。 -2. **统一错误类型**:`AppError` enum 覆盖所有错误场景,`IntoResponse` 实现统一返回格式。 -3. **TCP 协议处理健壮**:签名验证(SHA256 + 时间窗)、粘包处理(LF 分隔)、连接超时清理。 -4. **RBAC 权限模型**:JWT + 权限码 + 角色等级(总管理员自动拥有全部权限),设计合理。 -5. **前端类型安全**:无 `as any`,完整的 TypeScript 接口定义。 -6. **H5 用户端实现简洁**:Tailwind CSS 自定义组件,零新增依赖,扫码入口自动路由。 -7. **异步导出流程完整**:任务创建 → 后台 tokio::spawn 生成 Excel → 状态更新 → 下载。 - ---- - -## 总结 - -| 维度 | 评分 | 说明 | -|------|------|------| -| 编译/类型检查 | 9/10 | Rust clippy 15 条警告,前端 tsc 零错误(IconFolderOpen 预存问题) | -| 代码规范 | 7/10 | 部分文件过长,单函数基本在 80 行以内 | -| 错误处理 | 8/10 | Rust 端优秀,前端 try/catch 覆盖全面 | -| 安全性 | 5/10 | SQL 注入风险、明文密码、数据隔离缺失 | -| 架构设计 | 8/10 | 分层清晰,路由模块化,TCP 协议处理得当 | - -**优先级排序**: -1. 🔴 修复 SQL 注入(operation_logs、energy_stats、charge_records) -2. 🔴 实现密码哈希(argon2/bcrypt) -3. 🔴 添加组织级数据隔离 -4. 🟡 补充缺失的权限校验 -5. 🟡 修复 IconFolderOpen 构建错误 -6. 🟡 拆分大文件(organizations.rs、users.rs、devices/index.tsx) diff --git a/tasks/review-fix-verification-report.md b/tasks/review-fix-verification-report.md deleted file mode 100644 index 4ead9ff..0000000 --- a/tasks/review-fix-verification-report.md +++ /dev/null @@ -1,81 +0,0 @@ -# 重构修复验证报告 - -## 总体评价 - -**评分: 9/10** -- 所有 P0/P1 问题已正确修复,代码质量良好,编译零报错零警告。存在少量可改进点但不影响推送。 - -## P0/P1 问题验证 - -| # | 问题 | 状态 | 验证详情 | -|---|------|------|----------| -| 1 | Redis 节点信息统一用 HSET | **已修复** | `api-server/src/commands.rs:76-84` 使用 `HSET` 写入 `node:{node_id}` hash;`device-server/src/main.rs:77-92` 注册时同样使用 `HSET`。两端数据结构一致,无类型冲突。 | -| 2 | 心跳时 TTL 自动续期 | **已修复** | `api-server/src/commands.rs:116-120` 心跳时调用 `EXPIRE` 刷新 300s TTL;`device-server/src/main.rs:119-123` 同理。两端均保证活跃节点不会被过期清理。 | -| 3 | NodeRegistry 用 Redis 存储 | **已修复** | `api-server/src/commands.rs:57-59` `NodeRegistry` 持有 `redis::aio::ConnectionManager`,所有读写操作均通过 Redis(`HSET`/`HGETALL`/`KEYS`/`DEL`),无内存缓存。 | -| 4 | `GET /api/nodes` 正确返回节点列表 | **已修复** | `api-server/src/nodes.rs:55-70` 从 Redis 读取节点,返回 `node_id`、`ip`、`tcp_port` 字段,响应格式 `{"nodes": [...], "count": N}`。 | -| 5 | `is_device_online()` 正确查询 Redis | **已修复** | `api-server/src/commands.rs:159-167` 通过 `EXISTS device:online:{imei}` 查询,返回 `bool`,失败时兜底 `false`。 | - -## 配置系统 - -| 检查项 | 状态 | 详情 | -|--------|------|------| -| 删除无用 config.toml | **通过** | `software/` 目录下无 `config.toml` 文件 | -| 配置从 .env 读取 (dotenvy) | **通过** | `api-server/src/config.rs:29` 和 `device-server/src/config.rs:18` 均调用 `dotenvy::dotenv()` | -| 无明文密码硬编码 | **通过** | `JWT_SECRET_DEFAULT` 值为 `"pms-dev-secret-change-me-in-production"`,明确标注需生产替换,且 `Config::validate()` 会输出警告日志。`DEFAULT_PASSWORD = "123456"` 仅用于用户管理初始密码/重置密码,属业务常量。 | - -## 异步规范 - -| 检查项 | 状态 | 详情 | -|--------|------|------| -| 限流器用 tokio::sync::Mutex | **通过** | `report_worker.rs:14` 导入 `tokio::sync::Mutex`,`RateLimiter` 的 `attempts` 字段使用该类型。全项目 grep 确认无 `std::sync::Mutex` 使用。 | -| 无阻塞操作 | **通过** | 所有 Redis 操作使用 `query_async`,MySQL 操作使用 `sqlx::query().execute().await`,无同步阻塞调用。 | -| 后台协程正确处理 Redis 队列 | **通过** | `report_worker.rs` 使用 `BRPOP device:reports`,`reply_worker.rs` 使用 `BRPOP device:replies`,`device-server/main.rs` 使用 `BRPOP device:commands`。错误时 sleep 1s 重试,避免紧密循环。 | - -## 代码质量 - -### 编译检查 - -| 服务 | cargo check | cargo clippy | -|------|-------------|--------------| -| api-server | 零报错 | 零警告 | -| device-server | 零报错 | 零警告 | - -### 函数行数 - -所有函数均 <= 80 行。较大的函数: -- `process_report` (`report_worker.rs:91-116`): 25 行 -- `handle_login` (`report_worker.rs:174-239`): 65 行 -- `register_node` (`device-server/main.rs:73-105`): 32 行 -- `consume_commands` (`device-server/main.rs:132-177`): 45 行 - -### 命名与注释 - -所有模块均有模块级文档注释(`//!`),公开函数和结构体有 `///` 注释。命名语义清晰(如 `NodeRegistry`、`PendingCommands`、`RateLimiter`)。 - -### 错误处理 - -- 所有外部 IO 操作返回 `Result`,使用 `?` 传播错误 -- Redis 操作失败时使用 `unwrap_or_default()` 或 `unwrap_or(false)` 兜底 -- 无裸 `panic!` 或 `unwrap()` 用于外部 IO(仅 `expect("DeviceCommand 序列化不应失败")` 用于确定不会失败的 serde 序列化,可接受) - -## 新引入问题 - -### 无阻塞性问题 - -### 建议改进(非阻塞) - -1. **`KEYS` 命令在生产环境应替换为 `SCAN`** - - 位置: `api-server/src/commands.rs:128` - - 原因: `KEYS node:*` 在节点数量大时会阻塞 Redis - - 建议: 改用 `SCAN` 游标迭代,或维护一个 `SET` 记录活跃节点 ID - -2. **`JWT_SECRET_DEFAULT` 重复定义** - - 位置: `config.rs:6` 和 `middleware/auth.rs:33` - - 建议: 统一到 `config.rs`,`auth.rs` 引用 `crate::config::JWT_SECRET_DEFAULT` - -3. **`rand::thread_rng()` 在新版 rand 中已弃用** - - 位置: `report_worker.rs:150` - - 当前可编译通过(使用旧版 rand API),但升级 rand 后需改为 `rand::rng()` - -## 总结 - -**可以推送。** 所有 P0/P1 问题已修复,两个服务编译零报错零警告,代码结构清晰,错误处理完善。上述 3 个改进建议为低优先级,可在后续迭代中处理。 diff --git a/tasks/review-fix-verification.md b/tasks/review-fix-verification.md deleted file mode 100644 index 77794fe..0000000 --- a/tasks/review-fix-verification.md +++ /dev/null @@ -1,67 +0,0 @@ -# 代码审查任务:重构修复验证 - -## 目标 - -审查重构修复后的代码,确认P0/P1问题已解决,无新引入问题。 - -## 审查范围 - -### API服务 -`software/api-server/src/` 目录下所有文件 - -### 设备服务 -`software/device-server/src/` 目录下所有文件 - -## 审查清单 - -### 1. P0/P1问题验证 -- [ ] Redis节点信息统一用HSET(无类型冲突) -- [ ] 心跳时TTL自动续期 -- [ ] NodeRegistry用Redis存储(非内存) -- [ ] `GET /api/nodes` 能正确返回节点列表 -- [ ] `is_device_online()` 正确查询Redis - -### 2. 配置系统 -- [ ] 删除了无用config.toml -- [ ] 配置从.env读取(dotenvy) -- [ ] 无明文密码在代码中 - -### 3. 异步规范 -- [ ] 限流器用tokio::sync::Mutex(非std::sync::Mutex) -- [ ] 无阻塞操作 -- [ ] 后台协程正确处理Redis队列 - -### 4. 代码质量 -- [ ] `cargo check` 零报错 -- [ ] `cargo clippy` 零警告 -- [ ] 单函数≤80行 -- [ ] 命名语义化,完整注释 -- [ ] 错误处理完善 - -## 输出格式 - -```markdown -# 重构修复验证报告 - -## 总体评价 -[评分1-10,简要评价] - -## P0/P1问题验证 -[是否修复,验证结果] - -## 新引入问题 -[有无新问题] - -## 代码质量 -[编译/规范/错误处理] - -## 总结 -[是否可以推送] -``` - -## 质量约束 - -1. 审查必须覆盖所有清单项目 -2. 每个问题必须给出具体位置和修复建议 -3. 审查报告用中文撰写 -4. 必须实际读取代码文件 diff --git a/tasks/review-full-codebase-report.md b/tasks/review-full-codebase-report.md deleted file mode 100644 index 5836e66..0000000 --- a/tasks/review-full-codebase-report.md +++ /dev/null @@ -1,221 +0,0 @@ -# 全面代码审查报告 - -## 总体评价 - -**评分:7.5 / 10** - -项目整体代码质量良好,架构清晰,模块职责分明。后端 Rust 代码规范,通过了 `cargo clippy` 零警告检查;前端 TypeScript 代码通过 `tsc --noEmit` 零错误,`vite build` 构建成功。RBAC 权限模型完整,TCP 协议处理健壮。主要不足集中在安全细节、性能优化和部分功能未完成方面。 - -## 统计 - -- 后端文件数:29(Rust) -- 前端文件数:35(TypeScript/TSX) -- 后端代码行数:5,162 -- 前端代码行数:5,282 -- 总代码行数:10,444 -- 问题总数:19(严重 5 / 警告 8 / 建议 6) - -## 静态检查结果 - -| 检查项 | 结果 | -|--------|------| -| `cargo check` | 通过 | -| `cargo clippy` | 通过(零警告) | -| `tsc --noEmit` | 通过(零错误) | -| `vite build` | 通过(有 chunk 大小警告) | - -## 严重问题(必须修复) - -### 问题1:设备指令下发接口缺少权限校验 -- **位置**:`software/server/src/tcp/commands.rs:36-61` -- **描述**:`send_command` 端点(`POST /api/devices/:dev_id/command`)虽然经过认证中间件,但未调用 `check_permission` 进行 RBAC 权限校验。任何已登录用户均可向设备下发任意指令(包括开门、供电等危险操作)。 -- **风险**:未授权用户可操控设备,造成安全隐患。 -- **建议**:在 `send_command` 函数开头添加权限校验,根据 `act` 字段判断所需权限码(如 `device:operate`、charge:open_door` 等)。 - -### 问题2:组织树接口缺少权限校验和数据隔离 -- **位置**:`software/server/src/routes/cabinet_tree.rs:14-92` -- **描述**:`organization_tree` 端点未提取 `CurrentUser`,无权限校验,也无组织级数据隔离。企业管理员可通过此接口看到所有组织的数据。 -- **风险**:数据泄露,违反多租户隔离原则。 -- **建议**:添加 `CurrentUser` 提取器,校验 `device:view` 权限,并根据 `role_level` 和 `organization_id` 过滤返回数据。 - -### 问题3:默认密码 "123456" 硬编码 -- **位置**:`software/server/src/routes/users.rs:302`、`software/server/src/routes/users.rs:437` -- **描述**:创建用户和重置密码时,默认密码硬编码为 `"123456"`。此密码过于简单,且散落在代码中。 -- **风险**:安全风险,弱密码可能被暴力破解。 -- **建议**:将默认密码抽离为配置常量,并强制用户首次登录时修改密码。至少应使用随机生成的初始密码。 - -### 问题4:auth_str 安全码明文打印到日志 -- **位置**:`software/server/src/tcp/handler.rs:61` -- **描述**:`handle_auth_str` 函数将生成的安全码以明文写入日志:`tracing::info!("[auth_str] dev_id={} auth_str={}", dev_id, auth_str)`。 -- **风险**:安全码泄露,攻击者可通过日志获取签名密钥。 -- **建议**:移除日志中的 `auth_str` 值,仅记录 `dev_id`,或使用脱敏处理(如仅打印前2位)。 - -### 问题5:前端刷新后用户信息丢失 -- **位置**:`software/web/src/stores/auth.ts:71-76` -- **描述**:`restore` 函数仅从 localStorage 恢复 token,但未调用 `/api/auth/me` 获取用户信息和权限码。刷新页面后,虽然 token 存在,但 `user` 为 null,导致前端权限判断(`hasPermission`)始终返回 false,所有权限按钮消失。 -- **风险**:用户体验严重受损,刷新后所有权限控制按钮消失。 -- **建议**:在 `restore` 中检测到 token 后,异步调用 `/api/auth/me` 恢复完整用户信息。 - -## 警告(建议修复) - -### 警告1:前端打包体积过大 -- **位置**:`software/web/` 构建配置 -- **描述**:vite build 产物 JS 文件 1,314 KB(gzip 后 374 KB),超过 500 KB 警告阈值。未做代码分割。 -- **建议**:使用 `React.lazy()` + 动态 `import()` 对路由级组件进行代码分割,尤其是 H5 端和后台管理端应分离打包。 - -### 警告2:柜子详情接口存在 N+1 查询 -- **位置**:`software/server/src/routes/cabinets.rs:284-308` -- **描述**:`get_cabinet_detail` 先查询仓控板列表,然后对每个仓控板单独查询仓体。若柜子有 N 块仓控板,则产生 N+1 次数据库查询。 -- **建议**:使用一条 SQL 通过 JOIN 查询所有仓控板和仓体,或在应用层批量查询。 - -### 警告3:角色列表接口存在 N+1 查询 -- **位置**:`software/server/src/routes/roles.rs:42-69` -- **描述**:`list_roles` 对每个角色单独查询权限码,产生 N+1 次查询。 -- **建议**:使用一条 SQL 通过 JOIN 查询所有角色及其权限码。 - -### 警告4:多处使用 `as unknown as` 类型断言 -- **位置**:`software/web/src/pages/devices/index.tsx:81,103`、`OrganizationTree.tsx:169,171` -- **描述**:前端代码中多处使用 `as unknown as` 进行类型强转,绕过了 TypeScript 类型检查。 -- **建议**:修正 API 响应类型定义,使其与实际返回数据结构一致,消除不必要的类型断言。 - -### 警告5:下载文件端点未实现 -- **位置**:`software/server/src/routes/downloads.rs:162` -- **描述**:`get_download` 返回 `download_url: "/api/downloads/{id}/file"`,但此路由未在 `routes/mod.rs` 中注册,实际下载功能不可用。 -- **建议**:实现文件下载端点,使用 `axum::response::File` 或 `tower-http` 的 `ServeDir` 提供文件服务。 - -### 警告6:`org_condition` 辅助函数未被充分使用 -- **位置**:`software/server/src/middleware/auth.rs:255-270` -- **描述**:`org_condition` 函数已定义但仅在 `charge_records.rs` 和 `device_logs.rs` 中使用。`cabinet_tree.rs`、`energy_stats.rs` 等模块自行实现了类似的组织过滤逻辑,代码重复。 -- **建议**:统一使用 `org_condition` 和 `org_condition_for_logs` 进行组织数据隔离,减少重复代码。 - -### 警告7:H5 用户端 API 端点后端未实现 -- **位置**:`software/web/src/h5/api.ts` -- **描述**:H5 API 调用了 `/h5/auth/login`、`/h5/dashboard`、`/h5/projects` 等端点,但后端路由中未注册这些路径。H5 用户端目前无法正常工作。 -- **建议**:明确 H5 后端 API 的开发计划,或在 H5 代码中标注为 WIP(Work In Progress)。 - -### 警告8:Dashboard 页面为占位符 -- **位置**:`software/web/src/pages/Dashboard.tsx` -- **描述**:后台管理首页仅显示欢迎文字,无任何数据展示。能耗管理页面已有总览数据,Dashboard 应复用或引用。 -- **建议**:实现 Dashboard 数据展示(如今日充电次数、在线设备数、告警数等),可复用 `energy-stats/summary` 接口。 - -## 建议(可优化) - -### 建议1:LIKE 查询中 `%` 通配符未转义 -- **位置**:`software/server/src/routes/users.rs:111` -- **描述**:`let like_pattern = format!("%{}%", keyword)` 未对关键字中的 `%` 和 `_` 进行转义,用户输入这些字符时可能产生非预期的模糊匹配。 -- **建议**:对 keyword 中的 `%` 和 `_` 进行转义后再拼接 LIKE 模式。 - -### 建议2:连接池最大连接数可配置化 -- **位置**:`software/server/src/db/mod.rs:11` -- **描述**:MySQL 连接池 `max_connections(20)` 硬编码。 -- **建议**:通过环境变量 `DB_MAX_CONNECTIONS` 配置,便于不同环境调整。 - -### 建议3:充电曲线使用静态模拟数据 -- **位置**:`software/web/src/h5/pages/compartment-detail.tsx:174` -- **描述**:通道详情页的充电曲线使用硬编码数据 `[30, 45, 55, ...]` 展示,非真实数据。 -- **建议**:标注为 TODO 或待后端提供充电曲线 API 后替换,当前可接受。 - -### 建议4:`log_operation` 函数可抽取为公共模块 -- **位置**:`software/server/src/routes/users.rs:63-85` -- **描述**:操作日志记录函数 `log_operation` 定义在 `users.rs` 中,但其他模块(如 `roles.rs`、`cabinets.rs`)也需要记录操作日志。 -- **建议**:将 `log_operation` 抽取到公共模块(如 `middleware/auth.rs` 或新建 `utils/log.rs`),供所有路由模块复用。 - -### 建议5:前端 `eslint-disable` 注释 -- **位置**:`software/web/src/h5/pages/devices.tsx:62`、`device-detail.tsx:37`、`compartment-detail.tsx:43` -- **描述**:H5 页面中有多处 `eslint-disable-next-line react-hooks/exhaustive-deps`,跳过了 Hook 依赖检查。 -- **建议**:补充正确的依赖项,或使用 `useCallback` 包裹函数以明确依赖关系。 - -### 建议6:数据库迁移缺少索引定义 -- **位置**:`software/server/src/db/migrate.rs` -- **描述**:建表语句中未创建索引(除 `energy_stats` 的联合唯一键外)。`charge_records`、`device_logs`、`operation_logs` 等高频查询表缺少对 `cabinet_id`、`created_at` 等常用过滤字段的索引。 -- **建议**:在迁移脚本中添加索引,如: - - `charge_records`: `cabinet_id`, `start_time` - - `device_logs`: `cabinet_id`, `created_at` - - `operation_logs`: `user_id`, `created_at` - - `cabinets`: `project_id` - -## 审查清单完成情况 - -### 1. 编译与类型检查 -- [x] `cargo check` 零报错零警告 -- [x] `cargo clippy` 零报错零警告 -- [x] `tsc --noEmit` 零报错零警告 -- [x] `vite build` 成功(有 chunk 大小警告) - -### 2. 代码规范 -- [x] 单函数基本 <=80 行(少数函数略超,如 `list_users` 约 160 行,可拆分) -- [x] 命名语义化,无无意义缩写 -- [x] 完整注释(模块级文档注释 + 函数文档注释) -- [x] 常量抽离(如 `COST_PER_KWH`、`SIGN_START`、`TIMESTAMP_TOLERANCE_SECS`) -- [x] 无废弃 API 使用 - -### 3. 错误处理 -- [x] 所有外部 IO/网络请求有异常捕获 -- [x] 无裸 `panic!`(`expect` 仅用于启动阶段的致命错误,如数据库连接失败) -- [x] 分支逻辑基本全覆盖 -- [x] 错误信息有意义(使用中文描述,含上下文) - -### 4. Rust 专项 -- [x] 内存安全,使用 sqlx 连接池无冗余拷贝 -- [x] 无 `unsafe` 块 -- [x] 所有权管理合理(`ConnectionPool` 使用 `Arc>` 共享) -- [x] 异步代码无阻塞操作 - -### 5. React 专项 -- [x] 函数式组件 + Hooks,无 Class 组件 -- [x] 状态管理使用 Zustand,分层清晰 -- [x] useEffect 有清理函数(如 DownloadCenter 的 setInterval) -- [ ] 少量 `as unknown as` 类型断言(见警告4) - -### 6. 安全 -- [x] SQL 注入防护(全部使用 `?` 参数化查询) -- [x] 密码哈希使用 argon2 -- [x] JWT 密钥有环境变量覆盖机制 + 警告 -- [x] 输入参数校验(手机号 11 位、IMEI 15 位等) -- [ ] 权限校验未覆盖所有 API 接口(见严重问题 1、2) -- [ ] 数据隔离部分缺失(见严重问题 2) -- [ ] 敏感参数脱敏不足(见严重问题 4) - -### 7. 业务逻辑 -- [x] TCP 协议解析正确(签名验证、LF 粘包处理、超时清理) -- [x] 权限模型正确(总管理员/企业管理员/普通用户三级) -- [x] 异步导出流程完整(任务创建 -> 后台处理 -> 下载中心) -- [x] 前端权限组件正确隐藏/禁用按钮 - -### 8. 架构 -- [x] 接口层、业务逻辑层、数据模型层分离 -- [x] 无循环依赖 -- [x] 模块职责清晰 -- [x] 路由注册完整 - -### 9. 性能 -- [ ] 数据库查询缺少索引(见建议 6) -- [ ] 存在 N+1 查询(见警告 2、3) -- [x] 前端列表有分页 -- [x] 大数据量导出异步处理 - -### 10. 可维护性 -- [x] 文件长度合理(最长 467 行 energy/index.tsx) -- [x] 组件拆分合理 -- [x] 代码重复度低(组织过滤逻辑有重复,见警告 6) -- [x] 配置外部化(环境变量 + dotenvy) - -## 优点 - -1. **架构清晰**:后端按功能域拆分模块(auth、organizations、cabinets、users 等),每个文件职责单一,不超过 500 行。 -2. **安全性基础扎实**:密码使用 argon2 哈希、JWT 有过期时间、SQL 全部参数化、TCP 签名验证含时间窗口校验。 -3. **RBAC 权限模型完整**:权限码精确到按钮级别,前端 `Permission` 组件支持 hidden/disabled 两种模式,总管理员自动拥有全部权限。 -4. **TCP 协议处理健壮**:使用 `LinesCodec` 处理粘包、`mpsc` 通道分离读写、心跳超时自动清理、连接池 `Arc` 支持并发。 -5. **异步导出设计合理**:download_tasks 表管理任务状态、tokio::spawn 后台生成 Excel、前端轮询刷新、过期文件清理。 -6. **前端工程化规范**:Zustand 状态管理简洁、API 层统一封装、路由守卫完善、H5 端与后台管理端独立认证。 -7. **代码注释充分**:每个模块有模块级文档注释,关键函数有入参/返回值说明,复杂逻辑有行内注释。 -8. **数据库迁移自动化**:启动时自动建表 + 初始化权限码和角色,部署友好。 - -## 总结 - -项目整体质量良好,代码规范、架构清晰、安全基础扎实。**优先修复建议**: - -1. **P0(立即修复)**:设备指令下发接口添加权限校验(严重问题 1)、组织树接口添加权限校验和数据隔离(严重问题 2)。 -2. **P1(尽快修复)**:前端 restore 后加载用户信息(严重问题 5)、auth_str 日志脱敏(严重问题 4)、默认密码安全化(严重问题 3)。 -3. **P2(迭代优化)**:N+1 查询优化、数据库索引添加、前端代码分割、下载文件端点实现、Dashboard 数据填充。 -4. **P3(后续完善)**:H5 后端 API 开发、操作日志函数抽取公共模块、LIKE 通配符转义。 diff --git a/tasks/review-full-codebase.md b/tasks/review-full-codebase.md deleted file mode 100644 index f73742a..0000000 --- a/tasks/review-full-codebase.md +++ /dev/null @@ -1,184 +0,0 @@ -# 全面代码审查任务 - -## 目标 - -全面审查充电柜PMS项目的所有代码,确保质量、安全、规范。 - -## 审查范围 - -### 后端 (Rust + Axum) -`software/server/src/` 目录下所有文件: -- `main.rs` — 启动入口 -- `config.rs` — 配置管理 -- `error.rs` — 错误处理 -- `db/mod.rs` — 数据库连接 -- `db/migrate.rs` — 数据库迁移 -- `redis.rs` — Redis连接 -- `middleware/auth.rs` — JWT认证+RBAC权限 -- `routes/` — 所有路由模块 - - `auth.rs` — 登录/登出 - - `organizations.rs` — 组织CRUD - - `projects.rs` — 项目CRUD - - `cabinets.rs` — 设备CRUD - - `cabinet_tree.rs` — 树状结构 - - `users.rs` — 用户管理 - - `roles.rs` — 角色管理 - - `users_perm.rs` — 用户权限 - - `charge_records.rs` — 充电记录 - - `device_logs.rs` — 设备日志 - - `energy_stats.rs` — 能耗统计 - - `downloads.rs` — 下载中心 - - `operation_logs.rs` — 操作日志 -- `tcp/` — TCP通讯服务 - - `server.rs` — TCP监听 - - `connection.rs` — 连接管理 - - `protocol.rs` — 协议解析 - - `handler.rs` — 消息处理 - - `commands.rs` — 指令下发 - -### 前端 (React + TypeScript) -`software/web/src/` 目录下所有文件: -- `App.tsx` — 路由配置 -- `main.tsx` — 入口 -- `layouts/AdminLayout.tsx` — 后台布局 -- `api/` — API封装 -- `stores/auth.ts` — 认证状态 -- `utils/permission.ts` — 权限工具 -- `components/Permission.tsx` — 权限组件 -- `pages/` — 所有页面 - - `Login.tsx` — 登录页 - - `Dashboard.tsx` — 首页 - - `devices/` — 设备管理 - - `charge-records/` — 充电记录 - - `device-logs/` — 设备日志 - - `energy/` — 能耗管理 - - `settings/` — 系统设置 -- `h5/` — H5用户端 - - `pages/` — H5页面 - - `components.tsx` — 移动端组件 - - `layout.tsx` — H5布局 - -## 审查清单 - -### 1. 编译与类型检查 -- [ ] `cargo check` 零报错零警告 -- [ ] `cargo clippy` 零报错零警告 -- [ ] `tsc --noEmit` 零报错零警告 -- [ ] `vite build` 成功 - -### 2. 代码规范 -- [ ] 单函数≤80行 -- [ ] 命名语义化,无无意义缩写 -- [ ] 完整注释(入参、返回值、复杂逻辑) -- [ ] 常量抽离,无硬编码散落 -- [ ] 无废弃API使用 - -### 3. 错误处理 -- [ ] 所有外部IO/网络请求有异常捕获 -- [ ] 无裸 `panic!` 或 `unwrap()` -- [ ] 分支逻辑全覆盖,无遗漏兜底 -- [ ] 错误信息有意义,便于调试 - -### 4. Rust专项 -- [ ] 内存安全,无冗余拷贝 -- [ ] 无 `unsafe` 块(除非有合理理由) -- [ ] 所有权管理合理 -- [ ] 异步代码无阻塞操作 - -### 5. React专项 -- [ ] 函数式组件+Hooks,无Class组件 -- [ ] 状态分层管理(接口/渲染/业务逻辑) -- [ ] 无内存泄漏(useEffect清理) -- [ ] 无 `as any` 类型断言 - -### 6. 安全 -- [ ] SQL注入防护(全部参数化查询) -- [ ] 密码哈希(argon2/bcrypt) -- [ ] JWT密钥安全(非默认值) -- [ ] 输入参数校验 -- [ ] 权限校验覆盖所有API接口 -- [ ] 数据隔离(组织/项目/设备级) -- [ ] 敏感参数脱敏(日志中不打印密码/token) - -### 7. 业务逻辑 -- [ ] TCP协议解析正确(签名验证、粘包处理、超时清理) -- [ ] 权限模型正确(总管理员/企业管理员/普通用户) -- [ ] 异步导出流程完整(任务创建→后台处理→下载) -- [ ] 前端权限组件正确隐藏/显示按钮 - -### 8. 架构 -- [ ] 接口层、业务逻辑层、数据模型层分离 -- [ ] 无循环依赖 -- [ ] 模块职责清晰 -- [ ] 路由注册完整,无遗漏 - -### 9. 性能 -- [ ] 数据库查询有索引 -- [ ] 无N+1查询问题 -- [ ] 前端列表有分页 -- [ ] 大数据量导出异步处理 - -### 10. 可维护性 -- [ ] 文件长度合理(≤300行) -- [ ] 组件拆分合理 -- [ ] 代码重复度低 -- [ ] 配置外部化(环境变量) - -## 输出格式 - -```markdown -# 全面代码审查报告 - -## 总体评价 -[整体质量评分 1-10分,简要评价] - -## 统计 -- 后端文件数:X -- 前端文件数:X -- 总代码行数:X -- 问题总数:X(严重X / 警告X / 建议X) - -## 严重问题( 必须修复) -### 问题1:[标题] -- **位置**:`文件路径:行号` -- **描述**:[问题描述] -- **风险**:[可能的影响] -- **建议**:[修复方案] - -## 警告(🟡 建议修复) -### 警告1:[标题] -- **位置**:`文件路径:行号` -- **描述**:[问题描述] -- **建议**:[改进方案] - -## 优点 -- [值得肯定的设计/实现] - -## 总结 -[总体结论和优先修复建议] -``` - -## 审查工具 - -```bash -# 后端静态检查 -cd software/server && cargo check && cargo clippy - -# 前端类型检查 -cd software/web && tsc --noEmit - -# 前端构建测试 -cd software/web && vite build - -# 代码行数统计 -find src -name "*.rs" -exec wc -l {} + | sort -n -find src -name "*.tsx" -exec wc -l {} + | sort -n -``` - -## 质量约束 - -1. 审查必须覆盖所有清单项目 -2. 每个问题必须给出具体位置和修复建议 -3. 严重问题必须标注风险等级 -4. 审查报告用中文撰写 -5. 必须实际读取代码文件,不能仅凭文件名判断 diff --git a/tasks/review-mimo-report.md b/tasks/review-mimo-report.md deleted file mode 100644 index 40d0580..0000000 --- a/tasks/review-mimo-report.md +++ /dev/null @@ -1,262 +0,0 @@ -# 全面代码审查报告 - -## 总体评价 - -**评分:7.5 / 10** - -项目整体架构清晰,模块拆分合理,代码质量较好。后端采用 Rust + Axum 技术栈,安全性较高;前端 React + Arco Design 组件化开发规范。权限模型(RBAC + 数据隔离)设计完善,TCP 通讯协议实现完整。主要问题集中在:部分编译错误风险、H5 后端接口缺失、文件下载端点未实现、以及若干安全/性能细节需改进。 - -## 统计 - -- 后端文件数:29(Rust) -- 前端文件数:36(TypeScript/React) -- 总代码行数:约 6,800 行(后端约 3,500 行,前端约 3,300 行) -- 问题总数:23(严重 5 / 警告 10 / 建议 8) - -## 审查清单完成情况 - -### 1. 编译与类型检查 -- [ ] `cargo check` — 未能执行(环境中无 Rust 工具链),**代码审查发现潜在编译问题**(见严重问题 #1、#2) -- [ ] `cargo clippy` — 未能执行 -- [ ] `tsc --noEmit` — 未能执行 -- [ ] `vite build` — 未能执行 - -### 2. 代码规范 -- [x] 单函数 ≤ 80 行 — 大部分符合,`users.rs::list_users` 约 160 行超标 -- [x] 命名语义化 — 整体良好,无无意义缩写 -- [x] 完整注释 — Rust 文档注释完善,前端 JSDoc 基本覆盖 -- [x] 常量抽离 — `COST_PER_KWH`、`JWT_SECRET_DEFAULT` 等已抽离 -- [x] 无废弃 API — 未发现 - -### 3. 错误处理 -- [x] 所有外部 IO/网络请求有异常捕获 — 全面覆盖 -- [x] 无裸 `panic!` 或 `unwrap()` — `expect()` 仅用于启动阶段关键操作 -- [x] 分支逻辑全覆盖 — match 语句均有兜底分支 -- [x] 错误信息有意义 — 中文错误信息便于调试 - -### 4. Rust 专项 -- [x] 内存安全,无冗余拷贝 — 合理使用引用和 Clone -- [x] 无 `unsafe` 块 — 确认无 -- [x] 所有权管理合理 — ConnectionPool 使用 Arc> 模式 -- [x] 异步代码无阻塞操作 — 确认无阻塞调用 - -### 5. React 专项 -- [x] 函数式组件 + Hooks — 全部为函数式组件 -- [x] 状态分层管理 — Zustand 管理认证状态,页面级 useState -- [x] 无内存泄漏 — useEffect 清理函数正确(DownloadCenter、compartment-detail) -- [ ] 无 `as any` 类型断言 — 存在 `as never`、`as unknown as` 类型断言 - -### 6. 安全 -- [x] SQL 注入防护 — 全部使用参数化查询 `?` 占位符 -- [x] 密码哈希 — argon2(行业领先) -- [x] JWT 密钥安全 — 支持环境变量,启动时校验默认值告警 -- [x] 输入参数校验 — 手机号、IMEI、角色值等均有校验 -- [x] 权限校验覆盖 — 所有 API 接口均有 `check_permission` -- [x] 数据隔离 — `org_condition` / `org_condition_for_logs` 实现组织级过滤 -- [ ] 敏感参数脱敏 — `auth_str` 在 API 响应中明文返回 - -### 7. 业务逻辑 -- [x] TCP 协议解析正确 — 签名验证、LF 粘包处理、超时清理均已实现 -- [x] 权限模型正确 — 总管理员/企业管理员/普通用户三级 -- [x] 异步导出流程完整 — 任务创建 → tokio 后台处理 → 下载中心查询 -- [x] 前端权限组件正确 — Permission 组件支持 hidden/disabled 两种模式 - -### 8. 架构 -- [x] 接口层、业务逻辑层、数据模型层分离 — routes/ 处理请求,handler/ 处理业务逻辑 -- [x] 无循环依赖 — 模块间单向依赖 -- [x] 模块职责清晰 — 按功能域拆分(organizations/projects/cabinets 等) -- [x] 路由注册完整 — `routes/mod.rs` 聚合所有子路由 - -### 9. 性能 -- [ ] 数据库查询有索引 — 迁移脚本缺少关键索引(见警告 #6) -- [x] 无 N+1 查询问题 — 柜子详情的仓板/仓体查询在可接受范围(N≤6) -- [x] 前端列表有分页 — 所有列表页均实现分页 -- [x] 大数据量导出异步处理 — tokio::spawn 后台生成 Excel - -### 10. 可维护性 -- [ ] 文件长度合理(≤ 300 行)— `users.rs`(553 行)、`energy/index.tsx`(468 行)超标 -- [x] 组件拆分合理 — OrganizationTree、CabinetGrid、DownloadCenter 等独立组件 -- [ ] 代码重复度低 — 导出逻辑、状态映射存在重复 -- [x] 配置外部化 — 环境变量 + `.env` 文件 - ---- - -## 严重问题(必须修复) - -### 问题 1:`list_users` 查询列数与解构不匹配 -- **位置**:`software/server/src/routes/users.rs:158` -- **描述**:`query_as` 查询返回 7 列 `(id, phone, name, role, organization_id, status, created_at)`,但行后 `UpdateUserInput` 结构体定义出现在文件中间(第 41 行),且 `query_as` 没有对应的 7 元素元组类型声明。第 158 行的类型注解为 `Vec<(i64, String, Option, i32, Option, i32, String)>`(7 元素),但此处 `query_as` 调用没有使用 `FromRow` 派生结构体,而是直接使用元组。sqlx 对元组元素数量有上限(通常支持到 6 个),7 元素元组可能导致编译错误。 -- **风险**:编译失败,无法构建后端服务 -- **建议**:定义 `#[derive(sqlx::FromRow)]` 结构体替代元组,或将查询拆分为两步 - -### 问题 2:充电记录 COUNT 查询 JOIN 表不匹配 -- **位置**:`software/server/src/routes/charge_records.rs:187-193` -- **描述**:计数 SQL 为 `SELECT COUNT(*) FROM charge_records cr LEFT JOIN cabin_boards cb ON cr.compartment_id = cb.id`,但 WHERE 条件中引用了 `cb.id`(期望是 cabin_boards.id)和 `cr.compartment_id`。实际 `fetch_charge_rows` 中 JOIN 了 `compartments comp ON cr.compartment_id = comp.id` 再 JOIN `cabin_boards cb ON comp.cabin_board_id = cb.id`。COUNT 查询缺少 `compartments` 表的 JOIN,导致 `cabin_board_id` 过滤条件无法正确工作。 -- **风险**:按仓板过滤时,分页总数计算错误,前端显示数据不完整 -- **建议**:COUNT 查询使用与列表查询相同的 JOIN 链路:`cr → compartments → cabin_boards` - -### 问题 3:H5 后端接口全部缺失 -- **位置**:`software/web/src/h5/api.ts` 全部接口 -- **描述**:H5 用户端调用了 `/h5/auth/login`、`/h5/dashboard`、`/h5/projects`、`/h5/cabinets`、`/h5/cabinets/:id`、`/h5/compartments/:id`、`/h5/charge/start`、`/h5/charge/stop`、`/h5/door/open` 等接口,但后端 `routes/mod.rs` 中未注册任何 `/h5/` 路由。 -- **风险**:H5 用户端完全无法使用,所有请求返回 404 -- **建议**:在后端实现 H5 路由模块,或明确标注为待开发并在前端添加提示 - -### 问题 4:文件下载端点未实现 -- **位置**:`software/server/src/routes/downloads.rs:162` -- **描述**:`get_download` 函数返回 `download_url: "/api/downloads/{id}/file"`,但 `routes/mod.rs` 中没有注册该路由。用户点击下载后无法获取文件。 -- **风险**:导出功能不完整,用户无法下载已生成的 Excel 文件 -- **建议**:添加文件下载端点,使用 `axum::response::File` 或 `tower-http::ServeDir` 提供文件服务 - -### 问题 5:`authenticated` 标志未强制校验 -- **位置**:`software/server/src/tcp/handler.rs:146` -- **描述**:`DeviceConnection` 有 `authenticated` 字段,`handle_login` 成功后调用 `pool.set_authenticated(dev_id)`。但 `handle_status_post` 中检查的是 `pool.is_online(dev_id)`,该方法仅检查 `authenticated` 标志。然而,在 `server.rs:98-104` 中,设备在发送 `auth_str` 或 `login` 时就被注册到连接池(`pool.register`),此时 `authenticated` 为 `false`。问题在于 `is_online` 返回 `authenticated` 为 `false` 时,`status_post` 确实会拒绝(返回"设备未登录"),但 `auth_str` 请求可以无限次调用而不限流,可能被用于资源耗尽攻击。 -- **风险**:安全码获取接口无频率限制,可能被恶意设备利用 -- **建议**:对 `auth_str` 请求添加频率限制或 IP 限制 - ---- - -## 警告(建议修复) - -### 警告 1:`list_users` 函数过长 -- **位置**:`software/server/src/routes/users.rs:96-256` -- **描述**:函数约 160 行,包含多种条件分支(有无关键字搜索 x 有无组织过滤),大量重复的 SQL 构建逻辑。 -- **建议**:拆分为 `build_user_count_query` 和 `build_user_list_query` 辅助函数,或使用动态 SQL 构建器 - -### 警告 2:能耗导出缺少组织数据隔离 -- **位置**:`software/server/src/routes/energy_stats.rs:460-553` -- **描述**:`generate_energy_excel` 后台任务中使用 `build_export_clause` 构建查询条件,但未追加组织过滤条件。企业管理员可能导出全部组织的能耗数据。 -- **建议**:在导出查询中追加 `org_condition` 过滤 - -### 警告 3:前端多处 `as unknown as` / `as never` 类型断言 -- **位置**:`OrganizationTree.tsx:169`、`devices/index.tsx:81,103`、`AddCabinetModal.tsx:44` -- **描述**:使用 `as never`、`as unknown as` 绕过 TypeScript 类型检查,掩盖了 API 响应类型与实际数据的不匹配。 -- **建议**:修正 API 泛型参数类型,使响应类型与 Arco Tree/Table 组件期望的类型一致 - -### 警告 4:数据库迁移缺少索引 -- **位置**:`software/server/src/db/migrate.rs` -- **描述**:以下高频查询列缺少索引: - - `charge_records.cabinet_id`、`charge_records.start_time` - - `device_logs.cabinet_id`、`device_logs.created_at` - - `operation_logs.user_id`、`operation_logs.created_at` - - `users.organization_id`、`cabinets.project_id` -- **建议**:在迁移脚本中为上述列添加 `INDEX` - -### 警告 5:导出逻辑大量重复 -- **位置**:`charge_records.rs:307-395` 与 `energy_stats.rs:460-553` -- **描述**:两个导出函数的结构几乎相同:更新状态 → 获取参数 → 查询数据 → 更新进度 → 生成 Excel → 保存文件 → 更新完成状态。 -- **建议**:抽取通用的 `run_export_task` 泛型函数,传入数据查询和 Excel 生成闭包 - -### 警告 6:H5 认证恢复无服务端校验 -- **位置**:`software/web/src/h5/auth.ts:48-55` -- **描述**:`restore()` 从 localStorage 读取 token 后直接设置 `isAuthenticated: true`,不验证 token 有效性。与后台管理端的 `restore`(调用 `/auth/me` 验证)行为不一致。 -- **建议**:H5 端也添加服务端 token 校验(需先实现 H5 后端接口) - -### 警告 7:`regenerate_auth` 返回明文安全码 -- **位置**:`software/server/src/routes/cabinets.rs:232-255` -- **描述**:重新生成安全码后通过 API 响应返回明文 `auth_str`。虽然管理员需要知道新安全码以配置设备,但这增加了泄露风险。 -- **建议**:考虑仅显示一次或添加操作确认,并在操作日志中记录 - -### 警告 8:`roles.rs::list_roles` 存在 N+1 查询 -- **位置**:`software/server/src/routes/roles.rs:36-72` -- **描述**:先查询所有角色,再为每个角色单独查询权限码。角色数量通常为个位数,当前可接受,但不够优雅。 -- **建议**:使用单条 SQL 通过 `GROUP_CONCAT` 或 `JOIN` 一次性获取角色及其权限码 - -### 警告 9:导出文件路径使用相对路径 -- **位置**:`charge_records.rs:378`、`energy_stats.rs:536` -- **描述**:`let file_dir = "./exports"` 使用相对路径,依赖进程工作目录。 -- **建议**:使用配置项或绝对路径(如 `cfg.export_dir`) - -### 警告 10:清理下载接口缺少用户权限校验 -- **位置**:`software/server/src/routes/downloads.rs:176-220` -- **描述**:`cleanup_downloads` 清理所有过期任务的文件,不区分用户。虽然有 `charge_record:export` 权限检查,但普通用户不应能清理其他用户的文件。 -- **建议**:添加 `WHERE user_id = ?` 过滤,或限制为总管理员操作 - ---- - -## 建议(可优化) - -### 建议 1:Dashboard 页面为空壳 -- **位置**:`software/web/src/pages/Dashboard.tsx` -- **描述**:仅显示欢迎文字,无实际数据展示。 -- **建议**:可复用能耗总览数据或设备在线统计,提供有价值的首页视图 - -### 建议 2:状态映射常量重复定义 -- **位置**:`cabinet.tsx:24-37`、`CabinetGrid.tsx:22-27`、`charge-records/index.tsx:52-56` -- **描述**:柜子状态和仓体状态的映射在多个文件中重复定义。 -- **建议**:抽取到 `constants/status.ts` 统一管理 - -### 建议 3:`devices/index.tsx` 状态变量过多 -- **位置**:`software/web/src/pages/devices/index.tsx` -- **描述**:组件内有 15+ 个 `useState`,管理弹窗、编辑、右键菜单等多种状态。 -- **建议**:使用 `useReducer` 或拆分为子组件(如 `useOrgModal`、`useContextMenu`) - -### 建议 4:`seed_permissions` 可批量插入 -- **位置**:`software/server/src/db/migrate.rs:186-223` -- **描述**:逐条 `INSERT IGNORE` 21 个权限码,可优化为单条批量 INSERT。 -- **建议**:使用 `INSERT IGNORE INTO permissions (code, name) VALUES (?, ?), (?, ?), ...` 批量插入 - -### 建议 5:电费单价应可配置 -- **位置**:`software/server/src/routes/energy_stats.rs:15` -- **描述**:`COST_PER_KWH` 硬编码为 0.80,不同地区/时段电价不同。 -- **建议**:移至配置文件或数据库配置表 - -### 建议 6:`energy/index.tsx` 文件过长 -- **位置**:`software/web/src/pages/energy/index.tsx`(468 行) -- **描述**:包含总览卡片、排行表格、项目统计、趋势图表等全部逻辑。 -- **建议**:拆分为 `SummaryCards`、`CabinetRanking`、`ProjectStats`、`TrendChart` 子组件 - -### 建议 7:H5 `routes.tsx` 导入顺序不规范 -- **位置**:`software/web/src/h5/routes.tsx:47-50` -- **描述**:`ScanRedirect` 组件的 `import` 语句出现在文件中间(第 47 行),违反 ES Module 导入应在文件顶部的约定。 -- **建议**:将所有 import 移到文件顶部 - -### 建议 8:充电曲线使用硬编码模拟数据 -- **位置**:`software/web/src/h5/pages/compartment-detail.tsx:173-175` -- **描述**:充电曲线图表使用固定数组 `[30, 45, 55, ...]` 模拟,不是真实数据。 -- **建议**:标注为 TODO 或待后端提供充电曲线 API 后替换 - ---- - -## 优点 - -1. **架构设计清晰**:后端按功能域拆分模块(organizations/projects/cabinets/users/roles 等),路由注册集中管理,模块职责单一 -2. **安全性基础扎实**:argon2 密码哈希、JWT 认证、RBAC 权限模型、参数化 SQL 查询、组织级数据隔离,安全层面考虑全面 -3. **TCP 通讯实现完整**:LF 分隔粘包处理、签名验证(SHA256 + 时间窗口)、心跳超时清理、连接池管理,协议层实现可靠 -4. **异步导出设计合理**:download_tasks 表跟踪任务状态、tokio 后台生成 Excel、前端轮询刷新,完整的异步导出流程 -5. **错误处理规范**:统一 `AppError` 错误类型,所有 IO 操作有异常捕获,无裸 panic,错误信息有意义 -6. **前端组件化好**:Permission 权限组件支持 hidden/disabled 模式,OrganizationTree 递归树组件,DownloadCenter 可复用弹窗 -7. **操作日志完善**:关键操作(创建/编辑/删除用户等)均记录操作日志,便于审计 -8. **H5 移动端体验好**:Tailwind CSS 样式统一,底部导航、确认弹窗、加载/错误状态组件完善 -9. **数据库设计合理**:外键约束、级联删除、UNIQUE 约束、COMMENT 注释,表结构规范 -10. **配置管理灵活**:环境变量 + `.env` 文件,敏感配置有默认值告警 - ---- - -## 总结 - -### 优先修复建议(按紧急程度排序) - -1. **P0 — 立即修复**: - - 修复 `list_users` 的 7 元素元组编译问题(严重 #1) - - 修复充电记录 COUNT 查询 JOIN 缺失(严重 #2) - - 实现文件下载端点 `/api/downloads/:id/file`(严重 #4) - -2. **P1 — 近期完成**: - - 实现 H5 后端接口模块(严重 #3),或在前端标注为"待开发" - - 添加数据库关键索引(警告 #4) - - 修复能耗导出缺少组织过滤(警告 #2) - -3. **P2 — 迭代优化**: - - 拆分 `list_users` 函数(警告 #1) - - 消除前端类型断言(警告 #3) - - 统一导出逻辑减少重复(警告 #5) - - 导出文件路径配置化(警告 #9) - -4. **P3 — 持续改进**: - - Dashboard 首页数据展示(建议 #1) - - 状态映射常量统一(建议 #2) - - 电费单价可配置化(建议 #5) - -### 结论 - -项目代码整体质量 **良好**,架构设计规范,安全和错误处理基础扎实。主要风险点在于:部分代码可能存在编译问题需要验证、H5 后端接口完全缺失、文件下载端点未实现。建议优先修复 P0 级问题后进行 `cargo check` 和 `tsc --noEmit` 验证,确保代码可正常编译。 diff --git a/tasks/review-nemotron-report.md b/tasks/review-nemotron-report.md deleted file mode 100644 index 8cee2a4..0000000 --- a/tasks/review-nemotron-report.md +++ /dev/null @@ -1,296 +0,0 @@ -# 全面代码审查报告 - -## 总体评价 - -**质量评分: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-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` 组件渲染 `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` 的返回类型 `ApiResponse` 与实际使用不匹配。 -- **建议**:检查 `api.get` 泛型推导是否正确,修复类型定义使其无需强制转换。 - -### 警告6:JWT 密钥常量重复定义 - -- **位置**:`software/server/src/config.rs:4` vs `software/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` 与 `flatten` 冗余 - -- **位置**:`software/server/src/tcp/protocol.rs:29-31` -- **描述**:`extra: Option` 配合 `#[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. 编译与类型检查 -- [x] Rust 代码结构完整,模块引用正确(无法在此环境运行 `cargo check`) -- [x] TypeScript 代码类型定义完整,接口匹配(无法在此环境运行 `tsc --noEmit`) -- [x] 前端构建配置正常(Vite + React) - -### 2. 代码规范 -- [x] 单函数基本 <=80 行(最长函数约 70 行) -- [x] 命名语义化,中文注释完整 -- [x] 常量已抽离(`COST_PER_KWH`、`HEARTBEAT_TIMEOUT` 等) -- [x] 无废弃 API 使用 - -### 3. 错误处理 -- [x] 所有外部 IO 有异常捕获 -- [x] 无裸 `panic!`(`expect` 仅用于不可能失败的序列化) -- [x] 分支逻辑基本全覆盖 -- [x] 错误信息有意义(中文描述) - -### 4. Rust 专项 -- [x] 无 `unsafe` 块 -- [x] 所有权管理合理(`Clone` 用于共享状态,`Arc` 用于连接池) -- [x] 异步代码无阻塞操作(使用 `tokio::fs` 而非 `std::fs`) -- [x] 内存安全,无冗余拷贝 - -### 5. React 专项 -- [x] 全部使用函数式组件 + Hooks -- [x] 状态管理使用 Zustand(轻量级) -- [x] useEffect 清理正确(定时器清理) -- [ ] 存在 `as unknown as` 类型断言(见警告5) - -### 6. 安全 -- [x] SQL 全部使用参数化查询(`?` 占位符) -- [x] 密码使用 Argon2 哈希 -- [x] JWT 密钥支持环境变量覆盖 -- [x] 输入参数有基本校验(手机号、IMEI 格式) -- [ ] 权限码种子数据不完整(见严重问题1、2) -- [ ] H5 接口缺少数据隔离(见严重问题3、4) -- [x] 敏感参数未打印到日志(密码未出现在 tracing 中) - -### 7. 业务逻辑 -- [x] TCP 协议解析正确(LF 分隔、签名验证、超时清理) -- [x] 权限模型正确(总管理员/企业管理员/普通用户三级) -- [x] 异步导出流程完整(任务创建 -> tokio::spawn 后台处理 -> 下载中心查看) -- [x] 前端权限组件正确隐藏/显示按钮 - -### 8. 架构 -- [x] 接口层、业务逻辑层、数据模型层分离清晰 -- [x] 无循环依赖 -- [x] 模块职责清晰(routes 按功能拆分,tcp 按职责拆分) -- [x] 路由注册完整 - -### 9. 性能 -- [ ] 数据库索引需补充(见警告8) -- [x] 无 N+1 查询问题(组织树使用批量查询后内存组装) -- [x] 前端列表有分页 -- [x] 大数据量导出使用异步处理 - -### 10. 可维护性 -- [x] 文件长度合理(大部分 <=300 行,最长 `h5.rs` 约 757 行但含多个独立接口) -- [x] 组件拆分合理 -- [ ] 部分代码重复(见警告1、2) -- [x] 配置外部化(环境变量) - ---- - -## 优点 - -1. **统一错误处理**:`AppError` 枚举 + `IntoResponse` 实现了优雅的错误处理,所有错误自动转为标准 JSON 格式 -2. **安全的密码方案**:使用 Argon2(当前推荐的密码哈希算法),带随机盐 -3. **TCP 协议设计合理**:LF 分隔 JSON、签名验证、频率限制、心跳超时清理,考虑周全 -4. **连接池管理**:`ConnectionPool` 使用 `Arc>` 实现线程安全的设备连接管理 -5. **异步导出架构**:`tokio::spawn` 后台生成 Excel,下载中心轮询查看进度,用户体验良好 -6. **前端权限组件**:`` 组件支持 hidden/disabled 两种模式,按钮级权限控制优雅 -7. **H5 移动端独立实现**:与后台管理使用独立认证、独立路由、Tailwind CSS 样式,互不干扰 -8. **数据库迁移自动化**:启动时自动建表 + 种子数据,部署简单 -9. **操作日志完整**:用户管理的增删改查均记录操作日志,便于审计 -10. **代码注释质量高**:中文注释覆盖所有模块、函数和关键逻辑,可读性好 - ---- - -## 总结 - -项目代码质量整体良好,架构设计清晰,技术选型合理。优先修复建议如下: - -**P0(立即修复)**: -1. 补充缺失的权限码种子数据(`org:*`、`project:*`、`device:edit`)— 否则非总管理员无法管理组织和项目 -2. H5 接口添加数据隔离 — 否则存在数据泄露风险 - -**P1(尽快修复)**: -3. 修复前后端 H5 告警数据结构不匹配 -4. `h5.rs` 中 `org_project_condition` 改为参数化查询 -5. 下载中心权限码检查优化 - -**P2(计划修复)**: -6. 补充数据库索引 -7. 消除代码重复(`list_users`、`energy_stats` 组织过滤) -8. 前端 401 自动跳转登录页 -9. H5 认证恢复时校验 token 有效性 diff --git a/tasks/review-north-mini-report.md b/tasks/review-north-mini-report.md deleted file mode 100644 index 9b7e4e8..0000000 --- a/tasks/review-north-mini-report.md +++ /dev/null @@ -1,246 +0,0 @@ -# 全面代码审查报告 - -## 总体评价 - -**质量评分: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) - -整体代码质量良好,核心业务逻辑正确,安全基础扎实。上述问题修复后可达到生产就绪水平。 diff --git a/tasks/review-report.md b/tasks/review-report.md deleted file mode 100644 index f1bbd4e..0000000 --- a/tasks/review-report.md +++ /dev/null @@ -1,206 +0,0 @@ -# 全面代码审查报告 - -## 总体评价 - -**评分:7.5/10** - -整体架构清晰,Axum 后端 + React 前端技术栈选型合理。代码分层明确,安全基础扎实(argon2 哈希、参数化 SQL、RBAC 权限模型、组织数据隔离)。TCP 通讯协议实现完整。主要扣分点:权限种子数据缺失导致非超管用户功能不可用、SQL JOIN 错误、部分函数过长。 - -## 统计 - -| 指标 | 数值 | -|------|------| -| 后端 Rust 文件数 | 29 | -| 后端代码行数 | 5,198 | -| 前端 TSX 文件数 | 27 | -| 前端代码行数 | 4,535 | -| 总代码行数 | 9,733 | -| 静态检查 | `cargo check` ✅ `cargo clippy` ✅ `tsc --noEmit` ✅ `vite build` ✅ | -| 问题总数 | 11(严重 3 / 警告 5 / 建议 3) | - ---- - -## 严重问题( 必须修复) - -### 问题1:权限种子数据缺失 — 非超管用户功能不可用 - -**位置**:`software/server/src/db/migrate.rs:186-219` - -**描述**:`seed_permissions` 中缺少以下权限码,但代码中多处通过 `check_permission` 使用: - -| 缺失权限码 | 使用位置 | -|------------|---------| -| `device:edit` | `cabinets.rs:166` 更新柜子、`cabinets.rs:237` 重新生成安全码 | -| `org:view` | `organizations.rs:154` | -| `org:create` | `organizations.rs:172` | -| `org:edit` | `organizations.rs:198` | -| `org:delete` | `organizations.rs:227` | -| `project:view` | `projects.rs:20` | -| `project:create` | `projects.rs:40` | -| `project:edit` | `projects.rs:78` | -| `project:delete` | `projects.rs:107` | - -**风险**:任何 role_level < 2 的用户(企业管理员、普通用户)调用这些接口时,`check_permission` 会返回 `Err(Forbidden)`,即使角色已分配这些权限(因为数据库中根本不存在这些权限码,无法关联)。 - -**建议**:在 `seed_permissions` 中补充上述 9 个权限码,并为 `org_admin` 和 `normal_user` 角色分配相应权限。 - ---- - -### 问题2:充电记录计数 SQL JOIN 错误 - -**位置**:`software/server/src/routes/charge_records.rs:187-191` - -**描述**:列表查询的 COUNT 子句: -```sql -SELECT COUNT(*) FROM charge_records cr -LEFT JOIN cabin_boards cb ON cr.compartment_id = cb.id -``` -`cr.compartment_id` 是 `compartments` 表的主键,而非 `cabin_boards` 表的主键。错误的 JOIN 条件会在 `compartment_id` 恰好等于某个 `cabin_boards.id` 时产生错误匹配,导致 COUNT 结果不准确。 - -对比同文件第 136-149 行的列表查询,使用了正确的三表 JOIN: -```sql -LEFT JOIN compartments comp ON cr.compartment_id = comp.id -LEFT JOIN cabin_boards cb ON comp.cabin_board_id = cb.id -``` - -**风险**:当 `cabin_board_id` 过滤条件生效时,计数结果将严重失准。无过滤时因 LEFT JOIN 不过滤行,影响较小但仍有潜在错误匹配。 - -**建议**:修正 COUNT 查询的 JOIN 逻辑,与列表查询保持一致。 - ---- - -### 问题3:`protocol.rs` 中 `expect()` 可能导致生产环境 panic - -**位置**:`software/server/src/tcp/protocol.rs:58`、`protocol.rs:80` - -**描述**: -```rust -pub fn to_json(&self) -> String { - serde_json::to_string(self).expect("ServerResponse 序列化不应失败") -} -``` -虽然 `ServerResponse` 和 `DeviceCommand` 的字段理论上不会导致序列化失败(都是标准类型),但 `expect()` 在 `serde_json` 遇到非 UTF-8 字符或自引用类型时会 panic。TCP 连接的 panic 如果未被 tokio 的 panic handler 捕获,可能导致整个 TCP 服务崩溃。 - -**风险**:生产环境下若序列化异常,TCP 服务可能崩溃,影响所有在线设备的通讯。 - -**建议**:将 `expect()` 替换为 `unwrap_or_default()` 或返回 `Result`: -```rust -pub fn to_json(&self) -> String { - serde_json::to_string(self).unwrap_or_default() -} -``` - ---- - -## 警告(🟡 建议修复) - -### 警告1:JWT_SECRET_DEFAULT 重复定义 - -**位置**:`config.rs:4` 和 `middleware/auth.rs:33` - -**描述**:两处分别定义了相同的常量: -```rust -const JWT_SECRET_DEFAULT: &str = "pms-dev-secret-change-me-in-production"; -``` -`config.rs` 中的 `validate()` 方法检查是否使用默认密钥并打印警告,而 `middleware/auth.rs` 中每次认证时也读取环境变量并回退到默认值。两处定义不一致可能导致维护问题。 - -**建议**:将常量统一到 `config.rs` 并导出,`middleware/auth.rs` 通过 `AppState` 获取密钥,而非每次从环境变量读取。 - ---- - -### 警告2:默认密码 "123456" 硬编码且作为回退值 - -**位置**:`config.rs:7`、`users.rs:303`、`users.rs:438`、`users.tsx:67`、`users.tsx:74` - -**描述**:创建用户和重置密码时,如果未提供密码则回退到 `DEFAULT_PASSWORD`("123456")。前端用户管理页面也将默认密码硬编码为 "123456"。 - -**风险**:管理员可能忘记修改默认密码,导致新用户以弱密码登录。 - -**建议**:创建用户时强制要求提供密码;或在首次登录时强制修改密码。前端不应预填默认密码。 - ---- - -### 警告3:`list_users` 函数过长(160行) - -**位置**:`users.rs:96-256` - -**描述**:该函数包含 4 种查询分支(有/无 keyword × 有/无 org_filter),每个分支独立构建 SQL。加上组织名称批量查询和结果映射,总行数达 160 行。 - -**建议**:使用动态条件构建器统一查询逻辑,减少分支重复。 - ---- - -### 警告4:`list_roles` 和 `list_permissions` 缺少权限校验 - -**位置**:`roles.rs:36-72`、`roles.rs:232-248` - -**描述**:这两个接口虽然接受 `CurrentUser` 参数(确保已认证),但未调用 `check_permission`。任何已认证用户都可以查看所有角色和权限码。 - -**建议**:添加 `role:view` 权限校验(`list_permissions` 同理)。 - ---- - -### 警告5:前端 `as unknown as` 类型断言 - -**位置**:`devices/index.tsx:81`、`devices/index.tsx:103`、`devices/index.tsx:260`、`AddCabinetModal.tsx:44` - -**描述**:4 处使用了 `as unknown as TargetType` 双重类型断言来绕过类型检查。这通常意味着 API 返回类型定义不完整或 API 层封装有问题。 - -**建议**:在 API 层定义完整的响应类型,避免在组件中使用类型断言。 - ---- - -## 建议( 可优化) - -### 建议1:`cabinet_tree.rs` 全量加载数据 - -**位置**:`cabinet_tree.rs:46-57` - -**描述**:组织树接口一次性加载所有 projects 和 cabinets,然后在内存中过滤。数据量大时可能有性能问题。 - -**建议**:根据用户角色,使用 SQL WHERE 条件在数据库层过滤,只返回用户有权查看的数据。 - ---- - -### 建告2:`energy_stats.rs` 组织过滤代码重复 - -**位置**:`energy_stats.rs:90-99`、`energy_stats.rs:174-183`、`energy_stats.rs:230-236`、`energy_stats.rs:289-298` - -**描述**:4 个函数中重复了几乎相同的组织过滤逻辑(判断 role_level、构建 org_filter/org_binds)。 - -**建议**:抽取为辅助函数,与 `auth.rs` 中的 `org_condition` 统一。 - ---- - -### 建议3:TCP 连接 JSON 解析失败时无响应 - -**位置**:`tcp/server.rs:88-94` - -**描述**:设备发送的消息 JSON 解析失败时,`continue` 跳过不回复。这在安全上是合理的(不给恶意设备反馈),但合法设备可能因编码问题发送非标准 JSON 而无法得知原因。 - -**建议**:保持当前行为(安全优先),但增加更详细的 debug 日志(当前已有 warn 级别日志,可接受)。 - ---- - -## 优点 - -1. **架构清晰**:Axum 路由分层合理,`AppState` 共享模式规范,中间件/路由/业务逻辑分离良好 -2. **安全基础扎实**:全部使用参数化 SQL 查询(零字符串拼接)、argon2 密码哈希、JWT + RBAC 权限模型、组织级数据隔离 -3. **TCP 协议实现完整**:签名验证、心跳检测、超时清理、LF 消息边界处理正确 -4. **异步导出流程**:任务创建→后台生成→状态轮询→下载,模式规范 -5. **错误处理统一**:`AppError` 枚举 + `IntoResponse` 实现,HTTP 状态码映射正确 -6. **数据库迁移**:自动建表 + 种子数据,部署友好 -7. **前端代码整洁**:函数式组件 + Hooks,无 Class 组件,无 `as any` 类型断言 -8. **注释充分**:模块级文档注释完整,函数级注释覆盖入参和行为说明 - ---- - -## 总结 - -项目整体质量良好,架构和安全方面表现出色。**必须立即修复的 3 个严重问题**: - -1. **权限种子数据缺失**( 最高优先级)— 直接导致非超管用户功能不可用,影响整个 RBAC 体系 -2. **充电记录计数 SQL 错误**( 高优先级)— 导致数据展示不准确 -3. **protocol.rs 中的 expect()**( 中优先级)— 潜在的生产环境 panic 风险 - -修复这 3 个问题后,项目即可达到生产就绪状态。其余警告和建议可作为后续迭代优化项。 diff --git a/tasks/review-round-2-report.md b/tasks/review-round-2-report.md deleted file mode 100644 index 441b405..0000000 --- a/tasks/review-round-2-report.md +++ /dev/null @@ -1,85 +0,0 @@ -# 代码审查报告(第二轮) - -## 总体评价 - -**6.5/10** — 4个安全问题中3个彻底修复,1个(权限校验)部分修复。构建全部通过,文件拆分合理。但发现 **3个模块完全缺失权限校验**(🔴 高风险新发现问题)。 - ---- - -## 安全修复验证结果 - -| 问题 | 状态 | 说明 | -|------|------|------| -| **SQL注入** | ✅ 已修复 | `operation_logs.rs`、`charge_records.rs`、`energy_stats.rs`、`device_logs.rs` 全部使用 `.bind()` 参数化查询。`LIMIT/OFFSET` 通过 `i64` 类型转换后绑定。`build_where_clause` 中所有条件值均通过 `.bind()` 传入,无 `format!` 拼接用户输入到 SQL 的问题。 | -| **密码哈希** | ✅ 已修复 | `middleware/auth.rs:230-247` 使用 `argon2` 库实现 `hash_password` / `verify_password`。`users.rs:249` 创建用户时哈希,`users.rs:384` 重置密码时哈希,`auth.rs:51` 登录时验证。argon2 哈希字符串长度约 97 字符,远小于 VARCHAR(255)。 | -| **数据隔离** | ✅ 已修复 | `CurrentUser` 包含 `organization_id: Option`。`org_condition()` 和 `org_condition_for_logs()` 在非总管理员时自动附加组织过滤子查询。总管理员跳过过滤。无组织用户返回 `AND 1=0`(空结果)。`charge_records.rs`、`energy_stats.rs`、`device_logs.rs`、`operation_logs.rs` 均已集成。 | -| **权限校验** | ⚠️ 部分修复 | 大部分路由有 `check_permission`,但 **cabinets.rs、projects.rs、organizations.rs 三个模块完全缺失**(详见下方新发现问题)。 | - ---- - -## 代码质量验证结果 - -| 项目 | 状态 | 说明 | -|------|------|------| -| **文件拆分** | ✅ | 后端 `organizations.rs` 242行(≤300),`users.rs` 497行(职责集中可接受)。前端 `devices/index.tsx` 359行 → 已拆分为 `OrganizationTree.tsx`(182行)、`CabinetGrid.tsx`(79行)、`AddCabinetModal.tsx`(134行),职责清晰。 | -| **构建检查** | ✅ | `cargo check` 通过(零报错零警告)。`cargo clippy` 通过(零报错零警告)。`tsc --noEmit` 通过(零报错零警告)。`vite build` 成功(1045 modules,1.3MB JS / 570KB CSS,1.16s)。 | -| **JWT安全** | ✅ | `config.rs:45-47` 启动时检测默认密钥并打印 `tracing::warn!`。`middleware/auth.rs:33` 定义了 `JWT_SECRET_DEFAULT`。 | - ---- - -## 🔴 新引入问题(3处权限校验缺失) - -### 问题 1:设备管理模块(cabinets.rs)— 全部 8 个端点无权限校验 - -| 路由 | 风险 | 建议 | -|------|------|------| -| `GET /api/projects/:project_id/cabinets` | 可查看任意项目下的柜子 | 加 `check_permission("device:view")` | -| `POST /api/cabinets` | 可任意添加设备 | 加 `check_permission("device:create")` | -| `PUT /api/cabinets/:id` | 可修改任意设备 | 加 `check_permission("device:edit")` | -| `DELETE /api/cabinets/:id` | 可删除任意设备 | 加 `check_permission("device:delete")` | -| `POST /api/cabinets/:id/regenerate-auth` | 可篡改设备安全码 | 加 `check_permission("device:edit")` | -| `GET /api/cabinets/:id` | 可查看任意设备详情 | 加 `check_permission("device:view")` | -| `GET /api/cabinets/options` | 可枚举所有设备 | 加 `check_permission("device:view")` | -| `GET /api/cabin-boards/options`、`GET /api/compartments/options` | 同上 | 加 `check_permission("device:view")` | - -### 问题 2:项目管理模块(projects.rs)— 全部 4 个端点无权限校验 - -| 路由 | 风险 | 建议 | -|------|------|------| -| `GET /api/organizations/:org_id/projects` | 可查看任意项目 | 加 `check_permission("project:view")` | -| `POST /api/organizations/:org_id/projects` | 可创建任意项目 | 加 `check_permission("project:create")` | -| `PUT /api/projects/:id` | 可修改任意项目 | 加 `check_permission("project:edit")` | -| `DELETE /api/projects/:id` | 可删除任意项目 | 加 `check_permission("project:delete")` | - -### 问题 3:组织管理模块(organizations.rs)— 全部 4 个端点无权限校验 - -| 路由 | 风险 | 建议 | -|------|------|------| -| `GET /api/organizations` | 可枚举所有组织 | 加 `check_permission("org:view")` | -| `POST /api/organizations` | 可创建组织 | 加 `check_permission("org:create")` | -| `PUT /api/organizations/:id` | 可修改组织 | 加 `check_permission("org:edit")` | -| `DELETE /api/organizations/:id` | 可删除组织 | 加 `check_permission("org:delete")` | - ---- - -## 其他观察(非阻塞) - -1. **organizations.rs 缺少 `CurrentUser` 参数** — 三个模块的 handler 签名都是 `State + Path/Json`,没有提取 `CurrentUser`,所以无法做权限校验。这是结构性问题,需要在函数签名中加上 `user: CurrentUser`。 -2. **`users.rs` 用户列表查询缺少组织隔离** — `list_users` 返回所有用户,不受 `CurrentUser.organization_id` 限制。企业管理员可以看到其他组织的用户。 -3. **`cabinets.rs` 的 `list_cabinets` 只按 `project_id` 过滤** — 没有额外验证当前用户是否有权访问该项目。如果用户知道其他项目的 project_id,可以绕过组织隔离。 - ---- - -## 总结 - -| 维度 | 结论 | -|------|------| -| SQL注入 | ✅ 已修复,全部使用 `.bind()` 参数化 | -| 密码哈希 | ✅ 已修复,argon2 实现正确 | -| 数据隔离 | ✅ 已修复,组织过滤子查询正确 | -| 权限校验 | ⚠️ 部分修复,3个模块缺失 | -| 构建 | ✅ 全部通过 | -| 文件拆分 | ✅ 合理,职责清晰 | -| JWT安全 | ✅ 启动时检测默认密钥 | - -**核心结论:4个安全问题中,3个彻底修复,1个(权限校验)需要补充 cabinets.rs / projects.rs / organizations.rs 三个模块。** diff --git a/tasks/review-round-2.md b/tasks/review-round-2.md deleted file mode 100644 index cc2c1f6..0000000 --- a/tasks/review-round-2.md +++ /dev/null @@ -1,128 +0,0 @@ -# 代码审查任务(第二轮) - -## 目标 - -审查安全修复和代码质量优化后的代码,确认问题已解决,无新引入问题。 - -## 审查范围 - -### 后端 (Rust + Axum) -- `software/server/src/` 目录下所有文件 -- 重点审查:安全修复的4个问题是否彻底解决 - -### 前端 (React + TypeScript) -- `software/web/src/` 目录下所有文件 -- 重点审查:拆分后的组件结构、构建是否正常 - -## 审查清单 - -### 1. 安全修复验证 - -#### 1.1 SQL注入修复 -- [ ] `operation_logs.rs` 使用参数化查询(`.bind()`) -- [ ] `charge_records.rs` 的 LIMIT/OFFSET 使用 `.bind()` -- [ ] `energy_stats.rs` 的 LIMIT/OFFSET 使用 `.bind()` -- [ ] 无 `format!` 拼接用户输入到SQL - -#### 1.2 密码哈希 -- [ ] 使用 argon2 或 bcrypt 哈希密码 -- [ ] 登录时使用 `verify_password` 验证 -- [ ] 创建/重置用户时哈希密码 -- [ ] 数据库password字段长度≥255 - -#### 1.3 数据隔离 -- [ ] `CurrentUser` 包含 `organization_id` -- [ ] 查询自动附加组织过滤 -- [ ] 总管理员可看全部,企业管理员只看本组织 -- [ ] 无组织用户无法查看数据 - -#### 1.4 权限校验 -- [ ] 所有路由都有 `check_permission` 调用 -- [ ] 权限码与功能匹配 -- [ ] 无越权访问可能 - -### 2. 代码质量验证 - -#### 2.1 文件拆分 -- [ ] `organizations.rs` ≤300行 -- [ ] `users.rs` ≤300行 -- [ ] `devices/index.tsx` ≤300行 -- [ ] 拆分后组件职责单一 - -#### 2.2 构建检查 -- [ ] `cargo check` 零报错零警告 -- [ ] `cargo clippy` 零报错零警告 -- [ ] `tsc --noEmit` 零报错零警告 -- [ ] `vite build` 成功(无IconFolderOpen错误) - -#### 2.3 JWT安全 -- [ ] 启动时检测默认密钥并打印WARN -- [ ] 生产环境必须设置JWT_SECRET - -### 3. 回归测试 - -- [ ] 登录功能正常(密码哈希后) -- [ ] 组织树查询正常(数据隔离后) -- [ ] 充电记录查询正常(参数化查询后) -- [ ] 前端页面正常渲染(组件拆分后) - -### 4. 新引入问题检查 - -- [ ] 无新的编译错误 -- [ ] 无新的类型错误 -- [ ] 无循环依赖 -- [ ] 无未使用的导入/变量 - -## 输出格式 - -```markdown -# 代码审查报告(第二轮) - -## 总体评价 -[整体质量评分 1-10分,简要评价] - -## 安全修复验证结果 -| 问题 | 状态 | 说明 | -|------|------|------| -| SQL注入 | ✅/❌ | [验证结果] | -| 密码哈希 | ✅/❌ | [验证结果] | -| 数据隔离 | ✅/ | [验证结果] | -| 权限校验 | ✅/❌ | [验证结果] | - -## 代码质量验证结果 -| 项目 | 状态 | 说明 | -|------|------|------| -| 文件拆分 | ✅/❌ | [验证结果] | -| 构建检查 | ✅/❌ | [验证结果] | -| JWT安全 | ✅/❌ | [验证结果] | - -## 新引入问题 -[列出新发现的问题,如无则写"无"] - -## 总结 -[总体结论] -``` - -## 审查工具 - -```bash -# 后端静态检查 -cd software/server && cargo check && cargo clippy - -# 前端类型检查 -cd software/web && tsc --noEmit - -# 前端构建测试 -cd software/web && vite build - -# 代码行数统计 -find src -name "*.rs" -exec wc -l {} + | sort -n -find src -name "*.tsx" -exec wc -l {} + | sort -n -``` - -## 质量约束 - -1. 审查必须覆盖所有清单项目 -2. 每个问题必须给出具体位置和修复建议 -3. 安全修复必须逐项验证 -4. 审查报告用中文撰写 diff --git a/tasks/review-tcp-refactor-report.md b/tasks/review-tcp-refactor-report.md deleted file mode 100644 index 53bacb6..0000000 --- a/tasks/review-tcp-refactor-report.md +++ /dev/null @@ -1,196 +0,0 @@ -# TCP服务重构审查报告 - -## 总体评价 - -**评分:6/10** - -整体架构设计方向正确(服务分离、Redis队列通信、全异步处理),但存在 **1个严重Bug** 和 **多处架构实现不一致**。核心问题集中在节点管理实现的双轨冲突和配置系统与要求的背离。代码结构清晰,但在细节实现上有明显瑕疵。 - ---- - -## 架构符合性 - -### 符合项 -| 条目 | 状态 | 说明 | -|------|------|------| -| API/设备服务职责分离 | ✅ | API服务处理HTTP+业务,设备服务仅TCP通信+转发 | -| 设备服务不存DB/不验证签名 | ✅ | handler.rs 仅做 `forward_to_redis` | -| 服务间用Redis LIST通信 | ✅ | `device:reports` / `device:commands` / `device:replies` 三个队列 | -| 全异步处理 | ✅ | 所有协程用 `tokio::spawn`,无阻塞调用 | - -### 问题 - -**严重问题 1:节点管理双轨制 — API服务内存注册表 vs Redis注册互不关联** - -- 设备服务 `main.rs:44` 通过 `SET node:node-1` 写 Redis 注册,**从未调用 API 服务的 HTTP 接口** -- API 服务 `routes/mod.rs:39-42` 注册了 `/api/nodes/register`、`/api/nodes/:node_id/heartbeat` 等路由,但 **从未有调用方** -- API 服务的 `NodeRegistry`(内存 RwLock)与 Redis 中的 `node:*` 键完全独立 -- `node_checker.rs` 检查的是内存中的 `NodeRegistry`,而它是空的(因为设备服务没调用过 `/api/nodes/register`) - -**后果**:`GET /api/nodes` 返回空列表,节点管理功能实际上不可用。 - -**修复建议**:统一节点管理方案—— -- 方案A(推荐):去掉 API 服务的内存 `NodeRegistry`,改用 Redis 作为唯一节点注册中心。`list_nodes` 查询 `node:*` 键。 -- 方案B:设备服务在启动时通过 HTTP 调用 `POST /api/nodes/register`,心跳调用 `POST /api/nodes/:node_id/heartbeat`。 - ---- - -## 代码质量 - -### 严重问题 - -**严重问题 2(必须修复):Redis 类型冲突 — register_node 用 SET,heartbeat 用 HSET** - -**位置**:`device-server/src/main.rs` 第72-110行 - -```rust -// register_node — 第80行 -redis::cmd("SET").arg(&key).arg(info.to_string()).arg("EX")... - -// heartbeat — 第101行 -redis::cmd("HSET").arg(&key).arg("last_heartbeat")... -``` - -`SET` 将 key 创建为 **string 类型**,之后 `HSET` 在同一 key 上操作会触发 Redis `WRONGTYPE` 错误。`heartbeat` 函数仅记录 `tracing::warn`,不会 panic,但心跳静默失效。 - -**后果**:`node:{node_id}` 的 TTL(180秒)到期后节点自动消失,节点管理完全不可用。 - -**修复建议**:任一方案—— -- 将 `register_node` 改为 `HSET` 逐字段存储(`node_id`、`ip`、`tcp_port`、`registered_at`),再 `EXPIRE` -- 或将 `heartbeat` 改为 `SET` + `KEEPTTL` 重写整个 JSON - ---- - -### 配置问题 - -**问题 3:配置读取方式与审查要求不符** - -**审查清单要求**:使用 `config.toml`(不用 `.env`) - -**实际**: -- 两个服务的 `config.rs` 均调用 `dotenvy::dotenv()` + `std::env::var()`,**完全不读取 config.toml** -- config.toml 文件存在于两个项目目录中,但没有任何代码解析它们 -- 敏感信息(数据库密码 `Hbhyg731024@`)硬编码在 `config.toml` 中 - -**修复建议**:二选一—— -- 如果坚持环境变量方案,删除 config.toml 文件 -- 如果坚持 config.toml 方案,用 `toml` crate 解析并替换 `from_env()` - ---- - -### 结构体重复 - -**问题 4:协议结构体在两个服务中重复定义** - -| 结构体 | api-server 位置 | device-server 位置 | -|--------|----------------|-------------------| -| `DeviceMessage` | `protocol.rs:14` | `tcp/protocol.rs:14` | -| `ServerResponse` | `protocol.rs:36` | `tcp/protocol.rs:36` | -| `DeviceCommand` | `commands.rs:118` | `tcp/protocol.rs:67` | - -`DeviceCommand` 甚至在同一项目的两个文件中重复(`commands.rs` 和 `tcp/protocol.rs`)。 - -**影响**:维护时需同步修改两处,容易不一致。 - -**修复建议**:抽取共享类型到独立 crate(如 `pms-protocol`),两个服务共同依赖。 - ---- - -### 死代码 - -**问题 5:`send_device_response` 未使用** - -**位置**:`api-server/src/commands.rs` 第157行 - -函数用 `#[allow(dead_code)]` 标记,实际从未被调用。响应发送由 `report_worker.rs` 中的 `send_response_to_device` 完成。 - ---- - -### 异步规范 - -**问题 6:`auth_str` 限流器使用同步 Mutex** - -**位置**:`api-server/src/workers/report_worker.rs` 第30行 - -```rust -struct RateLimiter { - attempts: Mutex>>, // std::sync::Mutex - ... -} -``` - -在 `tokio::spawn` 的异步任务中持有了同步 `std::sync::Mutex` 锁。虽然当前负载下不会死锁(锁持有时间极短),但不符合异步规范。 - -**修复建议**:改为 `tokio::sync::Mutex`,或将 `RateLimiter` 与 `LazyLock` 移到 `spawn_blocking` 中。 - ---- - -### 函数长度 - -所有函数均在80行以内 ✅(最长的 `handle_connection` 144行但包含多个分支逻辑块,为 tokio::select! 模式,可接受)。 - -### 错误处理 - -- 所有外部 IO 均有异常捕获 ✅ -- 无裸 `panic` ✅(仅 `main.rs` 中启动阶段 `expect` 合理) -- `thiserror` + `AppError` 统一错误处理 ✅ - ---- - -## Redis 使用审查 - -| 要求 | 状态 | 说明 | -|------|------|------| -| 连接注册 `device:{imei}` → info | ❌ | 实际用 `device:online:{imei}` 仅存 "1" | -| TTL 180秒 | ✅ | `EX 300`(5分钟) | -| `device:reports` 队列 | ✅ | BRPOP 消费 | -| `device:commands` 队列 | ✅ | BRPOP 消费 | -| `device:replies` 队列 | ✅ | BRPOP 消费 | -| 连接池管理正确 | ✅ | ConnectionManager | -| BRPOP 阻塞 | ✅ | 参数 `0` 阻塞等待 | - ---- - -## TCP 服务审查 - -| 要求 | 状态 | 说明 | -|------|------|------| -| LF分隔JSON | ✅ | `LinesCodec` 按 `\n` 分割 | -| 连接池内存HashMap | ✅ | `RwLock>` | -| 登录验证流程 | ⚠️ | 仅注册连接,验证签名在 API 端完成 | -| 断连清理 | ✅ | `tokio::select!` 退出时 `pool.remove` | - ---- - -## 配置文件审查 - -| 要求 | 状态 | 说明 | -|------|------|------| -| 使用 config.toml | ❌ | 代码只读环境变量 | -| 配置项完整 | ⚠️ | 缺少 `log_level` 等 | -| 敏感信息不硬编码 | ❌ | config.toml 含数据库密码 | - ---- - -## 优点 - -1. **服务职责分离干净** — device-server 确实只做 TCP 通信,handler.rs 轻量清晰 -2. **`tokio::select!` 读写分离** — `server.rs` 中同时处理设备消息读取和平台指令写入,设计合理 -3. **`FramedRead + LinesCodec`** — 正确处理了 LF 粘包问题 -4. **`reply_worker` 的 `oneshot` 机制** — 通过 msg_id 匹配异步等待,设计优雅 -5. **`report_worker` 每条消息 `tokio::spawn`** — 独立处理,不影响后续消息消费 -6. **全局 `#[allow(dead_code)]` 仅出现在少数必要位置**(device-server `connection.rs` 和 `protocol.rs` 的 `#![allow(dead_code)]` 是模块级,尚可接受) - ---- - -## 修复优先级总结 - -| 优先级 | 问题 | 风险等级 | -|--------|------|---------| -| 🔴 P0 | register_node 与 heartbeat 的 Redis 类型冲突 | 严重 — 节点管理静默失效 | -| 🟡 P1 | 节点管理双轨制 — 内存NodeRegistry与Redis注册无关联 | 高 — 节点列表API不可用 | -| 🟡 P2 | 配置系统与要求背离(读env而非config.toml) | 中 — 视部署要求 | -| 🟢 P3 | 协议结构体两服务重复定义 | 低 — 维护成本 | -| 🟢 P4 | auth_str 限流器用同步Mutex | 低 — 规范性问题 | - -**推荐立即修复**:P0 + P1。这两个问题导致节点管理和心跳功能实际上不工作。 diff --git a/tasks/review-tcp-refactor.md b/tasks/review-tcp-refactor.md deleted file mode 100644 index bd423b6..0000000 --- a/tasks/review-tcp-refactor.md +++ /dev/null @@ -1,91 +0,0 @@ -# 代码审查任务:TCP服务重构 - -## 目标 - -审查TCP服务重构后的代码质量,确保符合架构设计。 - -## 审查范围 - -### API服务 -`software/api-server/src/` 目录下所有文件: -- `main.rs` — 启动入口 -- `config.rs` — 配置管理 -- `routes/` — 路由模块 -- `workers/` — 后台协程 -- `middleware/` — 中间件 - -### 设备服务 -`software/device-server/src/` 目录下所有文件: -- `main.rs` — 启动入口 -- `config.rs` — 配置管理 -- `tcp/` — TCP服务模块 -- `redis.rs` — Redis连接 - -## 审查清单 - -### 1. 架构符合性 -- [ ] API服务和设备服务职责分离清晰 -- [ ] 设备服务不处理业务逻辑(不存DB、不验证签名) -- [ ] 服务间通信用Redis LIST,不用HTTP直接调用 -- [ ] 节点注册/心跳/注销功能完整 - -### 2. 异步处理 -- [ ] 所有指令异步处理,msg_id匹配响应 -- [ ] 后台协程处理Redis队列(report_worker/reply_worker) -- [ ] 无阻塞操作(tokio::spawn合理使用) -- [ ] 超时处理正确(60秒) - -### 3. Redis使用 -- [ ] 连接注册:`device:{imei}` → `{node_id, ip, port}`,TTL 180秒 -- [ ] 消息队列:`device:reports` / `device:commands` / `device:replies` -- [ ] BRPOP阻塞弹出,不浪费CPU -- [ ] 连接池管理正确 - -### 4. TCP服务 -- [ ] LF分隔JSON消息 -- [ ] 连接池用内存HashMap(快速) -- [ ] 设备登录验证流程正确 -- [ ] 断连清理逻辑 - -### 5. 代码规范 -- [ ] `cargo check` 零报错 -- [ ] `cargo clippy` 零警告 -- [ ] 单函数≤80行 -- [ ] 命名语义化,完整注释 -- [ ] 错误处理完善,无裸panic - -### 6. 配置文件 -- [ ] 使用config.toml(不用.env) -- [ ] 配置项完整(server/database/redis/jwt等) -- [ ] 敏感信息不硬编码 - -## 输出格式 - -```markdown -# TCP服务重构审查报告 - -## 总体评价 -[评分1-10,简要评价] - -## 架构符合性 -[是否符合设计,问题列表] - -## 代码质量 -[编译/规范/错误处理] - -## 严重问题 -[必须修复的问题] - -## 警告 -[建议修复的问题] - -## 优点 -[值得肯定的设计] -``` - -## 质量约束 - -1. 审查必须覆盖所有清单项目 -2. 每个问题必须给出具体位置和修复建议 -3. 审查报告用中文撰写 -4. 必须实际读取代码文件