396 lines
13 KiB
Rust
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()));
|
|
}
|
|
}
|