diff --git a/backend/migrations/004_add_redeem_code_notes.sql b/backend/migrations/004_add_redeem_code_notes.sql index 7fed6ec0..eeb37b10 100644 --- a/backend/migrations/004_add_redeem_code_notes.sql +++ b/backend/migrations/004_add_redeem_code_notes.sql @@ -1,6 +1,6 @@ --- 为 redeem_codes 表添加备注字段 - -ALTER TABLE redeem_codes -ADD COLUMN IF NOT EXISTS notes TEXT DEFAULT NULL; - -COMMENT ON COLUMN redeem_codes.notes IS '备注说明(管理员调整时的原因说明)'; +-- 为 redeem_codes 表添加备注字段 + +ALTER TABLE redeem_codes +ADD COLUMN IF NOT EXISTS notes TEXT DEFAULT NULL; + +COMMENT ON COLUMN redeem_codes.notes IS '备注说明(管理员调整时的原因说明)'; diff --git a/backend/migrations/005_schema_parity.sql b/backend/migrations/005_schema_parity.sql index 9b065fd2..0ee3f121 100644 --- a/backend/migrations/005_schema_parity.sql +++ b/backend/migrations/005_schema_parity.sql @@ -1,42 +1,42 @@ --- Align SQL migrations with current GORM persistence models. --- This file is designed to be safe on both fresh installs and existing databases. - --- users: add fields added after initial migration -ALTER TABLE users ADD COLUMN IF NOT EXISTS username VARCHAR(100) NOT NULL DEFAULT ''; -ALTER TABLE users ADD COLUMN IF NOT EXISTS wechat VARCHAR(100) NOT NULL DEFAULT ''; -ALTER TABLE users ADD COLUMN IF NOT EXISTS notes TEXT NOT NULL DEFAULT ''; - --- api_keys: allow longer keys (GORM model uses size:128) -ALTER TABLE api_keys ALTER COLUMN key TYPE VARCHAR(128); - --- accounts: scheduling and rate-limit fields used by repository queries -ALTER TABLE accounts ADD COLUMN IF NOT EXISTS schedulable BOOLEAN NOT NULL DEFAULT TRUE; -ALTER TABLE accounts ADD COLUMN IF NOT EXISTS rate_limited_at TIMESTAMPTZ; -ALTER TABLE accounts ADD COLUMN IF NOT EXISTS rate_limit_reset_at TIMESTAMPTZ; -ALTER TABLE accounts ADD COLUMN IF NOT EXISTS overload_until TIMESTAMPTZ; -ALTER TABLE accounts ADD COLUMN IF NOT EXISTS session_window_start TIMESTAMPTZ; -ALTER TABLE accounts ADD COLUMN IF NOT EXISTS session_window_end TIMESTAMPTZ; -ALTER TABLE accounts ADD COLUMN IF NOT EXISTS session_window_status VARCHAR(20); - -CREATE INDEX IF NOT EXISTS idx_accounts_schedulable ON accounts(schedulable); -CREATE INDEX IF NOT EXISTS idx_accounts_rate_limited_at ON accounts(rate_limited_at); -CREATE INDEX IF NOT EXISTS idx_accounts_rate_limit_reset_at ON accounts(rate_limit_reset_at); -CREATE INDEX IF NOT EXISTS idx_accounts_overload_until ON accounts(overload_until); - --- redeem_codes: subscription redeem fields -ALTER TABLE redeem_codes ADD COLUMN IF NOT EXISTS group_id BIGINT REFERENCES groups(id) ON DELETE SET NULL; -ALTER TABLE redeem_codes ADD COLUMN IF NOT EXISTS validity_days INT NOT NULL DEFAULT 30; -CREATE INDEX IF NOT EXISTS idx_redeem_codes_group_id ON redeem_codes(group_id); - --- usage_logs: billing type used by filters and stats -ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS billing_type SMALLINT NOT NULL DEFAULT 0; -CREATE INDEX IF NOT EXISTS idx_usage_logs_billing_type ON usage_logs(billing_type); - --- settings: key-value store -CREATE TABLE IF NOT EXISTS settings ( - id BIGSERIAL PRIMARY KEY, - key VARCHAR(100) NOT NULL UNIQUE, - value TEXT NOT NULL, - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - +-- Align SQL migrations with current GORM persistence models. +-- This file is designed to be safe on both fresh installs and existing databases. + +-- users: add fields added after initial migration +ALTER TABLE users ADD COLUMN IF NOT EXISTS username VARCHAR(100) NOT NULL DEFAULT ''; +ALTER TABLE users ADD COLUMN IF NOT EXISTS wechat VARCHAR(100) NOT NULL DEFAULT ''; +ALTER TABLE users ADD COLUMN IF NOT EXISTS notes TEXT NOT NULL DEFAULT ''; + +-- api_keys: allow longer keys (GORM model uses size:128) +ALTER TABLE api_keys ALTER COLUMN key TYPE VARCHAR(128); + +-- accounts: scheduling and rate-limit fields used by repository queries +ALTER TABLE accounts ADD COLUMN IF NOT EXISTS schedulable BOOLEAN NOT NULL DEFAULT TRUE; +ALTER TABLE accounts ADD COLUMN IF NOT EXISTS rate_limited_at TIMESTAMPTZ; +ALTER TABLE accounts ADD COLUMN IF NOT EXISTS rate_limit_reset_at TIMESTAMPTZ; +ALTER TABLE accounts ADD COLUMN IF NOT EXISTS overload_until TIMESTAMPTZ; +ALTER TABLE accounts ADD COLUMN IF NOT EXISTS session_window_start TIMESTAMPTZ; +ALTER TABLE accounts ADD COLUMN IF NOT EXISTS session_window_end TIMESTAMPTZ; +ALTER TABLE accounts ADD COLUMN IF NOT EXISTS session_window_status VARCHAR(20); + +CREATE INDEX IF NOT EXISTS idx_accounts_schedulable ON accounts(schedulable); +CREATE INDEX IF NOT EXISTS idx_accounts_rate_limited_at ON accounts(rate_limited_at); +CREATE INDEX IF NOT EXISTS idx_accounts_rate_limit_reset_at ON accounts(rate_limit_reset_at); +CREATE INDEX IF NOT EXISTS idx_accounts_overload_until ON accounts(overload_until); + +-- redeem_codes: subscription redeem fields +ALTER TABLE redeem_codes ADD COLUMN IF NOT EXISTS group_id BIGINT REFERENCES groups(id) ON DELETE SET NULL; +ALTER TABLE redeem_codes ADD COLUMN IF NOT EXISTS validity_days INT NOT NULL DEFAULT 30; +CREATE INDEX IF NOT EXISTS idx_redeem_codes_group_id ON redeem_codes(group_id); + +-- usage_logs: billing type used by filters and stats +ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS billing_type SMALLINT NOT NULL DEFAULT 0; +CREATE INDEX IF NOT EXISTS idx_usage_logs_billing_type ON usage_logs(billing_type); + +-- settings: key-value store +CREATE TABLE IF NOT EXISTS settings ( + id BIGSERIAL PRIMARY KEY, + key VARCHAR(100) NOT NULL UNIQUE, + value TEXT NOT NULL, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + diff --git a/backend/migrations/006_fix_invalid_subscription_expires_at.sql b/backend/migrations/006_fix_invalid_subscription_expires_at.sql index 66cf0af3..7a0c2642 100644 --- a/backend/migrations/006_fix_invalid_subscription_expires_at.sql +++ b/backend/migrations/006_fix_invalid_subscription_expires_at.sql @@ -1,10 +1,10 @@ --- Fix legacy subscription records with invalid expires_at (year > 2099). -DO $$ -BEGIN - IF to_regclass('public.user_subscriptions') IS NOT NULL THEN - UPDATE user_subscriptions - SET expires_at = TIMESTAMPTZ '2099-12-31 23:59:59+00' - WHERE expires_at > TIMESTAMPTZ '2099-12-31 23:59:59+00'; - END IF; -END $$; - +-- Fix legacy subscription records with invalid expires_at (year > 2099). +DO $$ +BEGIN + IF to_regclass('public.user_subscriptions') IS NOT NULL THEN + UPDATE user_subscriptions + SET expires_at = TIMESTAMPTZ '2099-12-31 23:59:59+00' + WHERE expires_at > TIMESTAMPTZ '2099-12-31 23:59:59+00'; + END IF; +END $$; + diff --git a/backend/migrations/007_add_user_allowed_groups.sql b/backend/migrations/007_add_user_allowed_groups.sql index 78aa1240..a61400d2 100644 --- a/backend/migrations/007_add_user_allowed_groups.sql +++ b/backend/migrations/007_add_user_allowed_groups.sql @@ -1,20 +1,20 @@ --- Add user_allowed_groups join table to replace users.allowed_groups (BIGINT[]). --- Phase 1: create table + backfill from the legacy array column. - -CREATE TABLE IF NOT EXISTS user_allowed_groups ( - user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, - group_id BIGINT NOT NULL REFERENCES groups(id) ON DELETE CASCADE, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - PRIMARY KEY (user_id, group_id) -); - -CREATE INDEX IF NOT EXISTS idx_user_allowed_groups_group_id ON user_allowed_groups(group_id); - --- Backfill from the legacy users.allowed_groups array. -INSERT INTO user_allowed_groups (user_id, group_id) -SELECT u.id, x.group_id -FROM users u -CROSS JOIN LATERAL unnest(u.allowed_groups) AS x(group_id) -JOIN groups g ON g.id = x.group_id -WHERE u.allowed_groups IS NOT NULL -ON CONFLICT DO NOTHING; +-- Add user_allowed_groups join table to replace users.allowed_groups (BIGINT[]). +-- Phase 1: create table + backfill from the legacy array column. + +CREATE TABLE IF NOT EXISTS user_allowed_groups ( + user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + group_id BIGINT NOT NULL REFERENCES groups(id) ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + PRIMARY KEY (user_id, group_id) +); + +CREATE INDEX IF NOT EXISTS idx_user_allowed_groups_group_id ON user_allowed_groups(group_id); + +-- Backfill from the legacy users.allowed_groups array. +INSERT INTO user_allowed_groups (user_id, group_id) +SELECT u.id, x.group_id +FROM users u +CROSS JOIN LATERAL unnest(u.allowed_groups) AS x(group_id) +JOIN groups g ON g.id = x.group_id +WHERE u.allowed_groups IS NOT NULL +ON CONFLICT DO NOTHING; diff --git a/backend/migrations/008_seed_default_group.sql b/backend/migrations/008_seed_default_group.sql index a05917c0..cfe2640f 100644 --- a/backend/migrations/008_seed_default_group.sql +++ b/backend/migrations/008_seed_default_group.sql @@ -1,4 +1,4 @@ --- Seed a default group for fresh installs. -INSERT INTO groups (name, description, created_at, updated_at) -SELECT 'default', 'Default group', NOW(), NOW() -WHERE NOT EXISTS (SELECT 1 FROM groups); +-- Seed a default group for fresh installs. +INSERT INTO groups (name, description, created_at, updated_at) +SELECT 'default', 'Default group', NOW(), NOW() +WHERE NOT EXISTS (SELECT 1 FROM groups); diff --git a/backend/migrations/009_fix_usage_logs_cache_columns.sql b/backend/migrations/009_fix_usage_logs_cache_columns.sql index 07aba064..979405af 100644 --- a/backend/migrations/009_fix_usage_logs_cache_columns.sql +++ b/backend/migrations/009_fix_usage_logs_cache_columns.sql @@ -1,37 +1,37 @@ --- Ensure usage_logs cache token columns use the underscored names expected by code. --- Backfill from legacy column names if they exist. - -ALTER TABLE usage_logs - ADD COLUMN IF NOT EXISTS cache_creation_5m_tokens INT NOT NULL DEFAULT 0; - -ALTER TABLE usage_logs - ADD COLUMN IF NOT EXISTS cache_creation_1h_tokens INT NOT NULL DEFAULT 0; - -DO $$ -BEGIN - IF EXISTS ( - SELECT 1 - FROM information_schema.columns - WHERE table_schema = 'public' - AND table_name = 'usage_logs' - AND column_name = 'cache_creation5m_tokens' - ) THEN - UPDATE usage_logs - SET cache_creation_5m_tokens = cache_creation5m_tokens - WHERE cache_creation_5m_tokens = 0 - AND cache_creation5m_tokens <> 0; - END IF; - - IF EXISTS ( - SELECT 1 - FROM information_schema.columns - WHERE table_schema = 'public' - AND table_name = 'usage_logs' - AND column_name = 'cache_creation1h_tokens' - ) THEN - UPDATE usage_logs - SET cache_creation_1h_tokens = cache_creation1h_tokens - WHERE cache_creation_1h_tokens = 0 - AND cache_creation1h_tokens <> 0; - END IF; -END $$; +-- Ensure usage_logs cache token columns use the underscored names expected by code. +-- Backfill from legacy column names if they exist. + +ALTER TABLE usage_logs + ADD COLUMN IF NOT EXISTS cache_creation_5m_tokens INT NOT NULL DEFAULT 0; + +ALTER TABLE usage_logs + ADD COLUMN IF NOT EXISTS cache_creation_1h_tokens INT NOT NULL DEFAULT 0; + +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'usage_logs' + AND column_name = 'cache_creation5m_tokens' + ) THEN + UPDATE usage_logs + SET cache_creation_5m_tokens = cache_creation5m_tokens + WHERE cache_creation_5m_tokens = 0 + AND cache_creation5m_tokens <> 0; + END IF; + + IF EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'usage_logs' + AND column_name = 'cache_creation1h_tokens' + ) THEN + UPDATE usage_logs + SET cache_creation_1h_tokens = cache_creation1h_tokens + WHERE cache_creation_1h_tokens = 0 + AND cache_creation1h_tokens <> 0; + END IF; +END $$; diff --git a/backend/migrations/010_add_usage_logs_aggregated_indexes.sql b/backend/migrations/010_add_usage_logs_aggregated_indexes.sql index 55850c8b..ab2dbbc1 100644 --- a/backend/migrations/010_add_usage_logs_aggregated_indexes.sql +++ b/backend/migrations/010_add_usage_logs_aggregated_indexes.sql @@ -1,4 +1,4 @@ --- 为聚合查询补充复合索引 -CREATE INDEX IF NOT EXISTS idx_usage_logs_account_created_at ON usage_logs(account_id, created_at); -CREATE INDEX IF NOT EXISTS idx_usage_logs_api_key_created_at ON usage_logs(api_key_id, created_at); -CREATE INDEX IF NOT EXISTS idx_usage_logs_model_created_at ON usage_logs(model, created_at); +-- 为聚合查询补充复合索引 +CREATE INDEX IF NOT EXISTS idx_usage_logs_account_created_at ON usage_logs(account_id, created_at); +CREATE INDEX IF NOT EXISTS idx_usage_logs_api_key_created_at ON usage_logs(api_key_id, created_at); +CREATE INDEX IF NOT EXISTS idx_usage_logs_model_created_at ON usage_logs(model, created_at); diff --git a/backend/migrations/011_remove_duplicate_unique_indexes.sql b/backend/migrations/011_remove_duplicate_unique_indexes.sql index e6fa1813..8fd62710 100644 --- a/backend/migrations/011_remove_duplicate_unique_indexes.sql +++ b/backend/migrations/011_remove_duplicate_unique_indexes.sql @@ -1,39 +1,39 @@ --- 011_remove_duplicate_unique_indexes.sql --- 移除重复的唯一索引 --- 这些字段在 ent schema 的 Fields() 中已声明 .Unique(), --- 因此在 Indexes() 中再次声明 index.Fields("x").Unique() 会创建重复索引。 --- 本迁移脚本清理这些冗余索引。 - --- 重复索引命名约定(由 Ent 自动生成/历史迁移遗留): --- - 字段级 Unique() 创建的索引名: __key --- - Indexes() 中的 Unique() 创建的索引名:
_ --- - 初始化迁移中的非唯一索引: idx_
_ - --- 仅当索引存在时才删除(幂等操作) - --- api_keys 表: key 字段 -DROP INDEX IF EXISTS apikey_key; -DROP INDEX IF EXISTS api_keys_key; -DROP INDEX IF EXISTS idx_api_keys_key; - --- users 表: email 字段 -DROP INDEX IF EXISTS user_email; -DROP INDEX IF EXISTS users_email; -DROP INDEX IF EXISTS idx_users_email; - --- settings 表: key 字段 -DROP INDEX IF EXISTS settings_key; -DROP INDEX IF EXISTS idx_settings_key; - --- redeem_codes 表: code 字段 -DROP INDEX IF EXISTS redeemcode_code; -DROP INDEX IF EXISTS redeem_codes_code; -DROP INDEX IF EXISTS idx_redeem_codes_code; - --- groups 表: name 字段 -DROP INDEX IF EXISTS group_name; -DROP INDEX IF EXISTS groups_name; -DROP INDEX IF EXISTS idx_groups_name; - --- 注意: 每个字段的唯一约束仍由字段级 Unique() 创建的约束保留, --- 如 api_keys_key_key、users_email_key 等。 +-- 011_remove_duplicate_unique_indexes.sql +-- 移除重复的唯一索引 +-- 这些字段在 ent schema 的 Fields() 中已声明 .Unique(), +-- 因此在 Indexes() 中再次声明 index.Fields("x").Unique() 会创建重复索引。 +-- 本迁移脚本清理这些冗余索引。 + +-- 重复索引命名约定(由 Ent 自动生成/历史迁移遗留): +-- - 字段级 Unique() 创建的索引名:
__key +-- - Indexes() 中的 Unique() 创建的索引名:
_ +-- - 初始化迁移中的非唯一索引: idx_
_ + +-- 仅当索引存在时才删除(幂等操作) + +-- api_keys 表: key 字段 +DROP INDEX IF EXISTS apikey_key; +DROP INDEX IF EXISTS api_keys_key; +DROP INDEX IF EXISTS idx_api_keys_key; + +-- users 表: email 字段 +DROP INDEX IF EXISTS user_email; +DROP INDEX IF EXISTS users_email; +DROP INDEX IF EXISTS idx_users_email; + +-- settings 表: key 字段 +DROP INDEX IF EXISTS settings_key; +DROP INDEX IF EXISTS idx_settings_key; + +-- redeem_codes 表: code 字段 +DROP INDEX IF EXISTS redeemcode_code; +DROP INDEX IF EXISTS redeem_codes_code; +DROP INDEX IF EXISTS idx_redeem_codes_code; + +-- groups 表: name 字段 +DROP INDEX IF EXISTS group_name; +DROP INDEX IF EXISTS groups_name; +DROP INDEX IF EXISTS idx_groups_name; + +-- 注意: 每个字段的唯一约束仍由字段级 Unique() 创建的约束保留, +-- 如 api_keys_key_key、users_email_key 等。 diff --git a/backend/migrations/012_add_user_subscription_soft_delete.sql b/backend/migrations/012_add_user_subscription_soft_delete.sql index a3204523..b6cb7366 100644 --- a/backend/migrations/012_add_user_subscription_soft_delete.sql +++ b/backend/migrations/012_add_user_subscription_soft_delete.sql @@ -1,13 +1,13 @@ --- 012: 为 user_subscriptions 表添加软删除支持 --- 任务:fix-medium-data-hygiene 1.1 - --- 添加 deleted_at 字段 -ALTER TABLE user_subscriptions -ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ DEFAULT NULL; - --- 添加 deleted_at 索引以优化软删除查询 -CREATE INDEX IF NOT EXISTS usersubscription_deleted_at -ON user_subscriptions (deleted_at); - --- 注释:与其他使用软删除的实体保持一致 -COMMENT ON COLUMN user_subscriptions.deleted_at IS '软删除时间戳,NULL 表示未删除'; +-- 012: 为 user_subscriptions 表添加软删除支持 +-- 任务:fix-medium-data-hygiene 1.1 + +-- 添加 deleted_at 字段 +ALTER TABLE user_subscriptions +ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ DEFAULT NULL; + +-- 添加 deleted_at 索引以优化软删除查询 +CREATE INDEX IF NOT EXISTS usersubscription_deleted_at +ON user_subscriptions (deleted_at); + +-- 注释:与其他使用软删除的实体保持一致 +COMMENT ON COLUMN user_subscriptions.deleted_at IS '软删除时间戳,NULL 表示未删除'; diff --git a/backend/migrations/013_log_orphan_allowed_groups.sql b/backend/migrations/013_log_orphan_allowed_groups.sql index 80db30d8..976c0aca 100644 --- a/backend/migrations/013_log_orphan_allowed_groups.sql +++ b/backend/migrations/013_log_orphan_allowed_groups.sql @@ -1,32 +1,32 @@ --- 013: 记录 users.allowed_groups 中的孤立 group_id --- 任务:fix-medium-data-hygiene 3.1 --- --- 目的:在删除 legacy allowed_groups 列前,记录所有引用了不存在 group 的孤立记录 --- 这些记录可用于审计或后续数据修复 - --- 创建审计表存储孤立的 allowed_groups 记录 -CREATE TABLE IF NOT EXISTS orphan_allowed_groups_audit ( - id BIGSERIAL PRIMARY KEY, - user_id BIGINT NOT NULL, - group_id BIGINT NOT NULL, - recorded_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - UNIQUE (user_id, group_id) -); - --- 记录孤立的 group_id(存在于 users.allowed_groups 但不存在于 groups 表) -INSERT INTO orphan_allowed_groups_audit (user_id, group_id) -SELECT u.id, x.group_id -FROM users u -CROSS JOIN LATERAL unnest(u.allowed_groups) AS x(group_id) -LEFT JOIN groups g ON g.id = x.group_id -WHERE u.allowed_groups IS NOT NULL - AND g.id IS NULL -ON CONFLICT (user_id, group_id) DO NOTHING; - --- 添加索引便于查询 -CREATE INDEX IF NOT EXISTS idx_orphan_allowed_groups_audit_user_id -ON orphan_allowed_groups_audit(user_id); - --- 记录迁移完成信息 -COMMENT ON TABLE orphan_allowed_groups_audit IS -'审计表:记录 users.allowed_groups 中引用的不存在的 group_id,用于数据清理前的审计'; +-- 013: 记录 users.allowed_groups 中的孤立 group_id +-- 任务:fix-medium-data-hygiene 3.1 +-- +-- 目的:在删除 legacy allowed_groups 列前,记录所有引用了不存在 group 的孤立记录 +-- 这些记录可用于审计或后续数据修复 + +-- 创建审计表存储孤立的 allowed_groups 记录 +CREATE TABLE IF NOT EXISTS orphan_allowed_groups_audit ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL, + group_id BIGINT NOT NULL, + recorded_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (user_id, group_id) +); + +-- 记录孤立的 group_id(存在于 users.allowed_groups 但不存在于 groups 表) +INSERT INTO orphan_allowed_groups_audit (user_id, group_id) +SELECT u.id, x.group_id +FROM users u +CROSS JOIN LATERAL unnest(u.allowed_groups) AS x(group_id) +LEFT JOIN groups g ON g.id = x.group_id +WHERE u.allowed_groups IS NOT NULL + AND g.id IS NULL +ON CONFLICT (user_id, group_id) DO NOTHING; + +-- 添加索引便于查询 +CREATE INDEX IF NOT EXISTS idx_orphan_allowed_groups_audit_user_id +ON orphan_allowed_groups_audit(user_id); + +-- 记录迁移完成信息 +COMMENT ON TABLE orphan_allowed_groups_audit IS +'审计表:记录 users.allowed_groups 中引用的不存在的 group_id,用于数据清理前的审计'; diff --git a/backend/migrations/014_drop_legacy_allowed_groups.sql b/backend/migrations/014_drop_legacy_allowed_groups.sql index 7dc6ea9e..2c2a3d45 100644 --- a/backend/migrations/014_drop_legacy_allowed_groups.sql +++ b/backend/migrations/014_drop_legacy_allowed_groups.sql @@ -1,15 +1,15 @@ --- 014: 删除 legacy users.allowed_groups 列 --- 任务:fix-medium-data-hygiene 3.3 --- --- 前置条件: --- - 迁移 007 已将数据回填到 user_allowed_groups 联接表 --- - 迁移 013 已记录所有孤立的 group_id 到审计表 --- - 应用代码已停止写入该列(3.2 完成) --- --- 该列现已废弃,所有读写操作均使用 user_allowed_groups 联接表。 - --- 删除 allowed_groups 列 -ALTER TABLE users DROP COLUMN IF EXISTS allowed_groups; - --- 添加注释记录删除原因 -COMMENT ON TABLE users IS '用户表。注:原 allowed_groups BIGINT[] 列已迁移至 user_allowed_groups 联接表'; +-- 014: 删除 legacy users.allowed_groups 列 +-- 任务:fix-medium-data-hygiene 3.3 +-- +-- 前置条件: +-- - 迁移 007 已将数据回填到 user_allowed_groups 联接表 +-- - 迁移 013 已记录所有孤立的 group_id 到审计表 +-- - 应用代码已停止写入该列(3.2 完成) +-- +-- 该列现已废弃,所有读写操作均使用 user_allowed_groups 联接表。 + +-- 删除 allowed_groups 列 +ALTER TABLE users DROP COLUMN IF EXISTS allowed_groups; + +-- 添加注释记录删除原因 +COMMENT ON TABLE users IS '用户表。注:原 allowed_groups BIGINT[] 列已迁移至 user_allowed_groups 联接表'; diff --git a/backend/migrations/015_fix_settings_unique_constraint.sql b/backend/migrations/015_fix_settings_unique_constraint.sql index e675ee18..60f8fcad 100644 --- a/backend/migrations/015_fix_settings_unique_constraint.sql +++ b/backend/migrations/015_fix_settings_unique_constraint.sql @@ -1,19 +1,19 @@ --- 015_fix_settings_unique_constraint.sql --- 修复 settings 表 key 字段缺失的唯一约束 --- 此约束是 ON CONFLICT ("key") DO UPDATE 语句所必需的 - --- 检查并添加唯一约束(如果不存在) -DO $$ -BEGIN - -- 检查是否已存在唯一约束 - IF NOT EXISTS ( - SELECT 1 FROM pg_constraint - WHERE conrelid = 'settings'::regclass - AND contype = 'u' - AND conname = 'settings_key_key' - ) THEN - -- 添加唯一约束 - ALTER TABLE settings ADD CONSTRAINT settings_key_key UNIQUE (key); - END IF; -END -$$; +-- 015_fix_settings_unique_constraint.sql +-- 修复 settings 表 key 字段缺失的唯一约束 +-- 此约束是 ON CONFLICT ("key") DO UPDATE 语句所必需的 + +-- 检查并添加唯一约束(如果不存在) +DO $$ +BEGIN + -- 检查是否已存在唯一约束 + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint + WHERE conrelid = 'settings'::regclass + AND contype = 'u' + AND conname = 'settings_key_key' + ) THEN + -- 添加唯一约束 + ALTER TABLE settings ADD CONSTRAINT settings_key_key UNIQUE (key); + END IF; +END +$$; diff --git a/backend/migrations/016_soft_delete_partial_unique_indexes.sql b/backend/migrations/016_soft_delete_partial_unique_indexes.sql index e3a1ea6b..b006b775 100644 --- a/backend/migrations/016_soft_delete_partial_unique_indexes.sql +++ b/backend/migrations/016_soft_delete_partial_unique_indexes.sql @@ -1,51 +1,51 @@ --- 016_soft_delete_partial_unique_indexes.sql --- 修复软删除 + 唯一约束冲突问题 --- 将普通唯一约束替换为部分唯一索引(WHERE deleted_at IS NULL) --- 这样软删除的记录不会占用唯一约束位置,允许删后重建同名/同邮箱/同订阅关系 - --- ============================================================================ --- 1. users 表: email 字段 --- ============================================================================ - --- 删除旧的唯一约束(可能的命名方式) -ALTER TABLE users DROP CONSTRAINT IF EXISTS users_email_key; -DROP INDEX IF EXISTS users_email_key; -DROP INDEX IF EXISTS user_email_key; - --- 创建部分唯一索引:只对未删除的记录建立唯一约束 -CREATE UNIQUE INDEX IF NOT EXISTS users_email_unique_active - ON users(email) - WHERE deleted_at IS NULL; - --- ============================================================================ --- 2. groups 表: name 字段 --- ============================================================================ - --- 删除旧的唯一约束 -ALTER TABLE groups DROP CONSTRAINT IF EXISTS groups_name_key; -DROP INDEX IF EXISTS groups_name_key; -DROP INDEX IF EXISTS group_name_key; - --- 创建部分唯一索引 -CREATE UNIQUE INDEX IF NOT EXISTS groups_name_unique_active - ON groups(name) - WHERE deleted_at IS NULL; - --- ============================================================================ --- 3. user_subscriptions 表: (user_id, group_id) 组合字段 --- ============================================================================ - --- 删除旧的唯一约束/索引 -ALTER TABLE user_subscriptions DROP CONSTRAINT IF EXISTS user_subscriptions_user_id_group_id_key; -DROP INDEX IF EXISTS user_subscriptions_user_id_group_id_key; -DROP INDEX IF EXISTS usersubscription_user_id_group_id; - --- 创建部分唯一索引 -CREATE UNIQUE INDEX IF NOT EXISTS user_subscriptions_user_group_unique_active - ON user_subscriptions(user_id, group_id) - WHERE deleted_at IS NULL; - --- ============================================================================ --- 注意: api_keys 表的 key 字段保留普通唯一约束 --- API Key 即使软删除后也不应该重复使用(安全考虑) --- ============================================================================ +-- 016_soft_delete_partial_unique_indexes.sql +-- 修复软删除 + 唯一约束冲突问题 +-- 将普通唯一约束替换为部分唯一索引(WHERE deleted_at IS NULL) +-- 这样软删除的记录不会占用唯一约束位置,允许删后重建同名/同邮箱/同订阅关系 + +-- ============================================================================ +-- 1. users 表: email 字段 +-- ============================================================================ + +-- 删除旧的唯一约束(可能的命名方式) +ALTER TABLE users DROP CONSTRAINT IF EXISTS users_email_key; +DROP INDEX IF EXISTS users_email_key; +DROP INDEX IF EXISTS user_email_key; + +-- 创建部分唯一索引:只对未删除的记录建立唯一约束 +CREATE UNIQUE INDEX IF NOT EXISTS users_email_unique_active + ON users(email) + WHERE deleted_at IS NULL; + +-- ============================================================================ +-- 2. groups 表: name 字段 +-- ============================================================================ + +-- 删除旧的唯一约束 +ALTER TABLE groups DROP CONSTRAINT IF EXISTS groups_name_key; +DROP INDEX IF EXISTS groups_name_key; +DROP INDEX IF EXISTS group_name_key; + +-- 创建部分唯一索引 +CREATE UNIQUE INDEX IF NOT EXISTS groups_name_unique_active + ON groups(name) + WHERE deleted_at IS NULL; + +-- ============================================================================ +-- 3. user_subscriptions 表: (user_id, group_id) 组合字段 +-- ============================================================================ + +-- 删除旧的唯一约束/索引 +ALTER TABLE user_subscriptions DROP CONSTRAINT IF EXISTS user_subscriptions_user_id_group_id_key; +DROP INDEX IF EXISTS user_subscriptions_user_id_group_id_key; +DROP INDEX IF EXISTS usersubscription_user_id_group_id; + +-- 创建部分唯一索引 +CREATE UNIQUE INDEX IF NOT EXISTS user_subscriptions_user_group_unique_active + ON user_subscriptions(user_id, group_id) + WHERE deleted_at IS NULL; + +-- ============================================================================ +-- 注意: api_keys 表的 key 字段保留普通唯一约束 +-- API Key 即使软删除后也不应该重复使用(安全考虑) +-- ============================================================================ diff --git a/backend/migrations/018_user_attributes.sql b/backend/migrations/018_user_attributes.sql index 8290c9d5..d2dad80d 100644 --- a/backend/migrations/018_user_attributes.sql +++ b/backend/migrations/018_user_attributes.sql @@ -1,48 +1,48 @@ --- Add user attribute definitions and values tables for custom user attributes. - --- User Attribute Definitions table (with soft delete support) -CREATE TABLE IF NOT EXISTS user_attribute_definitions ( - id BIGSERIAL PRIMARY KEY, - key VARCHAR(100) NOT NULL, - name VARCHAR(255) NOT NULL, - description TEXT DEFAULT '', - type VARCHAR(20) NOT NULL, - options JSONB DEFAULT '[]'::jsonb, - required BOOLEAN NOT NULL DEFAULT FALSE, - validation JSONB DEFAULT '{}'::jsonb, - placeholder VARCHAR(255) DEFAULT '', - display_order INT NOT NULL DEFAULT 0, - enabled BOOLEAN NOT NULL DEFAULT TRUE, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - deleted_at TIMESTAMPTZ -); - --- Partial unique index for key (only for non-deleted records) --- Allows reusing keys after soft delete -CREATE UNIQUE INDEX IF NOT EXISTS idx_user_attribute_definitions_key_unique - ON user_attribute_definitions(key) WHERE deleted_at IS NULL; - -CREATE INDEX IF NOT EXISTS idx_user_attribute_definitions_enabled - ON user_attribute_definitions(enabled); -CREATE INDEX IF NOT EXISTS idx_user_attribute_definitions_display_order - ON user_attribute_definitions(display_order); -CREATE INDEX IF NOT EXISTS idx_user_attribute_definitions_deleted_at - ON user_attribute_definitions(deleted_at); - --- User Attribute Values table (hard delete only, no deleted_at) -CREATE TABLE IF NOT EXISTS user_attribute_values ( - id BIGSERIAL PRIMARY KEY, - user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, - attribute_id BIGINT NOT NULL REFERENCES user_attribute_definitions(id) ON DELETE CASCADE, - value TEXT DEFAULT '', - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - - UNIQUE(user_id, attribute_id) -); - -CREATE INDEX IF NOT EXISTS idx_user_attribute_values_user_id - ON user_attribute_values(user_id); -CREATE INDEX IF NOT EXISTS idx_user_attribute_values_attribute_id - ON user_attribute_values(attribute_id); +-- Add user attribute definitions and values tables for custom user attributes. + +-- User Attribute Definitions table (with soft delete support) +CREATE TABLE IF NOT EXISTS user_attribute_definitions ( + id BIGSERIAL PRIMARY KEY, + key VARCHAR(100) NOT NULL, + name VARCHAR(255) NOT NULL, + description TEXT DEFAULT '', + type VARCHAR(20) NOT NULL, + options JSONB DEFAULT '[]'::jsonb, + required BOOLEAN NOT NULL DEFAULT FALSE, + validation JSONB DEFAULT '{}'::jsonb, + placeholder VARCHAR(255) DEFAULT '', + display_order INT NOT NULL DEFAULT 0, + enabled BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +-- Partial unique index for key (only for non-deleted records) +-- Allows reusing keys after soft delete +CREATE UNIQUE INDEX IF NOT EXISTS idx_user_attribute_definitions_key_unique + ON user_attribute_definitions(key) WHERE deleted_at IS NULL; + +CREATE INDEX IF NOT EXISTS idx_user_attribute_definitions_enabled + ON user_attribute_definitions(enabled); +CREATE INDEX IF NOT EXISTS idx_user_attribute_definitions_display_order + ON user_attribute_definitions(display_order); +CREATE INDEX IF NOT EXISTS idx_user_attribute_definitions_deleted_at + ON user_attribute_definitions(deleted_at); + +-- User Attribute Values table (hard delete only, no deleted_at) +CREATE TABLE IF NOT EXISTS user_attribute_values ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + attribute_id BIGINT NOT NULL REFERENCES user_attribute_definitions(id) ON DELETE CASCADE, + value TEXT DEFAULT '', + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + UNIQUE(user_id, attribute_id) +); + +CREATE INDEX IF NOT EXISTS idx_user_attribute_values_user_id + ON user_attribute_values(user_id); +CREATE INDEX IF NOT EXISTS idx_user_attribute_values_attribute_id + ON user_attribute_values(attribute_id); diff --git a/backend/migrations/019_migrate_wechat_to_attributes.sql b/backend/migrations/019_migrate_wechat_to_attributes.sql index ac98f99f..765ca498 100644 --- a/backend/migrations/019_migrate_wechat_to_attributes.sql +++ b/backend/migrations/019_migrate_wechat_to_attributes.sql @@ -1,83 +1,83 @@ --- Migration: Move wechat field from users table to user_attribute_values --- This migration: --- 1. Creates a "wechat" attribute definition --- 2. Migrates existing wechat data to user_attribute_values --- 3. Does NOT drop the wechat column (for rollback safety, can be done in a later migration) - --- +goose Up --- +goose StatementBegin - --- Step 1: Insert wechat attribute definition if not exists -INSERT INTO user_attribute_definitions (key, name, description, type, options, required, validation, placeholder, display_order, enabled, created_at, updated_at) -SELECT 'wechat', '微信', '用户微信号', 'text', '[]'::jsonb, false, '{}'::jsonb, '请输入微信号', 0, true, NOW(), NOW() -WHERE NOT EXISTS ( - SELECT 1 FROM user_attribute_definitions WHERE key = 'wechat' AND deleted_at IS NULL -); - --- Step 2: Migrate existing wechat values to user_attribute_values --- Only migrate non-empty values -INSERT INTO user_attribute_values (user_id, attribute_id, value, created_at, updated_at) -SELECT - u.id, - (SELECT id FROM user_attribute_definitions WHERE key = 'wechat' AND deleted_at IS NULL LIMIT 1), - u.wechat, - NOW(), - NOW() -FROM users u -WHERE u.wechat IS NOT NULL - AND u.wechat != '' - AND u.deleted_at IS NULL - AND NOT EXISTS ( - SELECT 1 FROM user_attribute_values uav - WHERE uav.user_id = u.id - AND uav.attribute_id = (SELECT id FROM user_attribute_definitions WHERE key = 'wechat' AND deleted_at IS NULL LIMIT 1) - ); - --- Step 3: Update display_order to ensure wechat appears first -UPDATE user_attribute_definitions -SET display_order = -1 -WHERE key = 'wechat' AND deleted_at IS NULL; - --- Reorder all attributes starting from 0 -WITH ordered AS ( - SELECT id, ROW_NUMBER() OVER (ORDER BY display_order, id) - 1 as new_order - FROM user_attribute_definitions - WHERE deleted_at IS NULL -) -UPDATE user_attribute_definitions -SET display_order = ordered.new_order -FROM ordered -WHERE user_attribute_definitions.id = ordered.id; - --- Step 4: Drop the redundant wechat column from users table -ALTER TABLE users DROP COLUMN IF EXISTS wechat; - --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin - --- Restore wechat column -ALTER TABLE users ADD COLUMN IF NOT EXISTS wechat VARCHAR(100) DEFAULT ''; - --- Copy attribute values back to users.wechat column -UPDATE users u -SET wechat = uav.value -FROM user_attribute_values uav -JOIN user_attribute_definitions uad ON uav.attribute_id = uad.id -WHERE uav.user_id = u.id - AND uad.key = 'wechat' - AND uad.deleted_at IS NULL; - --- Delete migrated attribute values -DELETE FROM user_attribute_values -WHERE attribute_id IN ( - SELECT id FROM user_attribute_definitions WHERE key = 'wechat' AND deleted_at IS NULL -); - --- Soft-delete the wechat attribute definition -UPDATE user_attribute_definitions -SET deleted_at = NOW() -WHERE key = 'wechat' AND deleted_at IS NULL; - --- +goose StatementEnd +-- Migration: Move wechat field from users table to user_attribute_values +-- This migration: +-- 1. Creates a "wechat" attribute definition +-- 2. Migrates existing wechat data to user_attribute_values +-- 3. Does NOT drop the wechat column (for rollback safety, can be done in a later migration) + +-- +goose Up +-- +goose StatementBegin + +-- Step 1: Insert wechat attribute definition if not exists +INSERT INTO user_attribute_definitions (key, name, description, type, options, required, validation, placeholder, display_order, enabled, created_at, updated_at) +SELECT 'wechat', '微信', '用户微信号', 'text', '[]'::jsonb, false, '{}'::jsonb, '请输入微信号', 0, true, NOW(), NOW() +WHERE NOT EXISTS ( + SELECT 1 FROM user_attribute_definitions WHERE key = 'wechat' AND deleted_at IS NULL +); + +-- Step 2: Migrate existing wechat values to user_attribute_values +-- Only migrate non-empty values +INSERT INTO user_attribute_values (user_id, attribute_id, value, created_at, updated_at) +SELECT + u.id, + (SELECT id FROM user_attribute_definitions WHERE key = 'wechat' AND deleted_at IS NULL LIMIT 1), + u.wechat, + NOW(), + NOW() +FROM users u +WHERE u.wechat IS NOT NULL + AND u.wechat != '' + AND u.deleted_at IS NULL + AND NOT EXISTS ( + SELECT 1 FROM user_attribute_values uav + WHERE uav.user_id = u.id + AND uav.attribute_id = (SELECT id FROM user_attribute_definitions WHERE key = 'wechat' AND deleted_at IS NULL LIMIT 1) + ); + +-- Step 3: Update display_order to ensure wechat appears first +UPDATE user_attribute_definitions +SET display_order = -1 +WHERE key = 'wechat' AND deleted_at IS NULL; + +-- Reorder all attributes starting from 0 +WITH ordered AS ( + SELECT id, ROW_NUMBER() OVER (ORDER BY display_order, id) - 1 as new_order + FROM user_attribute_definitions + WHERE deleted_at IS NULL +) +UPDATE user_attribute_definitions +SET display_order = ordered.new_order +FROM ordered +WHERE user_attribute_definitions.id = ordered.id; + +-- Step 4: Drop the redundant wechat column from users table +ALTER TABLE users DROP COLUMN IF EXISTS wechat; + +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin + +-- Restore wechat column +ALTER TABLE users ADD COLUMN IF NOT EXISTS wechat VARCHAR(100) DEFAULT ''; + +-- Copy attribute values back to users.wechat column +UPDATE users u +SET wechat = uav.value +FROM user_attribute_values uav +JOIN user_attribute_definitions uad ON uav.attribute_id = uad.id +WHERE uav.user_id = u.id + AND uad.key = 'wechat' + AND uad.deleted_at IS NULL; + +-- Delete migrated attribute values +DELETE FROM user_attribute_values +WHERE attribute_id IN ( + SELECT id FROM user_attribute_definitions WHERE key = 'wechat' AND deleted_at IS NULL +); + +-- Soft-delete the wechat attribute definition +UPDATE user_attribute_definitions +SET deleted_at = NOW() +WHERE key = 'wechat' AND deleted_at IS NULL; + +-- +goose StatementEnd diff --git a/backend/migrations/024_add_gemini_tier_id.sql b/backend/migrations/024_add_gemini_tier_id.sql index 1629238a..d9ac7afe 100644 --- a/backend/migrations/024_add_gemini_tier_id.sql +++ b/backend/migrations/024_add_gemini_tier_id.sql @@ -1,30 +1,30 @@ --- +goose Up --- +goose StatementBegin --- 为 Gemini Code Assist OAuth 账号添加默认 tier_id --- 包括显式标记为 code_assist 的账号,以及 legacy 账号(oauth_type 为空但 project_id 存在) -UPDATE accounts -SET credentials = jsonb_set( - credentials, - '{tier_id}', - '"LEGACY"', - true -) -WHERE platform = 'gemini' - AND type = 'oauth' - AND jsonb_typeof(credentials) = 'object' - AND credentials->>'tier_id' IS NULL - AND ( - credentials->>'oauth_type' = 'code_assist' - OR (credentials->>'oauth_type' IS NULL AND credentials->>'project_id' IS NOT NULL) - ); --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin --- 回滚:删除 tier_id 字段 -UPDATE accounts -SET credentials = credentials - 'tier_id' -WHERE platform = 'gemini' - AND type = 'oauth' - AND credentials ? 'tier_id'; --- +goose StatementEnd +-- +goose Up +-- +goose StatementBegin +-- 为 Gemini Code Assist OAuth 账号添加默认 tier_id +-- 包括显式标记为 code_assist 的账号,以及 legacy 账号(oauth_type 为空但 project_id 存在) +UPDATE accounts +SET credentials = jsonb_set( + credentials, + '{tier_id}', + '"LEGACY"', + true +) +WHERE platform = 'gemini' + AND type = 'oauth' + AND jsonb_typeof(credentials) = 'object' + AND credentials->>'tier_id' IS NULL + AND ( + credentials->>'oauth_type' = 'code_assist' + OR (credentials->>'oauth_type' IS NULL AND credentials->>'project_id' IS NOT NULL) + ); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +-- 回滚:删除 tier_id 字段 +UPDATE accounts +SET credentials = credentials - 'tier_id' +WHERE platform = 'gemini' + AND type = 'oauth' + AND credentials ? 'tier_id'; +-- +goose StatementEnd