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::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())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user