custom #1

Open
huangzhenpc wants to merge 9 commits from custom into main
16 changed files with 453 additions and 453 deletions
Showing only changes of commit f2b1fc0ace - Show all commits

View File

@@ -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 '备注说明(管理员调整时的原因说明)';

View File

@@ -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()
);

View File

@@ -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 $$;

View File

@@ -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;

View File

@@ -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);

View File

@@ -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 $$;

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_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);

View File

@@ -1,39 +1,39 @@
-- 011_remove_duplicate_unique_indexes.sql
-- 移除重复的唯一索引
-- 这些字段在 ent schema 的 Fields() 中已声明 .Unique()
-- 因此在 Indexes() 中再次声明 index.Fields("x").Unique() 会创建重复索引。
-- 本迁移脚本清理这些冗余索引。
-- 重复索引命名约定(由 Ent 自动生成/历史迁移遗留):
-- - 字段级 Unique() 创建的索引名: <table>_<field>_key
-- - Indexes() 中的 Unique() 创建的索引名: <table>_<field>
-- - 初始化迁移中的非唯一索引: idx_<table>_<field>
-- 仅当索引存在时才删除(幂等操作)
-- 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() 创建的索引名: <table>_<field>_key
-- - Indexes() 中的 Unique() 创建的索引名: <table>_<field>
-- - 初始化迁移中的非唯一索引: idx_<table>_<field>
-- 仅当索引存在时才删除(幂等操作)
-- 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 等。

View File

@@ -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 表示未删除';

View File

@@ -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用于数据清理前的审计';

View File

@@ -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 联接表';

View File

@@ -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
$$;

View File

@@ -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 即使软删除后也不应该重复使用(安全考虑)
-- ============================================================================

View File

@@ -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);

View File

@@ -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

View File

@@ -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