2026-07-02 05:38:01 +08:00
|
|
|
//! 操作日志 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>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-20 00:01:01 +08:00
|
|
|
/// 操作日志数据库行映射
|
|
|
|
|
#[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,
|
|
|
|
|
}
|
2026-07-02 05:38:01 +08:00
|
|
|
|
2026-09-20 00:01:01 +08:00
|
|
|
/// 构建 WHERE 子句与绑定值
|
|
|
|
|
///
|
|
|
|
|
/// 动态拼装过滤条件(使用 ? 占位符防止 SQL 注入),并追加组织数据隔离。
|
|
|
|
|
fn build_log_filters(user: &CurrentUser, query: &LogQuery) -> (String, Vec<String>) {
|
2026-07-02 05:38:01 +08:00
|
|
|
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());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 追加组织过滤
|
2026-09-20 00:01:01 +08:00
|
|
|
let (org_cond, org_binds) = auth::org_condition_for_logs(user, "o");
|
2026-07-02 05:38:01 +08:00
|
|
|
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 "))
|
|
|
|
|
};
|
2026-09-20 00:01:01 +08:00
|
|
|
(where_clause, bind_values)
|
|
|
|
|
}
|
2026-07-02 05:38:01 +08:00
|
|
|
|
2026-09-20 00:01:01 +08:00
|
|
|
/// 统计符合条件的操作日志总数
|
|
|
|
|
async fn count_operation_logs(
|
|
|
|
|
pool: &sqlx::MySqlPool,
|
|
|
|
|
where_clause: &str,
|
|
|
|
|
bind_values: &[String],
|
|
|
|
|
) -> Result<i64, AppError> {
|
2026-07-02 05:38:01 +08:00
|
|
|
let count_sql = format!("SELECT COUNT(*) FROM operation_logs o {}", where_clause);
|
|
|
|
|
let mut count_query = sqlx::query_scalar::<_, i64>(&count_sql);
|
2026-09-20 00:01:01 +08:00
|
|
|
for val in bind_values {
|
2026-07-02 05:38:01 +08:00
|
|
|
count_query = count_query.bind(val);
|
|
|
|
|
}
|
2026-09-20 00:01:01 +08:00
|
|
|
Ok(count_query.fetch_one(pool).await?)
|
|
|
|
|
}
|
2026-07-02 05:38:01 +08:00
|
|
|
|
2026-09-20 00:01:01 +08:00
|
|
|
/// 查询操作日志行(关联用户姓名,分页)
|
|
|
|
|
async fn fetch_operation_log_rows(
|
|
|
|
|
pool: &sqlx::MySqlPool,
|
|
|
|
|
where_clause: &str,
|
|
|
|
|
bind_values: &[String],
|
|
|
|
|
page_size: u32,
|
|
|
|
|
offset: u32,
|
|
|
|
|
) -> Result<Vec<LogRow>, AppError> {
|
2026-07-02 05:38:01 +08:00
|
|
|
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);
|
2026-09-20 00:01:01 +08:00
|
|
|
for val in bind_values {
|
2026-07-02 05:38:01 +08:00
|
|
|
list_query = list_query.bind(val);
|
|
|
|
|
}
|
2026-09-20 00:01:01 +08:00
|
|
|
Ok(list_query
|
2026-07-02 05:38:01 +08:00
|
|
|
.bind(page_size)
|
|
|
|
|
.bind(offset)
|
|
|
|
|
.fetch_all(pool)
|
2026-09-20 00:01:01 +08:00
|
|
|
.await?)
|
|
|
|
|
}
|
2026-07-02 05:38:01 +08:00
|
|
|
|
2026-09-20 00:01:01 +08:00
|
|
|
/// 将操作日志行映射为响应 JSON 列表
|
|
|
|
|
fn operation_logs_to_json(rows: Vec<LogRow>) -> Vec<Value> {
|
|
|
|
|
rows.into_iter()
|
2026-07-02 05:38:01 +08:00
|
|
|
.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,
|
2026-09-19 23:40:46 +08:00
|
|
|
"created_at": r.created_at.format("%Y-%m-%d %H:%M:%S").to_string(),
|
2026-07-02 05:38:01 +08:00
|
|
|
})
|
|
|
|
|
})
|
2026-09-20 00:01:01 +08:00
|
|
|
.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);
|
2026-07-02 05:38:01 +08:00
|
|
|
|
|
|
|
|
Ok(Json(json!({
|
|
|
|
|
"code": 0,
|
|
|
|
|
"data": { "list": list, "total": total },
|
|
|
|
|
"message": "ok"
|
|
|
|
|
})))
|
|
|
|
|
}
|