根因:主包 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
292 lines
9.7 KiB
Rust
292 lines
9.7 KiB
Rust
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);
|
||
}
|
||
}
|