访客首页文章列表调用 /health/articles 需要 JWT 认证导致 401。 新增 GET /public/articles?tenant_id=xxx 端点,强制只返回已发布文章, 无需认证。小程序访客首页改用此公开端点。
255 lines
7.6 KiB
Rust
255 lines
7.6 KiB
Rust
use serde::{Deserialize, Serialize};
|
||
use utoipa::{IntoParams, ToSchema};
|
||
use uuid::Uuid;
|
||
|
||
use erp_core::sanitize::{sanitize_option, sanitize_string, strip_html_tags};
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 文章 DTOs
|
||
// ---------------------------------------------------------------------------
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||
pub struct ArticleResp {
|
||
pub id: Uuid,
|
||
pub title: String,
|
||
pub summary: Option<String>,
|
||
pub content: Option<String>,
|
||
pub cover_image: Option<String>,
|
||
pub category: Option<String>,
|
||
pub author: Option<String>,
|
||
pub published_at: Option<chrono::DateTime<chrono::Utc>>,
|
||
pub status: String,
|
||
pub slug: Option<String>,
|
||
pub content_type: String,
|
||
pub reviewed_by: Option<Uuid>,
|
||
pub reviewed_at: Option<chrono::DateTime<chrono::Utc>>,
|
||
pub review_note: Option<String>,
|
||
pub view_count: i32,
|
||
pub sort_order: i32,
|
||
/// 文章关联的分类 ID(来自 article_category 表)
|
||
pub category_id: Option<Uuid>,
|
||
/// 文章关联的标签名称列表
|
||
pub tags: Vec<String>,
|
||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||
pub version: i32,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||
pub struct ArticleListItem {
|
||
pub id: Uuid,
|
||
pub title: String,
|
||
pub summary: Option<String>,
|
||
pub cover_image: Option<String>,
|
||
pub category: Option<String>,
|
||
pub author: Option<String>,
|
||
pub published_at: Option<chrono::DateTime<chrono::Utc>>,
|
||
pub status: String,
|
||
pub view_count: i32,
|
||
/// 分类 ID
|
||
pub category_id: Option<Uuid>,
|
||
/// 标签名称列表
|
||
pub tags: Vec<String>,
|
||
pub version: i32,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Deserialize, IntoParams)]
|
||
pub struct ArticleListParams {
|
||
/// 公开端点必需:租户 ID
|
||
pub tenant_id: Option<Uuid>,
|
||
pub page: Option<u64>,
|
||
pub page_size: Option<u64>,
|
||
pub category: Option<String>,
|
||
/// 按状态筛选
|
||
pub status: Option<String>,
|
||
/// 按分类 ID 筛选
|
||
pub category_id: Option<Uuid>,
|
||
/// 按标签 ID 筛选
|
||
pub tag_id: Option<Uuid>,
|
||
/// 关键词搜索(标题模糊匹配)
|
||
pub keyword: Option<String>,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||
pub struct CreateArticleReq {
|
||
pub title: String,
|
||
pub summary: Option<String>,
|
||
pub content: Option<String>,
|
||
pub cover_image: Option<String>,
|
||
pub category: Option<String>,
|
||
pub author: Option<String>,
|
||
pub published_at: Option<chrono::DateTime<chrono::Utc>>,
|
||
pub slug: Option<String>,
|
||
pub content_type: Option<String>,
|
||
/// 分类 ID
|
||
pub category_id: Option<Uuid>,
|
||
/// 标签 ID 列表
|
||
#[serde(default)]
|
||
pub tag_ids: Vec<Uuid>,
|
||
}
|
||
|
||
impl CreateArticleReq {
|
||
pub fn sanitize(&mut self) {
|
||
self.title = sanitize_string(&self.title);
|
||
self.summary = sanitize_option(self.summary.take());
|
||
self.content = sanitize_option(self.content.take());
|
||
self.category = sanitize_option(self.category.take());
|
||
self.author = sanitize_option(self.author.take());
|
||
self.slug = sanitize_option(self.slug.take());
|
||
self.content_type = sanitize_option(self.content_type.take());
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||
pub struct UpdateArticleReq {
|
||
pub title: Option<String>,
|
||
pub summary: Option<String>,
|
||
pub content: Option<String>,
|
||
pub cover_image: Option<String>,
|
||
pub category: Option<String>,
|
||
pub author: Option<String>,
|
||
pub published_at: Option<chrono::DateTime<chrono::Utc>>,
|
||
pub slug: Option<String>,
|
||
pub content_type: Option<String>,
|
||
/// 分类 ID
|
||
pub category_id: Option<Uuid>,
|
||
/// 标签 ID 列表(传入则整体替换)
|
||
pub tag_ids: Option<Vec<Uuid>>,
|
||
pub sort_order: Option<i32>,
|
||
pub version: i32,
|
||
}
|
||
|
||
impl UpdateArticleReq {
|
||
pub fn sanitize(&mut self) {
|
||
if let Some(ref mut v) = self.title {
|
||
*v = strip_html_tags(v);
|
||
}
|
||
self.summary = sanitize_option(self.summary.take());
|
||
self.content = sanitize_option(self.content.take());
|
||
self.category = sanitize_option(self.category.take());
|
||
self.author = sanitize_option(self.author.take());
|
||
self.slug = sanitize_option(self.slug.take());
|
||
self.content_type = sanitize_option(self.content_type.take());
|
||
}
|
||
}
|
||
|
||
/// 审核文章请求
|
||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||
pub struct ReviewArticleReq {
|
||
/// 审核备注
|
||
pub note: Option<String>,
|
||
/// 文章版本号(乐观锁)
|
||
pub version: Option<i32>,
|
||
}
|
||
|
||
impl ReviewArticleReq {
|
||
pub fn sanitize(&mut self) {
|
||
self.note = sanitize_option(self.note.take());
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 修订历史 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
|
||
// ---------------------------------------------------------------------------
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||
pub struct CategoryResp {
|
||
pub id: Uuid,
|
||
pub name: String,
|
||
pub slug: Option<String>,
|
||
pub parent_id: Option<Uuid>,
|
||
pub description: Option<String>,
|
||
pub sort_order: i32,
|
||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||
pub version: i32,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||
pub struct CreateCategoryReq {
|
||
pub name: String,
|
||
pub slug: Option<String>,
|
||
pub parent_id: Option<Uuid>,
|
||
pub description: Option<String>,
|
||
pub sort_order: Option<i32>,
|
||
}
|
||
|
||
impl CreateCategoryReq {
|
||
pub fn sanitize(&mut self) {
|
||
self.name = sanitize_string(&self.name);
|
||
self.slug = sanitize_option(self.slug.take());
|
||
self.description = sanitize_option(self.description.take());
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||
pub struct UpdateCategoryReq {
|
||
pub name: Option<String>,
|
||
pub slug: Option<String>,
|
||
pub parent_id: Option<Uuid>,
|
||
pub description: Option<String>,
|
||
pub sort_order: Option<i32>,
|
||
pub version: i32,
|
||
}
|
||
|
||
impl UpdateCategoryReq {
|
||
pub fn sanitize(&mut self) {
|
||
if let Some(ref mut v) = self.name {
|
||
*v = strip_html_tags(v);
|
||
}
|
||
self.slug = sanitize_option(self.slug.take());
|
||
self.description = sanitize_option(self.description.take());
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 标签 DTOs
|
||
// ---------------------------------------------------------------------------
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||
pub struct TagResp {
|
||
pub id: Uuid,
|
||
pub name: String,
|
||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||
pub version: i32,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||
pub struct CreateTagReq {
|
||
pub name: String,
|
||
}
|
||
|
||
impl CreateTagReq {
|
||
pub fn sanitize(&mut self) {
|
||
self.name = sanitize_string(&self.name);
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||
pub struct UpdateTagReq {
|
||
pub name: String,
|
||
pub version: i32,
|
||
}
|
||
|
||
impl UpdateTagReq {
|
||
pub fn sanitize(&mut self) {
|
||
self.name = sanitize_string(&self.name);
|
||
}
|
||
}
|