diff --git a/crates/zclaw-saas/migrations/down/20260330000001_scheduled_tasks.sql b/crates/zclaw-saas/migrations/down/20260330000001_scheduled_tasks.sql new file mode 100644 index 0000000..68b8776 --- /dev/null +++ b/crates/zclaw-saas/migrations/down/20260330000001_scheduled_tasks.sql @@ -0,0 +1,2 @@ +-- Down: Drop scheduled_tasks table +DROP TABLE IF EXISTS scheduled_tasks; diff --git a/crates/zclaw-saas/migrations/down/20260331000001_accounts_llm_routing.sql b/crates/zclaw-saas/migrations/down/20260331000001_accounts_llm_routing.sql new file mode 100644 index 0000000..c99026f --- /dev/null +++ b/crates/zclaw-saas/migrations/down/20260331000001_accounts_llm_routing.sql @@ -0,0 +1,2 @@ +-- Down: Remove llm_routing column from accounts +ALTER TABLE accounts DROP COLUMN IF EXISTS llm_routing; diff --git a/crates/zclaw-saas/migrations/down/20260331000002_agent_templates_extensions.sql b/crates/zclaw-saas/migrations/down/20260331000002_agent_templates_extensions.sql new file mode 100644 index 0000000..c0c9237 --- /dev/null +++ b/crates/zclaw-saas/migrations/down/20260331000002_agent_templates_extensions.sql @@ -0,0 +1,11 @@ +-- Down: Remove extension columns from agent_templates +DROP INDEX IF EXISTS idx_agent_templates_source_id; +ALTER TABLE agent_templates DROP COLUMN IF EXISTS source_id; +ALTER TABLE agent_templates DROP COLUMN IF EXISTS version; +ALTER TABLE agent_templates DROP COLUMN IF EXISTS emoji; +ALTER TABLE agent_templates DROP COLUMN IF EXISTS communication_style; +ALTER TABLE agent_templates DROP COLUMN IF EXISTS personality; +ALTER TABLE agent_templates DROP COLUMN IF EXISTS quick_commands; +ALTER TABLE agent_templates DROP COLUMN IF EXISTS welcome_message; +ALTER TABLE agent_templates DROP COLUMN IF EXISTS scenarios; +ALTER TABLE agent_templates DROP COLUMN IF EXISTS soul_content; diff --git a/crates/zclaw-saas/migrations/down/20260401000001_provider_keys_last_used.sql b/crates/zclaw-saas/migrations/down/20260401000001_provider_keys_last_used.sql new file mode 100644 index 0000000..8db2d53 --- /dev/null +++ b/crates/zclaw-saas/migrations/down/20260401000001_provider_keys_last_used.sql @@ -0,0 +1,2 @@ +-- Down: Remove last_used_at from provider_keys +ALTER TABLE provider_keys DROP COLUMN IF EXISTS last_used_at; diff --git a/crates/zclaw-saas/migrations/down/20260401000002_remove_quota_reset_interval.sql b/crates/zclaw-saas/migrations/down/20260401000002_remove_quota_reset_interval.sql new file mode 100644 index 0000000..50672fa --- /dev/null +++ b/crates/zclaw-saas/migrations/down/20260401000002_remove_quota_reset_interval.sql @@ -0,0 +1,2 @@ +-- Down: Re-add quota_reset_interval to provider_keys +ALTER TABLE provider_keys ADD COLUMN IF NOT EXISTS quota_reset_interval TEXT; diff --git a/crates/zclaw-saas/migrations/down/20260401000003_models_is_embedding.sql b/crates/zclaw-saas/migrations/down/20260401000003_models_is_embedding.sql new file mode 100644 index 0000000..1cb396c --- /dev/null +++ b/crates/zclaw-saas/migrations/down/20260401000003_models_is_embedding.sql @@ -0,0 +1,5 @@ +-- Down: Remove is_embedding and model_type from models +DROP INDEX IF EXISTS idx_models_model_type; +DROP INDEX IF EXISTS idx_models_is_embedding; +ALTER TABLE models DROP COLUMN IF EXISTS model_type; +ALTER TABLE models DROP COLUMN IF EXISTS is_embedding; diff --git a/crates/zclaw-saas/migrations/down/20260401000004_accounts_password_version.sql b/crates/zclaw-saas/migrations/down/20260401000004_accounts_password_version.sql new file mode 100644 index 0000000..72efda0 --- /dev/null +++ b/crates/zclaw-saas/migrations/down/20260401000004_accounts_password_version.sql @@ -0,0 +1,4 @@ +-- Down: Remove password security columns from accounts +ALTER TABLE accounts DROP COLUMN IF EXISTS locked_until; +ALTER TABLE accounts DROP COLUMN IF EXISTS failed_login_count; +ALTER TABLE accounts DROP COLUMN IF EXISTS password_version; diff --git a/crates/zclaw-saas/migrations/down/20260401000005_rate_limit_events.sql b/crates/zclaw-saas/migrations/down/20260401000005_rate_limit_events.sql new file mode 100644 index 0000000..3d88d9f --- /dev/null +++ b/crates/zclaw-saas/migrations/down/20260401000005_rate_limit_events.sql @@ -0,0 +1,2 @@ +-- Down: Drop rate_limit_events table +DROP TABLE IF EXISTS rate_limit_events; diff --git a/crates/zclaw-saas/migrations/down/20260402000001_billing_tables.sql b/crates/zclaw-saas/migrations/down/20260402000001_billing_tables.sql new file mode 100644 index 0000000..af943c4 --- /dev/null +++ b/crates/zclaw-saas/migrations/down/20260402000001_billing_tables.sql @@ -0,0 +1,6 @@ +-- Down: Drop billing tables (reverse creation order due to FK) +DROP TABLE IF EXISTS billing_usage_quotas; +DROP TABLE IF EXISTS billing_payments; +DROP TABLE IF EXISTS billing_invoices; +DROP TABLE IF EXISTS billing_subscriptions; +DROP TABLE IF EXISTS billing_plans; diff --git a/crates/zclaw-saas/migrations/down/20260402000002_knowledge_base.sql b/crates/zclaw-saas/migrations/down/20260402000002_knowledge_base.sql new file mode 100644 index 0000000..c41ba07 --- /dev/null +++ b/crates/zclaw-saas/migrations/down/20260402000002_knowledge_base.sql @@ -0,0 +1,7 @@ +-- Down: Drop knowledge base tables (reverse creation order due to FK) +-- Note: This does NOT reverse the pgvector extension installation or role permission updates. +DROP TABLE IF EXISTS knowledge_usage; +DROP TABLE IF EXISTS knowledge_versions; +DROP TABLE IF EXISTS knowledge_chunks; +DROP TABLE IF EXISTS knowledge_items; +DROP TABLE IF EXISTS knowledge_categories; diff --git a/crates/zclaw-saas/migrations/down/20260402000003_scheduled_task_results.sql b/crates/zclaw-saas/migrations/down/20260402000003_scheduled_task_results.sql new file mode 100644 index 0000000..ab62f36 --- /dev/null +++ b/crates/zclaw-saas/migrations/down/20260402000003_scheduled_task_results.sql @@ -0,0 +1,3 @@ +-- Down: Remove result columns from scheduled_tasks +ALTER TABLE scheduled_tasks DROP COLUMN IF EXISTS last_duration_ms; +ALTER TABLE scheduled_tasks DROP COLUMN IF EXISTS last_result; diff --git a/crates/zclaw-saas/migrations/down/20260403000001_accounts_template_assignment.sql b/crates/zclaw-saas/migrations/down/20260403000001_accounts_template_assignment.sql new file mode 100644 index 0000000..3ce0425 --- /dev/null +++ b/crates/zclaw-saas/migrations/down/20260403000001_accounts_template_assignment.sql @@ -0,0 +1,2 @@ +-- Down: Remove assigned_template_id from accounts +ALTER TABLE accounts DROP COLUMN IF EXISTS assigned_template_id; diff --git a/crates/zclaw-saas/migrations/down/20260403000002_webhooks.sql b/crates/zclaw-saas/migrations/down/20260403000002_webhooks.sql new file mode 100644 index 0000000..b54dc6f --- /dev/null +++ b/crates/zclaw-saas/migrations/down/20260403000002_webhooks.sql @@ -0,0 +1,3 @@ +-- Down: Drop webhook tables (reverse creation order due to FK) +DROP TABLE IF EXISTS webhook_deliveries; +DROP TABLE IF EXISTS webhook_subscriptions; diff --git a/crates/zclaw-saas/migrations/down/20260404000001_model_groups.sql b/crates/zclaw-saas/migrations/down/20260404000001_model_groups.sql new file mode 100644 index 0000000..3e09411 --- /dev/null +++ b/crates/zclaw-saas/migrations/down/20260404000001_model_groups.sql @@ -0,0 +1,3 @@ +-- Down: Drop model group tables (reverse creation order due to FK) +DROP TABLE IF EXISTS model_group_members; +DROP TABLE IF EXISTS model_groups; diff --git a/crates/zclaw-saas/migrations/down/20260405000001_industry_agent_templates.sql b/crates/zclaw-saas/migrations/down/20260405000001_industry_agent_templates.sql new file mode 100644 index 0000000..644b9a3 --- /dev/null +++ b/crates/zclaw-saas/migrations/down/20260405000001_industry_agent_templates.sql @@ -0,0 +1,6 @@ +-- Down: Remove industry agent template seed data +DELETE FROM agent_templates WHERE id IN ( + 'edu-teacher-001', + 'healthcare-admin-001', + 'design-shantou-001' +); diff --git a/crates/zclaw-saas/src/db.rs b/crates/zclaw-saas/src/db.rs index 851d682..286b64c 100644 --- a/crates/zclaw-saas/src/db.rs +++ b/crates/zclaw-saas/src/db.rs @@ -123,6 +123,50 @@ async fn run_migration_files(pool: &PgPool, dir: &std::path::Path) -> SaasResult Ok(()) } +/// 从 down/ 目录加载并执行回滚迁移文件(按文件名倒序) +/// +/// 用法: `task=migrate_down timestamp=20260402000001` +/// 会回滚 >= 该时间戳的所有增量迁移。不包含初始 schema 和 seed data。 +pub async fn run_down_migrations(pool: &PgPool, target: &str) -> SaasResult<()> { + let down_dir = std::path::Path::new("crates/zclaw-saas/migrations/down"); + let alt_dir = std::path::Path::new("migrations/down"); + let dir = if down_dir.exists() { down_dir } else if alt_dir.exists() { alt_dir } else { + tracing::warn!("No down migrations directory found"); + return Ok(()); + }; + + let mut entries: Vec = std::fs::read_dir(dir)? + .filter_map(|e| e.ok()) + .map(|e| e.path()) + .filter(|p| { + p.extension().map(|ext| ext == "sql").unwrap_or(false) + && p.file_name() + .map(|n| n.to_string_lossy().starts_with(target)) + .unwrap_or(false) + }) + .collect(); + entries.sort_by(|a, b| b.cmp(a)); // Reverse order (newest first) + + if entries.is_empty() { + tracing::info!("No down migrations match prefix '{}'", target); + return Ok(()); + } + + for path in &entries { + let filename = path.file_name().unwrap_or_default().to_string_lossy(); + tracing::info!("Running down migration: {}", filename); + let content = std::fs::read_to_string(path)?; + for stmt in split_sql_statements(&content) { + let trimmed = stmt.trim(); + if !trimmed.is_empty() && !trimmed.starts_with("--") { + sqlx::query(trimmed).execute(pool).await?; + } + } + } + tracing::info!("Down migrations complete (prefix={})", target); + Ok(()) +} + /// 按语句分割 SQL 文件内容,正确处理: /// - 单引号字符串 `'...'` /// - 双引号标识符 `"..."` diff --git a/crates/zclaw-saas/src/tasks/mod.rs b/crates/zclaw-saas/src/tasks/mod.rs index 1ac7c36..bc26dba 100644 --- a/crates/zclaw-saas/src/tasks/mod.rs +++ b/crates/zclaw-saas/src/tasks/mod.rs @@ -3,7 +3,7 @@ //! 提供可手动执行的运维命令: //! - seed_admin — 创建管理员账号 //! - cleanup_devices — 清理不活跃设备 -//! - migrate_schema — 手动触发 schema 迁移 +//! - migrate_down — 回滚指定迁移(timestamp=20260402 回滚所有 >= 该前缀的迁移) use std::collections::HashMap; use sqlx::PgPool; @@ -27,6 +27,7 @@ pub fn builtin_tasks() -> Vec> { vec![ Box::new(SeedAdminTask), Box::new(CleanupDevicesTask), + Box::new(MigrateDownTask), ] } @@ -86,3 +87,18 @@ impl Task for CleanupDevicesTask { Ok(()) } } + +/// 回滚数据库迁移 +struct MigrateDownTask; + +#[async_trait::async_trait] +impl Task for MigrateDownTask { + fn name(&self) -> &str { "migrate_down" } + fn description(&self) -> &str { "回滚指定时间戳之后的迁移(timestamp=20260402 回滚 >= 该前缀的所有增量迁移)" } + + async fn run(&self, db: &PgPool, args: &HashMap) -> SaasResult<()> { + let timestamp = args.get("timestamp") + .ok_or_else(|| crate::error::SaasError::InvalidInput("Missing 'timestamp' argument (e.g. 20260402)".into()))?; + crate::db::run_down_migrations(db, timestamp).await + } +} diff --git a/docs/features/AUDIT_TRACKER.md b/docs/features/AUDIT_TRACKER.md index 5960265..bb9c6dc 100644 --- a/docs/features/AUDIT_TRACKER.md +++ b/docs/features/AUDIT_TRACKER.md @@ -228,7 +228,7 @@ | AUD3-FE-04 | window 全局变量存 interval | **FALSE_POSITIVE** | App.tsx 已使用 useRef,非 window 全局变量 | | AUD3-FE-05 | 25+ 处 mixin `as any` | **FIXED** | gateway-heartbeat.ts 删除(死代码) + kernel-*.ts 改为 Record + chatStore 改为 Record; as any 38→3 | | AUD3-FE-06 | PropertyPanel 17 处 `as any` | **FIXED** | 13 处 (data as any) → 类型安全 d 访问器 (Record) | -| AUD3-DB-01 | 无 down migration | OPEN | crates/zclaw-saas/migrations/ | +| AUD3-DB-01 | 无 down migration | **FIXED** | 16 个 down SQL + run_down_migrations() + migrate_down task | | AUD3-DB-02 | format! SQL 模式 | **FALSE_POSITIVE** | 同 SEC2-P2-08,白名单+防御性验证已到位 | | AUD3-API-02 | 前端错误处理不统一 | **DOCUMENTED** | error-handling.ts 基础设施已完善,100+ 文件渐进式迁移至 reportError/reportApiError | | AUD3-CONC-02 | ~15 处 fire-and-forget tokio::spawn | **FALSE_POSITIVE** | 与 SEC2-P2-05 相同,已添加 NOTE(fire-and-forget) 注释 | @@ -273,6 +273,7 @@ | 2026-04-05 | SEC2-P3-02 | OPEN → DOCUMENTED | P4 级差异,admin 独立类型系统 | | 2026-04-05 | V11-P2-05 | 部分关闭 → FIXED | 完整审计: 160 @connected + 16 @reserved + 1 未注册 = 177 总命令 | | 2026-04-05 | TYPE-01 | OPEN → FIXED | ConnectionState 统一(含 handshaking); saas-types PromptTemplate/PromptVariable union literal; CreateRoleRequest optional; PropertyPanel as any→类型安全; chatStore window cast | +| 2026-04-05 | AUD3-DB-01 | OPEN → FIXED | 16 个 down migration SQL + db::run_down_migrations() + migrate_down task (timestamp=20260402) | ## V12 模块化端到端审计修复 (2026-04-04)