feat: 品牌重命名 Sub2API -> TianShuAPI
- 前端: 所有界面显示、i18n 文本、组件中的品牌名称 - 后端: 服务层、设置默认值、邮件模板、安装向导 - 数据库: 迁移脚本注释 - 保持功能完全一致,仅更改品牌名称 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,172 +1,172 @@
|
||||
-- Sub2API 初始化数据库迁移脚本
|
||||
-- PostgreSQL 15+
|
||||
|
||||
-- 1. proxies 代理IP表(无外键依赖)
|
||||
CREATE TABLE IF NOT EXISTS proxies (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
protocol VARCHAR(20) NOT NULL, -- http/https/socks5
|
||||
host VARCHAR(255) NOT NULL,
|
||||
port INT NOT NULL,
|
||||
username VARCHAR(100),
|
||||
password VARCHAR(100),
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active', -- active/disabled
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_proxies_status ON proxies(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_proxies_deleted_at ON proxies(deleted_at);
|
||||
|
||||
-- 2. groups 分组表(无外键依赖)
|
||||
CREATE TABLE IF NOT EXISTS groups (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
rate_multiplier DECIMAL(10, 4) NOT NULL DEFAULT 1.0, -- 费率倍率
|
||||
is_exclusive BOOLEAN NOT NULL DEFAULT FALSE, -- 是否专属分组
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active', -- active/disabled
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_groups_name ON groups(name);
|
||||
CREATE INDEX IF NOT EXISTS idx_groups_status ON groups(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_groups_is_exclusive ON groups(is_exclusive);
|
||||
CREATE INDEX IF NOT EXISTS idx_groups_deleted_at ON groups(deleted_at);
|
||||
|
||||
-- 3. users 用户表(无外键依赖)
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
email VARCHAR(255) NOT NULL UNIQUE,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
role VARCHAR(20) NOT NULL DEFAULT 'user', -- admin/user
|
||||
balance DECIMAL(20, 8) NOT NULL DEFAULT 0, -- 余额(可为负数)
|
||||
concurrency INT NOT NULL DEFAULT 5, -- 并发数限制
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active', -- active/disabled
|
||||
allowed_groups BIGINT[] DEFAULT NULL, -- 允许绑定的分组ID列表
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_status ON users(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_deleted_at ON users(deleted_at);
|
||||
|
||||
-- 4. accounts 上游账号表(依赖proxies)
|
||||
CREATE TABLE IF NOT EXISTS accounts (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
platform VARCHAR(50) NOT NULL, -- anthropic/openai/gemini
|
||||
type VARCHAR(20) NOT NULL, -- oauth/apikey
|
||||
credentials JSONB NOT NULL DEFAULT '{}', -- 凭证信息(加密存储)
|
||||
extra JSONB NOT NULL DEFAULT '{}', -- 扩展信息
|
||||
proxy_id BIGINT REFERENCES proxies(id) ON DELETE SET NULL,
|
||||
concurrency INT NOT NULL DEFAULT 3, -- 账号并发限制
|
||||
priority INT NOT NULL DEFAULT 50, -- 调度优先级(1-100,越小越高)
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active', -- active/disabled/error
|
||||
error_message TEXT,
|
||||
last_used_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_accounts_platform ON accounts(platform);
|
||||
CREATE INDEX IF NOT EXISTS idx_accounts_type ON accounts(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_accounts_status ON accounts(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_accounts_proxy_id ON accounts(proxy_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_accounts_priority ON accounts(priority);
|
||||
CREATE INDEX IF NOT EXISTS idx_accounts_last_used_at ON accounts(last_used_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_accounts_deleted_at ON accounts(deleted_at);
|
||||
|
||||
-- 5. api_keys API密钥表(依赖users, groups)
|
||||
CREATE TABLE IF NOT EXISTS api_keys (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
key VARCHAR(64) NOT NULL UNIQUE, -- sk-xxx格式
|
||||
name VARCHAR(100) NOT NULL,
|
||||
group_id BIGINT REFERENCES groups(id) ON DELETE SET NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active', -- active/disabled
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_api_keys_key ON api_keys(key);
|
||||
CREATE INDEX IF NOT EXISTS idx_api_keys_user_id ON api_keys(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_api_keys_group_id ON api_keys(group_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_api_keys_status ON api_keys(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_api_keys_deleted_at ON api_keys(deleted_at);
|
||||
|
||||
-- 6. account_groups 账号-分组关联表(依赖accounts, groups)
|
||||
CREATE TABLE IF NOT EXISTS account_groups (
|
||||
account_id BIGINT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
|
||||
group_id BIGINT NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
|
||||
priority INT NOT NULL DEFAULT 50, -- 分组内优先级
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY (account_id, group_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_account_groups_group_id ON account_groups(group_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_account_groups_priority ON account_groups(priority);
|
||||
|
||||
-- 7. redeem_codes 卡密表(依赖users)
|
||||
CREATE TABLE IF NOT EXISTS redeem_codes (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
code VARCHAR(32) NOT NULL UNIQUE, -- 兑换码
|
||||
type VARCHAR(20) NOT NULL DEFAULT 'balance', -- balance
|
||||
value DECIMAL(20, 8) NOT NULL, -- 面值(USD)
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'unused', -- unused/used
|
||||
used_by BIGINT REFERENCES users(id) ON DELETE SET NULL,
|
||||
used_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_redeem_codes_code ON redeem_codes(code);
|
||||
CREATE INDEX IF NOT EXISTS idx_redeem_codes_status ON redeem_codes(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_redeem_codes_used_by ON redeem_codes(used_by);
|
||||
|
||||
-- 8. usage_logs 使用记录表(依赖users, api_keys, accounts)
|
||||
CREATE TABLE IF NOT EXISTS usage_logs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
api_key_id BIGINT NOT NULL REFERENCES api_keys(id) ON DELETE CASCADE,
|
||||
account_id BIGINT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
|
||||
request_id VARCHAR(64),
|
||||
model VARCHAR(100) NOT NULL,
|
||||
|
||||
-- Token使用量(4类)
|
||||
input_tokens INT NOT NULL DEFAULT 0,
|
||||
output_tokens INT NOT NULL DEFAULT 0,
|
||||
cache_creation_tokens INT NOT NULL DEFAULT 0,
|
||||
cache_read_tokens INT NOT NULL DEFAULT 0,
|
||||
|
||||
-- 详细的缓存创建分类
|
||||
cache_creation_5m_tokens INT NOT NULL DEFAULT 0,
|
||||
cache_creation_1h_tokens INT NOT NULL DEFAULT 0,
|
||||
|
||||
-- 费用(USD)
|
||||
input_cost DECIMAL(20, 10) NOT NULL DEFAULT 0,
|
||||
output_cost DECIMAL(20, 10) NOT NULL DEFAULT 0,
|
||||
cache_creation_cost DECIMAL(20, 10) NOT NULL DEFAULT 0,
|
||||
cache_read_cost DECIMAL(20, 10) NOT NULL DEFAULT 0,
|
||||
total_cost DECIMAL(20, 10) NOT NULL DEFAULT 0, -- 原始总费用
|
||||
actual_cost DECIMAL(20, 10) NOT NULL DEFAULT 0, -- 实际扣除费用
|
||||
|
||||
-- 元数据
|
||||
stream BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
duration_ms INT,
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_usage_logs_user_id ON usage_logs(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_usage_logs_api_key_id ON usage_logs(api_key_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_usage_logs_account_id ON usage_logs(account_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_usage_logs_model ON usage_logs(model);
|
||||
CREATE INDEX IF NOT EXISTS idx_usage_logs_created_at ON usage_logs(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_usage_logs_user_created ON usage_logs(user_id, created_at);
|
||||
-- TianShuAPI 初始化数据库迁移脚本
|
||||
-- PostgreSQL 15+
|
||||
|
||||
-- 1. proxies 代理IP表(无外键依赖)
|
||||
CREATE TABLE IF NOT EXISTS proxies (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
protocol VARCHAR(20) NOT NULL, -- http/https/socks5
|
||||
host VARCHAR(255) NOT NULL,
|
||||
port INT NOT NULL,
|
||||
username VARCHAR(100),
|
||||
password VARCHAR(100),
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active', -- active/disabled
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_proxies_status ON proxies(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_proxies_deleted_at ON proxies(deleted_at);
|
||||
|
||||
-- 2. groups 分组表(无外键依赖)
|
||||
CREATE TABLE IF NOT EXISTS groups (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
rate_multiplier DECIMAL(10, 4) NOT NULL DEFAULT 1.0, -- 费率倍率
|
||||
is_exclusive BOOLEAN NOT NULL DEFAULT FALSE, -- 是否专属分组
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active', -- active/disabled
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_groups_name ON groups(name);
|
||||
CREATE INDEX IF NOT EXISTS idx_groups_status ON groups(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_groups_is_exclusive ON groups(is_exclusive);
|
||||
CREATE INDEX IF NOT EXISTS idx_groups_deleted_at ON groups(deleted_at);
|
||||
|
||||
-- 3. users 用户表(无外键依赖)
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
email VARCHAR(255) NOT NULL UNIQUE,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
role VARCHAR(20) NOT NULL DEFAULT 'user', -- admin/user
|
||||
balance DECIMAL(20, 8) NOT NULL DEFAULT 0, -- 余额(可为负数)
|
||||
concurrency INT NOT NULL DEFAULT 5, -- 并发数限制
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active', -- active/disabled
|
||||
allowed_groups BIGINT[] DEFAULT NULL, -- 允许绑定的分组ID列表
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_status ON users(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_deleted_at ON users(deleted_at);
|
||||
|
||||
-- 4. accounts 上游账号表(依赖proxies)
|
||||
CREATE TABLE IF NOT EXISTS accounts (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
platform VARCHAR(50) NOT NULL, -- anthropic/openai/gemini
|
||||
type VARCHAR(20) NOT NULL, -- oauth/apikey
|
||||
credentials JSONB NOT NULL DEFAULT '{}', -- 凭证信息(加密存储)
|
||||
extra JSONB NOT NULL DEFAULT '{}', -- 扩展信息
|
||||
proxy_id BIGINT REFERENCES proxies(id) ON DELETE SET NULL,
|
||||
concurrency INT NOT NULL DEFAULT 3, -- 账号并发限制
|
||||
priority INT NOT NULL DEFAULT 50, -- 调度优先级(1-100,越小越高)
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active', -- active/disabled/error
|
||||
error_message TEXT,
|
||||
last_used_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_accounts_platform ON accounts(platform);
|
||||
CREATE INDEX IF NOT EXISTS idx_accounts_type ON accounts(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_accounts_status ON accounts(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_accounts_proxy_id ON accounts(proxy_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_accounts_priority ON accounts(priority);
|
||||
CREATE INDEX IF NOT EXISTS idx_accounts_last_used_at ON accounts(last_used_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_accounts_deleted_at ON accounts(deleted_at);
|
||||
|
||||
-- 5. api_keys API密钥表(依赖users, groups)
|
||||
CREATE TABLE IF NOT EXISTS api_keys (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
key VARCHAR(64) NOT NULL UNIQUE, -- sk-xxx格式
|
||||
name VARCHAR(100) NOT NULL,
|
||||
group_id BIGINT REFERENCES groups(id) ON DELETE SET NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active', -- active/disabled
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_api_keys_key ON api_keys(key);
|
||||
CREATE INDEX IF NOT EXISTS idx_api_keys_user_id ON api_keys(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_api_keys_group_id ON api_keys(group_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_api_keys_status ON api_keys(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_api_keys_deleted_at ON api_keys(deleted_at);
|
||||
|
||||
-- 6. account_groups 账号-分组关联表(依赖accounts, groups)
|
||||
CREATE TABLE IF NOT EXISTS account_groups (
|
||||
account_id BIGINT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
|
||||
group_id BIGINT NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
|
||||
priority INT NOT NULL DEFAULT 50, -- 分组内优先级
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY (account_id, group_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_account_groups_group_id ON account_groups(group_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_account_groups_priority ON account_groups(priority);
|
||||
|
||||
-- 7. redeem_codes 卡密表(依赖users)
|
||||
CREATE TABLE IF NOT EXISTS redeem_codes (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
code VARCHAR(32) NOT NULL UNIQUE, -- 兑换码
|
||||
type VARCHAR(20) NOT NULL DEFAULT 'balance', -- balance
|
||||
value DECIMAL(20, 8) NOT NULL, -- 面值(USD)
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'unused', -- unused/used
|
||||
used_by BIGINT REFERENCES users(id) ON DELETE SET NULL,
|
||||
used_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_redeem_codes_code ON redeem_codes(code);
|
||||
CREATE INDEX IF NOT EXISTS idx_redeem_codes_status ON redeem_codes(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_redeem_codes_used_by ON redeem_codes(used_by);
|
||||
|
||||
-- 8. usage_logs 使用记录表(依赖users, api_keys, accounts)
|
||||
CREATE TABLE IF NOT EXISTS usage_logs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
api_key_id BIGINT NOT NULL REFERENCES api_keys(id) ON DELETE CASCADE,
|
||||
account_id BIGINT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
|
||||
request_id VARCHAR(64),
|
||||
model VARCHAR(100) NOT NULL,
|
||||
|
||||
-- Token使用量(4类)
|
||||
input_tokens INT NOT NULL DEFAULT 0,
|
||||
output_tokens INT NOT NULL DEFAULT 0,
|
||||
cache_creation_tokens INT NOT NULL DEFAULT 0,
|
||||
cache_read_tokens INT NOT NULL DEFAULT 0,
|
||||
|
||||
-- 详细的缓存创建分类
|
||||
cache_creation_5m_tokens INT NOT NULL DEFAULT 0,
|
||||
cache_creation_1h_tokens INT NOT NULL DEFAULT 0,
|
||||
|
||||
-- 费用(USD)
|
||||
input_cost DECIMAL(20, 10) NOT NULL DEFAULT 0,
|
||||
output_cost DECIMAL(20, 10) NOT NULL DEFAULT 0,
|
||||
cache_creation_cost DECIMAL(20, 10) NOT NULL DEFAULT 0,
|
||||
cache_read_cost DECIMAL(20, 10) NOT NULL DEFAULT 0,
|
||||
total_cost DECIMAL(20, 10) NOT NULL DEFAULT 0, -- 原始总费用
|
||||
actual_cost DECIMAL(20, 10) NOT NULL DEFAULT 0, -- 实际扣除费用
|
||||
|
||||
-- 元数据
|
||||
stream BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
duration_ms INT,
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_usage_logs_user_id ON usage_logs(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_usage_logs_api_key_id ON usage_logs(api_key_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_usage_logs_account_id ON usage_logs(account_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_usage_logs_model ON usage_logs(model);
|
||||
CREATE INDEX IF NOT EXISTS idx_usage_logs_created_at ON usage_logs(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_usage_logs_user_created ON usage_logs(user_id, created_at);
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
-- Sub2API 账号类型迁移脚本
|
||||
-- 将 'official' 类型账号迁移为 'oauth' 或 'setup-token'
|
||||
-- 根据 credentials->>'scope' 字段判断:
|
||||
-- - 包含 'user:profile' 的是 'oauth' 类型
|
||||
-- - 只有 'user:inference' 的是 'setup-token' 类型
|
||||
|
||||
-- 1. 将包含 profile scope 的 official 账号迁移为 oauth
|
||||
UPDATE accounts
|
||||
SET type = 'oauth',
|
||||
updated_at = NOW()
|
||||
WHERE type = 'official'
|
||||
AND credentials->>'scope' LIKE '%user:profile%';
|
||||
|
||||
-- 2. 将只有 inference scope 的 official 账号迁移为 setup-token
|
||||
UPDATE accounts
|
||||
SET type = 'setup-token',
|
||||
updated_at = NOW()
|
||||
WHERE type = 'official'
|
||||
AND (
|
||||
credentials->>'scope' = 'user:inference'
|
||||
OR credentials->>'scope' NOT LIKE '%user:profile%'
|
||||
);
|
||||
|
||||
-- 3. 处理没有 scope 字段的旧账号(默认为 oauth)
|
||||
UPDATE accounts
|
||||
SET type = 'oauth',
|
||||
updated_at = NOW()
|
||||
WHERE type = 'official'
|
||||
AND (credentials->>'scope' IS NULL OR credentials->>'scope' = '');
|
||||
|
||||
-- 4. 验证迁移结果(查询是否还有 official 类型账号)
|
||||
-- SELECT COUNT(*) FROM accounts WHERE type = 'official';
|
||||
-- 如果结果为 0,说明迁移成功
|
||||
-- TianShuAPI 账号类型迁移脚本
|
||||
-- 将 'official' 类型账号迁移为 'oauth' 或 'setup-token'
|
||||
-- 根据 credentials->>'scope' 字段判断:
|
||||
-- - 包含 'user:profile' 的是 'oauth' 类型
|
||||
-- - 只有 'user:inference' 的是 'setup-token' 类型
|
||||
|
||||
-- 1. 将包含 profile scope 的 official 账号迁移为 oauth
|
||||
UPDATE accounts
|
||||
SET type = 'oauth',
|
||||
updated_at = NOW()
|
||||
WHERE type = 'official'
|
||||
AND credentials->>'scope' LIKE '%user:profile%';
|
||||
|
||||
-- 2. 将只有 inference scope 的 official 账号迁移为 setup-token
|
||||
UPDATE accounts
|
||||
SET type = 'setup-token',
|
||||
updated_at = NOW()
|
||||
WHERE type = 'official'
|
||||
AND (
|
||||
credentials->>'scope' = 'user:inference'
|
||||
OR credentials->>'scope' NOT LIKE '%user:profile%'
|
||||
);
|
||||
|
||||
-- 3. 处理没有 scope 字段的旧账号(默认为 oauth)
|
||||
UPDATE accounts
|
||||
SET type = 'oauth',
|
||||
updated_at = NOW()
|
||||
WHERE type = 'official'
|
||||
AND (credentials->>'scope' IS NULL OR credentials->>'scope' = '');
|
||||
|
||||
-- 4. 验证迁移结果(查询是否还有 official 类型账号)
|
||||
-- SELECT COUNT(*) FROM accounts WHERE type = 'official';
|
||||
-- 如果结果为 0,说明迁移成功
|
||||
|
||||
@@ -1,65 +1,65 @@
|
||||
-- Sub2API 订阅功能迁移脚本
|
||||
-- 添加订阅分组和用户订阅功能
|
||||
|
||||
-- 1. 扩展 groups 表添加订阅相关字段
|
||||
ALTER TABLE groups ADD COLUMN IF NOT EXISTS platform VARCHAR(50) NOT NULL DEFAULT 'anthropic';
|
||||
ALTER TABLE groups ADD COLUMN IF NOT EXISTS subscription_type VARCHAR(20) NOT NULL DEFAULT 'standard';
|
||||
ALTER TABLE groups ADD COLUMN IF NOT EXISTS daily_limit_usd DECIMAL(20, 8) DEFAULT NULL;
|
||||
ALTER TABLE groups ADD COLUMN IF NOT EXISTS weekly_limit_usd DECIMAL(20, 8) DEFAULT NULL;
|
||||
ALTER TABLE groups ADD COLUMN IF NOT EXISTS monthly_limit_usd DECIMAL(20, 8) DEFAULT NULL;
|
||||
ALTER TABLE groups ADD COLUMN IF NOT EXISTS default_validity_days INT NOT NULL DEFAULT 30;
|
||||
|
||||
-- 添加索引
|
||||
CREATE INDEX IF NOT EXISTS idx_groups_platform ON groups(platform);
|
||||
CREATE INDEX IF NOT EXISTS idx_groups_subscription_type ON groups(subscription_type);
|
||||
|
||||
-- 2. 创建 user_subscriptions 用户订阅表
|
||||
CREATE TABLE IF NOT EXISTS user_subscriptions (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
group_id BIGINT NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
|
||||
|
||||
-- 订阅有效期
|
||||
starts_at TIMESTAMPTZ NOT NULL,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active', -- active/expired/suspended
|
||||
|
||||
-- 滑动窗口起始时间(NULL=未激活)
|
||||
daily_window_start TIMESTAMPTZ,
|
||||
weekly_window_start TIMESTAMPTZ,
|
||||
monthly_window_start TIMESTAMPTZ,
|
||||
|
||||
-- 当前窗口已用额度(USD,基于 total_cost 计算)
|
||||
daily_usage_usd DECIMAL(20, 10) NOT NULL DEFAULT 0,
|
||||
weekly_usage_usd DECIMAL(20, 10) NOT NULL DEFAULT 0,
|
||||
monthly_usage_usd DECIMAL(20, 10) NOT NULL DEFAULT 0,
|
||||
|
||||
-- 管理员分配信息
|
||||
assigned_by BIGINT REFERENCES users(id) ON DELETE SET NULL,
|
||||
assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
notes TEXT,
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
-- 唯一约束:每个用户对每个分组只能有一个订阅
|
||||
UNIQUE(user_id, group_id)
|
||||
);
|
||||
|
||||
-- user_subscriptions 索引
|
||||
CREATE INDEX IF NOT EXISTS idx_user_subscriptions_user_id ON user_subscriptions(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_subscriptions_group_id ON user_subscriptions(group_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_subscriptions_status ON user_subscriptions(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_subscriptions_expires_at ON user_subscriptions(expires_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_subscriptions_assigned_by ON user_subscriptions(assigned_by);
|
||||
|
||||
-- 3. 扩展 usage_logs 表添加分组和订阅关联
|
||||
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS group_id BIGINT REFERENCES groups(id) ON DELETE SET NULL;
|
||||
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS subscription_id BIGINT REFERENCES user_subscriptions(id) ON DELETE SET NULL;
|
||||
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS rate_multiplier DECIMAL(10, 4) NOT NULL DEFAULT 1;
|
||||
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS first_token_ms INT;
|
||||
|
||||
-- usage_logs 新索引
|
||||
CREATE INDEX IF NOT EXISTS idx_usage_logs_group_id ON usage_logs(group_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_usage_logs_subscription_id ON usage_logs(subscription_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_usage_logs_sub_created ON usage_logs(subscription_id, created_at);
|
||||
-- TianShuAPI 订阅功能迁移脚本
|
||||
-- 添加订阅分组和用户订阅功能
|
||||
|
||||
-- 1. 扩展 groups 表添加订阅相关字段
|
||||
ALTER TABLE groups ADD COLUMN IF NOT EXISTS platform VARCHAR(50) NOT NULL DEFAULT 'anthropic';
|
||||
ALTER TABLE groups ADD COLUMN IF NOT EXISTS subscription_type VARCHAR(20) NOT NULL DEFAULT 'standard';
|
||||
ALTER TABLE groups ADD COLUMN IF NOT EXISTS daily_limit_usd DECIMAL(20, 8) DEFAULT NULL;
|
||||
ALTER TABLE groups ADD COLUMN IF NOT EXISTS weekly_limit_usd DECIMAL(20, 8) DEFAULT NULL;
|
||||
ALTER TABLE groups ADD COLUMN IF NOT EXISTS monthly_limit_usd DECIMAL(20, 8) DEFAULT NULL;
|
||||
ALTER TABLE groups ADD COLUMN IF NOT EXISTS default_validity_days INT NOT NULL DEFAULT 30;
|
||||
|
||||
-- 添加索引
|
||||
CREATE INDEX IF NOT EXISTS idx_groups_platform ON groups(platform);
|
||||
CREATE INDEX IF NOT EXISTS idx_groups_subscription_type ON groups(subscription_type);
|
||||
|
||||
-- 2. 创建 user_subscriptions 用户订阅表
|
||||
CREATE TABLE IF NOT EXISTS user_subscriptions (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
group_id BIGINT NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
|
||||
|
||||
-- 订阅有效期
|
||||
starts_at TIMESTAMPTZ NOT NULL,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active', -- active/expired/suspended
|
||||
|
||||
-- 滑动窗口起始时间(NULL=未激活)
|
||||
daily_window_start TIMESTAMPTZ,
|
||||
weekly_window_start TIMESTAMPTZ,
|
||||
monthly_window_start TIMESTAMPTZ,
|
||||
|
||||
-- 当前窗口已用额度(USD,基于 total_cost 计算)
|
||||
daily_usage_usd DECIMAL(20, 10) NOT NULL DEFAULT 0,
|
||||
weekly_usage_usd DECIMAL(20, 10) NOT NULL DEFAULT 0,
|
||||
monthly_usage_usd DECIMAL(20, 10) NOT NULL DEFAULT 0,
|
||||
|
||||
-- 管理员分配信息
|
||||
assigned_by BIGINT REFERENCES users(id) ON DELETE SET NULL,
|
||||
assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
notes TEXT,
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
-- 唯一约束:每个用户对每个分组只能有一个订阅
|
||||
UNIQUE(user_id, group_id)
|
||||
);
|
||||
|
||||
-- user_subscriptions 索引
|
||||
CREATE INDEX IF NOT EXISTS idx_user_subscriptions_user_id ON user_subscriptions(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_subscriptions_group_id ON user_subscriptions(group_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_subscriptions_status ON user_subscriptions(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_subscriptions_expires_at ON user_subscriptions(expires_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_subscriptions_assigned_by ON user_subscriptions(assigned_by);
|
||||
|
||||
-- 3. 扩展 usage_logs 表添加分组和订阅关联
|
||||
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS group_id BIGINT REFERENCES groups(id) ON DELETE SET NULL;
|
||||
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS subscription_id BIGINT REFERENCES user_subscriptions(id) ON DELETE SET NULL;
|
||||
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS rate_multiplier DECIMAL(10, 4) NOT NULL DEFAULT 1;
|
||||
ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS first_token_ms INT;
|
||||
|
||||
-- usage_logs 新索引
|
||||
CREATE INDEX IF NOT EXISTS idx_usage_logs_group_id ON usage_logs(group_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_usage_logs_subscription_id ON usage_logs(subscription_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_usage_logs_sub_created ON usage_logs(subscription_id, created_at);
|
||||
|
||||
@@ -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 '备注说明(管理员调整时的原因说明)';
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
|
||||
|
||||
@@ -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 $$;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 $$;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 等。
|
||||
|
||||
@@ -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 表示未删除';
|
||||
|
||||
@@ -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,用于数据清理前的审计';
|
||||
|
||||
@@ -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 联接表';
|
||||
|
||||
@@ -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
|
||||
$$;
|
||||
|
||||
@@ -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 即使软删除后也不应该重复使用(安全考虑)
|
||||
-- ============================================================================
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,178 +1,178 @@
|
||||
# Database Migrations
|
||||
|
||||
## Overview
|
||||
|
||||
This directory contains SQL migration files for database schema changes. The migration system uses SHA256 checksums to ensure migration immutability and consistency across environments.
|
||||
|
||||
## Migration File Naming
|
||||
|
||||
Format: `NNN_description.sql`
|
||||
- `NNN`: Sequential number (e.g., 001, 002, 003)
|
||||
- `description`: Brief description in snake_case
|
||||
|
||||
Example: `017_add_gemini_tier_id.sql`
|
||||
|
||||
## Migration File Structure
|
||||
|
||||
```sql
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
-- Your forward migration SQL here
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
-- Your rollback migration SQL here
|
||||
-- +goose StatementEnd
|
||||
```
|
||||
|
||||
## Important Rules
|
||||
|
||||
### ⚠️ Immutability Principle
|
||||
|
||||
**Once a migration is applied to ANY environment (dev, staging, production), it MUST NOT be modified.**
|
||||
|
||||
Why?
|
||||
- Each migration has a SHA256 checksum stored in the `schema_migrations` table
|
||||
- Modifying an applied migration causes checksum mismatch errors
|
||||
- Different environments would have inconsistent database states
|
||||
- Breaks audit trail and reproducibility
|
||||
|
||||
### ✅ Correct Workflow
|
||||
|
||||
1. **Create new migration**
|
||||
```bash
|
||||
# Create new file with next sequential number
|
||||
touch migrations/018_your_change.sql
|
||||
```
|
||||
|
||||
2. **Write Up and Down migrations**
|
||||
- Up: Apply the change
|
||||
- Down: Revert the change (should be symmetric with Up)
|
||||
|
||||
3. **Test locally**
|
||||
```bash
|
||||
# Apply migration
|
||||
make migrate-up
|
||||
|
||||
# Test rollback
|
||||
make migrate-down
|
||||
```
|
||||
|
||||
4. **Commit and deploy**
|
||||
```bash
|
||||
git add migrations/018_your_change.sql
|
||||
git commit -m "feat(db): add your change"
|
||||
```
|
||||
|
||||
### ❌ What NOT to Do
|
||||
|
||||
- ❌ Modify an already-applied migration file
|
||||
- ❌ Delete migration files
|
||||
- ❌ Change migration file names
|
||||
- ❌ Reorder migration numbers
|
||||
|
||||
### 🔧 If You Accidentally Modified an Applied Migration
|
||||
|
||||
**Error message:**
|
||||
```
|
||||
migration 017_add_gemini_tier_id.sql checksum mismatch (db=abc123... file=def456...)
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# 1. Find the original version
|
||||
git log --oneline -- migrations/017_add_gemini_tier_id.sql
|
||||
|
||||
# 2. Revert to the commit when it was first applied
|
||||
git checkout <commit-hash> -- migrations/017_add_gemini_tier_id.sql
|
||||
|
||||
# 3. Create a NEW migration for your changes
|
||||
touch migrations/018_your_new_change.sql
|
||||
```
|
||||
|
||||
## Migration System Details
|
||||
|
||||
- **Checksum Algorithm**: SHA256 of trimmed file content
|
||||
- **Tracking Table**: `schema_migrations` (filename, checksum, applied_at)
|
||||
- **Runner**: `internal/repository/migrations_runner.go`
|
||||
- **Auto-run**: Migrations run automatically on service startup
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Keep migrations small and focused**
|
||||
- One logical change per migration
|
||||
- Easier to review and rollback
|
||||
|
||||
2. **Write reversible migrations**
|
||||
- Always provide a working Down migration
|
||||
- Test rollback before committing
|
||||
|
||||
3. **Use transactions**
|
||||
- Wrap DDL statements in transactions when possible
|
||||
- Ensures atomicity
|
||||
|
||||
4. **Add comments**
|
||||
- Explain WHY the change is needed
|
||||
- Document any special considerations
|
||||
|
||||
5. **Test in development first**
|
||||
- Apply migration locally
|
||||
- Verify data integrity
|
||||
- Test rollback
|
||||
|
||||
## Example Migration
|
||||
|
||||
```sql
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
-- Add tier_id field to Gemini OAuth accounts for quota tracking
|
||||
UPDATE accounts
|
||||
SET credentials = jsonb_set(
|
||||
credentials,
|
||||
'{tier_id}',
|
||||
'"LEGACY"',
|
||||
true
|
||||
)
|
||||
WHERE platform = 'gemini'
|
||||
AND type = 'oauth'
|
||||
AND credentials->>'tier_id' IS NULL;
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
-- Remove tier_id field
|
||||
UPDATE accounts
|
||||
SET credentials = credentials - 'tier_id'
|
||||
WHERE platform = 'gemini'
|
||||
AND type = 'oauth'
|
||||
AND credentials->>'tier_id' = 'LEGACY';
|
||||
-- +goose StatementEnd
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Checksum Mismatch
|
||||
See "If You Accidentally Modified an Applied Migration" above.
|
||||
|
||||
### Migration Failed
|
||||
```bash
|
||||
# Check migration status
|
||||
psql -d sub2api -c "SELECT * FROM schema_migrations ORDER BY applied_at DESC;"
|
||||
|
||||
# Manually rollback if needed (use with caution)
|
||||
# Better to fix the migration and create a new one
|
||||
```
|
||||
|
||||
### Need to Skip a Migration (Emergency Only)
|
||||
```sql
|
||||
-- DANGEROUS: Only use in development or with extreme caution
|
||||
INSERT INTO schema_migrations (filename, checksum, applied_at)
|
||||
VALUES ('NNN_migration.sql', 'calculated_checksum', NOW());
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- Migration runner: `internal/repository/migrations_runner.go`
|
||||
- Goose syntax: https://github.com/pressly/goose
|
||||
- PostgreSQL docs: https://www.postgresql.org/docs/
|
||||
# Database Migrations
|
||||
|
||||
## Overview
|
||||
|
||||
This directory contains SQL migration files for database schema changes. The migration system uses SHA256 checksums to ensure migration immutability and consistency across environments.
|
||||
|
||||
## Migration File Naming
|
||||
|
||||
Format: `NNN_description.sql`
|
||||
- `NNN`: Sequential number (e.g., 001, 002, 003)
|
||||
- `description`: Brief description in snake_case
|
||||
|
||||
Example: `017_add_gemini_tier_id.sql`
|
||||
|
||||
## Migration File Structure
|
||||
|
||||
```sql
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
-- Your forward migration SQL here
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
-- Your rollback migration SQL here
|
||||
-- +goose StatementEnd
|
||||
```
|
||||
|
||||
## Important Rules
|
||||
|
||||
### ⚠️ Immutability Principle
|
||||
|
||||
**Once a migration is applied to ANY environment (dev, staging, production), it MUST NOT be modified.**
|
||||
|
||||
Why?
|
||||
- Each migration has a SHA256 checksum stored in the `schema_migrations` table
|
||||
- Modifying an applied migration causes checksum mismatch errors
|
||||
- Different environments would have inconsistent database states
|
||||
- Breaks audit trail and reproducibility
|
||||
|
||||
### ✅ Correct Workflow
|
||||
|
||||
1. **Create new migration**
|
||||
```bash
|
||||
# Create new file with next sequential number
|
||||
touch migrations/018_your_change.sql
|
||||
```
|
||||
|
||||
2. **Write Up and Down migrations**
|
||||
- Up: Apply the change
|
||||
- Down: Revert the change (should be symmetric with Up)
|
||||
|
||||
3. **Test locally**
|
||||
```bash
|
||||
# Apply migration
|
||||
make migrate-up
|
||||
|
||||
# Test rollback
|
||||
make migrate-down
|
||||
```
|
||||
|
||||
4. **Commit and deploy**
|
||||
```bash
|
||||
git add migrations/018_your_change.sql
|
||||
git commit -m "feat(db): add your change"
|
||||
```
|
||||
|
||||
### ❌ What NOT to Do
|
||||
|
||||
- ❌ Modify an already-applied migration file
|
||||
- ❌ Delete migration files
|
||||
- ❌ Change migration file names
|
||||
- ❌ Reorder migration numbers
|
||||
|
||||
### 🔧 If You Accidentally Modified an Applied Migration
|
||||
|
||||
**Error message:**
|
||||
```
|
||||
migration 017_add_gemini_tier_id.sql checksum mismatch (db=abc123... file=def456...)
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# 1. Find the original version
|
||||
git log --oneline -- migrations/017_add_gemini_tier_id.sql
|
||||
|
||||
# 2. Revert to the commit when it was first applied
|
||||
git checkout <commit-hash> -- migrations/017_add_gemini_tier_id.sql
|
||||
|
||||
# 3. Create a NEW migration for your changes
|
||||
touch migrations/018_your_new_change.sql
|
||||
```
|
||||
|
||||
## Migration System Details
|
||||
|
||||
- **Checksum Algorithm**: SHA256 of trimmed file content
|
||||
- **Tracking Table**: `schema_migrations` (filename, checksum, applied_at)
|
||||
- **Runner**: `internal/repository/migrations_runner.go`
|
||||
- **Auto-run**: Migrations run automatically on service startup
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Keep migrations small and focused**
|
||||
- One logical change per migration
|
||||
- Easier to review and rollback
|
||||
|
||||
2. **Write reversible migrations**
|
||||
- Always provide a working Down migration
|
||||
- Test rollback before committing
|
||||
|
||||
3. **Use transactions**
|
||||
- Wrap DDL statements in transactions when possible
|
||||
- Ensures atomicity
|
||||
|
||||
4. **Add comments**
|
||||
- Explain WHY the change is needed
|
||||
- Document any special considerations
|
||||
|
||||
5. **Test in development first**
|
||||
- Apply migration locally
|
||||
- Verify data integrity
|
||||
- Test rollback
|
||||
|
||||
## Example Migration
|
||||
|
||||
```sql
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
-- Add tier_id field to Gemini OAuth accounts for quota tracking
|
||||
UPDATE accounts
|
||||
SET credentials = jsonb_set(
|
||||
credentials,
|
||||
'{tier_id}',
|
||||
'"LEGACY"',
|
||||
true
|
||||
)
|
||||
WHERE platform = 'gemini'
|
||||
AND type = 'oauth'
|
||||
AND credentials->>'tier_id' IS NULL;
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
-- Remove tier_id field
|
||||
UPDATE accounts
|
||||
SET credentials = credentials - 'tier_id'
|
||||
WHERE platform = 'gemini'
|
||||
AND type = 'oauth'
|
||||
AND credentials->>'tier_id' = 'LEGACY';
|
||||
-- +goose StatementEnd
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Checksum Mismatch
|
||||
See "If You Accidentally Modified an Applied Migration" above.
|
||||
|
||||
### Migration Failed
|
||||
```bash
|
||||
# Check migration status
|
||||
psql -d sub2api -c "SELECT * FROM schema_migrations ORDER BY applied_at DESC;"
|
||||
|
||||
# Manually rollback if needed (use with caution)
|
||||
# Better to fix the migration and create a new one
|
||||
```
|
||||
|
||||
### Need to Skip a Migration (Emergency Only)
|
||||
```sql
|
||||
-- DANGEROUS: Only use in development or with extreme caution
|
||||
INSERT INTO schema_migrations (filename, checksum, applied_at)
|
||||
VALUES ('NNN_migration.sql', 'calculated_checksum', NOW());
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- Migration runner: `internal/repository/migrations_runner.go`
|
||||
- Goose syntax: https://github.com/pressly/goose
|
||||
- PostgreSQL docs: https://www.postgresql.org/docs/
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
// Package migrations 包含嵌入的 SQL 数据库迁移文件。
|
||||
//
|
||||
// 该包使用 Go 1.16+ 的 embed 功能将 SQL 文件嵌入到编译后的二进制文件中。
|
||||
// 这种方式的优点:
|
||||
// - 部署时无需额外的迁移文件
|
||||
// - 迁移文件与代码版本一致
|
||||
// - 便于版本控制和代码审查
|
||||
package migrations
|
||||
|
||||
import "embed"
|
||||
|
||||
// FS 包含本目录下所有嵌入的 SQL 迁移文件。
|
||||
//
|
||||
// 迁移命名规范:
|
||||
// - 使用零填充的数字前缀确保正确的执行顺序
|
||||
// - 格式:NNN_description.sql(如 001_init.sql, 002_add_users.sql)
|
||||
// - 描述部分使用下划线分隔的小写单词
|
||||
//
|
||||
// 迁移文件要求:
|
||||
// - 必须是幂等的(可重复执行而不产生错误)
|
||||
// - 推荐使用 IF NOT EXISTS / IF EXISTS 语法
|
||||
// - 一旦应用,不应修改已有的迁移文件(通过 checksum 校验)
|
||||
//
|
||||
// 示例迁移文件:
|
||||
//
|
||||
// -- 001_init.sql
|
||||
// CREATE TABLE IF NOT EXISTS users (
|
||||
// id BIGSERIAL PRIMARY KEY,
|
||||
// email VARCHAR(255) NOT NULL UNIQUE,
|
||||
// created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
// );
|
||||
//
|
||||
//go:embed *.sql
|
||||
var FS embed.FS
|
||||
// Package migrations 包含嵌入的 SQL 数据库迁移文件。
|
||||
//
|
||||
// 该包使用 Go 1.16+ 的 embed 功能将 SQL 文件嵌入到编译后的二进制文件中。
|
||||
// 这种方式的优点:
|
||||
// - 部署时无需额外的迁移文件
|
||||
// - 迁移文件与代码版本一致
|
||||
// - 便于版本控制和代码审查
|
||||
package migrations
|
||||
|
||||
import "embed"
|
||||
|
||||
// FS 包含本目录下所有嵌入的 SQL 迁移文件。
|
||||
//
|
||||
// 迁移命名规范:
|
||||
// - 使用零填充的数字前缀确保正确的执行顺序
|
||||
// - 格式:NNN_description.sql(如 001_init.sql, 002_add_users.sql)
|
||||
// - 描述部分使用下划线分隔的小写单词
|
||||
//
|
||||
// 迁移文件要求:
|
||||
// - 必须是幂等的(可重复执行而不产生错误)
|
||||
// - 推荐使用 IF NOT EXISTS / IF EXISTS 语法
|
||||
// - 一旦应用,不应修改已有的迁移文件(通过 checksum 校验)
|
||||
//
|
||||
// 示例迁移文件:
|
||||
//
|
||||
// -- 001_init.sql
|
||||
// CREATE TABLE IF NOT EXISTS users (
|
||||
// id BIGSERIAL PRIMARY KEY,
|
||||
// email VARCHAR(255) NOT NULL UNIQUE,
|
||||
// created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
// );
|
||||
//
|
||||
//go:embed *.sql
|
||||
var FS embed.FS
|
||||
|
||||
Reference in New Issue
Block a user