feat(admin): 实现系统数据导入导出功能,支持提供商和模型批量配置

This commit is contained in:
fawney19
2026-04-11 21:39:04 +08:00
parent 801e16c988
commit a9f610fa69
36 changed files with 3247 additions and 409 deletions
@@ -1,4 +1,4 @@
use crate::handlers::admin::provider::shared::payloads::AdminProviderKeyUpdateRequest;
use crate::handlers::admin::provider::shared::payloads::AdminProviderKeyUpdatePatch;
use crate::handlers::admin::provider::write::normalize::{
normalize_auth_type, validate_vertex_api_formats,
};
@@ -17,11 +17,11 @@ pub(crate) async fn build_admin_update_provider_key_record(
state: &AdminAppState<'_>,
provider: &StoredProviderCatalogProvider,
existing: &StoredProviderCatalogKey,
raw_payload: &serde_json::Map<String, serde_json::Value>,
payload: AdminProviderKeyUpdateRequest,
patch: AdminProviderKeyUpdatePatch,
) -> Result<StoredProviderCatalogKey, String> {
let state = state.as_ref();
let mut updated = existing.clone();
let (fields, payload) = patch.into_parts();
let current_auth_type = normalize_auth_type(Some(&existing.auth_type))?;
let target_auth_type = payload
.auth_type
@@ -34,7 +34,7 @@ pub(crate) async fn build_admin_update_provider_key_record(
.as_deref()
.is_some_and(|_| target_auth_type != current_auth_type);
let api_key_present = raw_payload.contains_key("api_key");
let api_key_present = fields.contains("api_key");
let api_key_value = payload
.api_key
.as_deref()
@@ -44,7 +44,7 @@ pub(crate) async fn build_admin_update_provider_key_record(
return Err("api_key 不能为空".to_string());
}
let auth_config_present = raw_payload.contains_key("auth_config");
let auth_config_present = fields.contains("auth_config");
let auth_config = normalize_json_object(payload.auth_config, "auth_config")?;
let auth_config_object = auth_config
.as_ref()
@@ -182,7 +182,7 @@ pub(crate) async fn build_admin_update_provider_key_record(
_ => {}
}
if raw_payload.contains_key("api_formats") {
if fields.contains("api_formats") {
let api_formats = normalize_string_list(payload.api_formats)
.ok_or_else(|| "api_formats 为必填字段".to_string())?;
validate_vertex_api_formats(&provider.provider_type, &target_auth_type, &api_formats)?;
@@ -201,30 +201,30 @@ pub(crate) async fn build_admin_update_provider_key_record(
}
updated.name = trimmed.to_string();
}
if raw_payload.contains_key("rate_multipliers") {
if fields.contains("rate_multipliers") {
updated.rate_multipliers =
normalize_json_object(payload.rate_multipliers, "rate_multipliers")?;
}
if let Some(internal_priority) = payload.internal_priority {
updated.internal_priority = internal_priority;
}
if raw_payload.contains_key("global_priority_by_format") {
if fields.contains("global_priority_by_format") {
updated.global_priority_by_format = normalize_json_object(
payload.global_priority_by_format,
"global_priority_by_format",
)?;
}
if raw_payload.contains_key("rpm_limit") {
if fields.contains("rpm_limit") {
updated.rpm_limit = payload.rpm_limit;
if payload.rpm_limit.is_none() {
updated.learned_rpm_limit = None;
}
}
if raw_payload.contains_key("allowed_models") {
if fields.contains("allowed_models") {
updated.allowed_models =
normalize_string_list(payload.allowed_models).map(|value| json!(value));
}
if raw_payload.contains_key("capabilities") {
if fields.contains("capabilities") {
updated.capabilities = normalize_json_object(payload.capabilities, "capabilities")?;
}
if let Some(cache_ttl_minutes) = payload.cache_ttl_minutes {
@@ -236,7 +236,7 @@ pub(crate) async fn build_admin_update_provider_key_record(
if let Some(is_active) = payload.is_active {
updated.is_active = is_active;
}
if raw_payload.contains_key("note") {
if fields.contains("note") {
updated.note = payload
.note
.map(|value| value.trim().to_string())
@@ -245,22 +245,22 @@ pub(crate) async fn build_admin_update_provider_key_record(
if let Some(auto_fetch_models) = payload.auto_fetch_models {
updated.auto_fetch_models = auto_fetch_models;
}
if raw_payload.contains_key("locked_models") {
if fields.contains("locked_models") {
updated.locked_models =
normalize_string_list(payload.locked_models).map(|value| json!(value));
}
if raw_payload.contains_key("model_include_patterns") {
if fields.contains("model_include_patterns") {
updated.model_include_patterns =
normalize_string_list(payload.model_include_patterns).map(|value| json!(value));
}
if raw_payload.contains_key("model_exclude_patterns") {
if fields.contains("model_exclude_patterns") {
updated.model_exclude_patterns =
normalize_string_list(payload.model_exclude_patterns).map(|value| json!(value));
}
if raw_payload.contains_key("proxy") {
if fields.contains("proxy") {
updated.proxy = normalize_json_object(payload.proxy, "proxy")?;
}
if raw_payload.contains_key("fingerprint") {
if fields.contains("fingerprint") {
updated.fingerprint = normalize_json_object(payload.fingerprint, "fingerprint")?;
}
if auth_config_present && !auth_type_switch && updated.auth_type != "api_key" {
@@ -1,4 +1,4 @@
use crate::handlers::admin::provider::shared::payloads::AdminProviderUpdateRequest;
use crate::handlers::admin::provider::shared::payloads::AdminProviderUpdatePatch;
use crate::handlers::admin::provider::shared::support::{
normalize_provider_billing_type, parse_optional_rfc3339_unix_secs,
};
@@ -11,15 +11,15 @@ use std::time::{SystemTime, UNIX_EPOCH};
pub(crate) async fn build_admin_update_provider_record(
state: &AdminAppState<'_>,
existing: &StoredProviderCatalogProvider,
raw_payload: &serde_json::Map<String, serde_json::Value>,
payload: AdminProviderUpdateRequest,
patch: AdminProviderUpdatePatch,
) -> Result<StoredProviderCatalogProvider, String> {
let state = state.as_ref();
let mut updated = existing.clone();
let (fields, payload) = patch.into_parts();
if let Some(value) = raw_payload.get("name") {
if fields.contains("name") {
let Some(name) = payload.name.as_deref() else {
return Err(if value.is_null() {
return Err(if fields.is_null("name") {
"name 不能为空".to_string()
} else {
"name 必须是字符串".to_string()
@@ -41,9 +41,9 @@ pub(crate) async fn build_admin_update_provider_record(
updated.name = trimmed.to_string();
}
let target_provider_type = if let Some(value) = raw_payload.get("provider_type") {
let target_provider_type = if fields.contains("provider_type") {
let Some(provider_type) = payload.provider_type.as_deref() else {
return Err(if value.is_null() {
return Err(if fields.is_null("provider_type") {
"provider_type 不能为空".to_string()
} else {
"provider_type 必须是字符串".to_string()
@@ -56,17 +56,17 @@ pub(crate) async fn build_admin_update_provider_record(
updated.provider_type.clone()
};
if raw_payload.contains_key("description") {
if fields.contains("description") {
updated.description = payload
.description
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty());
}
if let Some(value) = raw_payload.get("website") {
if fields.contains("website") {
updated.website = match payload.website {
None => {
if value.is_null() {
if fields.is_null("website") {
None
} else {
return Err("website 必须是字符串".to_string());
@@ -85,9 +85,9 @@ pub(crate) async fn build_admin_update_provider_record(
};
}
if let Some(value) = raw_payload.get("billing_type") {
if fields.contains("billing_type") {
let Some(billing_type) = payload.billing_type.as_deref() else {
return Err(if value.is_null() {
return Err(if fields.is_null("billing_type") {
"billing_type 不能为空".to_string()
} else {
"billing_type 必须是字符串".to_string()
@@ -96,8 +96,8 @@ pub(crate) async fn build_admin_update_provider_record(
updated.billing_type = Some(normalize_provider_billing_type(billing_type)?);
}
if let Some(value) = raw_payload.get("monthly_quota_usd") {
if value.is_null() {
if fields.contains("monthly_quota_usd") {
if fields.is_null("monthly_quota_usd") {
updated.monthly_quota_usd = None;
} else {
let Some(monthly_quota_usd) = payload.monthly_quota_usd else {
@@ -110,8 +110,8 @@ pub(crate) async fn build_admin_update_provider_record(
}
}
if let Some(value) = raw_payload.get("quota_reset_day") {
if value.is_null() {
if fields.contains("quota_reset_day") {
if fields.is_null("quota_reset_day") {
updated.quota_reset_day = None;
} else {
let Some(quota_reset_day) = payload.quota_reset_day else {
@@ -124,8 +124,8 @@ pub(crate) async fn build_admin_update_provider_record(
}
}
if let Some(value) = raw_payload.get("quota_last_reset_at") {
if value.is_null() {
if fields.contains("quota_last_reset_at") {
if fields.is_null("quota_last_reset_at") {
updated.quota_last_reset_at_unix_secs = None;
} else {
let Some(raw) = payload.quota_last_reset_at.as_deref() else {
@@ -138,8 +138,8 @@ pub(crate) async fn build_admin_update_provider_record(
}
}
if let Some(value) = raw_payload.get("quota_expires_at") {
if value.is_null() {
if fields.contains("quota_expires_at") {
if fields.is_null("quota_expires_at") {
updated.quota_expires_at_unix_secs = None;
} else {
let Some(raw) = payload.quota_expires_at.as_deref() else {
@@ -150,9 +150,9 @@ pub(crate) async fn build_admin_update_provider_record(
}
}
if let Some(value) = raw_payload.get("provider_priority") {
if fields.contains("provider_priority") {
let Some(provider_priority) = payload.provider_priority else {
return Err(if value.is_null() {
return Err(if fields.is_null("provider_priority") {
"provider_priority 不能为空".to_string()
} else {
"provider_priority 必须是整数".to_string()
@@ -164,21 +164,21 @@ pub(crate) async fn build_admin_update_provider_record(
updated.provider_priority = provider_priority;
}
if let Some(_value) = raw_payload.get("keep_priority_on_conversion") {
if fields.contains("keep_priority_on_conversion") {
let Some(keep_priority_on_conversion) = payload.keep_priority_on_conversion else {
return Err("keep_priority_on_conversion 必须是布尔值".to_string());
};
updated.keep_priority_on_conversion = keep_priority_on_conversion;
}
if let Some(_value) = raw_payload.get("is_active") {
if fields.contains("is_active") {
let Some(is_active) = payload.is_active else {
return Err("is_active 必须是布尔值".to_string());
};
updated.is_active = is_active;
}
if raw_payload.contains_key("concurrent_limit") {
if fields.contains("concurrent_limit") {
updated.concurrent_limit = match payload.concurrent_limit {
Some(value) if value >= 0 => Some(value),
Some(_) => return Err("concurrent_limit 必须是非负整数".to_string()),
@@ -186,7 +186,7 @@ pub(crate) async fn build_admin_update_provider_record(
};
}
if raw_payload.contains_key("max_retries") {
if fields.contains("max_retries") {
updated.max_retries = match payload.max_retries {
Some(value) if (0..=999).contains(&value) => Some(value),
Some(_) => return Err("max_retries 必须是 0 到 999 之间的整数".to_string()),
@@ -194,11 +194,11 @@ pub(crate) async fn build_admin_update_provider_record(
};
}
if raw_payload.contains_key("proxy") {
if fields.contains("proxy") {
updated.proxy = normalize_json_object(payload.proxy, "proxy")?;
}
if raw_payload.contains_key("stream_first_byte_timeout") {
if fields.contains("stream_first_byte_timeout") {
updated.stream_first_byte_timeout_secs = match payload.stream_first_byte_timeout {
Some(value) if (1.0..=300.0).contains(&value) => Some(value),
Some(_) => {
@@ -208,7 +208,7 @@ pub(crate) async fn build_admin_update_provider_record(
};
}
if raw_payload.contains_key("request_timeout") {
if fields.contains("request_timeout") {
updated.request_timeout_secs = match payload.request_timeout {
Some(value) if (1.0..=600.0).contains(&value) => Some(value),
Some(_) => return Err("request_timeout 必须是 1 到 600 之间的数字".to_string()),
@@ -216,14 +216,14 @@ pub(crate) async fn build_admin_update_provider_record(
};
}
if let Some(_value) = raw_payload.get("enable_format_conversion") {
if fields.contains("enable_format_conversion") {
let Some(enable_format_conversion) = payload.enable_format_conversion else {
return Err("enable_format_conversion 必须是布尔值".to_string());
};
updated.enable_format_conversion = enable_format_conversion;
}
let config_seed = if raw_payload.contains_key("config") {
let config_seed = if fields.contains("config") {
normalize_json_object(payload.config, "config")?
} else {
updated.config.clone()
@@ -232,11 +232,8 @@ pub(crate) async fn build_admin_update_provider_record(
.and_then(|value| value.as_object().cloned())
.unwrap_or_default();
if raw_payload.contains_key("claude_code_advanced") {
if raw_payload
.get("claude_code_advanced")
.is_some_and(serde_json::Value::is_null)
{
if fields.contains("claude_code_advanced") {
if fields.is_null("claude_code_advanced") {
config_map.remove("claude_code_advanced");
} else {
if target_provider_type != "claude_code" {
@@ -251,11 +248,8 @@ pub(crate) async fn build_admin_update_provider_record(
config_map.remove("claude_code_advanced");
}
if raw_payload.contains_key("pool_advanced") {
if raw_payload
.get("pool_advanced")
.is_some_and(serde_json::Value::is_null)
{
if fields.contains("pool_advanced") {
if fields.is_null("pool_advanced") {
config_map.remove("pool_advanced");
} else {
let value = normalize_json_object(payload.pool_advanced, "pool_advanced")?
@@ -264,11 +258,8 @@ pub(crate) async fn build_admin_update_provider_record(
}
}
if raw_payload.contains_key("failover_rules") {
if raw_payload
.get("failover_rules")
.is_some_and(serde_json::Value::is_null)
{
if fields.contains("failover_rules") {
if fields.is_null("failover_rules") {
config_map.remove("failover_rules");
} else {
let value = normalize_json_object(payload.failover_rules, "failover_rules")?