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:
iven
2026-03-29 19:21:48 +08:00
parent 5fdf96c3f5
commit 8b9d506893
64 changed files with 3348 additions and 520 deletions

View File

@@ -4,6 +4,7 @@ use sqlx::PgPool;
use crate::error::{SaasError, SaasResult};
use crate::common::PaginatedResponse;
use crate::common::normalize_pagination;
use crate::models::{PromptTemplateRow, PromptVersionRow};
use super::types::*;
/// 创建提示词模板 + 初始版本
@@ -50,30 +51,28 @@ pub async fn create_template(
/// 获取单个模板
pub async fn get_template(db: &PgPool, id: &str) -> SaasResult<PromptTemplateInfo> {
let row: Option<(String, String, String, Option<String>, String, i32, String, String, String)> =
let row: Option<PromptTemplateRow> =
sqlx::query_as(
"SELECT id, name, category, description, source, current_version, status, created_at, updated_at
FROM prompt_templates WHERE id = $1"
).bind(id).fetch_optional(db).await?;
let (id, name, category, description, source, current_version, status, created_at, updated_at) =
row.ok_or_else(|| SaasError::NotFound(format!("提示词模板 {} 不存在", id)))?;
let r = row.ok_or_else(|| SaasError::NotFound(format!("提示词模板 {} 不存在", id)))?;
Ok(PromptTemplateInfo { id, name, category, description, source, current_version, status, created_at, updated_at })
Ok(PromptTemplateInfo { id: r.id, name: r.name, category: r.category, description: r.description, source: r.source, current_version: r.current_version, status: r.status, created_at: r.created_at, updated_at: r.updated_at })
}
/// 按名称获取模板
pub async fn get_template_by_name(db: &PgPool, name: &str) -> SaasResult<PromptTemplateInfo> {
let row: Option<(String, String, String, Option<String>, String, i32, String, String, String)> =
let row: Option<PromptTemplateRow> =
sqlx::query_as(
"SELECT id, name, category, description, source, current_version, status, created_at, updated_at
FROM prompt_templates WHERE name = $1"
).bind(name).fetch_optional(db).await?;
let (id, name, category, description, source, current_version, status, created_at, updated_at) =
row.ok_or_else(|| SaasError::NotFound(format!("提示词模板 '{}' 不存在", name)))?;
let r = row.ok_or_else(|| SaasError::NotFound(format!("提示词模板 '{}' 不存在", name)))?;
Ok(PromptTemplateInfo { id, name, category, description, source, current_version, status, created_at, updated_at })
Ok(PromptTemplateInfo { id: r.id, name: r.name, category: r.category, description: r.description, source: r.source, current_version: r.current_version, status: r.status, created_at: r.created_at, updated_at: r.updated_at })
}
/// 列表模板
@@ -83,35 +82,59 @@ pub async fn list_templates(
) -> SaasResult<PaginatedResponse<PromptTemplateInfo>> {
let (page, page_size, offset) = normalize_pagination(query.page, query.page_size);
let mut where_clauses = vec!["1=1".to_string()];
let mut count_sql = String::from("SELECT COUNT(*) FROM prompt_templates WHERE ");
let mut data_sql = String::from(
"SELECT id, name, category, description, source, current_version, status, created_at, updated_at
FROM prompt_templates WHERE "
);
// 使用参数化查询构建,防止 SQL 注入
let mut param_idx = 1usize;
let mut conditions = Vec::new();
let mut cat_bind: Option<String> = None;
let mut src_bind: Option<String> = None;
let mut status_bind: Option<String> = None;
if let Some(ref cat) = query.category {
where_clauses.push(format!("category = '{}'", cat.replace('\'', "''")));
conditions.push(format!("category = ${}", param_idx));
cat_bind = Some(cat.clone());
param_idx += 1;
}
if let Some(ref src) = query.source {
where_clauses.push(format!("source = '{}'", src.replace('\'', "''")));
conditions.push(format!("source = ${}", param_idx));
src_bind = Some(src.clone());
param_idx += 1;
}
if let Some(ref st) = query.status {
where_clauses.push(format!("status = '{}'", st.replace('\'', "''")));
conditions.push(format!("status = ${}", param_idx));
status_bind = Some(st.clone());
param_idx += 1;
}
let where_clause = where_clauses.join(" AND ");
count_sql.push_str(&where_clause);
data_sql.push_str(&where_clause);
data_sql.push_str(&format!(" ORDER BY updated_at DESC LIMIT {} OFFSET {}", page_size, offset));
let where_clause = if conditions.is_empty() {
"1=1".to_string()
} else {
conditions.join(" AND ")
};
let total: i64 = sqlx::query_scalar(&count_sql).fetch_one(db).await?;
let count_sql = format!("SELECT COUNT(*) FROM prompt_templates WHERE {}", where_clause);
let data_sql = format!(
"SELECT id, name, category, description, source, current_version, status, created_at, updated_at \
FROM prompt_templates WHERE {} ORDER BY updated_at DESC LIMIT {} OFFSET {}",
where_clause, page_size, offset
);
let rows: Vec<(String, String, String, Option<String>, String, i32, String, String, String)> =
sqlx::query_as(&data_sql).fetch_all(db).await?;
// 动态绑定参数到 count 查询
let mut count_query = sqlx::query_scalar::<_, i64>(&count_sql);
if let Some(ref v) = cat_bind { count_query = count_query.bind(v); }
if let Some(ref v) = src_bind { count_query = count_query.bind(v); }
if let Some(ref v) = status_bind { count_query = count_query.bind(v); }
let total = count_query.fetch_one(db).await?;
let items: Vec<PromptTemplateInfo> = rows.into_iter().map(|(id, name, category, description, source, current_version, status, created_at, updated_at)| {
PromptTemplateInfo { id, name, category, description, source, current_version, status, created_at, updated_at }
// 动态绑定参数到 data 查询
let mut data_query = sqlx::query_as::<_, PromptTemplateRow>(&data_sql);
if let Some(ref v) = cat_bind { data_query = data_query.bind(v); }
if let Some(ref v) = src_bind { data_query = data_query.bind(v); }
if let Some(ref v) = status_bind { data_query = data_query.bind(v); }
data_query = data_query.bind(page_size as i64).bind(offset as i64);
let rows = data_query.fetch_all(db).await?;
let items: Vec<PromptTemplateInfo> = rows.into_iter().map(|r| {
PromptTemplateInfo { id: r.id, name: r.name, category: r.category, description: r.description, source: r.source, current_version: r.current_version, status: r.status, created_at: r.created_at, updated_at: r.updated_at }
}).collect();
Ok(PaginatedResponse { items, total, page, page_size })
@@ -177,36 +200,34 @@ pub async fn create_version(
/// 获取特定版本
pub async fn get_version(db: &PgPool, version_id: &str) -> SaasResult<PromptVersionInfo> {
let row: Option<(String, String, i32, String, Option<String>, String, Option<String>, Option<String>, String)> =
let row: Option<PromptVersionRow> =
sqlx::query_as(
"SELECT id, template_id, version, system_prompt, user_prompt_template, variables, changelog, min_app_version, created_at
FROM prompt_versions WHERE id = $1"
).bind(version_id).fetch_optional(db).await?;
let (id, template_id, version, system_prompt, user_prompt_template, variables_str, changelog, min_app_version, created_at) =
row.ok_or_else(|| SaasError::NotFound(format!("提示词版本 {} 不存在", version_id)))?;
let r = row.ok_or_else(|| SaasError::NotFound(format!("提示词版本 {} 不存在", version_id)))?;
let variables: serde_json::Value = serde_json::from_str(&variables_str).unwrap_or(serde_json::json!([]));
let variables: serde_json::Value = serde_json::from_str(&r.variables).unwrap_or(serde_json::json!([]));
Ok(PromptVersionInfo { id, template_id, version, system_prompt, user_prompt_template, variables, changelog, min_app_version, created_at })
Ok(PromptVersionInfo { id: r.id, template_id: r.template_id, version: r.version, system_prompt: r.system_prompt, user_prompt_template: r.user_prompt_template, variables, changelog: r.changelog, min_app_version: r.min_app_version, created_at: r.created_at })
}
/// 获取模板的当前版本内容
pub async fn get_current_version(db: &PgPool, template_name: &str) -> SaasResult<PromptVersionInfo> {
let tmpl = get_template_by_name(db, template_name).await?;
let row: Option<(String, String, i32, String, Option<String>, String, Option<String>, Option<String>, String)> =
let row: Option<PromptVersionRow> =
sqlx::query_as(
"SELECT id, template_id, version, system_prompt, user_prompt_template, variables, changelog, min_app_version, created_at
FROM prompt_versions WHERE template_id = $1 AND version = $2"
).bind(&tmpl.id).bind(tmpl.current_version).fetch_optional(db).await?;
let (id, template_id, version, system_prompt, user_prompt_template, variables_str, changelog, min_app_version, created_at) =
row.ok_or_else(|| SaasError::NotFound(format!("提示词 '{}' 的版本 {} 不存在", template_name, tmpl.current_version)))?;
let r = row.ok_or_else(|| SaasError::NotFound(format!("提示词 '{}' 的版本 {} 不存在", template_name, tmpl.current_version)))?;
let variables: serde_json::Value = serde_json::from_str(&variables_str).unwrap_or(serde_json::json!([]));
let variables: serde_json::Value = serde_json::from_str(&r.variables).unwrap_or(serde_json::json!([]));
Ok(PromptVersionInfo { id, template_id, version, system_prompt, user_prompt_template, variables, changelog, min_app_version, created_at })
Ok(PromptVersionInfo { id: r.id, template_id: r.template_id, version: r.version, system_prompt: r.system_prompt, user_prompt_template: r.user_prompt_template, variables, changelog: r.changelog, min_app_version: r.min_app_version, created_at: r.created_at })
}
/// 列出模板的所有版本
@@ -214,15 +235,15 @@ pub async fn list_versions(
db: &PgPool,
template_id: &str,
) -> SaasResult<Vec<PromptVersionInfo>> {
let rows: Vec<(String, String, i32, String, Option<String>, String, Option<String>, Option<String>, String)> =
let rows: Vec<PromptVersionRow> =
sqlx::query_as(
"SELECT id, template_id, version, system_prompt, user_prompt_template, variables, changelog, min_app_version, created_at
FROM prompt_versions WHERE template_id = $1 ORDER BY version DESC"
).bind(template_id).fetch_all(db).await?;
Ok(rows.into_iter().map(|(id, template_id, version, system_prompt, user_prompt_template, variables_str, changelog, min_app_version, created_at)| {
let variables = serde_json::from_str(&variables_str).unwrap_or(serde_json::json!([]));
PromptVersionInfo { id, template_id, version, system_prompt, user_prompt_template, variables, changelog, min_app_version, created_at }
Ok(rows.into_iter().map(|r| {
let variables = serde_json::from_str(&r.variables).unwrap_or(serde_json::json!([]));
PromptVersionInfo { id: r.id, template_id: r.template_id, version: r.version, system_prompt: r.system_prompt, user_prompt_template: r.user_prompt_template, variables, changelog: r.changelog, min_app_version: r.min_app_version, created_at: r.created_at }
}).collect())
}