H7 贴纸 CRUD: - POST /diary/sticker-packs — 创建贴纸包 - DELETE /diary/sticker-packs/:id — 软删除贴纸包 - POST /diary/sticker-packs/:id/stickers — 添加贴纸 H8 主题编辑/停用: - PUT /diary/topics/:id — 编辑主题 (标题/描述/截止日期) - PATCH /diary/topics/:id/deactivate — 停用主题 管理端前端: - ClassList.tsx 对接 update/deactivate/reset-code (含 Popconfirm 确认) - JournalList.tsx 班级筛选改用 classApi.listAll() - classes.ts 新增 listAll/update/deactivate/resetCode API M2 HMS 遗留清理: - 删除 copilot.ts, healthFixtures.ts, healthHandlers.ts - AuditLogViewer 资源类型 → 日记模块 - auth.test.ts / renderWithProviders health.* → diary.* M4 编辑器加载: - EditorPage journalId 非空时从 Isar 恢复笔画/元素/标签/心情/标题 77 tests passed, cargo check ✅, tsc ✅, flutter analyze ✅
335 lines
11 KiB
Rust
335 lines
11 KiB
Rust
// 贴纸服务 — 贴纸包与贴纸管理
|
|
|
|
use sea_orm::{
|
|
ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, PaginatorTrait,
|
|
QueryFilter, QueryOrder, Set,
|
|
};
|
|
use uuid::Uuid;
|
|
|
|
use crate::dto::{CreateStickerPackReq, CreateStickerReq, StickerPackResp, StickerResp, TemplateResp};
|
|
use crate::entity::{sticker, sticker_pack, template};
|
|
use crate::error::{DiaryError, DiaryResult};
|
|
|
|
/// 贴纸服务 — 贴纸包浏览、贴纸查询、模板管理
|
|
pub struct StickerService;
|
|
|
|
impl StickerService {
|
|
/// 获取贴纸包列表
|
|
///
|
|
/// 返回所有可用的贴纸包,按分类和名称排序。
|
|
pub async fn list_sticker_packs(
|
|
tenant_id: Uuid,
|
|
category: Option<String>,
|
|
db: &DatabaseConnection,
|
|
) -> DiaryResult<Vec<StickerPackResp>> {
|
|
let mut query = sticker_pack::Entity::find()
|
|
.filter(sticker_pack::Column::TenantId.eq(tenant_id))
|
|
.filter(sticker_pack::Column::DeletedAt.is_null());
|
|
|
|
if let Some(ref cat) = category {
|
|
query = query.filter(sticker_pack::Column::Category.eq(cat));
|
|
}
|
|
|
|
let packs = query
|
|
.order_by_asc(sticker_pack::Column::Category)
|
|
.order_by_asc(sticker_pack::Column::Name)
|
|
.all(db)
|
|
.await?;
|
|
|
|
let mut result = Vec::with_capacity(packs.len());
|
|
for pack in packs {
|
|
let sticker_count = sticker::Entity::find()
|
|
.filter(sticker::Column::PackId.eq(pack.id))
|
|
.filter(sticker::Column::DeletedAt.is_null())
|
|
.count(db)
|
|
.await? as i32;
|
|
|
|
result.push(StickerPackResp {
|
|
id: pack.id,
|
|
name: pack.name,
|
|
description: pack.description,
|
|
cover_image_url: pack.thumbnail_url,
|
|
sticker_count,
|
|
is_free: pack.is_free,
|
|
category: pack.category,
|
|
});
|
|
}
|
|
|
|
Ok(result)
|
|
}
|
|
|
|
/// 获取贴纸包内的贴纸列表
|
|
pub async fn list_stickers_in_pack(
|
|
tenant_id: Uuid,
|
|
pack_id: Uuid,
|
|
db: &DatabaseConnection,
|
|
) -> DiaryResult<Vec<StickerResp>> {
|
|
// 验证贴纸包存在
|
|
let _pack = sticker_pack::Entity::find()
|
|
.filter(sticker_pack::Column::Id.eq(pack_id))
|
|
.filter(sticker_pack::Column::TenantId.eq(tenant_id))
|
|
.filter(sticker_pack::Column::DeletedAt.is_null())
|
|
.one(db)
|
|
.await?
|
|
.ok_or_else(|| DiaryError::NotFound(format!("贴纸包 {} 不存在", pack_id)))?;
|
|
|
|
let stickers = sticker::Entity::find()
|
|
.filter(sticker::Column::PackId.eq(pack_id))
|
|
.filter(sticker::Column::TenantId.eq(tenant_id))
|
|
.filter(sticker::Column::DeletedAt.is_null())
|
|
.all(db)
|
|
.await?;
|
|
|
|
Ok(stickers
|
|
.into_iter()
|
|
.map(|s| StickerResp {
|
|
id: s.id,
|
|
pack_id: s.pack_id,
|
|
name: s.name,
|
|
image_url: s.image_url,
|
|
category: s.category,
|
|
})
|
|
.collect())
|
|
}
|
|
|
|
/// 获取模板列表
|
|
///
|
|
/// 返回所有可用模板,包括官方模板和用户创建的模板。
|
|
pub async fn list_templates(
|
|
tenant_id: Uuid,
|
|
category: Option<String>,
|
|
db: &DatabaseConnection,
|
|
) -> DiaryResult<Vec<TemplateResp>> {
|
|
let mut query = template::Entity::find()
|
|
.filter(template::Column::TenantId.eq(tenant_id))
|
|
.filter(template::Column::DeletedAt.is_null());
|
|
|
|
if let Some(ref cat) = category {
|
|
query = query.filter(template::Column::Category.eq(cat));
|
|
}
|
|
|
|
let templates = query
|
|
.order_by_desc(template::Column::IsOfficial)
|
|
.order_by_asc(template::Column::Name)
|
|
.all(db)
|
|
.await?;
|
|
|
|
Ok(templates
|
|
.into_iter()
|
|
.map(|t| TemplateResp {
|
|
id: t.id,
|
|
name: t.name,
|
|
description: None, // template entity 无 description 字段
|
|
preview_url: t.thumbnail_url,
|
|
template_data: t.layout_data,
|
|
category: t.category,
|
|
is_free: true, // Phase 1 所有模板免费
|
|
})
|
|
.collect())
|
|
}
|
|
|
|
/// 获取模板详情
|
|
pub async fn get_template(
|
|
tenant_id: Uuid,
|
|
template_id: Uuid,
|
|
db: &DatabaseConnection,
|
|
) -> DiaryResult<TemplateResp> {
|
|
let model = template::Entity::find()
|
|
.filter(template::Column::Id.eq(template_id))
|
|
.filter(template::Column::TenantId.eq(tenant_id))
|
|
.filter(template::Column::DeletedAt.is_null())
|
|
.one(db)
|
|
.await?
|
|
.ok_or_else(|| DiaryError::NotFound(format!("模板 {} 不存在", template_id)))?;
|
|
|
|
Ok(TemplateResp {
|
|
id: model.id,
|
|
name: model.name,
|
|
description: None,
|
|
preview_url: model.thumbnail_url,
|
|
template_data: model.layout_data,
|
|
category: model.category,
|
|
is_free: true,
|
|
})
|
|
}
|
|
|
|
/// 创建贴纸包(管理端)
|
|
pub async fn create_sticker_pack(
|
|
tenant_id: Uuid,
|
|
user_id: Uuid,
|
|
req: &CreateStickerPackReq,
|
|
db: &DatabaseConnection,
|
|
) -> DiaryResult<StickerPackResp> {
|
|
let now = chrono::Utc::now();
|
|
let id = Uuid::now_v7();
|
|
|
|
let model = sticker_pack::ActiveModel {
|
|
id: Set(id),
|
|
tenant_id: Set(tenant_id),
|
|
name: Set(req.name.clone()),
|
|
description: Set(req.description.clone()),
|
|
thumbnail_url: Set(req.thumbnail_url.clone()),
|
|
is_free: Set(req.is_free),
|
|
price: Set(req.price),
|
|
category: Set(req.category.clone()),
|
|
created_at: Set(now),
|
|
updated_at: Set(now),
|
|
created_by: Set(user_id),
|
|
updated_by: Set(user_id),
|
|
deleted_at: Set(None),
|
|
version: Set(1),
|
|
};
|
|
let inserted = model.insert(db).await?;
|
|
|
|
Ok(StickerPackResp {
|
|
id: inserted.id,
|
|
name: inserted.name,
|
|
description: inserted.description,
|
|
cover_image_url: inserted.thumbnail_url,
|
|
sticker_count: 0,
|
|
is_free: inserted.is_free,
|
|
category: inserted.category,
|
|
})
|
|
}
|
|
|
|
/// 删除贴纸包(软删除)
|
|
pub async fn delete_sticker_pack(
|
|
tenant_id: Uuid,
|
|
pack_id: Uuid,
|
|
user_id: Uuid,
|
|
db: &DatabaseConnection,
|
|
) -> DiaryResult<()> {
|
|
let model = sticker_pack::Entity::find()
|
|
.filter(sticker_pack::Column::Id.eq(pack_id))
|
|
.filter(sticker_pack::Column::TenantId.eq(tenant_id))
|
|
.filter(sticker_pack::Column::DeletedAt.is_null())
|
|
.one(db)
|
|
.await?
|
|
.ok_or_else(|| DiaryError::NotFound(format!("贴纸包 {} 不存在", pack_id)))?;
|
|
|
|
let now = chrono::Utc::now();
|
|
let mut active: sticker_pack::ActiveModel = model.into();
|
|
active.deleted_at = Set(Some(now));
|
|
active.updated_at = Set(now);
|
|
active.updated_by = Set(user_id);
|
|
active.update(db).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// 创建贴纸(管理端)
|
|
pub async fn create_sticker(
|
|
tenant_id: Uuid,
|
|
user_id: Uuid,
|
|
pack_id: Uuid,
|
|
req: &CreateStickerReq,
|
|
db: &DatabaseConnection,
|
|
) -> DiaryResult<StickerResp> {
|
|
// 验证贴纸包存在
|
|
let _pack = sticker_pack::Entity::find()
|
|
.filter(sticker_pack::Column::Id.eq(pack_id))
|
|
.filter(sticker_pack::Column::TenantId.eq(tenant_id))
|
|
.filter(sticker_pack::Column::DeletedAt.is_null())
|
|
.one(db)
|
|
.await?
|
|
.ok_or_else(|| DiaryError::NotFound(format!("贴纸包 {} 不存在", pack_id)))?;
|
|
|
|
let now = chrono::Utc::now();
|
|
let id = Uuid::now_v7();
|
|
|
|
let model = sticker::ActiveModel {
|
|
id: Set(id),
|
|
tenant_id: Set(tenant_id),
|
|
pack_id: Set(pack_id),
|
|
name: Set(req.name.clone()),
|
|
image_url: Set(req.image_url.clone()),
|
|
category: Set(req.category.clone()),
|
|
tags: Set(None),
|
|
created_at: Set(now),
|
|
updated_at: Set(now),
|
|
created_by: Set(user_id),
|
|
updated_by: Set(user_id),
|
|
deleted_at: Set(None),
|
|
version: Set(1),
|
|
};
|
|
let inserted = model.insert(db).await?;
|
|
|
|
Ok(StickerResp {
|
|
id: inserted.id,
|
|
pack_id: inserted.pack_id,
|
|
name: inserted.name,
|
|
image_url: inserted.image_url,
|
|
category: inserted.category,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
// ===== DTO 序列化测试 =====
|
|
|
|
#[test]
|
|
fn sticker_pack_resp_serializes() {
|
|
let resp = StickerPackResp {
|
|
id: Uuid::nil(),
|
|
name: "可爱猫咪".into(),
|
|
description: Some("超萌的猫咪贴纸".into()),
|
|
cover_image_url: Some("https://example.com/cat.png".into()),
|
|
sticker_count: 24,
|
|
is_free: true,
|
|
category: Some("动物".into()),
|
|
};
|
|
let json = serde_json::to_string(&resp).unwrap();
|
|
assert!(json.contains("\"sticker_count\":24"));
|
|
assert!(json.contains("\"is_free\":true"));
|
|
assert!(json.contains("\"category\":\"动物\""));
|
|
}
|
|
|
|
#[test]
|
|
fn sticker_resp_serializes() {
|
|
let resp = StickerResp {
|
|
id: Uuid::nil(),
|
|
pack_id: Uuid::nil(),
|
|
name: "笑脸猫".into(),
|
|
image_url: "https://example.com/cat-smile.png".into(),
|
|
category: Some("表情".into()),
|
|
};
|
|
let json = serde_json::to_string(&resp).unwrap();
|
|
assert!(json.contains("\"name\":\"笑脸猫\""));
|
|
}
|
|
|
|
#[test]
|
|
fn template_resp_serializes() {
|
|
let resp = TemplateResp {
|
|
id: Uuid::nil(),
|
|
name: "今日心情".into(),
|
|
description: Some("记录今天的心情".into()),
|
|
preview_url: None,
|
|
template_data: Some(serde_json::json!({"layout": "grid"})),
|
|
category: Some("日常".into()),
|
|
is_free: true,
|
|
};
|
|
let json = serde_json::to_string(&resp).unwrap();
|
|
assert!(json.contains("\"is_free\":true"));
|
|
assert!(json.contains("\"layout\":\"grid\""));
|
|
}
|
|
|
|
// ===== 错误处理测试 =====
|
|
|
|
#[test]
|
|
fn sticker_pack_not_found_error() {
|
|
let pack_id = Uuid::now_v7();
|
|
let err = DiaryError::NotFound(format!("贴纸包 {} 不存在", pack_id));
|
|
assert!(err.to_string().contains(&pack_id.to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn template_not_found_error() {
|
|
let template_id = Uuid::now_v7();
|
|
let err = DiaryError::NotFound(format!("模板 {} 不存在", template_id));
|
|
assert!(err.to_string().contains(&template_id.to_string()));
|
|
}
|
|
}
|