Files
zclaw_openfang/crates/zclaw-saas/src/migration/handlers.rs
iven 5fdf96c3f5 chore: 提交所有工作进度 — SaaS 后端增强、Admin UI、桌面端集成
包含大量 SaaS 平台改进、Admin 管理后台更新、桌面端集成完善、
文档同步、测试文件重构等内容。为 QA 测试准备干净工作树。
2026-03-29 10:46:41 +08:00

182 lines
6.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 配置迁移 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<AppState>,
Query(query): Query<ConfigQuery>,
_ctx: Extension<AuthContext>,
) -> SaasResult<Json<PaginatedResponse<ConfigItemInfo>>> {
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<AppState>,
Path(id): Path<String>,
_ctx: Extension<AuthContext>,
) -> SaasResult<Json<ConfigItemInfo>> {
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<AppState>,
Extension(ctx): Extension<AuthContext>,
Json(req): Json<CreateConfigItemRequest>,
) -> SaasResult<(StatusCode, Json<ConfigItemInfo>)> {
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<AppState>,
Path(id): Path<String>,
Extension(ctx): Extension<AuthContext>,
Json(req): Json<UpdateConfigItemRequest>,
) -> SaasResult<Json<ConfigItemInfo>> {
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<AppState>,
Path(id): Path<String>,
Extension(ctx): Extension<AuthContext>,
) -> SaasResult<Json<serde_json::Value>> {
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<AppState>,
_ctx: Extension<AuthContext>,
) -> SaasResult<Json<ConfigAnalysis>> {
service::analyze_config(&state.db).await.map(Json)
}
/// POST /api/v1/config/seed (admin only)
pub async fn seed_config(
State(state): State<AppState>,
Extension(ctx): Extension<AuthContext>,
) -> SaasResult<Json<serde_json::Value>> {
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<AppState>,
Extension(ctx): Extension<AuthContext>,
Json(req): Json<SyncConfigRequest>,
) -> SaasResult<Json<super::service::ConfigSyncResult>> {
// 权限检查:仅 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<AppState>,
Extension(ctx): Extension<AuthContext>,
Json(req): Json<SyncConfigRequest>,
) -> SaasResult<Json<ConfigDiffResponse>> {
// 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<AppState>,
Extension(ctx): Extension<AuthContext>,
Query(params): Query<std::collections::HashMap<String, String>>,
) -> SaasResult<Json<crate::common::PaginatedResponse<ConfigSyncLogInfo>>> {
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<AppState>,
_ctx: Extension<AuthContext>,
Query(params): Query<std::collections::HashMap<String, String>>,
) -> SaasResult<Json<serde_json::Value>> {
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<serde_json::Value> = 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(),
})))
}