fix(voice): recover empty failed lessons

This commit is contained in:
zhangheng
2026-07-22 21:19:22 +08:00
parent 55084a4784
commit 80e670d72b
8 changed files with 226 additions and 13 deletions

View File

@@ -38,6 +38,7 @@ pub fn router() -> OpenApiRouter<SharedState> {
.routes(routes!(finish_lesson_session))
.routes(routes!(mark_lesson_applied))
.routes(routes!(retry_audio_segment))
.routes(routes!(discard_empty_failed_lesson))
.routes(routes!(discard_audio_segment))
.routes(routes!(create_feedback_summary_run))
.routes(routes!(get_feedback_summary_run))
@@ -796,6 +797,67 @@ async fn retry_audio_segment(
}))
}
#[utoipa::path(
delete,
path = "/api/v1/feedback-lessons/{lesson_id}",
tag = "语音反馈",
summary = "删除未保留音频的失败课堂",
description = "删除没有任何录音片段、因而无法重试转录的失败课堂记录。",
params(
("Authorization" = String, Header, description = "Bearer 会话令牌", example = "Bearer <access-token>"),
("lesson_id" = Uuid, Path, description = "失败课堂录音 ID")
),
responses(
(status = 204, description = "空失败课堂已删除"),
(status = 404, description = "课堂不存在、不属于当前用户或仍包含录音片段", body = ErrorBody),
CommonApiErrors
)
)]
async fn discard_empty_failed_lesson(
State(state): State<SharedState>,
headers: HeaderMap,
Path(lesson_id): Path<Uuid>,
) -> Result<StatusCode, ApiError> {
let owner_id = state
.auth
.authenticate(state.pool.as_ref(), &headers)
.await?
.user_id;
let mut transaction = pool(&state)?.begin().await?;
let session_id = sqlx::query_scalar::<_, Uuid>(
"SELECT f.id FROM lesson_sessions l \
JOIN feedback_sessions f ON f.id = l.feedback_session_id \
WHERE l.id = $1 AND f.owner_id = $2 AND f.status = 'active' \
AND l.status = 'failed' \
AND NOT EXISTS (SELECT 1 FROM audio_segments s WHERE s.lesson_session_id = l.id) \
FOR UPDATE OF l",
)
.bind(lesson_id)
.bind(owner_id)
.fetch_optional(&mut *transaction)
.await?
.ok_or(ApiError::NotFound)?;
sqlx::query("DELETE FROM lesson_sessions WHERE id = $1")
.bind(lesson_id)
.execute(&mut *transaction)
.await?;
sqlx::query("UPDATE feedback_sessions SET updated_at = now() WHERE id = $1")
.bind(session_id)
.execute(&mut *transaction)
.await?;
transaction.commit().await?;
tracing::info!(
event = "empty_failed_lesson_discarded",
feedback_session_id = %session_id,
lesson_id = %lesson_id,
status = "discarded",
"empty failed lesson discarded"
);
Ok(StatusCode::NO_CONTENT)
}
#[utoipa::path(
delete,
path = "/api/v1/audio-segments/{segment_id}",

View File

@@ -924,7 +924,8 @@ mod tests {
.filter(|operation| operation.get("responses").is_some())
.collect::<Vec<_>>();
assert_eq!(operations.len(), 27);
assert_eq!(operations.len(), 28);
assert!(paths.contains_key("/api/v1/feedback-lessons/{lesson_id}"));
assert!(paths.contains_key("/api/v1/feedback-sessions/{session_id}/summary-runs"));
assert!(paths.contains_key("/api/v1/feedback-summary-runs/{run_id}"));
assert!(paths.contains_key("/api/v1/feedback-summary-runs/{run_id}/apply"));