Files
hms/crates/erp-health/src/dto/article_dto.rs
iven 1e59007bd5 fix(mp): DevTools 卡死 + 主包 2MB→766KB + 代码质量 4 项全通过
根因:主包 2MB 全量组件注入导致 DevTools 渲染引擎内存渐增,
叠加离线时固定 3s 抑制期后的请求洪泛。

修复:
- app.config.ts 添加 lazyCodeLoading: requiredComponents
  主包 2.0MB→766KB,taro.js 526→131KB,vendors.js 230→28KB
- request.ts 离线抑制改为指数退避(3s→6s→12s→30s cap)
  后端不可达时自动延长抑制,防止请求风暴
- SegmentTabs Tab 接口改为 readonly,修复 TS 编译错误
- AbortController polyfill 补齐小程序运行时缺失
- 健康首页/设备同步/健康档案/报告/设置页 UI 重构
- 文章页公开端点适配游客访问
- 健康首页 Swiper 间隔优化 4s→5s,动画 500→300ms
2026-05-24 11:32:40 +08:00

292 lines
9.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
};
const fn default_true() -> bool {
true
}
// ---------------------------------------------------------------------------
// 文章 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,
/// 是否公开(游客可访问)
pub is_public: bool,
/// 文章关联的分类 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,
/// 是否公开(游客可访问)
pub is_public: bool,
/// 分类 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, Validate)]
pub struct CreateArticleReq {
#[validate(length(min = 1, max = 200, message = "文章标题长度须在1-200之间"))]
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>,
/// 标签 ID 列表
#[serde(default)]
pub tag_ids: Vec<Uuid>,
/// 是否公开(游客可访问),默认 true
#[serde(default = "default_true")]
pub is_public: bool,
}
impl CreateArticleReq {
pub fn sanitize(&mut self) {
self.title = sanitize_string(&self.title);
self.summary = sanitize_option(self.summary.take());
// content: rich_text 模式保留 HTML仅做安全清理其他模式剥离标签
self.content = sanitize_rich_html_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, Validate)]
pub struct UpdateArticleReq {
#[validate(length(min = 1, max = 200, message = "文章标题长度须在1-200之间"))]
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>,
/// 标签 ID 列表(传入则整体替换)
pub tag_ids: Option<Vec<Uuid>>,
pub sort_order: Option<i32>,
/// 是否公开(游客可访问)
pub is_public: Option<bool>,
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());
// content: rich_text 模式保留 HTML仅做安全清理其他模式剥离标签
self.content = sanitize_rich_html_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, Validate)]
pub struct ReviewArticleReq {
/// 审核备注
#[validate(length(max = 2000, message = "审核备注最多2000字"))]
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, 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>,
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, 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>,
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, Validate)]
pub struct CreateTagReq {
#[validate(length(min = 1, max = 50, message = "标签名长度须在1-50之间"))]
pub name: String,
}
impl CreateTagReq {
pub fn sanitize(&mut self) {
self.name = sanitize_string(&self.name);
}
}
#[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,
}
impl UpdateTagReq {
pub fn sanitize(&mut self) {
self.name = sanitize_string(&self.name);
}
}