fix: 恢复所有迁移文件以修复校验和错误
Some checks failed
CI / test (push) Has been cancelled
CI / golangci-lint (push) Has been cancelled

恢复了以下迁移文件到原始状态:
- 004_add_redeem_code_notes.sql
- 005_schema_parity.sql
- 006_fix_invalid_subscription_expires_at.sql
- 007_add_user_allowed_groups.sql
- 008_seed_default_group.sql
- 009_fix_usage_logs_cache_columns.sql
- 010_add_usage_logs_aggregated_indexes.sql
- 011_remove_duplicate_unique_indexes.sql
- 012_add_user_subscription_soft_delete.sql
- 013_log_orphan_allowed_groups.sql
- 014_drop_legacy_allowed_groups.sql
- 015_fix_settings_unique_constraint.sql
- 016_soft_delete_partial_unique_indexes.sql
- 018_user_attributes.sql
- 019_migrate_wechat_to_attributes.sql
- 024_add_gemini_tier_id.sql

数据库迁移文件不应在应用后修改,即使只是注释。
This commit is contained in:
huangzhenpc
2026-01-04 18:21:46 +08:00
parent 13b95049c3
commit f2b1fc0ace
16 changed files with 453 additions and 453 deletions

View File

@@ -1,6 +1,6 @@
-- 为 redeem_codes 表添加备注字段 -- 为 redeem_codes 表添加备注字段
ALTER TABLE redeem_codes ALTER TABLE redeem_codes
ADD COLUMN IF NOT EXISTS notes TEXT DEFAULT NULL; ADD COLUMN IF NOT EXISTS notes TEXT DEFAULT NULL;
COMMENT ON COLUMN redeem_codes.notes IS '备注说明(管理员调整时的原因说明)'; COMMENT ON COLUMN redeem_codes.notes IS '备注说明(管理员调整时的原因说明)';

View File

@@ -1,42 +1,42 @@
-- Align SQL migrations with current GORM persistence models. -- Align SQL migrations with current GORM persistence models.
-- This file is designed to be safe on both fresh installs and existing databases. -- This file is designed to be safe on both fresh installs and existing databases.
-- users: add fields added after initial migration -- 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 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 wechat VARCHAR(100) NOT NULL DEFAULT '';
ALTER TABLE users ADD COLUMN IF NOT EXISTS notes TEXT 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) -- api_keys: allow longer keys (GORM model uses size:128)
ALTER TABLE api_keys ALTER COLUMN key TYPE VARCHAR(128); ALTER TABLE api_keys ALTER COLUMN key TYPE VARCHAR(128);
-- accounts: scheduling and rate-limit fields used by repository queries -- 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 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_limited_at TIMESTAMPTZ;
ALTER TABLE accounts ADD COLUMN IF NOT EXISTS rate_limit_reset_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 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_start TIMESTAMPTZ;
ALTER TABLE accounts ADD COLUMN IF NOT EXISTS session_window_end 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); 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_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_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_rate_limit_reset_at ON accounts(rate_limit_reset_at);
CREATE INDEX IF NOT EXISTS idx_accounts_overload_until ON accounts(overload_until); CREATE INDEX IF NOT EXISTS idx_accounts_overload_until ON accounts(overload_until);
-- redeem_codes: subscription redeem fields -- 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 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; 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); CREATE INDEX IF NOT EXISTS idx_redeem_codes_group_id ON redeem_codes(group_id);
-- usage_logs: billing type used by filters and stats -- 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; 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); CREATE INDEX IF NOT EXISTS idx_usage_logs_billing_type ON usage_logs(billing_type);
-- settings: key-value store -- settings: key-value store
CREATE TABLE IF NOT EXISTS settings ( CREATE TABLE IF NOT EXISTS settings (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
key VARCHAR(100) NOT NULL UNIQUE, key VARCHAR(100) NOT NULL UNIQUE,
value TEXT NOT NULL, value TEXT NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
); );

View File

@@ -1,10 +1,10 @@
-- Fix legacy subscription records with invalid expires_at (year > 2099). -- Fix legacy subscription records with invalid expires_at (year > 2099).
DO $$ DO $$
BEGIN BEGIN
IF to_regclass('public.user_subscriptions') IS NOT NULL THEN IF to_regclass('public.user_subscriptions') IS NOT NULL THEN
UPDATE user_subscriptions UPDATE user_subscriptions
SET expires_at = TIMESTAMPTZ '2099-12-31 23:59:59+00' SET expires_at = TIMESTAMPTZ '2099-12-31 23:59:59+00'
WHERE expires_at > TIMESTAMPTZ '2099-12-31 23:59:59+00'; WHERE expires_at > TIMESTAMPTZ '2099-12-31 23:59:59+00';
END IF; END IF;
END $$; END $$;

View File

@@ -1,20 +1,20 @@
-- Add user_allowed_groups join table to replace users.allowed_groups (BIGINT[]). -- Add user_allowed_groups join table to replace users.allowed_groups (BIGINT[]).
-- Phase 1: create table + backfill from the legacy array column. -- Phase 1: create table + backfill from the legacy array column.
CREATE TABLE IF NOT EXISTS user_allowed_groups ( CREATE TABLE IF NOT EXISTS user_allowed_groups (
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
group_id BIGINT NOT NULL REFERENCES groups(id) ON DELETE CASCADE, group_id BIGINT NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (user_id, group_id) PRIMARY KEY (user_id, group_id)
); );
CREATE INDEX IF NOT EXISTS idx_user_allowed_groups_group_id ON user_allowed_groups(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. -- Backfill from the legacy users.allowed_groups array.
INSERT INTO user_allowed_groups (user_id, group_id) INSERT INTO user_allowed_groups (user_id, group_id)
SELECT u.id, x.group_id SELECT u.id, x.group_id
FROM users u FROM users u
CROSS JOIN LATERAL unnest(u.allowed_groups) AS x(group_id) CROSS JOIN LATERAL unnest(u.allowed_groups) AS x(group_id)
JOIN groups g ON g.id = x.group_id JOIN groups g ON g.id = x.group_id
WHERE u.allowed_groups IS NOT NULL WHERE u.allowed_groups IS NOT NULL
ON CONFLICT DO NOTHING; ON CONFLICT DO NOTHING;

View File

@@ -1,4 +1,4 @@
-- Seed a default group for fresh installs. -- Seed a default group for fresh installs.
INSERT INTO groups (name, description, created_at, updated_at) INSERT INTO groups (name, description, created_at, updated_at)
SELECT 'default', 'Default group', NOW(), NOW() SELECT 'default', 'Default group', NOW(), NOW()
WHERE NOT EXISTS (SELECT 1 FROM groups); WHERE NOT EXISTS (SELECT 1 FROM groups);

View File

@@ -1,37 +1,37 @@
-- Ensure usage_logs cache token columns use the underscored names expected by code. -- Ensure usage_logs cache token columns use the underscored names expected by code.
-- Backfill from legacy column names if they exist. -- Backfill from legacy column names if they exist.
ALTER TABLE usage_logs ALTER TABLE usage_logs
ADD COLUMN IF NOT EXISTS cache_creation_5m_tokens INT NOT NULL DEFAULT 0; ADD COLUMN IF NOT EXISTS cache_creation_5m_tokens INT NOT NULL DEFAULT 0;
ALTER TABLE usage_logs ALTER TABLE usage_logs
ADD COLUMN IF NOT EXISTS cache_creation_1h_tokens INT NOT NULL DEFAULT 0; ADD COLUMN IF NOT EXISTS cache_creation_1h_tokens INT NOT NULL DEFAULT 0;
DO $$ DO $$
BEGIN BEGIN
IF EXISTS ( IF EXISTS (
SELECT 1 SELECT 1
FROM information_schema.columns FROM information_schema.columns
WHERE table_schema = 'public' WHERE table_schema = 'public'
AND table_name = 'usage_logs' AND table_name = 'usage_logs'
AND column_name = 'cache_creation5m_tokens' AND column_name = 'cache_creation5m_tokens'
) THEN ) THEN
UPDATE usage_logs UPDATE usage_logs
SET cache_creation_5m_tokens = cache_creation5m_tokens SET cache_creation_5m_tokens = cache_creation5m_tokens
WHERE cache_creation_5m_tokens = 0 WHERE cache_creation_5m_tokens = 0
AND cache_creation5m_tokens <> 0; AND cache_creation5m_tokens <> 0;
END IF; END IF;
IF EXISTS ( IF EXISTS (
SELECT 1 SELECT 1
FROM information_schema.columns FROM information_schema.columns
WHERE table_schema = 'public' WHERE table_schema = 'public'
AND table_name = 'usage_logs' AND table_name = 'usage_logs'
AND column_name = 'cache_creation1h_tokens' AND column_name = 'cache_creation1h_tokens'
) THEN ) THEN
UPDATE usage_logs UPDATE usage_logs
SET cache_creation_1h_tokens = cache_creation1h_tokens SET cache_creation_1h_tokens = cache_creation1h_tokens
WHERE cache_creation_1h_tokens = 0 WHERE cache_creation_1h_tokens = 0
AND cache_creation1h_tokens <> 0; AND cache_creation1h_tokens <> 0;
END IF; END IF;
END $$; END $$;

View File

@@ -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_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_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_model_created_at ON usage_logs(model, created_at);

View File

@@ -1,39 +1,39 @@
-- 011_remove_duplicate_unique_indexes.sql -- 011_remove_duplicate_unique_indexes.sql
-- 移除重复的唯一索引 -- 移除重复的唯一索引
-- 这些字段在 ent schema 的 Fields() 中已声明 .Unique() -- 这些字段在 ent schema 的 Fields() 中已声明 .Unique()
-- 因此在 Indexes() 中再次声明 index.Fields("x").Unique() 会创建重复索引。 -- 因此在 Indexes() 中再次声明 index.Fields("x").Unique() 会创建重复索引。
-- 本迁移脚本清理这些冗余索引。 -- 本迁移脚本清理这些冗余索引。
-- 重复索引命名约定(由 Ent 自动生成/历史迁移遗留): -- 重复索引命名约定(由 Ent 自动生成/历史迁移遗留):
-- - 字段级 Unique() 创建的索引名: <table>_<field>_key -- - 字段级 Unique() 创建的索引名: <table>_<field>_key
-- - Indexes() 中的 Unique() 创建的索引名: <table>_<field> -- - Indexes() 中的 Unique() 创建的索引名: <table>_<field>
-- - 初始化迁移中的非唯一索引: idx_<table>_<field> -- - 初始化迁移中的非唯一索引: idx_<table>_<field>
-- 仅当索引存在时才删除(幂等操作) -- 仅当索引存在时才删除(幂等操作)
-- api_keys 表: key 字段 -- api_keys 表: key 字段
DROP INDEX IF EXISTS apikey_key; DROP INDEX IF EXISTS apikey_key;
DROP INDEX IF EXISTS api_keys_key; DROP INDEX IF EXISTS api_keys_key;
DROP INDEX IF EXISTS idx_api_keys_key; DROP INDEX IF EXISTS idx_api_keys_key;
-- users 表: email 字段 -- users 表: email 字段
DROP INDEX IF EXISTS user_email; DROP INDEX IF EXISTS user_email;
DROP INDEX IF EXISTS users_email; DROP INDEX IF EXISTS users_email;
DROP INDEX IF EXISTS idx_users_email; DROP INDEX IF EXISTS idx_users_email;
-- settings 表: key 字段 -- settings 表: key 字段
DROP INDEX IF EXISTS settings_key; DROP INDEX IF EXISTS settings_key;
DROP INDEX IF EXISTS idx_settings_key; DROP INDEX IF EXISTS idx_settings_key;
-- redeem_codes 表: code 字段 -- redeem_codes 表: code 字段
DROP INDEX IF EXISTS redeemcode_code; DROP INDEX IF EXISTS redeemcode_code;
DROP INDEX IF EXISTS redeem_codes_code; DROP INDEX IF EXISTS redeem_codes_code;
DROP INDEX IF EXISTS idx_redeem_codes_code; DROP INDEX IF EXISTS idx_redeem_codes_code;
-- groups 表: name 字段 -- groups 表: name 字段
DROP INDEX IF EXISTS group_name; DROP INDEX IF EXISTS group_name;
DROP INDEX IF EXISTS groups_name; DROP INDEX IF EXISTS groups_name;
DROP INDEX IF EXISTS idx_groups_name; DROP INDEX IF EXISTS idx_groups_name;
-- 注意: 每个字段的唯一约束仍由字段级 Unique() 创建的约束保留, -- 注意: 每个字段的唯一约束仍由字段级 Unique() 创建的约束保留,
-- 如 api_keys_key_key、users_email_key 等。 -- 如 api_keys_key_key、users_email_key 等。

View File

@@ -1,13 +1,13 @@
-- 012: 为 user_subscriptions 表添加软删除支持 -- 012: 为 user_subscriptions 表添加软删除支持
-- 任务fix-medium-data-hygiene 1.1 -- 任务fix-medium-data-hygiene 1.1
-- 添加 deleted_at 字段 -- 添加 deleted_at 字段
ALTER TABLE user_subscriptions ALTER TABLE user_subscriptions
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ DEFAULT NULL; ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ DEFAULT NULL;
-- 添加 deleted_at 索引以优化软删除查询 -- 添加 deleted_at 索引以优化软删除查询
CREATE INDEX IF NOT EXISTS usersubscription_deleted_at CREATE INDEX IF NOT EXISTS usersubscription_deleted_at
ON user_subscriptions (deleted_at); ON user_subscriptions (deleted_at);
-- 注释:与其他使用软删除的实体保持一致 -- 注释:与其他使用软删除的实体保持一致
COMMENT ON COLUMN user_subscriptions.deleted_at IS '软删除时间戳NULL 表示未删除'; COMMENT ON COLUMN user_subscriptions.deleted_at IS '软删除时间戳NULL 表示未删除';

View File

@@ -1,32 +1,32 @@
-- 013: 记录 users.allowed_groups 中的孤立 group_id -- 013: 记录 users.allowed_groups 中的孤立 group_id
-- 任务fix-medium-data-hygiene 3.1 -- 任务fix-medium-data-hygiene 3.1
-- --
-- 目的:在删除 legacy allowed_groups 列前,记录所有引用了不存在 group 的孤立记录 -- 目的:在删除 legacy allowed_groups 列前,记录所有引用了不存在 group 的孤立记录
-- 这些记录可用于审计或后续数据修复 -- 这些记录可用于审计或后续数据修复
-- 创建审计表存储孤立的 allowed_groups 记录 -- 创建审计表存储孤立的 allowed_groups 记录
CREATE TABLE IF NOT EXISTS orphan_allowed_groups_audit ( CREATE TABLE IF NOT EXISTS orphan_allowed_groups_audit (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL, user_id BIGINT NOT NULL,
group_id BIGINT NOT NULL, group_id BIGINT NOT NULL,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), recorded_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (user_id, group_id) UNIQUE (user_id, group_id)
); );
-- 记录孤立的 group_id存在于 users.allowed_groups 但不存在于 groups 表) -- 记录孤立的 group_id存在于 users.allowed_groups 但不存在于 groups 表)
INSERT INTO orphan_allowed_groups_audit (user_id, group_id) INSERT INTO orphan_allowed_groups_audit (user_id, group_id)
SELECT u.id, x.group_id SELECT u.id, x.group_id
FROM users u FROM users u
CROSS JOIN LATERAL unnest(u.allowed_groups) AS x(group_id) CROSS JOIN LATERAL unnest(u.allowed_groups) AS x(group_id)
LEFT JOIN groups g ON g.id = x.group_id LEFT JOIN groups g ON g.id = x.group_id
WHERE u.allowed_groups IS NOT NULL WHERE u.allowed_groups IS NOT NULL
AND g.id IS NULL AND g.id IS NULL
ON CONFLICT (user_id, group_id) DO NOTHING; ON CONFLICT (user_id, group_id) DO NOTHING;
-- 添加索引便于查询 -- 添加索引便于查询
CREATE INDEX IF NOT EXISTS idx_orphan_allowed_groups_audit_user_id CREATE INDEX IF NOT EXISTS idx_orphan_allowed_groups_audit_user_id
ON orphan_allowed_groups_audit(user_id); ON orphan_allowed_groups_audit(user_id);
-- 记录迁移完成信息 -- 记录迁移完成信息
COMMENT ON TABLE orphan_allowed_groups_audit IS COMMENT ON TABLE orphan_allowed_groups_audit IS
'审计表:记录 users.allowed_groups 中引用的不存在的 group_id用于数据清理前的审计'; '审计表:记录 users.allowed_groups 中引用的不存在的 group_id用于数据清理前的审计';

View File

@@ -1,15 +1,15 @@
-- 014: 删除 legacy users.allowed_groups 列 -- 014: 删除 legacy users.allowed_groups 列
-- 任务fix-medium-data-hygiene 3.3 -- 任务fix-medium-data-hygiene 3.3
-- --
-- 前置条件: -- 前置条件:
-- - 迁移 007 已将数据回填到 user_allowed_groups 联接表 -- - 迁移 007 已将数据回填到 user_allowed_groups 联接表
-- - 迁移 013 已记录所有孤立的 group_id 到审计表 -- - 迁移 013 已记录所有孤立的 group_id 到审计表
-- - 应用代码已停止写入该列3.2 完成) -- - 应用代码已停止写入该列3.2 完成)
-- --
-- 该列现已废弃,所有读写操作均使用 user_allowed_groups 联接表。 -- 该列现已废弃,所有读写操作均使用 user_allowed_groups 联接表。
-- 删除 allowed_groups 列 -- 删除 allowed_groups 列
ALTER TABLE users DROP COLUMN IF EXISTS allowed_groups; ALTER TABLE users DROP COLUMN IF EXISTS allowed_groups;
-- 添加注释记录删除原因 -- 添加注释记录删除原因
COMMENT ON TABLE users IS '用户表。注:原 allowed_groups BIGINT[] 列已迁移至 user_allowed_groups 联接表'; COMMENT ON TABLE users IS '用户表。注:原 allowed_groups BIGINT[] 列已迁移至 user_allowed_groups 联接表';

View File

@@ -1,19 +1,19 @@
-- 015_fix_settings_unique_constraint.sql -- 015_fix_settings_unique_constraint.sql
-- 修复 settings 表 key 字段缺失的唯一约束 -- 修复 settings 表 key 字段缺失的唯一约束
-- 此约束是 ON CONFLICT ("key") DO UPDATE 语句所必需的 -- 此约束是 ON CONFLICT ("key") DO UPDATE 语句所必需的
-- 检查并添加唯一约束(如果不存在) -- 检查并添加唯一约束(如果不存在)
DO $$ DO $$
BEGIN BEGIN
-- 检查是否已存在唯一约束 -- 检查是否已存在唯一约束
IF NOT EXISTS ( IF NOT EXISTS (
SELECT 1 FROM pg_constraint SELECT 1 FROM pg_constraint
WHERE conrelid = 'settings'::regclass WHERE conrelid = 'settings'::regclass
AND contype = 'u' AND contype = 'u'
AND conname = 'settings_key_key' AND conname = 'settings_key_key'
) THEN ) THEN
-- 添加唯一约束 -- 添加唯一约束
ALTER TABLE settings ADD CONSTRAINT settings_key_key UNIQUE (key); ALTER TABLE settings ADD CONSTRAINT settings_key_key UNIQUE (key);
END IF; END IF;
END END
$$; $$;

View File

@@ -1,51 +1,51 @@
-- 016_soft_delete_partial_unique_indexes.sql -- 016_soft_delete_partial_unique_indexes.sql
-- 修复软删除 + 唯一约束冲突问题 -- 修复软删除 + 唯一约束冲突问题
-- 将普通唯一约束替换为部分唯一索引WHERE deleted_at IS NULL -- 将普通唯一约束替换为部分唯一索引WHERE deleted_at IS NULL
-- 这样软删除的记录不会占用唯一约束位置,允许删后重建同名/同邮箱/同订阅关系 -- 这样软删除的记录不会占用唯一约束位置,允许删后重建同名/同邮箱/同订阅关系
-- ============================================================================ -- ============================================================================
-- 1. users 表: email 字段 -- 1. users 表: email 字段
-- ============================================================================ -- ============================================================================
-- 删除旧的唯一约束(可能的命名方式) -- 删除旧的唯一约束(可能的命名方式)
ALTER TABLE users DROP CONSTRAINT IF EXISTS users_email_key; ALTER TABLE users DROP CONSTRAINT IF EXISTS users_email_key;
DROP INDEX IF EXISTS users_email_key; DROP INDEX IF EXISTS users_email_key;
DROP INDEX IF EXISTS user_email_key; DROP INDEX IF EXISTS user_email_key;
-- 创建部分唯一索引:只对未删除的记录建立唯一约束 -- 创建部分唯一索引:只对未删除的记录建立唯一约束
CREATE UNIQUE INDEX IF NOT EXISTS users_email_unique_active CREATE UNIQUE INDEX IF NOT EXISTS users_email_unique_active
ON users(email) ON users(email)
WHERE deleted_at IS NULL; WHERE deleted_at IS NULL;
-- ============================================================================ -- ============================================================================
-- 2. groups 表: name 字段 -- 2. groups 表: name 字段
-- ============================================================================ -- ============================================================================
-- 删除旧的唯一约束 -- 删除旧的唯一约束
ALTER TABLE groups DROP CONSTRAINT IF EXISTS groups_name_key; ALTER TABLE groups DROP CONSTRAINT IF EXISTS groups_name_key;
DROP INDEX IF EXISTS groups_name_key; DROP INDEX IF EXISTS groups_name_key;
DROP INDEX IF EXISTS group_name_key; DROP INDEX IF EXISTS group_name_key;
-- 创建部分唯一索引 -- 创建部分唯一索引
CREATE UNIQUE INDEX IF NOT EXISTS groups_name_unique_active CREATE UNIQUE INDEX IF NOT EXISTS groups_name_unique_active
ON groups(name) ON groups(name)
WHERE deleted_at IS NULL; WHERE deleted_at IS NULL;
-- ============================================================================ -- ============================================================================
-- 3. user_subscriptions 表: (user_id, group_id) 组合字段 -- 3. user_subscriptions 表: (user_id, group_id) 组合字段
-- ============================================================================ -- ============================================================================
-- 删除旧的唯一约束/索引 -- 删除旧的唯一约束/索引
ALTER TABLE user_subscriptions DROP CONSTRAINT IF EXISTS user_subscriptions_user_id_group_id_key; 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 user_subscriptions_user_id_group_id_key;
DROP INDEX IF EXISTS usersubscription_user_id_group_id; DROP INDEX IF EXISTS usersubscription_user_id_group_id;
-- 创建部分唯一索引 -- 创建部分唯一索引
CREATE UNIQUE INDEX IF NOT EXISTS user_subscriptions_user_group_unique_active CREATE UNIQUE INDEX IF NOT EXISTS user_subscriptions_user_group_unique_active
ON user_subscriptions(user_id, group_id) ON user_subscriptions(user_id, group_id)
WHERE deleted_at IS NULL; WHERE deleted_at IS NULL;
-- ============================================================================ -- ============================================================================
-- 注意: api_keys 表的 key 字段保留普通唯一约束 -- 注意: api_keys 表的 key 字段保留普通唯一约束
-- API Key 即使软删除后也不应该重复使用(安全考虑) -- API Key 即使软删除后也不应该重复使用(安全考虑)
-- ============================================================================ -- ============================================================================

View File

@@ -1,48 +1,48 @@
-- Add user attribute definitions and values tables for custom user attributes. -- Add user attribute definitions and values tables for custom user attributes.
-- User Attribute Definitions table (with soft delete support) -- User Attribute Definitions table (with soft delete support)
CREATE TABLE IF NOT EXISTS user_attribute_definitions ( CREATE TABLE IF NOT EXISTS user_attribute_definitions (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
key VARCHAR(100) NOT NULL, key VARCHAR(100) NOT NULL,
name VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL,
description TEXT DEFAULT '', description TEXT DEFAULT '',
type VARCHAR(20) NOT NULL, type VARCHAR(20) NOT NULL,
options JSONB DEFAULT '[]'::jsonb, options JSONB DEFAULT '[]'::jsonb,
required BOOLEAN NOT NULL DEFAULT FALSE, required BOOLEAN NOT NULL DEFAULT FALSE,
validation JSONB DEFAULT '{}'::jsonb, validation JSONB DEFAULT '{}'::jsonb,
placeholder VARCHAR(255) DEFAULT '', placeholder VARCHAR(255) DEFAULT '',
display_order INT NOT NULL DEFAULT 0, display_order INT NOT NULL DEFAULT 0,
enabled BOOLEAN NOT NULL DEFAULT TRUE, enabled BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ deleted_at TIMESTAMPTZ
); );
-- Partial unique index for key (only for non-deleted records) -- Partial unique index for key (only for non-deleted records)
-- Allows reusing keys after soft delete -- Allows reusing keys after soft delete
CREATE UNIQUE INDEX IF NOT EXISTS idx_user_attribute_definitions_key_unique CREATE UNIQUE INDEX IF NOT EXISTS idx_user_attribute_definitions_key_unique
ON user_attribute_definitions(key) WHERE deleted_at IS NULL; ON user_attribute_definitions(key) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_user_attribute_definitions_enabled CREATE INDEX IF NOT EXISTS idx_user_attribute_definitions_enabled
ON user_attribute_definitions(enabled); ON user_attribute_definitions(enabled);
CREATE INDEX IF NOT EXISTS idx_user_attribute_definitions_display_order CREATE INDEX IF NOT EXISTS idx_user_attribute_definitions_display_order
ON user_attribute_definitions(display_order); ON user_attribute_definitions(display_order);
CREATE INDEX IF NOT EXISTS idx_user_attribute_definitions_deleted_at CREATE INDEX IF NOT EXISTS idx_user_attribute_definitions_deleted_at
ON user_attribute_definitions(deleted_at); ON user_attribute_definitions(deleted_at);
-- User Attribute Values table (hard delete only, no deleted_at) -- User Attribute Values table (hard delete only, no deleted_at)
CREATE TABLE IF NOT EXISTS user_attribute_values ( CREATE TABLE IF NOT EXISTS user_attribute_values (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
attribute_id BIGINT NOT NULL REFERENCES user_attribute_definitions(id) ON DELETE CASCADE, attribute_id BIGINT NOT NULL REFERENCES user_attribute_definitions(id) ON DELETE CASCADE,
value TEXT DEFAULT '', value TEXT DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(user_id, attribute_id) UNIQUE(user_id, attribute_id)
); );
CREATE INDEX IF NOT EXISTS idx_user_attribute_values_user_id CREATE INDEX IF NOT EXISTS idx_user_attribute_values_user_id
ON user_attribute_values(user_id); ON user_attribute_values(user_id);
CREATE INDEX IF NOT EXISTS idx_user_attribute_values_attribute_id CREATE INDEX IF NOT EXISTS idx_user_attribute_values_attribute_id
ON user_attribute_values(attribute_id); ON user_attribute_values(attribute_id);

View File

@@ -1,83 +1,83 @@
-- Migration: Move wechat field from users table to user_attribute_values -- Migration: Move wechat field from users table to user_attribute_values
-- This migration: -- This migration:
-- 1. Creates a "wechat" attribute definition -- 1. Creates a "wechat" attribute definition
-- 2. Migrates existing wechat data to user_attribute_values -- 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) -- 3. Does NOT drop the wechat column (for rollback safety, can be done in a later migration)
-- +goose Up -- +goose Up
-- +goose StatementBegin -- +goose StatementBegin
-- Step 1: Insert wechat attribute definition if not exists -- 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) 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() SELECT 'wechat', '微信', '用户微信号', 'text', '[]'::jsonb, false, '{}'::jsonb, '请输入微信号', 0, true, NOW(), NOW()
WHERE NOT EXISTS ( WHERE NOT EXISTS (
SELECT 1 FROM user_attribute_definitions WHERE key = 'wechat' AND deleted_at IS NULL SELECT 1 FROM user_attribute_definitions WHERE key = 'wechat' AND deleted_at IS NULL
); );
-- Step 2: Migrate existing wechat values to user_attribute_values -- Step 2: Migrate existing wechat values to user_attribute_values
-- Only migrate non-empty values -- Only migrate non-empty values
INSERT INTO user_attribute_values (user_id, attribute_id, value, created_at, updated_at) INSERT INTO user_attribute_values (user_id, attribute_id, value, created_at, updated_at)
SELECT SELECT
u.id, u.id,
(SELECT id FROM user_attribute_definitions WHERE key = 'wechat' AND deleted_at IS NULL LIMIT 1), (SELECT id FROM user_attribute_definitions WHERE key = 'wechat' AND deleted_at IS NULL LIMIT 1),
u.wechat, u.wechat,
NOW(), NOW(),
NOW() NOW()
FROM users u FROM users u
WHERE u.wechat IS NOT NULL WHERE u.wechat IS NOT NULL
AND u.wechat != '' AND u.wechat != ''
AND u.deleted_at IS NULL AND u.deleted_at IS NULL
AND NOT EXISTS ( AND NOT EXISTS (
SELECT 1 FROM user_attribute_values uav SELECT 1 FROM user_attribute_values uav
WHERE uav.user_id = u.id 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) 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 -- Step 3: Update display_order to ensure wechat appears first
UPDATE user_attribute_definitions UPDATE user_attribute_definitions
SET display_order = -1 SET display_order = -1
WHERE key = 'wechat' AND deleted_at IS NULL; WHERE key = 'wechat' AND deleted_at IS NULL;
-- Reorder all attributes starting from 0 -- Reorder all attributes starting from 0
WITH ordered AS ( WITH ordered AS (
SELECT id, ROW_NUMBER() OVER (ORDER BY display_order, id) - 1 as new_order SELECT id, ROW_NUMBER() OVER (ORDER BY display_order, id) - 1 as new_order
FROM user_attribute_definitions FROM user_attribute_definitions
WHERE deleted_at IS NULL WHERE deleted_at IS NULL
) )
UPDATE user_attribute_definitions UPDATE user_attribute_definitions
SET display_order = ordered.new_order SET display_order = ordered.new_order
FROM ordered FROM ordered
WHERE user_attribute_definitions.id = ordered.id; WHERE user_attribute_definitions.id = ordered.id;
-- Step 4: Drop the redundant wechat column from users table -- Step 4: Drop the redundant wechat column from users table
ALTER TABLE users DROP COLUMN IF EXISTS wechat; ALTER TABLE users DROP COLUMN IF EXISTS wechat;
-- +goose StatementEnd -- +goose StatementEnd
-- +goose Down -- +goose Down
-- +goose StatementBegin -- +goose StatementBegin
-- Restore wechat column -- Restore wechat column
ALTER TABLE users ADD COLUMN IF NOT EXISTS wechat VARCHAR(100) DEFAULT ''; ALTER TABLE users ADD COLUMN IF NOT EXISTS wechat VARCHAR(100) DEFAULT '';
-- Copy attribute values back to users.wechat column -- Copy attribute values back to users.wechat column
UPDATE users u UPDATE users u
SET wechat = uav.value SET wechat = uav.value
FROM user_attribute_values uav FROM user_attribute_values uav
JOIN user_attribute_definitions uad ON uav.attribute_id = uad.id JOIN user_attribute_definitions uad ON uav.attribute_id = uad.id
WHERE uav.user_id = u.id WHERE uav.user_id = u.id
AND uad.key = 'wechat' AND uad.key = 'wechat'
AND uad.deleted_at IS NULL; AND uad.deleted_at IS NULL;
-- Delete migrated attribute values -- Delete migrated attribute values
DELETE FROM user_attribute_values DELETE FROM user_attribute_values
WHERE attribute_id IN ( WHERE attribute_id IN (
SELECT id FROM user_attribute_definitions WHERE key = 'wechat' AND deleted_at IS NULL SELECT id FROM user_attribute_definitions WHERE key = 'wechat' AND deleted_at IS NULL
); );
-- Soft-delete the wechat attribute definition -- Soft-delete the wechat attribute definition
UPDATE user_attribute_definitions UPDATE user_attribute_definitions
SET deleted_at = NOW() SET deleted_at = NOW()
WHERE key = 'wechat' AND deleted_at IS NULL; WHERE key = 'wechat' AND deleted_at IS NULL;
-- +goose StatementEnd -- +goose StatementEnd

View File

@@ -1,30 +1,30 @@
-- +goose Up -- +goose Up
-- +goose StatementBegin -- +goose StatementBegin
-- 为 Gemini Code Assist OAuth 账号添加默认 tier_id -- 为 Gemini Code Assist OAuth 账号添加默认 tier_id
-- 包括显式标记为 code_assist 的账号,以及 legacy 账号oauth_type 为空但 project_id 存在) -- 包括显式标记为 code_assist 的账号,以及 legacy 账号oauth_type 为空但 project_id 存在)
UPDATE accounts UPDATE accounts
SET credentials = jsonb_set( SET credentials = jsonb_set(
credentials, credentials,
'{tier_id}', '{tier_id}',
'"LEGACY"', '"LEGACY"',
true true
) )
WHERE platform = 'gemini' WHERE platform = 'gemini'
AND type = 'oauth' AND type = 'oauth'
AND jsonb_typeof(credentials) = 'object' AND jsonb_typeof(credentials) = 'object'
AND credentials->>'tier_id' IS NULL AND credentials->>'tier_id' IS NULL
AND ( AND (
credentials->>'oauth_type' = 'code_assist' credentials->>'oauth_type' = 'code_assist'
OR (credentials->>'oauth_type' IS NULL AND credentials->>'project_id' IS NOT NULL) OR (credentials->>'oauth_type' IS NULL AND credentials->>'project_id' IS NOT NULL)
); );
-- +goose StatementEnd -- +goose StatementEnd
-- +goose Down -- +goose Down
-- +goose StatementBegin -- +goose StatementBegin
-- 回滚:删除 tier_id 字段 -- 回滚:删除 tier_id 字段
UPDATE accounts UPDATE accounts
SET credentials = credentials - 'tier_id' SET credentials = credentials - 'tier_id'
WHERE platform = 'gemini' WHERE platform = 'gemini'
AND type = 'oauth' AND type = 'oauth'
AND credentials ? 'tier_id'; AND credentials ? 'tier_id';
-- +goose StatementEnd -- +goose StatementEnd