feat(diary): 手写引擎 + 日记 CRUD + 同步 API (Phase F3 + B2)
Flutter 手写引擎 (Phase F3): - stroke_model.dart: 笔画数据模型 (StrokePoint/Stroke/BrushType) - stroke_renderer.dart: perfect_freehand 渲染管线 + 四画笔参数 - handwriting_canvas.dart: Listener 输入 + 掌心抑制 + 去抖过滤 - editor_bloc.dart: BLoC 状态管理 + 撤销/重做 (50步) Rust 日记 CRUD + 同步 (Phase B2): - journal_service.rs: CRUD + 软删除 + 分页列表 + 事件发布 - sync_service.rs: 版本号同步 + 冲突检测 - journal_handler.rs: 5个API端点 + utoipa注解 + 权限守卫 - sync_handler.rs: 同步API端点 - error.rs: From<DiaryError> for AppError + 8个单元测试 - 路由注册: /diary/journals + /diary/sync 验证: - cargo check: 0 error - cargo test: 433 测试全通过 - flutter analyze: 1 warning (unused private param)
This commit is contained in:
@@ -4,6 +4,8 @@ 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}")]
|
||||
@@ -35,8 +37,37 @@ pub enum DiaryError {
|
||||
|
||||
#[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,
|
||||
@@ -57,6 +88,7 @@ impl IntoResponse for DiaryError {
|
||||
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 {
|
||||
@@ -73,3 +105,89 @@ impl From<sea_orm::DbErr> for DiaryError {
|
||||
DiaryError::Internal(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user