feat(health): 内容管理模块 — 审核/分类/标签/富文本编辑器
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

后端:
- 文章审核状态机:draft → pending_review → published(含 reject/unpublish)
- 文章分类 CRUD(article_category entity + service + handler)
- 文章标签 CRUD(article_tag + article_article_tag 关联)
- 文章修订版快照(article_revision)
- 阅读计数、排序、slug、审核备注
- 新增 health.articles.review 权限

前端:
- ArticleManageList:状态标签页 + 分类筛选 + 关键字搜索 + 审核操作
- ArticleEditor:Wangeditor 富文本编辑器 + 元数据侧栏
- ArticleCategoryManage:分类 CRUD + 父子层级
- ArticleTagManage:标签 CRUD

修复:
- diagnosis_service/health_data_service/dialysis_service: 补充 key_version 字段
- ArticleCategoryManage: 补充 Select 组件导入
This commit is contained in:
iven
2026-04-26 12:51:30 +08:00
parent 49b8300fdc
commit 17b423b9b8
26 changed files with 3731 additions and 97 deletions

View File

@@ -5,7 +5,7 @@ use erp_core::error::AppError;
use erp_core::rbac::require_permission;
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
use crate::dto::article_dto::{ArticleListItem, ArticleListParams, ArticleResp, CreateArticleReq, UpdateArticleReq};
use crate::dto::article_dto::{ArticleListItem, ArticleListParams, ArticleResp, CreateArticleReq, ReviewArticleReq, UpdateArticleReq};
use crate::service::article_service;
use crate::state::HealthState;
@@ -22,8 +22,8 @@ where
let page = params.page.unwrap_or(1);
let page_size = params.page_size.unwrap_or(20);
let result = article_service::list_articles(
&state, ctx.tenant_id, page, page_size, params.category,
params.status, params.category_id, params.tag_id, params.keyword,
&state, ctx.tenant_id, page, page_size,
params.category, params.status, params.category_id, params.tag_id, params.keyword,
)
.await?;
Ok(Json(ApiResponse::ok(result)))
@@ -97,3 +97,102 @@ where
article_service::delete_article(&state, ctx.tenant_id, id, Some(ctx.user_id), req.version).await?;
Ok(Json(ApiResponse::ok(())))
}
// ---------------------------------------------------------------------------
// 审核工作流
// ---------------------------------------------------------------------------
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
pub struct VersionReq {
pub version: i32,
}
/// 提交审核
pub async fn submit_article<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path(id): Path<uuid::Uuid>,
Json(req): Json<VersionReq>,
) -> Result<Json<ApiResponse<ArticleResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.articles.manage")?;
let result = article_service::submit_article(
&state, ctx.tenant_id, id, Some(ctx.user_id), req.version,
).await?;
Ok(Json(ApiResponse::ok(result)))
}
/// 审核通过并发布
pub async fn approve_article<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path(id): Path<uuid::Uuid>,
mut req: Json<ReviewArticleReq>,
) -> Result<Json<ApiResponse<ArticleResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.articles.review")?;
req.sanitize();
let version = req.version.unwrap_or(0);
let result = article_service::approve_article(
&state, ctx.tenant_id, id, Some(ctx.user_id), req.0, version,
).await?;
Ok(Json(ApiResponse::ok(result)))
}
/// 审核拒绝
pub async fn reject_article<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path(id): Path<uuid::Uuid>,
mut req: Json<ReviewArticleReq>,
) -> Result<Json<ApiResponse<ArticleResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.articles.review")?;
req.sanitize();
let version = req.version.unwrap_or(0);
let result = article_service::reject_article(
&state, ctx.tenant_id, id, Some(ctx.user_id), req.0, version,
).await?;
Ok(Json(ApiResponse::ok(result)))
}
/// 撤回发布
pub async fn unpublish_article<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path(id): Path<uuid::Uuid>,
Json(req): Json<VersionReq>,
) -> Result<Json<ApiResponse<ArticleResp>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.articles.manage")?;
let result = article_service::unpublish_article(
&state, ctx.tenant_id, id, Some(ctx.user_id), req.version,
).await?;
Ok(Json(ApiResponse::ok(result)))
}
/// 浏览计数
pub async fn view_article<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path(id): Path<uuid::Uuid>,
) -> Result<Json<ApiResponse<()>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
article_service::increment_view_count(&state, ctx.tenant_id, id).await?;
Ok(Json(ApiResponse::ok(())))
}