// 班级 API 处理器 — 创建班级、加入班级、查询班级 use axum::extract::{Extension, FromRef, Path, State}; use axum::http::StatusCode; 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::{ClassMemberResp, ClassResp, CreateClassReq, JoinClassReq, ResetClassCodeResp, UpdateClassReq}; use crate::service::class_service::ClassService; use crate::state::DiaryState; #[utoipa::path( post, path = "/api/v1/diary/classes", request_body = CreateClassReq, responses( (status = 201, description = "创建成功", body = ApiResponse), (status = 400, description = "验证失败"), (status = 401, description = "未授权"), (status = 403, description = "权限不足"), ), security(("bearer_auth" = [])), tag = "班级管理" )] /// POST /api/v1/diary/classes /// /// 创建班级。需要 `diary.class.manage` 权限(老师角色)。 pub async fn create_class( State(state): State, Extension(ctx): Extension, Json(req): Json, ) -> Result<(StatusCode, Json>), AppError> where DiaryState: FromRef, S: Clone + Send + Sync + 'static, { req.validate().map_err(|e| AppError::Validation(e.to_string()))?; require_permission(&ctx, "diary.class.manage")?; if req.name.trim().is_empty() { return Err(AppError::Validation("班级名称不能为空".to_string())); } let resp = ClassService::create_class( ctx.tenant_id, ctx.user_id, req.name, req.school_name, &state.db, &state.event_bus, ) .await?; Ok((StatusCode::CREATED, Json(ApiResponse::ok(resp)))) } #[utoipa::path( post, path = "/api/v1/diary/classes/join", request_body = JoinClassReq, responses( (status = 201, description = "加入成功", body = ApiResponse), (status = 400, description = "班级码无效或已过期"), (status = 401, description = "未授权"), (status = 403, description = "权限不足"), ), security(("bearer_auth" = [])), tag = "班级管理" )] /// POST /api/v1/diary/classes/join /// /// 通过班级码加入班级。需要 `diary.journal.create` 权限(学生使用此权限加入)。 pub async fn join_class( State(state): State, Extension(ctx): Extension, Json(req): Json, ) -> Result<(StatusCode, Json>), AppError> where DiaryState: FromRef, S: Clone + Send + Sync + 'static, { req.validate().map_err(|e| AppError::Validation(e.to_string()))?; req.validate_code().map_err(AppError::Validation)?; require_permission(&ctx, "diary.journal.create")?; if req.class_code.trim().is_empty() { return Err(AppError::Validation("班级码不能为空".to_string())); } let resp = ClassService::join_class( ctx.tenant_id, ctx.user_id, req.class_code, None, // 昵称暂不通过此接口传递 &state.db, state.redis.as_ref(), &state.event_bus, ) .await?; Ok((StatusCode::CREATED, Json(ApiResponse::ok(resp)))) } #[utoipa::path( get, path = "/api/v1/diary/classes/{id}", params(("id" = Uuid, Path, description = "班级ID")), responses( (status = 200, description = "成功", body = ApiResponse), (status = 401, description = "未授权"), (status = 403, description = "权限不足"), (status = 404, description = "班级不存在"), ), security(("bearer_auth" = [])), tag = "班级管理" )] /// GET /api/v1/diary/classes/:id /// /// 获取班级详情。需要 `diary.journal.read` 权限。 pub async fn get_class( State(state): State, Extension(ctx): Extension, Path(id): Path, ) -> Result>, AppError> where DiaryState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "diary.journal.read")?; let resp = ClassService::get_class(ctx.tenant_id, id, &state.db).await?; Ok(Json(ApiResponse::ok(resp))) } #[utoipa::path( get, path = "/api/v1/diary/classes/{id}/members", params(("id" = Uuid, Path, description = "班级ID")), responses( (status = 200, description = "成功", body = ApiResponse>), (status = 401, description = "未授权"), (status = 403, description = "权限不足"), (status = 404, description = "班级不存在"), ), security(("bearer_auth" = [])), tag = "班级管理" )] /// GET /api/v1/diary/classes/:id/members /// /// 获取班级成员列表。需要 `diary.journal.read` 权限。 pub async fn list_members( State(state): State, Extension(ctx): Extension, Path(id): Path, ) -> Result>>, AppError> where DiaryState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "diary.journal.read")?; let resp = ClassService::list_members(ctx.tenant_id, id, &state.db).await?; Ok(Json(ApiResponse::ok(resp))) } #[utoipa::path( get, path = "/api/v1/diary/classes/my", responses( (status = 200, description = "成功", body = ApiResponse>), (status = 401, description = "未授权"), (status = 403, description = "权限不足"), ), security(("bearer_auth" = [])), tag = "班级管理" )] /// GET /api/v1/diary/classes/my /// /// 获取当前用户加入的所有班级。需要 `diary.journal.read` 权限。 pub async fn my_classes( State(state): State, Extension(ctx): Extension, ) -> Result>>, AppError> where DiaryState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "diary.journal.read")?; let resp = ClassService::my_classes(ctx.tenant_id, ctx.user_id, &state.db).await?; Ok(Json(ApiResponse::ok(resp))) } #[utoipa::path( get, path = "/api/v1/diary/classes/all", responses( (status = 200, description = "成功", body = ApiResponse>), (status = 401, description = "未授权"), (status = 403, description = "权限不足"), ), security(("bearer_auth" = [])), tag = "班级管理" )] /// GET /api/v1/diary/classes/all /// /// 获取租户下所有班级(管理端用)。需要 `diary.class.manage` 权限。 pub async fn list_all_classes( State(state): State, Extension(ctx): Extension, ) -> Result>>, AppError> where DiaryState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "diary.class.manage")?; let resp = ClassService::list_all_classes(ctx.tenant_id, &state.db).await?; Ok(Json(ApiResponse::ok(resp))) } #[utoipa::path( put, path = "/api/v1/diary/classes/{id}", params(("id" = Uuid, Path, description = "班级ID")), request_body = UpdateClassReq, 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/classes/:id /// /// 更新班级信息。需要 `diary.class.manage` 权限(仅班级创建者可编辑)。 pub async fn update_class( State(state): State, Extension(ctx): Extension, Path(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.class.manage")?; if let Some(ref name) = req.name { if name.trim().is_empty() { return Err(AppError::Validation("班级名称不能为空".to_string())); } } let resp = ClassService::update_class( ctx.tenant_id, ctx.user_id, id, req.name, req.school_name, req.version, &state.db, ) .await?; Ok(Json(ApiResponse::ok(resp))) } #[utoipa::path( patch, path = "/api/v1/diary/classes/{id}/deactivate", params(("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/classes/:id/deactivate /// /// 停用班级。需要 `diary.class.manage` 权限(仅班级创建者可停用)。 pub async fn deactivate_class( State(state): State, Extension(ctx): Extension, Path(id): Path, ) -> Result>, AppError> where DiaryState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "diary.class.manage")?; let resp = ClassService::deactivate_class( ctx.tenant_id, ctx.user_id, id, &state.db, &state.event_bus, ) .await?; Ok(Json(ApiResponse::ok(resp))) } #[utoipa::path( post, path = "/api/v1/diary/classes/{id}/reset-code", params(("id" = Uuid, Path, description = "班级ID")), responses( (status = 200, description = "重置成功", body = ApiResponse), (status = 401, description = "未授权"), (status = 403, description = "权限不足"), (status = 404, description = "班级不存在"), ), security(("bearer_auth" = [])), tag = "班级管理" )] /// POST /api/v1/diary/classes/:id/reset-code /// /// 重置班级码。需要 `diary.class.manage` 权限(仅班级创建者可重置)。 pub async fn reset_class_code( State(state): State, Extension(ctx): Extension, Path(id): Path, ) -> Result>, AppError> where DiaryState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "diary.class.manage")?; let resp = ClassService::reset_class_code(ctx.tenant_id, ctx.user_id, id, &state.db).await?; Ok(Json(ApiResponse::ok(resp))) }