//! 操作日志 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, action: Option, start_time: Option, end_time: Option, page: Option, page_size: Option, } /// GET /api/operation-logs — 操作日志列表(分页) /// /// 支持 user_id / action / start_time / end_time 过滤。 pub async fn list_operation_logs( user: CurrentUser, State(state): State, Query(query): Query, ) -> Result, 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 mut conditions: Vec = Vec::new(); let mut bind_values: Vec = 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 ")) }; // 查询总数 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); } let total = count_query.fetch_one(pool).await?; // 查询列表(关联用户姓名) 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 ); #[derive(sqlx::FromRow)] struct LogRow { id: i64, user_id: i64, user_name: Option, action: String, target_type: Option, target_id: Option, detail: Option, created_at: String, } let mut list_query = sqlx::query_as::<_, LogRow>(&list_sql); for val in &bind_values { list_query = list_query.bind(val); } let rows = list_query .bind(page_size) .bind(offset) .fetch_all(pool) .await?; let list: Vec = 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, }) }) .collect(); Ok(Json(json!({ "code": 0, "data": { "list": list, "total": total }, "message": "ok" }))) }