145 lines
4.2 KiB
Rust
Raw Normal View History

//! 操作日志 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>,
}
/// 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 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 "))
};
// 查询总数
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<String>,
action: String,
target_type: Option<String>,
target_id: Option<i64>,
detail: Option<String>,
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<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,
})
})
.collect();
Ok(Json(json!({
"code": 0,
"data": { "list": list, "total": total },
"message": "ok"
})))
}