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

@@ -10,7 +10,7 @@ use erp_core::error::AppError;
use erp_core::rbac::require_permission;
use erp_core::types::{ApiResponse, TenantContext};
use crate::dto::{StickerPackResp, StickerResp, TemplateResp};
use crate::dto::{CreateStickerPackReq, CreateStickerReq, StickerPackResp, StickerResp, TemplateResp};
use crate::service::sticker_service::StickerService;
use crate::state::DiaryState;
@@ -85,7 +85,125 @@ where
Ok(Json(ApiResponse::ok(resp)))
}
/// 模板查询参数
#[utoipa::path(
post,
path = "/api/v1/diary/sticker-packs",
request_body = CreateStickerPackReq,
responses(
(status = 200, description = "创建成功", body = ApiResponse<StickerPackResp>),
(status = 400, description = "验证失败"),
(status = 401, description = "未授权"),
(status = 403, description = "权限不足"),
),
security(("bearer_auth" = [])),
tag = "贴纸管理"
)]
/// POST /api/v1/diary/sticker-packs
///
/// 创建贴纸包。需要 `diary.class.manage` 权限(管理端)。
pub async fn create_sticker_pack<S>(
State(state): State<DiaryState>,
Extension(ctx): Extension<TenantContext>,
Json(req): Json<CreateStickerPackReq>,
) -> Result<Json<ApiResponse<StickerPackResp>>, AppError>
where
DiaryState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "diary.class.manage")?;
if req.name.trim().is_empty() {
return Err(AppError::Validation("贴纸包名称不能为空".to_string()));
}
let resp = StickerService::create_sticker_pack(
ctx.tenant_id,
ctx.user_id,
&req,
&state.db,
)
.await?;
Ok(Json(ApiResponse::ok(resp)))
}
#[utoipa::path(
delete,
path = "/api/v1/diary/sticker-packs/{pack_id}",
params(("pack_id" = Uuid, Path, description = "贴纸包ID")),
responses(
(status = 200, description = "删除成功"),
(status = 401, description = "未授权"),
(status = 403, description = "权限不足"),
(status = 404, description = "贴纸包不存在"),
),
security(("bearer_auth" = [])),
tag = "贴纸管理"
)]
/// DELETE /api/v1/diary/sticker-packs/:pack_id
///
/// 删除贴纸包(软删除)。需要 `diary.class.manage` 权限。
pub async fn delete_sticker_pack<S>(
State(state): State<DiaryState>,
Extension(ctx): Extension<TenantContext>,
Path(pack_id): Path<Uuid>,
) -> Result<Json<ApiResponse<()>>, AppError>
where
DiaryState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "diary.class.manage")?;
StickerService::delete_sticker_pack(ctx.tenant_id, pack_id, ctx.user_id, &state.db).await?;
Ok(Json(ApiResponse::ok(())))
}
#[utoipa::path(
post,
path = "/api/v1/diary/sticker-packs/{pack_id}/stickers",
params(("pack_id" = Uuid, Path, description = "贴纸包ID")),
request_body = CreateStickerReq,
responses(
(status = 200, description = "创建成功", body = ApiResponse<StickerResp>),
(status = 400, description = "验证失败"),
(status = 401, description = "未授权"),
(status = 403, description = "权限不足"),
(status = 404, description = "贴纸包不存在"),
),
security(("bearer_auth" = [])),
tag = "贴纸管理"
)]
/// POST /api/v1/diary/sticker-packs/:pack_id/stickers
///
/// 在贴纸包内添加贴纸。需要 `diary.class.manage` 权限。
pub async fn create_sticker<S>(
State(state): State<DiaryState>,
Extension(ctx): Extension<TenantContext>,
Path(pack_id): Path<Uuid>,
Json(req): Json<CreateStickerReq>,
) -> Result<Json<ApiResponse<StickerResp>>, AppError>
where
DiaryState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "diary.class.manage")?;
if req.name.trim().is_empty() {
return Err(AppError::Validation("贴纸名称不能为空".to_string()));
}
let resp = StickerService::create_sticker(
ctx.tenant_id,
ctx.user_id,
pack_id,
&req,
&state.db,
)
.await?;
Ok(Json(ApiResponse::ok(resp)))
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct TemplateQuery {
pub category: Option<String>,

View File

@@ -8,7 +8,7 @@ use erp_core::error::AppError;
use erp_core::rbac::require_permission;
use erp_core::types::{ApiResponse, TenantContext};
use crate::dto::{CreateTopicReq, TopicResp};
use crate::dto::{CreateTopicReq, TopicResp, UpdateTopicReq};
use crate::service::topic_service::TopicService;
use crate::state::DiaryState;
@@ -88,3 +88,90 @@ where
let resp = TopicService::list_topics(ctx.tenant_id, class_id, &state.db).await?;
Ok(Json(ApiResponse::ok(resp)))
}
#[utoipa::path(
put,
path = "/api/v1/diary/topics/{topic_id}",
params(("topic_id" = Uuid, Path, description = "主题ID")),
request_body = UpdateTopicReq,
responses(
(status = 200, description = "更新成功", body = ApiResponse<TopicResp>),
(status = 400, description = "验证失败"),
(status = 401, description = "未授权"),
(status = 403, description = "权限不足"),
(status = 404, description = "主题不存在"),
(status = 409, description = "版本冲突"),
),
security(("bearer_auth" = [])),
tag = "主题布置"
)]
/// PUT /api/v1/diary/topics/:topic_id
///
/// 更新主题信息。需要 `diary.topic.assign` 权限(仅布置者可编辑)。
pub async fn update_topic<S>(
State(state): State<DiaryState>,
Extension(ctx): Extension<TenantContext>,
Path(topic_id): Path<Uuid>,
Json(req): Json<UpdateTopicReq>,
) -> Result<Json<ApiResponse<TopicResp>>, AppError>
where
DiaryState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "diary.topic.assign")?;
if let Some(ref title) = req.title {
if title.trim().is_empty() {
return Err(AppError::Validation("主题标题不能为空".to_string()));
}
}
let resp = TopicService::update_topic(
ctx.tenant_id,
ctx.user_id,
topic_id,
&req,
&state.db,
)
.await?;
Ok(Json(ApiResponse::ok(resp)))
}
#[utoipa::path(
patch,
path = "/api/v1/diary/topics/{topic_id}/deactivate",
params(("topic_id" = Uuid, Path, description = "主题ID")),
responses(
(status = 200, description = "停用成功", body = ApiResponse<TopicResp>),
(status = 401, description = "未授权"),
(status = 403, description = "权限不足"),
(status = 404, description = "主题不存在"),
),
security(("bearer_auth" = [])),
tag = "主题布置"
)]
/// PATCH /api/v1/diary/topics/:topic_id/deactivate
///
/// 停用主题。需要 `diary.topic.assign` 权限(仅布置者可停用)。
pub async fn deactivate_topic<S>(
State(state): State<DiaryState>,
Extension(ctx): Extension<TenantContext>,
Path(topic_id): Path<Uuid>,
) -> Result<Json<ApiResponse<TopicResp>>, AppError>
where
DiaryState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "diary.topic.assign")?;
let resp = TopicService::deactivate_topic(
ctx.tenant_id,
ctx.user_id,
topic_id,
&state.db,
)
.await?;
Ok(Json(ApiResponse::ok(resp)))
}