fix(health+server): 多专家组生产就绪度分析 — DTO 校验补全 + 审计日志用户名
五维度分析结果(DevOps 4.0/10, 医疗合规 9C/6P/1NC, 前端 Lighthouse 94/100/100): 1. Article/Category/Tag DTO 补全 #[derive(Validate)] + handler .validate() 调用(6 DTO + 8 handler) 2. 审计日志 API 新增 user_name 字段(批量关联 users 表),仪表盘显示用户名而非 UUID 3. 多专家组分析报告存档 docs/discussions/
This commit is contained in:
@@ -3,13 +3,12 @@ use axum::extract::{Extension, FromRef, Query, State};
|
||||
use axum::response::Json;
|
||||
use axum::routing::get;
|
||||
use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder};
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use erp_core::entity::audit_log;
|
||||
use erp_core::error::AppError;
|
||||
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
||||
|
||||
/// 审计日志查询参数。
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AuditLogQuery {
|
||||
pub resource_type: Option<String>,
|
||||
@@ -18,15 +17,82 @@ pub struct AuditLogQuery {
|
||||
pub page_size: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct AuditLogResp {
|
||||
pub id: uuid::Uuid,
|
||||
pub tenant_id: uuid::Uuid,
|
||||
pub user_id: Option<uuid::Uuid>,
|
||||
pub user_name: Option<String>,
|
||||
pub action: String,
|
||||
pub resource_type: String,
|
||||
pub resource_id: Option<uuid::Uuid>,
|
||||
pub old_value: Option<serde_json::Value>,
|
||||
pub new_value: Option<serde_json::Value>,
|
||||
pub ip_address: Option<String>,
|
||||
pub user_agent: Option<String>,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
impl From<audit_log::Model> for AuditLogResp {
|
||||
fn from(m: audit_log::Model) -> Self {
|
||||
Self {
|
||||
id: m.id,
|
||||
tenant_id: m.tenant_id,
|
||||
user_id: m.user_id,
|
||||
user_name: None,
|
||||
action: m.action,
|
||||
resource_type: m.resource_type,
|
||||
resource_id: m.resource_id,
|
||||
old_value: m.old_value,
|
||||
new_value: m.new_value,
|
||||
ip_address: m.ip_address,
|
||||
user_agent: m.user_agent,
|
||||
created_at: m.created_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn resolve_user_names(
|
||||
db: &sea_orm::DatabaseConnection,
|
||||
items: &[audit_log::Model],
|
||||
) -> std::collections::HashMap<uuid::Uuid, String> {
|
||||
use erp_auth::entity::user;
|
||||
|
||||
let user_ids: Vec<uuid::Uuid> = items
|
||||
.iter()
|
||||
.filter_map(|i| i.user_id)
|
||||
.collect::<std::collections::HashSet<_>>()
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
if user_ids.is_empty() {
|
||||
return std::collections::HashMap::new();
|
||||
}
|
||||
|
||||
let users = user::Entity::find()
|
||||
.filter(user::Column::Id.is_in(user_ids))
|
||||
.all(db)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
users
|
||||
.into_iter()
|
||||
.map(|u| {
|
||||
let name = u
|
||||
.display_name
|
||||
.filter(|n| !n.is_empty())
|
||||
.unwrap_or(u.username);
|
||||
(u.id, name)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// GET /audit-logs
|
||||
///
|
||||
/// 分页查询审计日志,支持按 resource_type 和 user_id 过滤。
|
||||
/// 租户隔离通过 JWT 中间件注入的 TenantContext 实现。
|
||||
pub async fn list_audit_logs<S>(
|
||||
State(db): State<sea_orm::DatabaseConnection>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Query(params): Query<AuditLogQuery>,
|
||||
) -> Result<Json<ApiResponse<PaginatedResponse<audit_log::Model>>>, AppError>
|
||||
) -> Result<Json<ApiResponse<PaginatedResponse<AuditLogResp>>>, AppError>
|
||||
where
|
||||
sea_orm::DatabaseConnection: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
@@ -58,10 +124,22 @@ where
|
||||
.await
|
||||
.map_err(|e| AppError::Internal(format!("查询审计日志失败: {e}")))?;
|
||||
|
||||
let user_map = resolve_user_names(&db, &items).await;
|
||||
|
||||
let resp_items: Vec<AuditLogResp> = items
|
||||
.into_iter()
|
||||
.map(|m| {
|
||||
let user_name = m.user_id.and_then(|uid| user_map.get(&uid).cloned());
|
||||
let mut resp = AuditLogResp::from(m);
|
||||
resp.user_name = user_name;
|
||||
resp
|
||||
})
|
||||
.collect();
|
||||
|
||||
let total_pages = total.div_ceil(page_size);
|
||||
|
||||
Ok(Json(ApiResponse::ok(PaginatedResponse {
|
||||
data: items,
|
||||
data: resp_items,
|
||||
total,
|
||||
page,
|
||||
page_size,
|
||||
|
||||
Reference in New Issue
Block a user