Files
nj/crates/erp-diary/src/service/sticker_service.rs
iven 45530616ee
Some checks failed
Main Merge / backend (push) Has been cancelled
Main Merge / frontend (push) Has been cancelled
feat(diary): 添加贴纸包 UpdateStickerPackReq DTO + update service/handler — Task 13
2026-06-02 23:54:04 +08:00

396 lines
13 KiB
Rust

// 贴纸服务 — 贴纸包与贴纸管理
use sea_orm::{
ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, PaginatorTrait,
QueryFilter, QueryOrder, Set,
};
use uuid::Uuid;
use crate::dto::{CreateStickerPackReq, CreateStickerReq, StickerPackResp, StickerResp, TemplateResp, UpdateStickerPackReq};
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 update_sticker_pack(
tenant_id: Uuid,
pack_id: Uuid,
user_id: Uuid,
req: &UpdateStickerPackReq,
db: &DatabaseConnection,
) -> DiaryResult<StickerPackResp> {
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();
if let Some(ref name) = req.name {
active.name = Set(name.clone());
}
if let Some(ref description) = req.description {
active.description = Set(Some(description.clone()));
}
if let Some(ref thumbnail_url) = req.thumbnail_url {
active.thumbnail_url = Set(Some(thumbnail_url.clone()));
}
if let Some(is_free) = req.is_free {
active.is_free = Set(is_free);
}
if let Some(price) = req.price {
active.price = Set(price);
}
if let Some(ref category) = req.category {
active.category = Set(Some(category.clone()));
}
active.updated_at = Set(now);
active.updated_by = Set(user_id);
let updated = active.update(db).await?;
// 查询贴纸数量
let sticker_count = sticker::Entity::find()
.filter(sticker::Column::PackId.eq(updated.id))
.filter(sticker::Column::DeletedAt.is_null())
.count(db)
.await? as i32;
Ok(StickerPackResp {
id: updated.id,
name: updated.name,
description: updated.description,
cover_image_url: updated.thumbnail_url,
sticker_count,
is_free: updated.is_free,
category: updated.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()));
}
}