//! 操作日志 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, } /// 操作日志数据库行映射 #[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: chrono::NaiveDateTime, } /// 构建 WHERE 子句与绑定值 /// /// 动态拼装过滤条件(使用 ? 占位符防止 SQL 注入),并追加组织数据隔离。 fn build_log_filters(user: &CurrentUser, query: &LogQuery) -> (String, Vec) { 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 ")) }; (where_clause, bind_values) } /// 统计符合条件的操作日志总数 async fn count_operation_logs( pool: &sqlx::MySqlPool, where_clause: &str, bind_values: &[String], ) -> Result { 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, 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) -> 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.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, 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 (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" }))) }