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:
iven
2026-06-01 00:36:05 +08:00
parent ee5ce9bc56
commit d0653614e0
12 changed files with 1727 additions and 4 deletions

View File

@@ -0,0 +1,53 @@
// 日记同步 API 处理器
use axum::extract::{Extension, FromRef, State};
use axum::response::Json;
use erp_core::error::AppError;
use erp_core::rbac::require_permission;
use erp_core::types::{ApiResponse, TenantContext};
use crate::dto::SyncReq;
use crate::dto::SyncResp;
use crate::service::sync_service::SyncService;
use crate::state::DiaryState;
#[utoipa::path(
post,
path = "/api/v1/diary/sync",
request_body = SyncReq,
responses(
(status = 200, description = "同步成功", body = ApiResponse<SyncResp>),
(status = 401, description = "未授权"),
(status = 403, description = "权限不足"),
(status = 409, description = "存在版本冲突"),
),
security(("bearer_auth" = [])),
tag = "日记同步"
)]
/// POST /api/v1/diary/sync
///
/// 日记同步端点。客户端提交本地变更,服务端返回服务端变更和冲突列表。
/// 需要 `diary.journal.read` 权限。
pub async fn sync_journals<S>(
State(state): State<DiaryState>,
Extension(ctx): Extension<TenantContext>,
Json(req): Json<SyncReq>,
) -> Result<Json<ApiResponse<SyncResp>>, AppError>
where
DiaryState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "diary.journal.read")?;
let resp = SyncService::sync(
ctx.tenant_id,
ctx.user_id,
req.last_sync_time,
req.changes,
&state.db,
)
.await?;
Ok(Json(ApiResponse::ok(resp)))
}