//! 配置迁移 HTTP 处理器 use axum::{ extract::{Extension, Path, Query, State}, http::StatusCode, Json, }; use crate::state::AppState; use crate::error::SaasResult; use crate::auth::types::AuthContext; use crate::auth::handlers::{check_permission, log_operation}; use crate::common::PaginatedResponse; use super::{types::*, service}; /// GET /api/v1/config/items?category=xxx&source=xxx&page=1&page_size=20 pub async fn list_config_items( State(state): State, Query(query): Query, _ctx: Extension, ) -> SaasResult>> { let filter_query = ConfigQuery { category: query.category.clone(), source: query.source.clone(), page: None, page_size: None, }; service::list_config_items(&state.db, &filter_query, query.page, query.page_size).await.map(Json) } /// GET /api/v1/config/items/:id pub async fn get_config_item( State(state): State, Path(id): Path, _ctx: Extension, ) -> SaasResult> { service::get_config_item(&state.db, &id).await.map(Json) } /// POST /api/v1/config/items (admin only) pub async fn create_config_item( State(state): State, Extension(ctx): Extension, Json(req): Json, ) -> SaasResult<(StatusCode, Json)> { check_permission(&ctx, "config:write")?; let item = service::create_config_item(&state.db, &req).await?; log_operation(&state.db, &ctx.account_id, "config.create", "config_item", &item.id, None, ctx.client_ip.as_deref()).await?; Ok((StatusCode::CREATED, Json(item))) } /// PATCH /api/v1/config/items/:id (admin only) pub async fn update_config_item( State(state): State, Path(id): Path, Extension(ctx): Extension, Json(req): Json, ) -> SaasResult> { check_permission(&ctx, "config:write")?; let item = service::update_config_item(&state.db, &id, &req).await?; log_operation(&state.db, &ctx.account_id, "config.update", "config_item", &id, None, ctx.client_ip.as_deref()).await?; Ok(Json(item)) } /// DELETE /api/v1/config/items/:id (admin only) pub async fn delete_config_item( State(state): State, Path(id): Path, Extension(ctx): Extension, ) -> SaasResult> { check_permission(&ctx, "config:write")?; service::delete_config_item(&state.db, &id).await?; log_operation(&state.db, &ctx.account_id, "config.delete", "config_item", &id, None, ctx.client_ip.as_deref()).await?; Ok(Json(serde_json::json!({"ok": true}))) } /// GET /api/v1/config/analysis pub async fn analyze_config( State(state): State, _ctx: Extension, ) -> SaasResult> { service::analyze_config(&state.db).await.map(Json) } /// POST /api/v1/config/seed (admin only) pub async fn seed_config( State(state): State, Extension(ctx): Extension, ) -> SaasResult> { check_permission(&ctx, "config:write")?; let count = service::seed_default_config_items(&state.db).await?; log_operation(&state.db, &ctx.account_id, "config.seed", "config_item", "batch", Some(serde_json::json!({"count": count})), ctx.client_ip.as_deref()).await?; Ok(Json(serde_json::json!({"created": count}))) } /// POST /api/v1/config/sync (需要 config:write 权限) pub async fn sync_config( State(state): State, Extension(ctx): Extension, Json(req): Json, ) -> SaasResult> { // 权限检查:仅 config:write 可推送配置 check_permission(&ctx, "config:write")?; let result = super::service::sync_config(&state.db, &ctx.account_id, &req).await?; // 审计日志 log_operation( &state.db, &ctx.account_id, "config.sync", "config", "batch", Some(serde_json::json!({ "client_fingerprint": req.client_fingerprint, "action": req.action, "config_count": req.config_keys.len(), })), ctx.client_ip.as_deref(), ).await.ok(); Ok(Json(result)) } /// POST /api/v1/config/diff /// 计算客户端与 SaaS 端的配置差异 (不修改数据) pub async fn config_diff( State(state): State, Extension(ctx): Extension, Json(req): Json, ) -> SaasResult> { // diff 操作虽然不修改数据,但涉及敏感配置信息,仍需认证用户 service::compute_config_diff(&state.db, &req).await.map(Json) } /// GET /api/v1/config/sync-logs?page=1&page_size=20 pub async fn list_sync_logs( State(state): State, Extension(ctx): Extension, Query(params): Query>, ) -> SaasResult>> { let page: u32 = params.get("page").and_then(|v| v.parse().ok()).unwrap_or(1).max(1); let page_size: u32 = params.get("page_size").and_then(|v| v.parse().ok()).unwrap_or(20).min(100); service::list_sync_logs(&state.db, &ctx.account_id, page, page_size).await.map(Json) } /// GET /api/v1/config/pull?since=2026-03-28T00:00:00Z /// 批量拉取配置(供桌面端启动时一次性拉取) /// 返回扁平的 key-value map,可选 since 参数过滤仅返回该时间之后更新的配置 pub async fn pull_config( State(state): State, _ctx: Extension, Query(params): Query>, ) -> SaasResult> { let since = params.get("since").cloned(); let items = service::fetch_all_config_items( &state.db, &ConfigQuery { category: None, source: None, page: None, page_size: None }, ).await?; let mut configs: Vec = Vec::new(); for item in items { // 如果指定了 since,只返回 updated_at > since 的配置 if let Some(ref since_val) = since { if item.updated_at <= *since_val { continue; } } configs.push(serde_json::json!({ "key": item.key_path, "category": item.category, "value": item.current_value, "value_type": item.value_type, "default": item.default_value, "updated_at": item.updated_at, })); } Ok(Json(serde_json::json!({ "configs": configs, "pulled_at": chrono::Utc::now().to_rfc3339(), }))) }