chore: 任务文件不同步
This commit is contained in:
parent
12395ee9e2
commit
3cc21b621f
3
.gitignore
vendored
3
.gitignore
vendored
@ -20,3 +20,6 @@ Thumbs.db
|
||||
# Temporary
|
||||
*.tmp
|
||||
*.log
|
||||
|
||||
# Tasks
|
||||
tasks/
|
||||
|
||||
@ -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无合理理由
|
||||
@ -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组件
|
||||
@ -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无合理理由
|
||||
@ -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
|
||||
@ -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
|
||||
// 按钮级权限控制
|
||||
<Permission code="device:operate">
|
||||
<Button>开始充电</Button>
|
||||
</Permission>
|
||||
|
||||
// 页面级权限控制
|
||||
<RequirePermission code="energy:view">
|
||||
<EnergyPage />
|
||||
</RequirePermission>
|
||||
```
|
||||
|
||||
### 角色管理页
|
||||
- 角色列表
|
||||
- 创建/编辑角色
|
||||
- 权限树勾选(按模块分组)
|
||||
|
||||
### 用户权限配置
|
||||
- 选择角色
|
||||
- 配置权限范围(组织/项目/设备)
|
||||
- 选择权限类型(查看/操作)
|
||||
|
||||
## 权限码列表
|
||||
|
||||
```
|
||||
# 设备管理
|
||||
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接口
|
||||
@ -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. 导出任务异步执行,不阻塞请求
|
||||
@ -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
|
||||
@ -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. 移动端适配,触摸友好
|
||||
@ -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
|
||||
@ -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<String> {
|
||||
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<bool> {
|
||||
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<i64>, // 新增
|
||||
pub permissions: HashSet<String>,
|
||||
}
|
||||
|
||||
// 查询时根据角色自动过滤
|
||||
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<MySqlPool>,
|
||||
query: Query<ChargeRecordQuery>,
|
||||
) -> Result<Json<Vec<ChargeRecord>>> {
|
||||
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. 修复后重新运行审查清单中的安全项目,确保全部通过
|
||||
@ -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. 拆分后的文件职责单一,无循环依赖
|
||||
@ -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<MySqlPool>,
|
||||
) -> Result<Json<Vec<Organization>>> {
|
||||
auth::check_permission(&user, "org:view")?; // 新增
|
||||
// ... 原有逻辑
|
||||
}
|
||||
|
||||
// 示例:创建组织
|
||||
#[post("/api/organizations")]
|
||||
async fn create_organization(
|
||||
user: CurrentUser, // 新增
|
||||
pool: State<MySqlPool>,
|
||||
json: Json<CreateOrganizationRequest>,
|
||||
) -> Result<Json<Organization>> {
|
||||
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`(如有测试)确保无回归
|
||||
@ -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<AppState>,
|
||||
Path(dev_id): Path<String>,
|
||||
Json(body): Json<CommandRequest>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
auth::check_permission(&user, "device:operate")?; // 新增
|
||||
// ... 原有逻辑
|
||||
}
|
||||
|
||||
// 修改 get_online_devices handler
|
||||
pub async fn get_online_devices(
|
||||
user: CurrentUser, // 新增
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<Value>, 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<AppState>,
|
||||
) -> Result<Json<Value>, 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. 修复后重新运行审查清单中的安全项目,确保全部通过
|
||||
@ -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<String>,
|
||||
role: i32,
|
||||
organization_id: Option<i64>,
|
||||
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<AppState>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
// 查询柜子统计、今日充电、告警等
|
||||
// ...
|
||||
}
|
||||
|
||||
/// GET /api/h5/projects — H5项目列表
|
||||
pub async fn get_projects(
|
||||
user: CurrentUser,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
// 查询用户有权访问的项目
|
||||
// ...
|
||||
}
|
||||
|
||||
/// GET /api/h5/cabinets — H5设备列表
|
||||
pub async fn get_cabinets(
|
||||
user: CurrentUser,
|
||||
State(state): State<AppState>,
|
||||
Path(project_id): Path<i64>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
// 查询项目下的柜子
|
||||
// ...
|
||||
}
|
||||
|
||||
/// GET /api/h5/cabinets/:id — H5设备详情
|
||||
pub async fn get_cabinet_detail(
|
||||
user: CurrentUser,
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<i64>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
// 查询柜子详情(仓控板+仓体)
|
||||
// ...
|
||||
}
|
||||
|
||||
/// POST /api/h5/charge/start — H5开始充电
|
||||
pub async fn start_charge(
|
||||
user: CurrentUser,
|
||||
State(state): State<AppState>,
|
||||
Json(body): Json<StartChargeRequest>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
auth::check_permission(&user, "charge:start")?;
|
||||
// 下发充电指令到设备
|
||||
// ...
|
||||
}
|
||||
|
||||
/// POST /api/h5/charge/stop — H5停止充电
|
||||
pub async fn stop_charge(
|
||||
user: CurrentUser,
|
||||
State(state): State<AppState>,
|
||||
Json(body): Json<StopChargeRequest>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
auth::check_permission(&user, "charge:stop")?;
|
||||
// 下发停止指令到设备
|
||||
// ...
|
||||
}
|
||||
|
||||
/// POST /api/h5/door/open — H5开门
|
||||
pub async fn open_door(
|
||||
user: CurrentUser,
|
||||
State(state): State<AppState>,
|
||||
Json(body): Json<OpenDoorRequest>,
|
||||
) -> Result<Json<Value>, 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<AppState>,
|
||||
Path(id): Path<i64>,
|
||||
) -> Result<Response<Body>, AppError> {
|
||||
auth::check_permission(&user, "charge_record:export")?;
|
||||
|
||||
let db = &state.mysql;
|
||||
|
||||
// 查询下载任务
|
||||
let task: Option<DownloadTask> = 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<RwLock<HashMap<String, Vec<Instant>>>>,
|
||||
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接口必须添加权限校验
|
||||
@ -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<AppState>,
|
||||
) -> Result<Json<Value>, 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<AppState>,
|
||||
Path(id): Path<i64>,
|
||||
) -> Result<Json<Value>, 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::<Vec<_>>()
|
||||
})))
|
||||
```
|
||||
|
||||
### 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接口必须添加权限校验和组织数据隔离
|
||||
@ -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<NodeRegisterRequest>) -> Result<Json<Value>> {
|
||||
// 记录节点信息到内存/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<String>) -> Result<Json<Value>> {
|
||||
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<String>) -> Result<Json<Value>> {
|
||||
nodes.remove(&node_id);
|
||||
Ok(Json(json!({"suc": 1})))
|
||||
}
|
||||
```
|
||||
|
||||
**节点列表:**
|
||||
```rust
|
||||
// GET /api/nodes
|
||||
async fn list_nodes() -> Result<Json<Vec<NodeInfo>>> {
|
||||
Ok(Json(json!(nodes.values().collect::<Vec<_>>())))
|
||||
}
|
||||
```
|
||||
|
||||
### 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<DeviceResponse> {
|
||||
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服务不处理业务逻辑,只负责通信和解析
|
||||
@ -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<Json<Vec<NodeInfo>>> {
|
||||
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<String, String> = 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无合理理由
|
||||
@ -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<Vec<String>> {
|
||||
let mut nodes = Vec::new();
|
||||
let mut cursor = 0i64;
|
||||
loop {
|
||||
let (new_cursor, keys): (i64, Vec<String>) = 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无合理理由
|
||||
@ -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)
|
||||
@ -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<T, E>`,使用 `?` 传播错误
|
||||
- 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 个改进建议为低优先级,可在后续迭代中处理。
|
||||
@ -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. 必须实际读取代码文件
|
||||
@ -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<RwLock<>>` 共享)
|
||||
- [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<RwLock>` 支持并发。
|
||||
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 通配符转义。
|
||||
@ -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. 必须实际读取代码文件,不能仅凭文件名判断
|
||||
@ -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<RwLock<>> 模式
|
||||
- [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<String>, i32, Option<i64>, 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` 验证,确保代码可正常编译。
|
||||
@ -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<T>` 的返回类型 `ApiResponse<T>` 与实际使用不匹配。
|
||||
- **建议**:检查 `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<Value>` 与 `flatten` 冗余
|
||||
|
||||
- **位置**:`software/server/src/tcp/protocol.rs:29-31`
|
||||
- **描述**:`extra: Option<Value>` 配合 `#[serde(flatten)]`,当无额外字段时值为 `Value::Null` 而非 `None`,`Option` 包裹无实际意义。
|
||||
- **建议**:改为 `extra: Value` 或去掉 `Option`。
|
||||
|
||||
---
|
||||
|
||||
## 建议
|
||||
|
||||
### 建议1:`Protocol::to_json` 使用 `expect` 可改为 `unwrap_or_default`
|
||||
|
||||
- **位置**:`software/server/src/tcp/protocol.rs:58,80`
|
||||
- **描述**:`ServerResponse::to_json` 和 `DeviceCommand::to_json` 使用 `expect` 序列化。虽然理论上不应失败,但为安全起见可改为 `unwrap_or_default()`。
|
||||
- **建议**:改为 `serde_json::to_string(self).unwrap_or_default()`。
|
||||
|
||||
### 建议2:`Dashboard.tsx` 为空白占位页
|
||||
|
||||
- **位置**:`software/web/src/pages/Dashboard.tsx`
|
||||
- **描述**:后台管理 Dashboard 页面仅为静态文本 "欢迎使用充电柜管理系统",未展示任何统计数据。
|
||||
- **建议**:参考 H5 首页实现,展示设备在线率、今日充电量、告警数量等关键指标。
|
||||
|
||||
### 建议3:柜子详情页显示硬编码零值
|
||||
|
||||
- **位置**:`software/web/src/pages/devices/cabinet.tsx:144-147`
|
||||
- **描述**:柜子详情的仓体卡片中,电压/电流/功率显示为硬编码的 `0.0V`/`0.0A`/`0W`,未从 Redis 实时数据获取。
|
||||
- **建议**:通过后端接口获取仓体实时数据并渲染。
|
||||
|
||||
### 建议4:`energy_stats.rs` 中 `get_summary` 执行了 4 次独立查询
|
||||
|
||||
- **位置**:`software/server/src/routes/energy_stats.rs:82-162`
|
||||
- **描述**:`get_summary` 分别执行今日能耗、本月能耗、柜子总数、在线柜子数 4 次查询,可合并为 1-2 次查询减少数据库往返。
|
||||
- **建议**:使用 `CASE WHEN` 或子查询合并。
|
||||
|
||||
### 建议5:H5 `DownloadCenter` 轮询定时器清理不完整
|
||||
|
||||
- **位置**:`software/web/src/pages/charge-records/DownloadCenter.tsx:79-86`
|
||||
- **描述**:`useEffect` 依赖 `[visible, tasks, fetchTasks]`,每次 `tasks` 变化都会清除并重建 `setInterval`。当有进行中任务时,每 5 秒触发一次重建。
|
||||
- **建议**:将 `tasks` 从依赖数组中移除,改用 `useRef` 跟踪任务状态。
|
||||
|
||||
### 建议6:`h5/routes.tsx` 中 import 语句位于文件中间
|
||||
|
||||
- **位置**:`software/web/src/h5/routes.tsx:47-50`
|
||||
- **描述**:`import { useEffect, useState }` 等导入语句出现在文件中间(第 47 行),违反 ES Module 规范(虽然 TypeScript 编译器允许)。
|
||||
- **建议**:将所有 import 语句移至文件顶部。
|
||||
|
||||
### 建议7:`config.rs` 中 `validate` 仅打印警告
|
||||
|
||||
- **位置**:`software/server/src/config.rs:47-51`
|
||||
- **描述**:使用默认 JWT 密钥时仅 `tracing::warn`,生产环境可能被忽略。
|
||||
- **建议**:生产环境应直接 panic 或通过环境变量强制检查(如检查 `NODE_ENV=production` 时拒绝默认密钥)。
|
||||
|
||||
### 建议8:H5 认证恢复未校验 token 有效性
|
||||
|
||||
- **位置**:`software/web/src/h5/auth.ts:48-55`
|
||||
- **描述**:`restore` 函数从 localStorage 恢复 token 后直接设置 `isAuthenticated: true`,未向后端验证 token 是否仍然有效。后台管理的 `auth.ts` 会调用 `/auth/me` 验证,H5 端缺少此步骤。
|
||||
- **建议**:H5 恢复时也调用 `/h5/dashboard` 或类似接口验证 token 有效性。
|
||||
|
||||
---
|
||||
|
||||
## 审查清单完成情况
|
||||
|
||||
### 1. 编译与类型检查
|
||||
- [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<RwLock>` 用于连接池)
|
||||
- [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<RwLock<HashMap>>` 实现线程安全的设备连接管理
|
||||
5. **异步导出架构**:`tokio::spawn` 后台生成 Excel,下载中心轮询查看进度,用户体验良好
|
||||
6. **前端权限组件**:`<Permission>` 组件支持 hidden/disabled 两种模式,按钮级权限控制优雅
|
||||
7. **H5 移动端独立实现**:与后台管理使用独立认证、独立路由、Tailwind CSS 样式,互不干扰
|
||||
8. **数据库迁移自动化**:启动时自动建表 + 种子数据,部署简单
|
||||
9. **操作日志完整**:用户管理的增删改查均记录操作日志,便于审计
|
||||
10. **代码注释质量高**:中文注释覆盖所有模块、函数和关键逻辑,可读性好
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
项目代码质量整体良好,架构设计清晰,技术选型合理。优先修复建议如下:
|
||||
|
||||
**P0(立即修复)**:
|
||||
1. 补充缺失的权限码种子数据(`org:*`、`project:*`、`device:edit`)— 否则非总管理员无法管理组织和项目
|
||||
2. H5 接口添加数据隔离 — 否则存在数据泄露风险
|
||||
|
||||
**P1(尽快修复)**:
|
||||
3. 修复前后端 H5 告警数据结构不匹配
|
||||
4. `h5.rs` 中 `org_project_condition` 改为参数化查询
|
||||
5. 下载中心权限码检查优化
|
||||
|
||||
**P2(计划修复)**:
|
||||
6. 补充数据库索引
|
||||
7. 消除代码重复(`list_users`、`energy_stats` 组织过滤)
|
||||
8. 前端 401 自动跳转登录页
|
||||
9. H5 认证恢复时校验 token 有效性
|
||||
@ -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)
|
||||
|
||||
整体代码质量良好,核心业务逻辑正确,安全基础扎实。上述问题修复后可达到生产就绪水平。
|
||||
@ -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 个问题后,项目即可达到生产就绪状态。其余警告和建议可作为后续迭代优化项。
|
||||
@ -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<i64>`。`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 三个模块。**
|
||||
@ -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. 审查报告用中文撰写
|
||||
@ -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<HashMap<String, Vec<Instant>>>, // 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<HashMap<String, DeviceConnection>>` |
|
||||
| 登录验证流程 | ⚠️ | 仅注册连接,验证签名在 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。这两个问题导致节点管理和心跳功能实际上不工作。
|
||||
@ -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. 必须实际读取代码文件
|
||||
Loading…
x
Reference in New Issue
Block a user