// 日记同步 API 处理器 use axum::extract::{Extension, FromRef, State}; use axum::response::Json; use validator::Validate; 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), (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( State(state): State, Extension(ctx): Extension, Json(req): Json, ) -> Result>, AppError> where DiaryState: FromRef, S: Clone + Send + Sync + 'static, { req.validate().map_err(|e| AppError::Validation(e.to_string()))?; req.validate_changes_data().map_err(AppError::Validation)?; 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))) }