feat: add WeChat session authentication
This commit is contained in:
@@ -183,7 +183,7 @@ struct AudioSegmentUpload {
|
||||
summary = "读取进行中的反馈批次",
|
||||
description = "读取当前用户指定学生下尚未保存的反馈批次;profile_id 留空时读取不关联学生的批次。",
|
||||
params(
|
||||
("X-User-Id" = Uuid, Header, description = "开发阶段用户 ID", example = json!("5a4da7f0-d70c-465f-bcab-124c504aa9f0")),
|
||||
("Authorization" = String, Header, description = "Bearer 会话令牌", example = "Bearer <access-token>"),
|
||||
("profile_id" = Option<Uuid>, Query, description = "学生档案 ID;留空表示不关联学生")
|
||||
),
|
||||
responses(
|
||||
@@ -196,7 +196,11 @@ async fn get_active_feedback_session(
|
||||
headers: HeaderMap,
|
||||
Query(query): Query<ActiveSessionQuery>,
|
||||
) -> Result<Json<Option<FeedbackSessionResponse>>, ApiError> {
|
||||
let owner_id = current_user(&headers)?;
|
||||
let owner_id = state
|
||||
.auth
|
||||
.authenticate(state.pool.as_ref(), &headers)
|
||||
.await?
|
||||
.user_id;
|
||||
let session = sqlx::query_as::<_, FeedbackSessionRow>(
|
||||
"SELECT id, profile_id, feedback_date, content, status, created_at, updated_at \
|
||||
FROM feedback_sessions \
|
||||
@@ -222,7 +226,7 @@ async fn get_active_feedback_session(
|
||||
tag = "语音反馈",
|
||||
summary = "创建或恢复反馈批次",
|
||||
description = "为学生创建一个反馈批次;已有未保存批次时直接恢复,并同步最新反馈日期。",
|
||||
params(("X-User-Id" = Uuid, Header, description = "开发阶段用户 ID", example = json!("5a4da7f0-d70c-465f-bcab-124c504aa9f0"))),
|
||||
params(("Authorization" = String, Header, description = "Bearer 会话令牌", example = "Bearer <access-token>")),
|
||||
request_body(content = FeedbackSessionInput, content_type = "application/json"),
|
||||
responses(
|
||||
(status = 200, description = "创建或恢复后的反馈批次", body = FeedbackSessionResponse),
|
||||
@@ -235,7 +239,11 @@ async fn ensure_feedback_session(
|
||||
headers: HeaderMap,
|
||||
Json(input): Json<FeedbackSessionInput>,
|
||||
) -> Result<Json<FeedbackSessionResponse>, ApiError> {
|
||||
let owner_id = current_user(&headers)?;
|
||||
let owner_id = state
|
||||
.auth
|
||||
.authenticate(state.pool.as_ref(), &headers)
|
||||
.await?
|
||||
.user_id;
|
||||
let pool = pool(&state)?;
|
||||
validate_draft_content(&input.content)?;
|
||||
if let Some(profile_id) = input.profile_id {
|
||||
@@ -284,7 +292,7 @@ async fn ensure_feedback_session(
|
||||
summary = "自动保存反馈草稿",
|
||||
description = "在用户编辑反馈正文或日期时自动保存进行中的反馈批次。",
|
||||
params(
|
||||
("X-User-Id" = Uuid, Header, description = "开发阶段用户 ID", example = json!("5a4da7f0-d70c-465f-bcab-124c504aa9f0")),
|
||||
("Authorization" = String, Header, description = "Bearer 会话令牌", example = "Bearer <access-token>"),
|
||||
("session_id" = Uuid, Path, description = "反馈批次 ID")
|
||||
),
|
||||
request_body(content = FeedbackSessionUpdateInput, content_type = "application/json"),
|
||||
@@ -300,7 +308,11 @@ async fn update_feedback_session(
|
||||
Path(session_id): Path<Uuid>,
|
||||
Json(input): Json<FeedbackSessionUpdateInput>,
|
||||
) -> Result<Json<FeedbackSessionResponse>, ApiError> {
|
||||
let owner_id = current_user(&headers)?;
|
||||
let owner_id = state
|
||||
.auth
|
||||
.authenticate(state.pool.as_ref(), &headers)
|
||||
.await?
|
||||
.user_id;
|
||||
validate_draft_content(&input.content)?;
|
||||
let session = sqlx::query_as::<_, FeedbackSessionRow>(
|
||||
"UPDATE feedback_sessions SET feedback_date = $3, content = $4, updated_at = now() \
|
||||
@@ -324,7 +336,7 @@ async fn update_feedback_session(
|
||||
summary = "开始一节课堂录音",
|
||||
description = "在反馈批次中自动创建下一节课堂录音。",
|
||||
params(
|
||||
("X-User-Id" = Uuid, Header, description = "开发阶段用户 ID", example = json!("5a4da7f0-d70c-465f-bcab-124c504aa9f0")),
|
||||
("Authorization" = String, Header, description = "Bearer 会话令牌", example = "Bearer <access-token>"),
|
||||
("session_id" = Uuid, Path, description = "反馈批次 ID")
|
||||
),
|
||||
responses(
|
||||
@@ -338,7 +350,11 @@ async fn create_lesson_session(
|
||||
headers: HeaderMap,
|
||||
Path(session_id): Path<Uuid>,
|
||||
) -> Result<(axum::http::StatusCode, Json<LessonCreatedResponse>), ApiError> {
|
||||
let owner_id = current_user(&headers)?;
|
||||
let owner_id = state
|
||||
.auth
|
||||
.authenticate(state.pool.as_ref(), &headers)
|
||||
.await?
|
||||
.user_id;
|
||||
assert_session_owner(pool(&state)?, owner_id, session_id).await?;
|
||||
sqlx::query(
|
||||
"DELETE FROM lesson_sessions \
|
||||
@@ -377,7 +393,7 @@ async fn create_lesson_session(
|
||||
summary = "上传并转录录音片段",
|
||||
description = "上传单个录音片段并加入后端转录队列。接口保存音频后立即返回,页面通过反馈批次状态无感获取转录结果。",
|
||||
params(
|
||||
("X-User-Id" = Uuid, Header, description = "开发阶段用户 ID", example = json!("5a4da7f0-d70c-465f-bcab-124c504aa9f0")),
|
||||
("Authorization" = String, Header, description = "Bearer 会话令牌", example = "Bearer <access-token>"),
|
||||
("lesson_id" = Uuid, Path, description = "课堂录音 ID")
|
||||
),
|
||||
request_body(content = AudioSegmentUpload, content_type = "multipart/form-data"),
|
||||
@@ -393,7 +409,11 @@ async fn upload_audio_segment(
|
||||
Path(lesson_id): Path<Uuid>,
|
||||
mut multipart: Multipart,
|
||||
) -> Result<(axum::http::StatusCode, Json<AudioSegmentResponse>), ApiError> {
|
||||
let owner_id = current_user(&headers)?;
|
||||
let owner_id = state
|
||||
.auth
|
||||
.authenticate(state.pool.as_ref(), &headers)
|
||||
.await?
|
||||
.user_id;
|
||||
let session_id = assert_lesson_owner(pool(&state)?, owner_id, lesson_id).await?;
|
||||
let mut sequence = None;
|
||||
let mut duration_ms = None;
|
||||
@@ -506,7 +526,7 @@ async fn upload_audio_segment(
|
||||
summary = "结束一节课堂录音",
|
||||
description = "汇总该节课的片段状态和转录文本;短录音会标记为可直接加入反馈。",
|
||||
params(
|
||||
("X-User-Id" = Uuid, Header, description = "开发阶段用户 ID", example = json!("5a4da7f0-d70c-465f-bcab-124c504aa9f0")),
|
||||
("Authorization" = String, Header, description = "Bearer 会话令牌", example = "Bearer <access-token>"),
|
||||
("lesson_id" = Uuid, Path, description = "课堂录音 ID")
|
||||
),
|
||||
responses(
|
||||
@@ -520,7 +540,11 @@ async fn finish_lesson_session(
|
||||
headers: HeaderMap,
|
||||
Path(lesson_id): Path<Uuid>,
|
||||
) -> Result<Json<FinishedLessonResponse>, ApiError> {
|
||||
let owner_id = current_user(&headers)?;
|
||||
let owner_id = state
|
||||
.auth
|
||||
.authenticate(state.pool.as_ref(), &headers)
|
||||
.await?
|
||||
.user_id;
|
||||
let session_id = assert_lesson_owner(pool(&state)?, owner_id, lesson_id).await?;
|
||||
let segments = sqlx::query_as::<_, (String, i32, Option<String>)>(
|
||||
"SELECT status, duration_ms, transcript FROM audio_segments \
|
||||
@@ -571,7 +595,7 @@ async fn finish_lesson_session(
|
||||
summary = "标记短录音已加入反馈",
|
||||
description = "短录音文字直接加入反馈内容后,标记该节课无需再次汇总。",
|
||||
params(
|
||||
("X-User-Id" = Uuid, Header, description = "开发阶段用户 ID", example = json!("5a4da7f0-d70c-465f-bcab-124c504aa9f0")),
|
||||
("Authorization" = String, Header, description = "Bearer 会话令牌", example = "Bearer <access-token>"),
|
||||
("lesson_id" = Uuid, Path, description = "课堂录音 ID")
|
||||
),
|
||||
request_body(content = MarkLessonAppliedInput, content_type = "application/json"),
|
||||
@@ -587,7 +611,11 @@ async fn mark_lesson_applied(
|
||||
Path(lesson_id): Path<Uuid>,
|
||||
Json(input): Json<MarkLessonAppliedInput>,
|
||||
) -> Result<axum::http::StatusCode, ApiError> {
|
||||
let owner_id = current_user(&headers)?;
|
||||
let owner_id = state
|
||||
.auth
|
||||
.authenticate(state.pool.as_ref(), &headers)
|
||||
.await?
|
||||
.user_id;
|
||||
validate_draft_content(&input.content)?;
|
||||
let pool = pool(&state)?;
|
||||
let session_id = assert_lesson_owner(pool, owner_id, lesson_id).await?;
|
||||
@@ -618,7 +646,7 @@ async fn mark_lesson_applied(
|
||||
summary = "重试失败的录音转录",
|
||||
description = "将后端保留的失败音频重新加入转录队列,接口立即返回。",
|
||||
params(
|
||||
("X-User-Id" = Uuid, Header, description = "开发阶段用户 ID", example = json!("5a4da7f0-d70c-465f-bcab-124c504aa9f0")),
|
||||
("Authorization" = String, Header, description = "Bearer 会话令牌", example = "Bearer <access-token>"),
|
||||
("segment_id" = Uuid, Path, description = "录音片段 ID")
|
||||
),
|
||||
responses(
|
||||
@@ -632,7 +660,11 @@ async fn retry_audio_segment(
|
||||
headers: HeaderMap,
|
||||
Path(segment_id): Path<Uuid>,
|
||||
) -> Result<Json<AudioSegmentResponse>, ApiError> {
|
||||
let owner_id = current_user(&headers)?;
|
||||
let owner_id = state
|
||||
.auth
|
||||
.authenticate(state.pool.as_ref(), &headers)
|
||||
.await?
|
||||
.user_id;
|
||||
let segment = sqlx::query_as::<_, (i32, i32, String, Uuid)>(
|
||||
"SELECT s.sequence, s.duration_ms, s.audio_path, l.id \
|
||||
FROM audio_segments s \
|
||||
@@ -684,7 +716,7 @@ async fn retry_audio_segment(
|
||||
summary = "生成汇总反馈",
|
||||
description = "将尚未处理的长录音或多节课堂转录与教师已有内容合并。未配置大模型时使用本地抽取式汇总。",
|
||||
params(
|
||||
("X-User-Id" = Uuid, Header, description = "开发阶段用户 ID", example = json!("5a4da7f0-d70c-465f-bcab-124c504aa9f0")),
|
||||
("Authorization" = String, Header, description = "Bearer 会话令牌", example = "Bearer <access-token>"),
|
||||
("session_id" = Uuid, Path, description = "反馈批次 ID")
|
||||
),
|
||||
request_body(content = GenerateFeedbackInput, content_type = "application/json"),
|
||||
@@ -700,7 +732,11 @@ async fn generate_feedback_content(
|
||||
Path(session_id): Path<Uuid>,
|
||||
Json(input): Json<GenerateFeedbackInput>,
|
||||
) -> Result<Json<GenerateFeedbackResponse>, ApiError> {
|
||||
let owner_id = current_user(&headers)?;
|
||||
let owner_id = state
|
||||
.auth
|
||||
.authenticate(state.pool.as_ref(), &headers)
|
||||
.await?
|
||||
.user_id;
|
||||
validate_draft_content(&input.existing_content)?;
|
||||
assert_session_owner(pool(&state)?, owner_id, session_id).await?;
|
||||
let pending_status = sqlx::query_as::<_, (bool, bool)>(
|
||||
@@ -867,14 +903,6 @@ async fn parse_multipart_i32(
|
||||
.map_err(|_| ApiError::BadRequest(format!("{name} must be an integer")))
|
||||
}
|
||||
|
||||
fn current_user(headers: &HeaderMap) -> Result<Uuid, ApiError> {
|
||||
let value = headers
|
||||
.get("x-user-id")
|
||||
.and_then(|header| header.to_str().ok())
|
||||
.ok_or(ApiError::Unauthorized)?;
|
||||
Uuid::parse_str(value).map_err(|_| ApiError::BadRequest("X-User-Id must be a UUID".to_owned()))
|
||||
}
|
||||
|
||||
fn pool(state: &AppState) -> Result<&PgPool, ApiError> {
|
||||
state.pool.as_ref().ok_or(ApiError::DatabaseUnavailable)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user