feat(health): 添加文章修订历史查询 API — GET /health/articles/{id}/revisions

补全 ArticleRevision 实体的读取查询(之前仅有写入 save_revision),
新增 list_revisions service + handler + 路由,支持分页。
This commit is contained in:
iven
2026-04-30 10:53:04 +08:00
parent 32eef5ecf1
commit b00fe44880
4 changed files with 96 additions and 0 deletions

View File

@@ -144,6 +144,21 @@ impl ReviewArticleReq {
}
}
// ---------------------------------------------------------------------------
// 修订历史 DTOs
// ---------------------------------------------------------------------------
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct ArticleRevisionResp {
pub id: Uuid,
pub article_id: Uuid,
pub revision_number: i32,
pub title: String,
pub summary: Option<String>,
pub created_by: Option<Uuid>,
pub created_at: chrono::DateTime<chrono::Utc>,
}
// ---------------------------------------------------------------------------
// 分类 DTOs
// ---------------------------------------------------------------------------

View File

@@ -203,3 +203,29 @@ where
article_service::increment_view_count(&state, ctx.tenant_id, id).await?;
Ok(Json(ApiResponse::ok(())))
}
#[derive(Debug, serde::Deserialize)]
pub struct ListRevisionsQuery {
pub page: Option<u64>,
pub page_size: Option<u64>,
}
/// 查询文章修订历史
pub async fn list_revisions<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path(id): Path<uuid::Uuid>,
Query(params): Query<ListRevisionsQuery>,
) -> Result<Json<ApiResponse<PaginatedResponse<crate::dto::article_dto::ArticleRevisionResp>>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.articles.list")?;
let page = params.page.unwrap_or(1);
let page_size = params.page_size.unwrap_or(20);
let result = article_service::list_revisions(
&state, ctx.tenant_id, id, page, page_size,
).await?;
Ok(Json(ApiResponse::ok(result)))
}

View File

@@ -433,6 +433,10 @@ impl HealthModule {
"/health/articles/{id}/view",
axum::routing::post(article_handler::view_article),
)
.route(
"/health/articles/{id}/revisions",
axum::routing::get(article_handler::list_revisions),
)
// 资讯分类
.route(
"/health/article-categories",

View File

@@ -588,3 +588,54 @@ async fn save_revision(
active.insert(&state.db).await?;
Ok(())
}
pub async fn list_revisions(
state: &HealthState,
tenant_id: Uuid,
article_id: Uuid,
page: u64,
page_size: u64,
) -> HealthResult<PaginatedResponse<crate::dto::article_dto::ArticleRevisionResp>> {
use crate::entity::article_revision;
let page = page.max(1);
let page_size = page_size.clamp(1, 100);
let condition = article_revision::Column::ArticleId
.eq(article_id)
.and(article_revision::Column::TenantId.eq(tenant_id));
let total = article_revision::Entity::find()
.filter(condition.clone())
.count(&state.db)
.await?;
let items = article_revision::Entity::find()
.filter(condition)
.order_by_desc(article_revision::Column::RevisionNumber)
.offset(((page - 1) * page_size) as u64)
.limit(page_size)
.all(&state.db)
.await?;
let data = items
.into_iter()
.map(|m| crate::dto::article_dto::ArticleRevisionResp {
id: m.id,
article_id: m.article_id,
revision_number: m.revision_number,
title: m.title,
summary: m.summary,
created_by: m.created_by,
created_at: m.created_at,
})
.collect();
Ok(PaginatedResponse {
data,
total,
page,
page_size,
total_pages: ((total as f64) / (page_size as f64)).ceil() as u64,
})
}