285 lines
7.7 KiB
Markdown
285 lines
7.7 KiB
Markdown
|
|
# 任务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接口必须添加权限校验
|