// 主题布置 API 处理器 — 老师布置/查询主题 use axum::extract::{Extension, FromRef, Path, State}; use axum::response::Json; use uuid::Uuid; use validator::Validate; use erp_core::error::AppError; use erp_core::rbac::require_permission; use erp_core::types::{ApiResponse, TenantContext}; use crate::dto::{CreateTopicReq, TopicResp, UpdateTopicReq}; use crate::service::topic_service::TopicService; use crate::state::DiaryState; #[utoipa::path( post, path = "/api/v1/diary/classes/{class_id}/topics", params(("class_id" = Uuid, Path, description = "班级ID")), request_body = CreateTopicReq, responses( (status = 200, description = "布置成功", body = ApiResponse), (status = 400, description = "验证失败"), (status = 401, description = "未授权"), (status = 403, description = "权限不足"), (status = 404, description = "班级不存在"), ), security(("bearer_auth" = [])), tag = "主题布置" )] /// POST /api/v1/diary/classes/:class_id/topics /// /// 布置日记主题。需要 `diary.topic.assign` 权限(老师角色)。 pub async fn assign_topic( State(state): State, Extension(ctx): Extension, Path(class_id): Path, Json(req): Json, ) -> Result>, AppError> where DiaryState: FromRef, S: Clone + Send + Sync + 'static, { req.validate().map_err(|e| AppError::Validation(e.to_string()))?; require_permission(&ctx, "diary.topic.assign")?; if req.title.trim().is_empty() { return Err(AppError::Validation("主题标题不能为空".to_string())); } let resp = TopicService::assign_topic( ctx.tenant_id, ctx.user_id, class_id, &req, &state.db, &state.event_bus, ) .await?; Ok(Json(ApiResponse::ok(resp))) } #[utoipa::path( get, path = "/api/v1/diary/classes/{class_id}/topics", params(("class_id" = Uuid, Path, description = "班级ID")), responses( (status = 200, description = "成功", body = ApiResponse>), (status = 401, description = "未授权"), (status = 403, description = "权限不足"), ), security(("bearer_auth" = [])), tag = "主题布置" )] /// GET /api/v1/diary/classes/:class_id/topics /// /// 获取班级主题列表。需要 `diary.journal.read` 权限。 pub async fn list_topics( State(state): State, Extension(ctx): Extension, Path(class_id): Path, ) -> Result>>, AppError> where DiaryState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "diary.journal.read")?; 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), (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( State(state): State, Extension(ctx): Extension, Path(topic_id): Path, Json(req): Json, ) -> Result>, AppError> where DiaryState: FromRef, S: Clone + Send + Sync + 'static, { req.validate().map_err(|e| AppError::Validation(e.to_string()))?; 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), (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( State(state): State, Extension(ctx): Extension, Path(topic_id): Path, ) -> Result>, AppError> where DiaryState: FromRef, 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))) }