12451 8bc7812733 refactor(api-server): 23 个超 80 行函数拆分为 ≤80 行(纯重构,行为不变)
拆分范围(14 个文件):migrate / users / operation_logs / energy_stats / charge_records /
batteries / device_logs / auth / captcha / main / routes::mod / h5 / cabinet_tree / report_worker

手法:长 SQL 抽模块级 const、拼装与映射与校验抽私有 fn、路由按业务域拆 Router 再 merge、
Excel 导出按"取数/表头/逐行/收尾"分阶段、连续代码块原样搬移+传参。

行为不变的证据:
- 路由 79 → 79 条逐条比对一致(路径+handler+方法链;中间件/CORS/with_state 作用域保持)
- 字符串字面量多重集比对:11 个文件唯一集合完全一致;report_worker 零差异(435 条字面量全等)
- 关键契约计数不变:CAST 14→14、AS SIGNED 4→4、COALESCE 6→6;SQL 文本与 Redis key 零增删
- 未新增 #[allow] / unwrap / expect / panic;captcha 的 RNG 消耗顺序保持(验证码分布不变)

验收:cargo check 0 error / 0 warning;cargo test --bins 39 passed / 0 failed;全仓已无 >80 行函数。
端到端回归:先部署测试环境跑 TCP 协议联调 13/13,且数据库终态与重构前逐字段一致
(orders.energy=22、start_soc=850、end_soc=1000、records.energy=22.00、max_temp=29800、占用状态正确复位)。
2026-09-20 00:01:01 +08:00

178 lines
5.3 KiB
Rust

//! 操作日志 API
//!
//! 提供操作日志查询接口,支持按用户、操作类型、时间范围过滤和分页:
//! - GET /api/operation-logs — 操作日志列表
use axum::extract::{Query, State};
use axum::Json;
use serde::Deserialize;
use serde_json::{json, Value};
use crate::error::AppError;
use crate::middleware::auth::{self, CurrentUser};
use crate::commands::AppState;
/// 操作日志查询参数
#[derive(Debug, Deserialize)]
pub(crate) struct LogQuery {
#[allow(dead_code)]
user_id: Option<i64>,
action: Option<String>,
start_time: Option<String>,
end_time: Option<String>,
page: Option<u32>,
page_size: Option<u32>,
}
/// 操作日志数据库行映射
#[derive(sqlx::FromRow)]
struct LogRow {
id: i64,
user_id: i64,
user_name: Option<String>,
action: String,
target_type: Option<String>,
target_id: Option<i64>,
detail: Option<String>,
created_at: chrono::NaiveDateTime,
}
/// 构建 WHERE 子句与绑定值
///
/// 动态拼装过滤条件(使用 ? 占位符防止 SQL 注入),并追加组织数据隔离。
fn build_log_filters(user: &CurrentUser, query: &LogQuery) -> (String, Vec<String>) {
let mut conditions: Vec<String> = Vec::new();
let mut bind_values: Vec<String> = Vec::new();
if let Some(uid) = query.user_id {
conditions.push("o.user_id = ?".to_string());
bind_values.push(uid.to_string());
}
if let Some(ref act) = query.action
&& !act.is_empty()
{
conditions.push("o.action = ?".to_string());
bind_values.push(act.clone());
}
if let Some(ref t) = query.start_time
&& !t.is_empty()
{
conditions.push("o.created_at >= ?".to_string());
bind_values.push(t.clone());
}
if let Some(ref t) = query.end_time
&& !t.is_empty()
{
conditions.push("o.created_at <= ?".to_string());
bind_values.push(t.clone());
}
// 追加组织过滤
let (org_cond, org_binds) = auth::org_condition_for_logs(user, "o");
if !org_cond.is_empty() {
conditions.push(org_cond.trim_start_matches(" AND ").to_string());
bind_values.extend(org_binds);
}
let where_clause = if conditions.is_empty() {
String::new()
} else {
format!("WHERE {}", conditions.join(" AND "))
};
(where_clause, bind_values)
}
/// 统计符合条件的操作日志总数
async fn count_operation_logs(
pool: &sqlx::MySqlPool,
where_clause: &str,
bind_values: &[String],
) -> Result<i64, AppError> {
let count_sql = format!("SELECT COUNT(*) FROM operation_logs o {}", where_clause);
let mut count_query = sqlx::query_scalar::<_, i64>(&count_sql);
for val in bind_values {
count_query = count_query.bind(val);
}
Ok(count_query.fetch_one(pool).await?)
}
/// 查询操作日志行(关联用户姓名,分页)
async fn fetch_operation_log_rows(
pool: &sqlx::MySqlPool,
where_clause: &str,
bind_values: &[String],
page_size: u32,
offset: u32,
) -> Result<Vec<LogRow>, AppError> {
let list_sql = format!(
r#"SELECT o.id, o.user_id, u.name AS user_name, o.action, o.target_type, o.target_id, o.detail, o.created_at
FROM operation_logs o
LEFT JOIN users u ON u.id = o.user_id
{}
ORDER BY o.id DESC
LIMIT ? OFFSET ?"#,
where_clause
);
let mut list_query = sqlx::query_as::<_, LogRow>(&list_sql);
for val in bind_values {
list_query = list_query.bind(val);
}
Ok(list_query
.bind(page_size)
.bind(offset)
.fetch_all(pool)
.await?)
}
/// 将操作日志行映射为响应 JSON 列表
fn operation_logs_to_json(rows: Vec<LogRow>) -> Vec<Value> {
rows.into_iter()
.map(|r| {
json!({
"id": r.id,
"user_id": r.user_id,
"user_name": r.user_name.unwrap_or_else(|| format!("#{}", r.user_id)),
"action": r.action,
"target_type": r.target_type,
"target_id": r.target_id,
"detail": r.detail,
"created_at": r.created_at.format("%Y-%m-%d %H:%M:%S").to_string(),
})
})
.collect()
}
/// GET /api/operation-logs — 操作日志列表(分页)
///
/// 支持 user_id / action / start_time / end_time 过滤。
pub async fn list_operation_logs(
user: CurrentUser,
State(state): State<AppState>,
Query(query): Query<LogQuery>,
) -> Result<Json<Value>, AppError> {
auth::check_permission(&user, "operation_log:view")?;
let pool = &state.mysql;
let page = query.page.unwrap_or(1).max(1);
let page_size = query.page_size.unwrap_or(20).min(100);
let offset = (page - 1) * page_size;
// 动态构建 WHERE 条件(使用 ? 占位符防止 SQL 注入)
let (where_clause, bind_values) = build_log_filters(&user, &query);
// 查询总数
let total = count_operation_logs(pool, &where_clause, &bind_values).await?;
// 查询列表(关联用户姓名)
let rows =
fetch_operation_log_rows(pool, &where_clause, &bind_values, page_size, offset).await?;
let list = operation_logs_to_json(rows);
Ok(Json(json!({
"code": 0,
"data": { "list": list, "total": total },
"message": "ok"
})))
}