- create_class: 班级创建 + 老师成员插入包裹在 db.transaction() 中 - join_class: 成员插入 + member_count 更新包裹在事务中 - delete_child_data: PIPL 删除权 — 逐条软删除包裹在事务中(避免部分删除) - DiaryError: 添加 From<TransactionError<DiaryError>> 支持事务闭包 审计 ID: B-07, B-11, 8a-C03
206 lines
6.7 KiB
Rust
206 lines
6.7 KiB
Rust
// erp-diary 错误类型
|
||
|
||
use axum::http::StatusCode;
|
||
use axum::response::{IntoResponse, Response};
|
||
use serde::Serialize;
|
||
|
||
use erp_core::error::AppError;
|
||
|
||
#[derive(Debug, thiserror::Error)]
|
||
pub enum DiaryError {
|
||
#[error("日记未找到: {0}")]
|
||
NotFound(String),
|
||
|
||
#[error("版本冲突: 本地版本 {local}, 服务端版本 {server}")]
|
||
VersionConflict { local: i32, server: i32 },
|
||
|
||
#[error("班级码无效")]
|
||
InvalidClassCode,
|
||
|
||
#[error("班级码已过期")]
|
||
ClassCodeExpired,
|
||
|
||
#[error("班级码尝试次数过多,请 {lockout_minutes} 分钟后重试")]
|
||
ClassCodeLocked { lockout_minutes: u32 },
|
||
|
||
#[error("无权限执行此操作")]
|
||
Forbidden,
|
||
|
||
#[error("内容安全检查未通过")]
|
||
ContentSafetyViolation,
|
||
|
||
#[error("同步失败: {0}")]
|
||
SyncFailed(String),
|
||
|
||
#[error("{0}")]
|
||
BadRequest(String),
|
||
|
||
#[error("内部错误: {0}")]
|
||
Internal(String),
|
||
|
||
#[error("{0}")]
|
||
Validation(String),
|
||
}
|
||
|
||
/// DiaryError -> AppError 转换
|
||
///
|
||
/// Handler 层统一返回 AppError,Service 层统一返回 DiaryError。
|
||
/// 这个 impl 让 Handler 中的 `?` 操作符自动完成转换。
|
||
impl From<DiaryError> for AppError {
|
||
fn from(err: DiaryError) -> Self {
|
||
match err {
|
||
DiaryError::NotFound(msg) => AppError::NotFound(msg),
|
||
DiaryError::VersionConflict { .. } => AppError::VersionMismatch,
|
||
DiaryError::InvalidClassCode | DiaryError::ClassCodeExpired => {
|
||
AppError::Validation(err.to_string())
|
||
}
|
||
DiaryError::ClassCodeLocked { .. } => AppError::TooManyRequests,
|
||
DiaryError::Forbidden => AppError::Forbidden("权限不足".to_string()),
|
||
DiaryError::ContentSafetyViolation => AppError::Validation(err.to_string()),
|
||
DiaryError::SyncFailed(_) => AppError::Internal(err.to_string()),
|
||
DiaryError::BadRequest(msg) => AppError::Validation(msg),
|
||
DiaryError::Internal(_) => AppError::Internal(err.to_string()),
|
||
DiaryError::Validation(msg) => AppError::Validation(msg),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Diary 模块 Result 类型别名
|
||
pub type DiaryResult<T> = Result<T, DiaryError>;
|
||
|
||
#[derive(Serialize)]
|
||
struct ErrorBody {
|
||
error: String,
|
||
message: String,
|
||
}
|
||
|
||
impl IntoResponse for DiaryError {
|
||
fn into_response(self) -> Response {
|
||
let (status, message) = match &self {
|
||
DiaryError::NotFound(_) => (StatusCode::NOT_FOUND, self.to_string()),
|
||
DiaryError::VersionConflict { .. } => (StatusCode::CONFLICT, self.to_string()),
|
||
DiaryError::InvalidClassCode | DiaryError::ClassCodeExpired => {
|
||
(StatusCode::BAD_REQUEST, self.to_string())
|
||
}
|
||
DiaryError::ClassCodeLocked { .. } => (StatusCode::TOO_MANY_REQUESTS, self.to_string()),
|
||
DiaryError::Forbidden => (StatusCode::FORBIDDEN, self.to_string()),
|
||
DiaryError::ContentSafetyViolation => (StatusCode::BAD_REQUEST, self.to_string()),
|
||
DiaryError::SyncFailed(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()),
|
||
DiaryError::BadRequest(_) => (StatusCode::BAD_REQUEST, self.to_string()),
|
||
DiaryError::Internal(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()),
|
||
DiaryError::Validation(_) => (StatusCode::BAD_REQUEST, self.to_string()),
|
||
};
|
||
|
||
let body = ErrorBody {
|
||
error: format!("diary.{}", status.as_u16()),
|
||
message,
|
||
};
|
||
|
||
(status, axum::Json(body)).into_response()
|
||
}
|
||
}
|
||
|
||
impl From<sea_orm::DbErr> for DiaryError {
|
||
fn from(err: sea_orm::DbErr) -> Self {
|
||
DiaryError::Internal(err.to_string())
|
||
}
|
||
}
|
||
|
||
/// 支持 db.transaction() 闭包的错误转换
|
||
impl From<sea_orm::TransactionError<DiaryError>> for DiaryError {
|
||
fn from(err: sea_orm::TransactionError<DiaryError>) -> Self {
|
||
match err {
|
||
sea_orm::TransactionError::Connection(db_err) => {
|
||
DiaryError::Internal(db_err.to_string())
|
||
}
|
||
sea_orm::TransactionError::Transaction(inner) => inner,
|
||
}
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use erp_core::error::AppError;
|
||
|
||
#[test]
|
||
fn diary_error_not_found_maps_to_app_not_found() {
|
||
let app: AppError = DiaryError::NotFound("journal-123".to_string()).into();
|
||
match app {
|
||
AppError::NotFound(msg) => assert_eq!(msg, "journal-123"),
|
||
other => panic!("Expected NotFound, got {:?}", other),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn diary_error_version_conflict_maps_to_version_mismatch() {
|
||
let app: AppError = DiaryError::VersionConflict {
|
||
local: 1,
|
||
server: 2,
|
||
}
|
||
.into();
|
||
match app {
|
||
AppError::VersionMismatch => {}
|
||
other => panic!("Expected VersionMismatch, got {:?}", other),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn diary_error_forbidden_maps_to_app_forbidden() {
|
||
let app: AppError = DiaryError::Forbidden.into();
|
||
match app {
|
||
AppError::Forbidden(_) => {}
|
||
other => panic!("Expected Forbidden, got {:?}", other),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn diary_error_internal_maps_to_app_internal() {
|
||
let app: AppError = DiaryError::Internal("db error".to_string()).into();
|
||
match app {
|
||
AppError::Internal(_) => {}
|
||
other => panic!("Expected Internal, got {:?}", other),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn diary_error_validation_maps_to_app_validation() {
|
||
let app: AppError = DiaryError::Validation("标题不能为空".to_string()).into();
|
||
match app {
|
||
AppError::Validation(msg) => assert_eq!(msg, "标题不能为空"),
|
||
other => panic!("Expected Validation, got {:?}", other),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn diary_error_bad_request_maps_to_app_validation() {
|
||
let app: AppError = DiaryError::BadRequest("参数错误".to_string()).into();
|
||
match app {
|
||
AppError::Validation(msg) => assert_eq!(msg, "参数错误"),
|
||
other => panic!("Expected Validation, got {:?}", other),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn diary_error_class_code_locked_maps_to_too_many_requests() {
|
||
let app: AppError = DiaryError::ClassCodeLocked {
|
||
lockout_minutes: 30,
|
||
}
|
||
.into();
|
||
match app {
|
||
AppError::TooManyRequests => {}
|
||
other => panic!("Expected TooManyRequests, got {:?}", other),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn db_err_maps_to_diary_internal() {
|
||
let err = sea_orm::DbErr::Custom("connection failed".to_string());
|
||
let diary_err: DiaryError = err.into();
|
||
match diary_err {
|
||
DiaryError::Internal(msg) => assert!(msg.contains("connection failed")),
|
||
other => panic!("Expected Internal, got {:?}", other),
|
||
}
|
||
}
|
||
}
|