//! 提示词模板服务层 use sqlx::PgPool; use crate::error::{SaasError, SaasResult}; use crate::common::PaginatedResponse; use crate::common::normalize_pagination; use crate::models::{PromptTemplateRow, PromptVersionRow, PromptSyncStatusRow}; use super::types::*; /// 创建提示词模板 + 初始版本 pub async fn create_template( db: &PgPool, name: &str, category: &str, description: Option<&str>, source: &str, system_prompt: &str, user_prompt_template: Option<&str>, variables: Option, min_app_version: Option<&str>, ) -> SaasResult { let id = uuid::Uuid::new_v4().to_string(); let version_id = uuid::Uuid::new_v4().to_string(); let now = chrono::Utc::now(); let vars_json = variables.unwrap_or(serde_json::json!([])).to_string(); // 插入模板 sqlx::query( "INSERT INTO prompt_templates (id, name, category, description, source, current_version, status, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, 1, 'active', $6, $6)" ) .bind(&id).bind(name).bind(category).bind(description).bind(source).bind(&now) .execute(db).await.map_err(|e| { if e.to_string().contains("unique") { SaasError::AlreadyExists(format!("提示词模板 '{}' 已存在", name)) } else { SaasError::Database(e) } })?; // 插入 v1 sqlx::query( "INSERT INTO prompt_versions (id, template_id, version, system_prompt, user_prompt_template, variables, min_app_version, created_at) VALUES ($1, $2, 1, $3, $4, $5, $6, $7)" ) .bind(&version_id).bind(&id).bind(system_prompt).bind(user_prompt_template).bind(&vars_json).bind(min_app_version).bind(&now) .execute(db).await?; get_template(db, &id).await } /// 获取单个模板 pub async fn get_template(db: &PgPool, id: &str) -> SaasResult { let row: Option = sqlx::query_as( "SELECT id, name, category, description, source, current_version, status, created_at::TEXT, updated_at::TEXT FROM prompt_templates WHERE id = $1" ).bind(id).fetch_optional(db).await?; let r = row.ok_or_else(|| SaasError::NotFound(format!("提示词模板 {} 不存在", id)))?; 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 { let row: Option = sqlx::query_as( "SELECT id, name, category, description, source, current_version, status, created_at::TEXT, updated_at::TEXT FROM prompt_templates WHERE name = $1" ).bind(name).fetch_optional(db).await?; let r = row.ok_or_else(|| SaasError::NotFound(format!("提示词模板 '{}' 不存在", name)))?; 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 }) } /// 列表模板 /// Static SQL with conditional filter pattern: ($N IS NULL OR col = $N). pub async fn list_templates( db: &PgPool, query: &PromptListQuery, ) -> SaasResult> { let (page, page_size, offset) = normalize_pagination(query.page, query.page_size); let count_sql = "SELECT COUNT(*) FROM prompt_templates WHERE ($1 IS NULL OR category = $1) AND ($2 IS NULL OR source = $2) AND ($3 IS NULL OR status = $3)"; let data_sql = "SELECT id, name, category, description, source, current_version, status, created_at::TEXT, updated_at::TEXT \ FROM prompt_templates WHERE ($1 IS NULL OR category = $1) AND ($2 IS NULL OR source = $2) AND ($3 IS NULL OR status = $3) ORDER BY updated_at DESC LIMIT $4 OFFSET $5"; let total: i64 = sqlx::query_scalar(count_sql) .bind(&query.category) .bind(&query.source) .bind(&query.status) .fetch_one(db).await?; let rows: Vec = sqlx::query_as(data_sql) .bind(&query.category) .bind(&query.source) .bind(&query.status) .bind(page_size as i64) .bind(offset as i64) .fetch_all(db).await?; let items: Vec = 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 }) } /// 更新模板元数据(不修改内容) pub async fn update_template( db: &PgPool, id: &str, description: Option<&str>, status: Option<&str>, ) -> SaasResult { let now = chrono::Utc::now(); if let Some(desc) = description { sqlx::query("UPDATE prompt_templates SET description = $1, updated_at = $2 WHERE id = $3") .bind(desc).bind(&now).bind(id).execute(db).await?; } if let Some(st) = status { let valid = ["active", "deprecated", "archived"]; if !valid.contains(&st) { return Err(SaasError::InvalidInput(format!("无效状态: {},允许: {}", st, valid.join(", ")))); } sqlx::query("UPDATE prompt_templates SET status = $1, updated_at = $2 WHERE id = $3") .bind(st).bind(&now).bind(id).execute(db).await?; } get_template(db, id).await } /// 发布新版本 pub async fn create_version( db: &PgPool, template_id: &str, system_prompt: &str, user_prompt_template: Option<&str>, variables: Option, changelog: Option<&str>, min_app_version: Option<&str>, ) -> SaasResult { let tmpl = get_template(db, template_id).await?; let new_version = tmpl.current_version + 1; let version_id = uuid::Uuid::new_v4().to_string(); let now = chrono::Utc::now(); let vars_json = variables.unwrap_or(serde_json::json!([])).to_string(); sqlx::query( "INSERT INTO prompt_versions (id, template_id, version, system_prompt, user_prompt_template, variables, changelog, min_app_version, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)" ) .bind(&version_id).bind(template_id).bind(new_version) .bind(system_prompt).bind(user_prompt_template).bind(&vars_json).bind(changelog).bind(min_app_version).bind(&now) .execute(db).await?; // 更新模板的 current_version sqlx::query("UPDATE prompt_templates SET current_version = $1, updated_at = $2 WHERE id = $3") .bind(new_version).bind(&now).bind(template_id) .execute(db).await?; get_version(db, &version_id).await } /// 获取特定版本 pub async fn get_version(db: &PgPool, version_id: &str) -> SaasResult { let row: Option = sqlx::query_as( "SELECT id, template_id, version, system_prompt, user_prompt_template, variables, changelog, min_app_version, created_at::TEXT FROM prompt_versions WHERE id = $1" ).bind(version_id).fetch_optional(db).await?; let r = row.ok_or_else(|| SaasError::NotFound(format!("提示词版本 {} 不存在", version_id)))?; let variables: serde_json::Value = serde_json::from_str(&r.variables).unwrap_or(serde_json::json!([])); 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 { let tmpl = get_template_by_name(db, template_name).await?; let row: Option = sqlx::query_as( "SELECT id, template_id, version, system_prompt, user_prompt_template, variables, changelog, min_app_version, created_at::TEXT FROM prompt_versions WHERE template_id = $1 AND version = $2" ).bind(&tmpl.id).bind(tmpl.current_version).fetch_optional(db).await?; let r = row.ok_or_else(|| SaasError::NotFound(format!("提示词 '{}' 的版本 {} 不存在", template_name, tmpl.current_version)))?; let variables: serde_json::Value = serde_json::from_str(&r.variables).unwrap_or(serde_json::json!([])); 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_version_by_number(db: &PgPool, template_name: &str, version_number: i32) -> SaasResult { let tmpl = get_template_by_name(db, template_name).await?; let row: Option = sqlx::query_as( "SELECT id, template_id, version, system_prompt, user_prompt_template, variables, changelog, min_app_version, created_at::TEXT FROM prompt_versions WHERE template_id = $1 AND version = $2" ).bind(&tmpl.id).bind(version_number).fetch_optional(db).await?; let r = row.ok_or_else(|| SaasError::NotFound(format!("提示词 '{}' 的版本 {} 不存在", template_name, version_number)))?; let variables: serde_json::Value = serde_json::from_str(&r.variables).unwrap_or(serde_json::json!([])); 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 list_versions( db: &PgPool, template_id: &str, ) -> SaasResult> { let rows: Vec = sqlx::query_as( "SELECT id, template_id, version, system_prompt, user_prompt_template, variables, changelog, min_app_version, created_at::TEXT FROM prompt_versions WHERE template_id = $1 ORDER BY version DESC" ).bind(template_id).fetch_all(db).await?; 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()) } /// 回退到指定版本 pub async fn rollback_version( db: &PgPool, template_id: &str, target_version: i32, ) -> SaasResult { // 验证目标版本存在 let exists: (bool,) = sqlx::query_as( "SELECT EXISTS(SELECT 1 FROM prompt_versions WHERE template_id = $1 AND version = $2)" ).bind(template_id).bind(target_version).fetch_one(db).await?; if !exists.0 { return Err(SaasError::NotFound(format!("版本 {} 不存在", target_version))); } let now = chrono::Utc::now(); sqlx::query("UPDATE prompt_templates SET current_version = $1, updated_at = $2 WHERE id = $3") .bind(target_version).bind(&now).bind(template_id) .execute(db).await?; get_template(db, template_id).await } /// OTA 批量检查更新 pub async fn check_updates( db: &PgPool, device_id: &str, client_versions: &std::collections::HashMap, ) -> SaasResult { let mut updates = Vec::new(); for (name, client_ver) in client_versions { let tmpl = match get_template_by_name(db, name).await { Ok(t) if t.status == "active" => t, _ => continue, }; if tmpl.current_version > *client_ver { // 获取最新版本内容 if let Ok(ver) = get_current_version(db, name).await { updates.push(PromptUpdatePayload { name: tmpl.name.clone(), version: ver.version, system_prompt: ver.system_prompt, user_prompt_template: ver.user_prompt_template, variables: ver.variables, source: tmpl.source.clone(), min_app_version: ver.min_app_version, changelog: ver.changelog, }); // 更新同步状态 let now = chrono::Utc::now(); sqlx::query( "INSERT INTO prompt_sync_status (device_id, template_id, synced_version, synced_at) VALUES ($1, $2, $3, $4) ON CONFLICT (device_id, template_id) DO UPDATE SET synced_version = $3, synced_at = $4" ) .bind(device_id).bind(&tmpl.id).bind(ver.version).bind(&now) .execute(db).await.ok(); // 非关键,忽略错误 } } } // 首次连接:返回所有 active 的 builtin 模板 if client_versions.is_empty() { let all_templates = list_templates(db, &PromptListQuery { source: Some("builtin".into()), status: Some("active".into()), category: None, page: None, page_size: Some(100), }).await?; for tmpl in &all_templates.items { if let Ok(ver) = get_current_version(db, &tmpl.name).await { updates.push(PromptUpdatePayload { name: tmpl.name.clone(), version: ver.version, system_prompt: ver.system_prompt, user_prompt_template: ver.user_prompt_template, variables: ver.variables, source: tmpl.source.clone(), min_app_version: ver.min_app_version, changelog: ver.changelog, }); } } } Ok(PromptCheckResponse { updates, server_time: chrono::Utc::now().to_rfc3339(), }) } /// 查询设备的提示词同步状态 pub async fn get_sync_status( db: &PgPool, device_id: &str, ) -> SaasResult> { let rows = sqlx::query_as::<_, PromptSyncStatusRow>( "SELECT device_id, template_id, synced_version, synced_at::TEXT \ FROM prompt_sync_status \ WHERE device_id = $1 \ ORDER BY synced_at DESC \ LIMIT 50" ) .bind(device_id) .fetch_all(db) .await?; Ok(rows) }