DTO 验证规则: - CreateJournalReq: title 1-200, tags ≤20 - UpdateJournalReq: title 1-200, tags ≤20 - CreateClassReq: name 1-50, school_name ≤100 - JoinClassReq: class_code = 6位 - UpdateClassReq: name 1-50, school_name ≤100 - SyncReq: changes ≤100 条 - CreateTopicReq: title 1-200, description ≤2000 - UpdateTopicReq: title 1-200, description ≤2000 - CreateCommentReq: content 1-1000 - CreateStickerPackReq: name 1-50, description ≤500 - UpdateStickerPackReq: name 1-50, description ≤500 - CreateStickerReq: name 1-30, image_url 1-500 - BindChildReq/DeleteChildDataReq: Validate derive (Uuid 已由 serde 验证) Handler 调用: validate() 放在 require_permission() 之前(先验证输入再检查权限) 审计 ID: 5a-C01, 5a-C02, 5a-C03
181 lines
5.5 KiB
Rust
181 lines
5.5 KiB
Rust
// 主题布置 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<TopicResp>),
|
|
(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<S>(
|
|
State(state): State<DiaryState>,
|
|
Extension(ctx): Extension<TenantContext>,
|
|
Path(class_id): Path<Uuid>,
|
|
Json(req): Json<CreateTopicReq>,
|
|
) -> Result<Json<ApiResponse<TopicResp>>, AppError>
|
|
where
|
|
DiaryState: FromRef<S>,
|
|
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<Vec<TopicResp>>),
|
|
(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<S>(
|
|
State(state): State<DiaryState>,
|
|
Extension(ctx): Extension<TenantContext>,
|
|
Path(class_id): Path<Uuid>,
|
|
) -> Result<Json<ApiResponse<Vec<TopicResp>>>, AppError>
|
|
where
|
|
DiaryState: FromRef<S>,
|
|
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<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,
|
|
{
|
|
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<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)))
|
|
}
|