feat(diary): Phase 1.3 完善修复 — 贴纸/主题 CRUD + 管理端对接 + HMS 清理
Some checks failed
Main Merge / backend (push) Has been cancelled
Main Merge / frontend (push) Has been cancelled

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 
This commit is contained in:
iven
2026-06-02 23:01:13 +08:00
parent 94bfb3297a
commit 8ea1032c9d
6 changed files with 470 additions and 8 deletions

View File

@@ -1,11 +1,12 @@
// 贴纸服务 — 贴纸包与贴纸管理
use sea_orm::{
ColumnTrait, DatabaseConnection, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder,
ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, PaginatorTrait,
QueryFilter, QueryOrder, Set,
};
use uuid::Uuid;
use crate::dto::{StickerPackResp, StickerResp, TemplateResp};
use crate::dto::{CreateStickerPackReq, CreateStickerReq, StickerPackResp, StickerResp, TemplateResp};
use crate::entity::{sticker, sticker_pack, template};
use crate::error::{DiaryError, DiaryResult};
@@ -151,6 +152,116 @@ impl StickerService {
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)]

View File

@@ -7,7 +7,7 @@ use sea_orm::{
};
use uuid::Uuid;
use crate::dto::{CreateTopicReq, TopicResp};
use crate::dto::{CreateTopicReq, TopicResp, UpdateTopicReq};
use crate::entity::topic_assignment;
use crate::error::{DiaryError, DiaryResult};
use crate::service::notification_service::NotificationService;
@@ -112,6 +112,93 @@ impl TopicService {
Ok(topics.into_iter().map(topic_model_to_resp).collect())
}
/// 更新主题信息(老师)
///
/// 仅主题创建者可修改标题、描述、截止日期。
pub async fn update_topic(
tenant_id: Uuid,
user_id: Uuid,
topic_id: Uuid,
req: &UpdateTopicReq,
db: &DatabaseConnection,
) -> DiaryResult<TopicResp> {
let model = topic_assignment::Entity::find()
.filter(topic_assignment::Column::Id.eq(topic_id))
.filter(topic_assignment::Column::TenantId.eq(tenant_id))
.filter(topic_assignment::Column::DeletedAt.is_null())
.one(db)
.await?
.ok_or_else(|| DiaryError::NotFound(format!("主题 {} 不存在", topic_id)))?;
// 仅布置者可编辑
if model.teacher_id != user_id {
return Err(DiaryError::Forbidden);
}
// 乐观锁
if model.version != req.version {
return Err(DiaryError::VersionConflict {
local: req.version,
server: model.version,
});
}
let now = Utc::now();
let mut active: topic_assignment::ActiveModel = model.into();
if let Some(ref title) = req.title {
active.title = Set(title.clone());
}
if let Some(ref desc) = req.description {
active.description = Set(Some(desc.clone()));
}
if req.due_date.is_some() {
active.due_date = Set(req.due_date);
}
active.updated_at = Set(now);
active.updated_by = Set(user_id);
active.version = Set(req.version + 1);
let updated = active.update(db).await?;
Ok(topic_model_to_resp(updated))
}
/// 停用主题(老师)
///
/// 停用后主题不再显示给学生,已提交的日记不受影响。
pub async fn deactivate_topic(
tenant_id: Uuid,
user_id: Uuid,
topic_id: Uuid,
db: &DatabaseConnection,
) -> DiaryResult<TopicResp> {
let model = topic_assignment::Entity::find()
.filter(topic_assignment::Column::Id.eq(topic_id))
.filter(topic_assignment::Column::TenantId.eq(tenant_id))
.filter(topic_assignment::Column::DeletedAt.is_null())
.one(db)
.await?
.ok_or_else(|| DiaryError::NotFound(format!("主题 {} 不存在", topic_id)))?;
if model.teacher_id != user_id {
return Err(DiaryError::Forbidden);
}
if !model.is_active {
return Err(DiaryError::BadRequest("主题已处于停用状态".to_string()));
}
let now = Utc::now();
let current_version = model.version;
let mut active: topic_assignment::ActiveModel = model.into();
active.is_active = Set(false);
active.updated_at = Set(now);
active.updated_by = Set(user_id);
active.version = Set(current_version + 1);
let updated = active.update(db).await?;
Ok(topic_model_to_resp(updated))
}
}
/// topic_assignment::Model -> TopicResp