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:
@@ -1,6 +1,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
use uuid::Uuid;
|
||||
use validator::Validate;
|
||||
|
||||
use erp_core::sanitize::{
|
||||
sanitize_option, sanitize_rich_html_option, sanitize_string, strip_html_tags,
|
||||
@@ -72,16 +73,23 @@ pub struct ArticleListParams {
|
||||
pub keyword: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct CreateArticleReq {
|
||||
#[validate(length(min = 1, max = 500, message = "文章标题长度须在1-500之间"))]
|
||||
pub title: String,
|
||||
#[validate(length(max = 2000, message = "摘要最多2000字"))]
|
||||
pub summary: Option<String>,
|
||||
pub content: Option<String>,
|
||||
#[validate(length(max = 500, message = "封面URL最多500字"))]
|
||||
pub cover_image: Option<String>,
|
||||
#[validate(length(max = 100, message = "分类名最多100字"))]
|
||||
pub category: Option<String>,
|
||||
#[validate(length(max = 100, message = "作者名最多100字"))]
|
||||
pub author: Option<String>,
|
||||
pub published_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
#[validate(length(max = 200, message = "slug最多200字"))]
|
||||
pub slug: Option<String>,
|
||||
#[validate(length(max = 50, message = "内容类型最多50字"))]
|
||||
pub content_type: Option<String>,
|
||||
/// 分类 ID
|
||||
pub category_id: Option<Uuid>,
|
||||
@@ -103,16 +111,23 @@ impl CreateArticleReq {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct UpdateArticleReq {
|
||||
#[validate(length(min = 1, max = 500, message = "文章标题长度须在1-500之间"))]
|
||||
pub title: Option<String>,
|
||||
#[validate(length(max = 2000, message = "摘要最多2000字"))]
|
||||
pub summary: Option<String>,
|
||||
pub content: Option<String>,
|
||||
#[validate(length(max = 500, message = "封面URL最多500字"))]
|
||||
pub cover_image: Option<String>,
|
||||
#[validate(length(max = 100, message = "分类名最多100字"))]
|
||||
pub category: Option<String>,
|
||||
#[validate(length(max = 100, message = "作者名最多100字"))]
|
||||
pub author: Option<String>,
|
||||
pub published_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
#[validate(length(max = 200, message = "slug最多200字"))]
|
||||
pub slug: Option<String>,
|
||||
#[validate(length(max = 50, message = "内容类型最多50字"))]
|
||||
pub content_type: Option<String>,
|
||||
/// 分类 ID
|
||||
pub category_id: Option<Uuid>,
|
||||
@@ -138,9 +153,10 @@ impl UpdateArticleReq {
|
||||
}
|
||||
|
||||
/// 审核文章请求
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct ReviewArticleReq {
|
||||
/// 审核备注
|
||||
#[validate(length(max = 2000, message = "审核备注最多2000字"))]
|
||||
pub note: Option<String>,
|
||||
/// 文章版本号(乐观锁)
|
||||
pub version: Option<i32>,
|
||||
@@ -184,8 +200,9 @@ pub struct CategoryResp {
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct CreateCategoryReq {
|
||||
#[validate(length(min = 1, max = 100, message = "分类名长度须在1-100之间"))]
|
||||
pub name: String,
|
||||
pub slug: Option<String>,
|
||||
pub parent_id: Option<Uuid>,
|
||||
@@ -201,8 +218,9 @@ impl CreateCategoryReq {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct UpdateCategoryReq {
|
||||
#[validate(length(min = 1, max = 100, message = "分类名长度须在1-100之间"))]
|
||||
pub name: Option<String>,
|
||||
pub slug: Option<String>,
|
||||
pub parent_id: Option<Uuid>,
|
||||
@@ -234,8 +252,9 @@ pub struct TagResp {
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct CreateTagReq {
|
||||
#[validate(length(min = 1, max = 50, message = "标签名长度须在1-50之间"))]
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
@@ -245,8 +264,9 @@ impl CreateTagReq {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Validate)]
|
||||
pub struct UpdateTagReq {
|
||||
#[validate(length(min = 1, max = 50, message = "标签名长度须在1-50之间"))]
|
||||
pub name: String,
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ use crate::dto::article_dto::{CategoryResp, CreateCategoryReq, UpdateCategoryReq
|
||||
use crate::service::article_category_service;
|
||||
use crate::state::HealthState;
|
||||
|
||||
use validator::Validate;
|
||||
|
||||
pub async fn list_categories<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
@@ -33,6 +35,9 @@ where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.articles.manage")?;
|
||||
(*req)
|
||||
.validate()
|
||||
.map_err(|e| AppError::Validation(e.to_string()))?;
|
||||
req.sanitize();
|
||||
let result =
|
||||
article_category_service::create_category(&state, ctx.tenant_id, Some(ctx.user_id), req.0)
|
||||
@@ -51,6 +56,9 @@ where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.articles.manage")?;
|
||||
(*req)
|
||||
.validate()
|
||||
.map_err(|e| AppError::Validation(e.to_string()))?;
|
||||
req.sanitize();
|
||||
let result = article_category_service::update_category(
|
||||
&state,
|
||||
|
||||
@@ -12,6 +12,8 @@ use crate::dto::article_dto::{
|
||||
use crate::service::article_service;
|
||||
use crate::state::HealthState;
|
||||
|
||||
use validator::Validate;
|
||||
|
||||
pub async fn list_articles<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
@@ -107,6 +109,9 @@ where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.articles.manage")?;
|
||||
(*req)
|
||||
.validate()
|
||||
.map_err(|e| AppError::Validation(e.to_string()))?;
|
||||
req.sanitize();
|
||||
if req.title.trim().is_empty() {
|
||||
return Err(AppError::Validation("文章标题不能为空".into()));
|
||||
@@ -127,6 +132,9 @@ where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.articles.manage")?;
|
||||
(*req)
|
||||
.validate()
|
||||
.map_err(|e| AppError::Validation(e.to_string()))?;
|
||||
req.sanitize();
|
||||
let result =
|
||||
article_service::update_article(&state, ctx.tenant_id, id, Some(ctx.user_id), req.0)
|
||||
@@ -194,6 +202,9 @@ where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.articles.review")?;
|
||||
(*req)
|
||||
.validate()
|
||||
.map_err(|e| AppError::Validation(e.to_string()))?;
|
||||
req.sanitize();
|
||||
let version = req.version.unwrap_or(0);
|
||||
let result = article_service::approve_article(
|
||||
@@ -220,6 +231,9 @@ where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.articles.review")?;
|
||||
(*req)
|
||||
.validate()
|
||||
.map_err(|e| AppError::Validation(e.to_string()))?;
|
||||
req.sanitize();
|
||||
let version = req.version.unwrap_or(0);
|
||||
let result = article_service::reject_article(
|
||||
|
||||
@@ -10,6 +10,8 @@ use crate::dto::article_dto::{CreateTagReq, TagResp, UpdateTagReq};
|
||||
use crate::service::article_tag_service;
|
||||
use crate::state::HealthState;
|
||||
|
||||
use validator::Validate;
|
||||
|
||||
pub async fn list_tags<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
@@ -33,6 +35,9 @@ where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.articles.manage")?;
|
||||
(*req)
|
||||
.validate()
|
||||
.map_err(|e| AppError::Validation(e.to_string()))?;
|
||||
req.sanitize();
|
||||
if req.name.trim().is_empty() {
|
||||
return Err(AppError::Validation("标签名称不能为空".into()));
|
||||
@@ -53,6 +58,9 @@ where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.articles.manage")?;
|
||||
(*req)
|
||||
.validate()
|
||||
.map_err(|e| AppError::Validation(e.to_string()))?;
|
||||
req.sanitize();
|
||||
let result =
|
||||
article_tag_service::update_tag(&state, ctx.tenant_id, id, Some(ctx.user_id), req.0)
|
||||
|
||||
Reference in New Issue
Block a user