refactor(saas): 架构重构 + 性能优化 — 借鉴 loco-rs 模式
Phase 0: 知识库
- docs/knowledge-base/loco-rs-patterns.md — loco-rs 10 个可借鉴模式研究
Phase 1: 数据层重构
- crates/zclaw-saas/src/models/ — 15 个 FromRow 类型化模型
- Login 3 次查询合并为 1 次 AccountLoginRow 查询
- 所有 service 文件从元组解构迁移到 FromRow 结构体
Phase 2: Worker + Scheduler 系统
- crates/zclaw-saas/src/workers/ — Worker trait + 5 个具体实现
- crates/zclaw-saas/src/scheduler.rs — TOML 声明式调度器
- crates/zclaw-saas/src/tasks/ — CLI 任务系统
Phase 3: 性能修复
- Relay N+1 查询 → 精准 SQL (relay/handlers.rs)
- Config RwLock → AtomicU32 无锁 rate limit (state.rs, middleware.rs)
- SSE std::sync::Mutex → tokio::sync::Mutex (relay/service.rs)
- /auth/refresh 阻塞清理 → Scheduler 定期执行
Phase 4: 多环境配置
- config/saas-{development,production,test}.toml
- ZCLAW_ENV 环境选择 + ZCLAW_SAAS_CONFIG 精确覆盖
- scheduler 配置集成到 TOML
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
|
||||
use sqlx::PgPool;
|
||||
use crate::error::{SaasError, SaasResult};
|
||||
use crate::models::{ProviderKeySelectRow, ProviderKeyRow};
|
||||
use crate::crypto;
|
||||
|
||||
/// 解密 key_value (如果已加密),否则原样返回
|
||||
@@ -40,7 +41,7 @@ pub async fn select_best_key(db: &PgPool, provider_id: &str, enc_key: &[u8; 32])
|
||||
let current_minute = chrono::Utc::now().format("%Y-%m-%dT%H:%M").to_string();
|
||||
|
||||
// 获取所有活跃 Key
|
||||
let rows: Vec<(String, String, i32, Option<i64>, Option<i64>, Option<String>)> =
|
||||
let rows: Vec<ProviderKeySelectRow> =
|
||||
sqlx::query_as(
|
||||
"SELECT id, key_value, priority, max_rpm, max_tpm, quota_reset_interval
|
||||
FROM provider_keys
|
||||
@@ -89,18 +90,18 @@ pub async fn select_best_key(db: &PgPool, provider_id: &str, enc_key: &[u8; 32])
|
||||
}
|
||||
|
||||
// 检查滑动窗口使用量
|
||||
for (id, key_value, priority, max_rpm, max_tpm, quota_reset_interval) in rows {
|
||||
for row in rows {
|
||||
// 检查 RPM 限额
|
||||
if let Some(rpm_limit) = max_rpm {
|
||||
if let Some(rpm_limit) = row.max_rpm {
|
||||
if rpm_limit > 0 {
|
||||
let window: Option<(i64,)> = sqlx::query_as(
|
||||
"SELECT COALESCE(SUM(request_count), 0) FROM key_usage_window
|
||||
WHERE key_id = $1 AND window_minute = $2"
|
||||
).bind(&id).bind(¤t_minute).fetch_optional(db).await?;
|
||||
).bind(&row.id).bind(¤t_minute).fetch_optional(db).await?;
|
||||
|
||||
if let Some((count,)) = window {
|
||||
if count >= rpm_limit {
|
||||
tracing::debug!("Key {} hit RPM limit ({}/{})", id, count, rpm_limit);
|
||||
tracing::debug!("Key {} hit RPM limit ({}/{})", row.id, count, rpm_limit);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -108,16 +109,16 @@ pub async fn select_best_key(db: &PgPool, provider_id: &str, enc_key: &[u8; 32])
|
||||
}
|
||||
|
||||
// 检查 TPM 限额
|
||||
if let Some(tpm_limit) = max_tpm {
|
||||
if let Some(tpm_limit) = row.max_tpm {
|
||||
if tpm_limit > 0 {
|
||||
let window: Option<(i64,)> = sqlx::query_as(
|
||||
"SELECT COALESCE(SUM(token_count), 0) FROM key_usage_window
|
||||
WHERE key_id = $1 AND window_minute = $2"
|
||||
).bind(&id).bind(¤t_minute).fetch_optional(db).await?;
|
||||
).bind(&row.id).bind(¤t_minute).fetch_optional(db).await?;
|
||||
|
||||
if let Some((tokens,)) = window {
|
||||
if tokens >= tpm_limit {
|
||||
tracing::debug!("Key {} hit TPM limit ({}/{})", id, tokens, tpm_limit);
|
||||
tracing::debug!("Key {} hit TPM limit ({}/{})", row.id, tokens, tpm_limit);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -125,17 +126,17 @@ pub async fn select_best_key(db: &PgPool, provider_id: &str, enc_key: &[u8; 32])
|
||||
}
|
||||
|
||||
// 此 Key 可用 — 解密 key_value
|
||||
let decrypted_kv = decrypt_key_value(&key_value, enc_key)?;
|
||||
let decrypted_kv = decrypt_key_value(&row.key_value, enc_key)?;
|
||||
return Ok(KeySelection {
|
||||
key: PoolKey {
|
||||
id: id.clone(),
|
||||
id: row.id.clone(),
|
||||
key_value: decrypted_kv,
|
||||
priority,
|
||||
max_rpm,
|
||||
max_tpm,
|
||||
quota_reset_interval,
|
||||
priority: row.priority,
|
||||
max_rpm: row.max_rpm,
|
||||
max_tpm: row.max_tpm,
|
||||
quota_reset_interval: row.quota_reset_interval,
|
||||
},
|
||||
key_id: id,
|
||||
key_id: row.id,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -229,7 +230,7 @@ pub async fn list_provider_keys(
|
||||
db: &PgPool,
|
||||
provider_id: &str,
|
||||
) -> SaasResult<Vec<serde_json::Value>> {
|
||||
let rows: Vec<(String, String, String, i32, Option<i64>, Option<i64>, Option<String>, bool, Option<String>, Option<String>, i64, i64, String, String)> =
|
||||
let rows: Vec<ProviderKeyRow> =
|
||||
sqlx::query_as(
|
||||
"SELECT id, provider_id, key_label, priority, max_rpm, max_tpm, quota_reset_interval, is_active,
|
||||
last_429_at, cooldown_until, total_requests, total_tokens, created_at, updated_at
|
||||
@@ -238,20 +239,20 @@ pub async fn list_provider_keys(
|
||||
|
||||
Ok(rows.into_iter().map(|r| {
|
||||
serde_json::json!({
|
||||
"id": r.0,
|
||||
"provider_id": r.1,
|
||||
"key_label": r.2,
|
||||
"priority": r.3,
|
||||
"max_rpm": r.4,
|
||||
"max_tpm": r.5,
|
||||
"quota_reset_interval": r.6,
|
||||
"is_active": r.7,
|
||||
"last_429_at": r.8,
|
||||
"cooldown_until": r.9,
|
||||
"total_requests": r.10,
|
||||
"total_tokens": r.11,
|
||||
"created_at": r.12,
|
||||
"updated_at": r.13,
|
||||
"id": r.id,
|
||||
"provider_id": r.provider_id,
|
||||
"key_label": r.key_label,
|
||||
"priority": r.priority,
|
||||
"max_rpm": r.max_rpm,
|
||||
"max_tpm": r.max_tpm,
|
||||
"quota_reset_interval": r.quota_reset_interval,
|
||||
"is_active": r.is_active,
|
||||
"last_429_at": r.last_429_at,
|
||||
"cooldown_until": r.cooldown_until,
|
||||
"total_requests": r.total_requests,
|
||||
"total_tokens": r.total_tokens,
|
||||
"created_at": r.created_at,
|
||||
"updated_at": r.updated_at,
|
||||
})
|
||||
}).collect())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user