664 lines
30 KiB
PL/PgSQL
664 lines
30 KiB
PL/PgSQL
-- ============================================================
|
|
-- 妙境 AI 创作平台 — 数据库初始化脚本
|
|
-- 适用于: PostgreSQL 14+ (Supabase / 自托管)
|
|
-- 执行方式: 在 Supabase SQL Editor 或 psql 中运行
|
|
-- ============================================================
|
|
|
|
-- 0. 启用必要扩展
|
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
|
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
|
|
|
-- 1. 创建 auth 模式和 users 表
|
|
CREATE SCHEMA IF NOT EXISTS auth;
|
|
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM pg_proc p
|
|
JOIN pg_namespace n ON n.oid = p.pronamespace
|
|
WHERE n.nspname = 'auth' AND p.proname = 'uid'
|
|
) THEN
|
|
EXECUTE 'CREATE FUNCTION auth.uid() RETURNS UUID AS $fn$ SELECT NULLIF(current_setting(''request.jwt.claim.sub'', true), '''')::UUID; $fn$ LANGUAGE SQL STABLE';
|
|
END IF;
|
|
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM pg_proc p
|
|
JOIN pg_namespace n ON n.oid = p.pronamespace
|
|
WHERE n.nspname = 'auth' AND p.proname = 'role'
|
|
) THEN
|
|
EXECUTE 'CREATE FUNCTION auth.role() RETURNS TEXT AS $fn$ SELECT COALESCE(NULLIF(current_setting(''request.jwt.claim.role'', true), ''''), ''anon''); $fn$ LANGUAGE SQL STABLE';
|
|
END IF;
|
|
END $$;
|
|
|
|
CREATE TABLE IF NOT EXISTS auth.users (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
email VARCHAR(255) UNIQUE,
|
|
password_hash TEXT,
|
|
raw_user_meta_data JSONB,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS auth_users_email_idx ON auth.users (email);
|
|
|
|
-- ============================================================
|
|
-- 1. 用户资料表 (profiles)
|
|
-- 与 Supabase Auth 的 auth.users 表关联
|
|
-- ============================================================
|
|
CREATE TABLE IF NOT EXISTS profiles (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
email VARCHAR(255) NOT NULL UNIQUE,
|
|
nickname VARCHAR(128),
|
|
avatar_url TEXT,
|
|
phone VARCHAR(20),
|
|
role VARCHAR(32) NOT NULL DEFAULT 'user', -- guest, user, vip, enterprise_admin, enterprise_member, admin
|
|
membership_tier VARCHAR(32) NOT NULL DEFAULT 'free', -- free, basic, pro, enterprise
|
|
membership_expires_at TIMESTAMPTZ,
|
|
credits_balance INTEGER NOT NULL DEFAULT 0,
|
|
daily_quota_used INTEGER NOT NULL DEFAULT 0,
|
|
daily_quota_limit INTEGER NOT NULL DEFAULT 5,
|
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
email_verified BOOLEAN NOT NULL DEFAULT false,
|
|
email_verified_at TIMESTAMPTZ,
|
|
email_bound_at TIMESTAMPTZ,
|
|
email_sender_domain VARCHAR(255),
|
|
preferred_theme VARCHAR(16) NOT NULL DEFAULT 'dark',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS profiles_email_idx ON profiles (email);
|
|
CREATE INDEX IF NOT EXISTS profiles_role_idx ON profiles (role);
|
|
|
|
-- ============================================================
|
|
-- 2. 创作作品表 (works)
|
|
-- ============================================================
|
|
CREATE TABLE IF NOT EXISTS works (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID DEFAULT auth.uid(),
|
|
title VARCHAR(255),
|
|
type VARCHAR(32) NOT NULL, -- text2img, img2img, text2video, img2video
|
|
prompt TEXT,
|
|
negative_prompt TEXT,
|
|
params JSONB, -- 生成参数 (画面比例、分辨率、模型等)
|
|
result_url TEXT, -- 生成文件的 URL
|
|
thumbnail_url TEXT,
|
|
width INTEGER,
|
|
height INTEGER,
|
|
duration NUMERIC(6, 2), -- 视频时长 (秒)
|
|
is_public BOOLEAN NOT NULL DEFAULT false,
|
|
likes_count INTEGER NOT NULL DEFAULT 0,
|
|
views_count INTEGER NOT NULL DEFAULT 0,
|
|
credits_cost INTEGER NOT NULL DEFAULT 0,
|
|
status VARCHAR(32) NOT NULL DEFAULT 'completed', -- pending, processing, completed, failed
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS works_user_id_idx ON works (user_id);
|
|
CREATE INDEX IF NOT EXISTS works_type_idx ON works (type);
|
|
CREATE INDEX IF NOT EXISTS works_is_public_idx ON works (is_public);
|
|
CREATE INDEX IF NOT EXISTS works_created_at_idx ON works (created_at);
|
|
CREATE INDEX IF NOT EXISTS works_status_idx ON works (status);
|
|
|
|
-- ============================================================
|
|
-- 3. 积分记录表 (credit_transactions)
|
|
-- ============================================================
|
|
CREATE TABLE IF NOT EXISTS credit_transactions (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID DEFAULT auth.uid(),
|
|
amount INTEGER NOT NULL, -- 正数=入账, 负数=消费
|
|
balance_after INTEGER NOT NULL,
|
|
type VARCHAR(32) NOT NULL, -- purchase, consume, gift, reward, refund
|
|
description VARCHAR(500),
|
|
related_work_id UUID,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS credit_transactions_user_id_idx ON credit_transactions (user_id);
|
|
CREATE INDEX IF NOT EXISTS credit_transactions_type_idx ON credit_transactions (type);
|
|
CREATE INDEX IF NOT EXISTS credit_transactions_created_at_idx ON credit_transactions (created_at);
|
|
|
|
-- ============================================================
|
|
-- 4. 订单表 (orders)
|
|
-- ============================================================
|
|
CREATE TABLE IF NOT EXISTS orders (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID DEFAULT auth.uid(),
|
|
order_no VARCHAR(64) NOT NULL UNIQUE,
|
|
product_type VARCHAR(32) NOT NULL, -- membership, credits, api
|
|
product_name VARCHAR(255) NOT NULL,
|
|
amount NUMERIC(10, 2) NOT NULL,
|
|
credits_amount INTEGER, -- 购买的积分数
|
|
status VARCHAR(32) NOT NULL DEFAULT 'pending', -- pending, paid, cancelled, refunded
|
|
payment_method VARCHAR(32), -- wechat, alipay, stripe
|
|
paid_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS orders_user_id_idx ON orders (user_id);
|
|
CREATE INDEX IF NOT EXISTS orders_order_no_idx ON orders (order_no);
|
|
CREATE INDEX IF NOT EXISTS orders_status_idx ON orders (status);
|
|
CREATE INDEX IF NOT EXISTS orders_created_at_idx ON orders (created_at);
|
|
|
|
-- ============================================================
|
|
-- 5. 生成任务队列表 (generation_jobs)
|
|
-- ============================================================
|
|
CREATE TABLE IF NOT EXISTS generation_jobs (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
type VARCHAR(16) NOT NULL,
|
|
status VARCHAR(16) NOT NULL DEFAULT 'queued',
|
|
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
result JSONB,
|
|
error TEXT,
|
|
user_id UUID,
|
|
provider VARCHAR(128),
|
|
model_name VARCHAR(255),
|
|
api_url TEXT,
|
|
progress JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
started_at TIMESTAMPTZ,
|
|
finished_at TIMESTAMPTZ,
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS generation_jobs_status_created_idx ON generation_jobs (status, created_at DESC);
|
|
CREATE INDEX IF NOT EXISTS generation_jobs_status_updated_idx ON generation_jobs (status, updated_at DESC);
|
|
CREATE INDEX IF NOT EXISTS generation_jobs_running_timeout_idx ON generation_jobs (updated_at) WHERE status = 'running';
|
|
CREATE INDEX IF NOT EXISTS generation_jobs_created_idx ON generation_jobs (created_at DESC);
|
|
CREATE INDEX IF NOT EXISTS generation_jobs_user_created_idx ON generation_jobs (user_id, created_at DESC);
|
|
CREATE INDEX IF NOT EXISTS generation_jobs_provider_model_created_idx ON generation_jobs (type, provider, model_name, created_at DESC);
|
|
|
|
-- ============================================================
|
|
-- 6. 用户自定义 API 密钥表 (user_api_keys)
|
|
-- ============================================================
|
|
CREATE TABLE IF NOT EXISTS user_api_keys (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID DEFAULT auth.uid(),
|
|
provider VARCHAR(64) NOT NULL, -- openai, stabilityai, runway, etc.
|
|
api_url TEXT, -- 完整 API 端点 URL
|
|
model_name VARCHAR(128), -- 具体模型名称
|
|
api_key_encrypted TEXT NOT NULL, -- 加密存储的 API Key
|
|
api_key_preview VARCHAR(20), -- Key 尾号 (如 sk-...4f3e)
|
|
supplier_name VARCHAR(128),
|
|
note TEXT NOT NULL DEFAULT '',
|
|
type VARCHAR(16) NOT NULL DEFAULT 'image',
|
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS user_api_keys_user_id_idx ON user_api_keys (user_id);
|
|
CREATE INDEX IF NOT EXISTS user_api_keys_provider_idx ON user_api_keys (provider);
|
|
|
|
-- ============================================================
|
|
-- 7. 作品点赞表 (work_likes)
|
|
-- ============================================================
|
|
CREATE TABLE IF NOT EXISTS work_likes (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID DEFAULT auth.uid(),
|
|
work_id UUID NOT NULL REFERENCES works(id) ON DELETE CASCADE,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS work_likes_user_id_idx ON work_likes (user_id);
|
|
CREATE INDEX IF NOT EXISTS work_likes_work_id_idx ON work_likes (work_id);
|
|
|
|
-- 唯一约束:每个用户对每个作品只能点赞一次
|
|
CREATE UNIQUE INDEX IF NOT EXISTS work_likes_user_work_uniq ON work_likes (user_id, work_id);
|
|
|
|
-- ============================================================
|
|
-- 8. 网站配置表 (site_config)
|
|
-- ============================================================
|
|
CREATE TABLE IF NOT EXISTS site_config (
|
|
id INTEGER PRIMARY KEY DEFAULT 1,
|
|
site_name VARCHAR(128) NOT NULL DEFAULT '妙境',
|
|
site_tab_title VARCHAR(255) NOT NULL DEFAULT '妙境 - AI创作平台',
|
|
site_description TEXT NOT NULL DEFAULT '',
|
|
site_keywords TEXT NOT NULL DEFAULT '',
|
|
logo_url TEXT,
|
|
favicon_url TEXT,
|
|
announcement TEXT NOT NULL DEFAULT '',
|
|
membership_enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
|
terms_of_service TEXT NOT NULL DEFAULT '',
|
|
privacy_policy TEXT NOT NULL DEFAULT '',
|
|
about_us TEXT NOT NULL DEFAULT '',
|
|
help_center TEXT NOT NULL DEFAULT '',
|
|
filing_info TEXT NOT NULL DEFAULT '',
|
|
filing_url TEXT NOT NULL DEFAULT '',
|
|
public_security_filing_info TEXT NOT NULL DEFAULT '',
|
|
public_security_filing_url TEXT NOT NULL DEFAULT '',
|
|
log_retention_days INTEGER NOT NULL DEFAULT 30,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ
|
|
);
|
|
|
|
-- 插入默认配置
|
|
INSERT INTO site_config (id, site_name, site_tab_title)
|
|
VALUES (1, '妙境', '妙境 - AI创作平台')
|
|
ON CONFLICT (id) DO NOTHING;
|
|
|
|
-- ============================================================
|
|
-- 9. 公告表 (announcements)
|
|
-- ============================================================
|
|
CREATE TABLE IF NOT EXISTS announcements (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
title VARCHAR(255) NOT NULL,
|
|
content TEXT NOT NULL, -- 支持 Markdown
|
|
type VARCHAR(32) NOT NULL DEFAULT 'site',
|
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
starts_at TIMESTAMPTZ,
|
|
expires_at TIMESTAMPTZ,
|
|
created_by UUID,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS announcements_is_active_idx ON announcements (is_active);
|
|
CREATE INDEX IF NOT EXISTS announcements_expires_at_idx ON announcements (expires_at);
|
|
|
|
-- ============================================================
|
|
-- 10. 网站统计表 (site_stats)
|
|
-- ============================================================
|
|
CREATE TABLE IF NOT EXISTS site_stats (
|
|
id INTEGER PRIMARY KEY DEFAULT 1,
|
|
total_visits BIGINT NOT NULL DEFAULT 0,
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
INSERT INTO site_stats (id, total_visits) VALUES (1, 0) ON CONFLICT (id) DO NOTHING;
|
|
|
|
-- ============================================================
|
|
-- 11. 平台日志
|
|
-- ============================================================
|
|
CREATE TABLE IF NOT EXISTS platform_log_settings (
|
|
id INTEGER PRIMARY KEY DEFAULT 1,
|
|
retention_days INTEGER NOT NULL DEFAULT 30,
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
INSERT INTO platform_log_settings (id, retention_days)
|
|
VALUES (1, 30)
|
|
ON CONFLICT (id) DO NOTHING;
|
|
|
|
CREATE TABLE IF NOT EXISTS platform_logs (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
type VARCHAR(32) NOT NULL,
|
|
level VARCHAR(16) NOT NULL DEFAULT 'info',
|
|
action VARCHAR(128) NOT NULL,
|
|
message TEXT NOT NULL,
|
|
user_id UUID,
|
|
user_name VARCHAR(255),
|
|
user_email VARCHAR(255),
|
|
target_type VARCHAR(64),
|
|
target_id VARCHAR(255),
|
|
ip_address VARCHAR(64),
|
|
user_agent TEXT,
|
|
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS platform_logs_type_created_idx ON platform_logs (type, created_at DESC);
|
|
CREATE INDEX IF NOT EXISTS platform_logs_level_created_idx ON platform_logs (level, created_at DESC);
|
|
CREATE INDEX IF NOT EXISTS platform_logs_user_created_idx ON platform_logs (user_id, created_at DESC);
|
|
CREATE INDEX IF NOT EXISTS platform_logs_created_idx ON platform_logs (created_at DESC);
|
|
|
|
-- ============================================================
|
|
-- 12. API 供应商与推荐模型配置
|
|
-- ============================================================
|
|
CREATE TABLE IF NOT EXISTS api_providers (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
name VARCHAR(128) NOT NULL UNIQUE,
|
|
default_api_url TEXT,
|
|
default_model VARCHAR(255),
|
|
type VARCHAR(16) NOT NULL DEFAULT 'image',
|
|
website TEXT,
|
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS model_recommendations (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
model_name VARCHAR(255) NOT NULL,
|
|
display_name VARCHAR(255),
|
|
type VARCHAR(16) NOT NULL DEFAULT 'image',
|
|
provider_id UUID REFERENCES api_providers(id) ON DELETE SET NULL,
|
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS api_providers_active_sort_idx ON api_providers (is_active, sort_order);
|
|
CREATE INDEX IF NOT EXISTS model_recommendations_active_type_sort_idx ON model_recommendations (is_active, type, sort_order);
|
|
CREATE INDEX IF NOT EXISTS model_recommendations_provider_idx ON model_recommendations (provider_id);
|
|
|
|
INSERT INTO api_providers (name, default_api_url, default_model, type, website, is_active, sort_order)
|
|
VALUES
|
|
('硅基流动', 'https://api.siliconflow.cn/v1/images/generations', 'black-forest-labs/FLUX.1-schnell', 'image', 'https://cloud.siliconflow.cn', true, 10),
|
|
('mozheAPI', 'https://openai.mozhevip.top', '', 'image', 'https://openai.mozhevip.top', true, 20),
|
|
('OpenAI', 'https://api.openai.com/v1/images/generations', 'dall-e-3', 'image', NULL, true, 30),
|
|
('Stability AI', 'https://api.stability.ai/v1/generation/stable-diffusion-xl/text-to-image', 'stable-diffusion-xl', 'image', NULL, true, 40),
|
|
('Midjourney', '', 'midjourney-v6', 'image', NULL, true, 50),
|
|
('Runway', 'https://api.runwayml.com/v1/image_to_video', 'gen-3-alpha', 'video', NULL, true, 60),
|
|
('Pika', '', 'pika-1.0', 'video', NULL, true, 70),
|
|
('Kling', '', 'kling-v1', 'video', NULL, true, 80),
|
|
('DeepSeek', 'https://api.deepseek.com/v1/chat/completions', 'deepseek-chat', 'text', NULL, true, 90),
|
|
('OpenAI GPT', 'https://api.openai.com/v1/chat/completions', 'gpt-4o', 'text', NULL, true, 100),
|
|
('自定义', '', '', 'image', NULL, true, 999)
|
|
ON CONFLICT (name) DO NOTHING;
|
|
|
|
INSERT INTO model_recommendations (model_name, display_name, type, provider_id, is_active, sort_order)
|
|
SELECT 'gpt-image-2', 'gpt-image-2', 'image', NULL, true, 10
|
|
WHERE NOT EXISTS (
|
|
SELECT 1 FROM model_recommendations
|
|
WHERE model_name = 'gpt-image-2' AND type = 'image' AND provider_id IS NULL
|
|
);
|
|
|
|
-- ============================================================
|
|
-- 兼容旧版本库结构的幂等补丁
|
|
-- ============================================================
|
|
ALTER TABLE profiles
|
|
ADD COLUMN IF NOT EXISTS email_verified BOOLEAN NOT NULL DEFAULT false,
|
|
ADD COLUMN IF NOT EXISTS email_verified_at TIMESTAMPTZ,
|
|
ADD COLUMN IF NOT EXISTS email_bound_at TIMESTAMPTZ,
|
|
ADD COLUMN IF NOT EXISTS email_sender_domain VARCHAR(255),
|
|
ADD COLUMN IF NOT EXISTS preferred_theme VARCHAR(16) NOT NULL DEFAULT 'dark';
|
|
|
|
UPDATE profiles
|
|
SET preferred_theme = 'dark'
|
|
WHERE preferred_theme IS NULL
|
|
OR preferred_theme NOT IN ('dark', 'light');
|
|
|
|
ALTER TABLE works
|
|
ADD COLUMN IF NOT EXISTS views_count INTEGER NOT NULL DEFAULT 0,
|
|
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ;
|
|
|
|
ALTER TABLE user_api_keys
|
|
ADD COLUMN IF NOT EXISTS supplier_name VARCHAR(128),
|
|
ADD COLUMN IF NOT EXISTS note TEXT NOT NULL DEFAULT '',
|
|
ADD COLUMN IF NOT EXISTS type VARCHAR(16) NOT NULL DEFAULT 'image';
|
|
|
|
ALTER TABLE site_config
|
|
ADD COLUMN IF NOT EXISTS site_description TEXT NOT NULL DEFAULT '',
|
|
ADD COLUMN IF NOT EXISTS site_keywords TEXT NOT NULL DEFAULT '',
|
|
ADD COLUMN IF NOT EXISTS announcement TEXT NOT NULL DEFAULT '',
|
|
ADD COLUMN IF NOT EXISTS membership_enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
|
ADD COLUMN IF NOT EXISTS terms_of_service TEXT NOT NULL DEFAULT '',
|
|
ADD COLUMN IF NOT EXISTS privacy_policy TEXT NOT NULL DEFAULT '',
|
|
ADD COLUMN IF NOT EXISTS about_us TEXT NOT NULL DEFAULT '',
|
|
ADD COLUMN IF NOT EXISTS help_center TEXT NOT NULL DEFAULT '',
|
|
ADD COLUMN IF NOT EXISTS filing_info TEXT NOT NULL DEFAULT '',
|
|
ADD COLUMN IF NOT EXISTS filing_url TEXT NOT NULL DEFAULT '',
|
|
ADD COLUMN IF NOT EXISTS public_security_filing_info TEXT NOT NULL DEFAULT '',
|
|
ADD COLUMN IF NOT EXISTS public_security_filing_url TEXT NOT NULL DEFAULT '',
|
|
ADD COLUMN IF NOT EXISTS log_retention_days INTEGER NOT NULL DEFAULT 30;
|
|
|
|
ALTER TABLE generation_jobs
|
|
ADD COLUMN IF NOT EXISTS user_id UUID,
|
|
ADD COLUMN IF NOT EXISTS provider VARCHAR(128),
|
|
ADD COLUMN IF NOT EXISTS model_name VARCHAR(255),
|
|
ADD COLUMN IF NOT EXISTS api_url TEXT,
|
|
ADD COLUMN IF NOT EXISTS progress JSONB NOT NULL DEFAULT '{}'::jsonb;
|
|
|
|
CREATE INDEX IF NOT EXISTS generation_jobs_user_created_idx ON generation_jobs (user_id, created_at DESC);
|
|
CREATE INDEX IF NOT EXISTS generation_jobs_provider_model_created_idx ON generation_jobs (type, provider, model_name, created_at DESC);
|
|
|
|
CREATE TABLE IF NOT EXISTS system_api_configs (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
provider VARCHAR(128),
|
|
name VARCHAR(255) NOT NULL,
|
|
api_url TEXT NOT NULL DEFAULT '',
|
|
model_name VARCHAR(255) NOT NULL,
|
|
note TEXT NOT NULL DEFAULT '',
|
|
api_key_encrypted TEXT NOT NULL DEFAULT '',
|
|
api_key_preview VARCHAR(64) NOT NULL DEFAULT '',
|
|
type VARCHAR(16) NOT NULL DEFAULT 'image',
|
|
credits_per_use INTEGER NOT NULL DEFAULT 10,
|
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS system_api_configs_active_type_sort_idx ON system_api_configs (is_active, type, sort_order);
|
|
|
|
CREATE TABLE IF NOT EXISTS payment_methods (
|
|
id VARCHAR(64) PRIMARY KEY,
|
|
type VARCHAR(32) NOT NULL,
|
|
name VARCHAR(128) NOT NULL,
|
|
is_active BOOLEAN NOT NULL DEFAULT FALSE,
|
|
public_config JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
secret_config_encrypted JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
secret_config_preview JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ
|
|
);
|
|
|
|
INSERT INTO payment_methods (id, type, name, is_active) VALUES
|
|
('pm-alipay', 'alipay', '支付宝', true),
|
|
('pm-wechat', 'wechat', '微信支付', false),
|
|
('pm-manual', 'manual', '手动转账', false),
|
|
('pm-stripe', 'stripe', 'Stripe', false)
|
|
ON CONFLICT (id) DO NOTHING;
|
|
|
|
CREATE INDEX IF NOT EXISTS platform_logs_user_name_idx ON platform_logs (LOWER(COALESCE(user_name, '')));
|
|
CREATE INDEX IF NOT EXISTS platform_logs_user_email_idx ON platform_logs (LOWER(COALESCE(user_email, '')));
|
|
|
|
ALTER TABLE announcements
|
|
ADD COLUMN IF NOT EXISTS type VARCHAR(32) NOT NULL DEFAULT 'site';
|
|
|
|
ALTER TABLE platform_log_settings ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE platform_logs ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE api_providers ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE model_recommendations ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE system_api_configs ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE payment_methods ENABLE ROW LEVEL SECURITY;
|
|
|
|
-- ============================================================
|
|
-- Row Level Security (RLS) 策略
|
|
-- ============================================================
|
|
|
|
-- 启用所有表的 RLS
|
|
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE works ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE credit_transactions ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE user_api_keys ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE work_likes ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE site_config ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE announcements ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE site_stats ENABLE ROW LEVEL SECURITY;
|
|
|
|
DROP POLICY IF EXISTS "profiles_read_own" ON profiles;
|
|
DROP POLICY IF EXISTS "profiles_update_own" ON profiles;
|
|
DROP POLICY IF EXISTS "profiles_admin_all" ON profiles;
|
|
DROP POLICY IF EXISTS "works_read_public" ON works;
|
|
DROP POLICY IF EXISTS "works_insert_own" ON works;
|
|
DROP POLICY IF EXISTS "works_update_own" ON works;
|
|
DROP POLICY IF EXISTS "works_delete_own" ON works;
|
|
DROP POLICY IF EXISTS "works_admin_all" ON works;
|
|
DROP POLICY IF EXISTS "credit_transactions_read_own" ON credit_transactions;
|
|
DROP POLICY IF EXISTS "credit_transactions_admin_all" ON credit_transactions;
|
|
DROP POLICY IF EXISTS "orders_read_own" ON orders;
|
|
DROP POLICY IF EXISTS "orders_insert_own" ON orders;
|
|
DROP POLICY IF EXISTS "orders_admin_all" ON orders;
|
|
DROP POLICY IF EXISTS "user_api_keys_read_own" ON user_api_keys;
|
|
DROP POLICY IF EXISTS "user_api_keys_insert_own" ON user_api_keys;
|
|
DROP POLICY IF EXISTS "user_api_keys_update_own" ON user_api_keys;
|
|
DROP POLICY IF EXISTS "user_api_keys_delete_own" ON user_api_keys;
|
|
DROP POLICY IF EXISTS "work_likes_read_all" ON work_likes;
|
|
DROP POLICY IF EXISTS "work_likes_insert_own" ON work_likes;
|
|
DROP POLICY IF EXISTS "work_likes_delete_own" ON work_likes;
|
|
DROP POLICY IF EXISTS "site_config_read_all" ON site_config;
|
|
DROP POLICY IF EXISTS "site_config_write_auth" ON site_config;
|
|
DROP POLICY IF EXISTS "site_config_admin_write" ON site_config;
|
|
DROP POLICY IF EXISTS "announcements_read_all" ON announcements;
|
|
DROP POLICY IF EXISTS "announcements_write_auth" ON announcements;
|
|
DROP POLICY IF EXISTS "announcements_admin_write" ON announcements;
|
|
DROP POLICY IF EXISTS "site_stats_read_all" ON site_stats;
|
|
DROP POLICY IF EXISTS "site_stats_write_auth" ON site_stats;
|
|
|
|
-- profiles: 用户可读自己的资料,管理员可读写所有
|
|
CREATE POLICY "profiles_read_own" ON profiles FOR SELECT USING (auth.uid() = id);
|
|
CREATE POLICY "profiles_update_own" ON profiles FOR UPDATE USING (auth.uid() = id);
|
|
CREATE POLICY "profiles_admin_all" ON profiles FOR ALL USING (
|
|
EXISTS (SELECT 1 FROM profiles WHERE id = auth.uid() AND role = 'admin')
|
|
) WITH CHECK (
|
|
EXISTS (SELECT 1 FROM profiles WHERE id = auth.uid() AND role = 'admin')
|
|
);
|
|
|
|
-- works: 用户可管理自己的作品,公开作品所有人可读
|
|
CREATE POLICY "works_read_public" ON works FOR SELECT USING (is_public = true OR auth.uid() = user_id);
|
|
CREATE POLICY "works_insert_own" ON works FOR INSERT WITH CHECK (auth.uid() = user_id);
|
|
CREATE POLICY "works_update_own" ON works FOR UPDATE USING (auth.uid() = user_id);
|
|
CREATE POLICY "works_delete_own" ON works FOR DELETE USING (auth.uid() = user_id);
|
|
CREATE POLICY "works_admin_all" ON works FOR ALL USING (
|
|
EXISTS (SELECT 1 FROM profiles WHERE id = auth.uid() AND role = 'admin')
|
|
) WITH CHECK (
|
|
EXISTS (SELECT 1 FROM profiles WHERE id = auth.uid() AND role = 'admin')
|
|
);
|
|
|
|
-- credit_transactions: 用户可读自己的记录
|
|
CREATE POLICY "credit_transactions_read_own" ON credit_transactions FOR SELECT USING (auth.uid() = user_id);
|
|
CREATE POLICY "credit_transactions_admin_all" ON credit_transactions FOR ALL USING (
|
|
EXISTS (SELECT 1 FROM profiles WHERE id = auth.uid() AND role = 'admin')
|
|
) WITH CHECK (
|
|
EXISTS (SELECT 1 FROM profiles WHERE id = auth.uid() AND role = 'admin')
|
|
);
|
|
|
|
-- orders: 用户可读自己的订单
|
|
CREATE POLICY "orders_read_own" ON orders FOR SELECT USING (auth.uid() = user_id);
|
|
CREATE POLICY "orders_insert_own" ON orders FOR INSERT WITH CHECK (auth.uid() = user_id);
|
|
CREATE POLICY "orders_admin_all" ON orders FOR ALL USING (
|
|
EXISTS (SELECT 1 FROM profiles WHERE id = auth.uid() AND role = 'admin')
|
|
) WITH CHECK (
|
|
EXISTS (SELECT 1 FROM profiles WHERE id = auth.uid() AND role = 'admin')
|
|
);
|
|
|
|
-- user_api_keys: 用户可管理自己的密钥
|
|
CREATE POLICY "user_api_keys_read_own" ON user_api_keys FOR SELECT USING (auth.uid() = user_id);
|
|
CREATE POLICY "user_api_keys_insert_own" ON user_api_keys FOR INSERT WITH CHECK (auth.uid() = user_id);
|
|
CREATE POLICY "user_api_keys_update_own" ON user_api_keys FOR UPDATE USING (auth.uid() = user_id);
|
|
CREATE POLICY "user_api_keys_delete_own" ON user_api_keys FOR DELETE USING (auth.uid() = user_id);
|
|
|
|
-- work_likes: 认证用户可点赞,所有人可读
|
|
CREATE POLICY "work_likes_read_all" ON work_likes FOR SELECT USING (true);
|
|
CREATE POLICY "work_likes_insert_own" ON work_likes FOR INSERT WITH CHECK (auth.uid() = user_id);
|
|
CREATE POLICY "work_likes_delete_own" ON work_likes FOR DELETE USING (auth.uid() = user_id);
|
|
|
|
-- site_config: 所有人可读,认证用户可写 (管理员操作通过 service role key)
|
|
CREATE POLICY "site_config_read_all" ON site_config FOR SELECT USING (true);
|
|
CREATE POLICY "site_config_admin_write" ON site_config FOR ALL USING (
|
|
EXISTS (SELECT 1 FROM profiles WHERE id = auth.uid() AND role = 'admin')
|
|
) WITH CHECK (
|
|
EXISTS (SELECT 1 FROM profiles WHERE id = auth.uid() AND role = 'admin')
|
|
);
|
|
|
|
-- announcements: 所有人可读,认证用户可写 (管理员操作)
|
|
CREATE POLICY "announcements_read_all" ON announcements FOR SELECT USING (true);
|
|
CREATE POLICY "announcements_admin_write" ON announcements FOR ALL USING (
|
|
EXISTS (SELECT 1 FROM profiles WHERE id = auth.uid() AND role = 'admin')
|
|
) WITH CHECK (
|
|
EXISTS (SELECT 1 FROM profiles WHERE id = auth.uid() AND role = 'admin')
|
|
);
|
|
|
|
-- site_stats: 公开读,访问量递增走 SECURITY DEFINER 函数
|
|
CREATE POLICY "site_stats_read_all" ON site_stats FOR SELECT USING (true);
|
|
|
|
-- ============================================================
|
|
-- Supabase Storage 桶 (通过 Supabase Dashboard 或 API 创建)
|
|
-- ============================================================
|
|
-- 需要在 Supabase Dashboard 中手动创建以下 Storage 桶:
|
|
-- 1. site-assets (公开读) — 存放网站 Logo、Favicon
|
|
-- 2. works (私有) — 存放用户生成的图片/视频文件
|
|
--
|
|
-- 或者通过 SQL (需要 service_role 权限):
|
|
-- INSERT INTO storage.buckets (id, name, public) VALUES ('site-assets', 'site-assets', true) ON CONFLICT DO NOTHING;
|
|
-- INSERT INTO storage.buckets (id, name, public) VALUES ('works', 'works', false) ON CONFLICT DO NOTHING;
|
|
|
|
-- ============================================================
|
|
-- 触发器: 自动更新 updated_at 字段
|
|
-- ============================================================
|
|
CREATE OR REPLACE FUNCTION update_updated_at()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
NEW.updated_at = now();
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
DROP TRIGGER IF EXISTS profiles_updated_at ON profiles;
|
|
DROP TRIGGER IF EXISTS works_updated_at ON works;
|
|
DROP TRIGGER IF EXISTS orders_updated_at ON orders;
|
|
DROP TRIGGER IF EXISTS user_api_keys_updated_at ON user_api_keys;
|
|
DROP TRIGGER IF EXISTS site_config_updated_at ON site_config;
|
|
DROP TRIGGER IF EXISTS announcements_updated_at ON announcements;
|
|
|
|
CREATE TRIGGER profiles_updated_at BEFORE UPDATE ON profiles FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
|
CREATE TRIGGER works_updated_at BEFORE UPDATE ON works FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
|
CREATE TRIGGER orders_updated_at BEFORE UPDATE ON orders FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
|
CREATE TRIGGER user_api_keys_updated_at BEFORE UPDATE ON user_api_keys FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
|
CREATE TRIGGER site_config_updated_at BEFORE UPDATE ON site_config FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
|
CREATE TRIGGER announcements_updated_at BEFORE UPDATE ON announcements FOR EACH ROW EXECUTE FUNCTION update_updated_at();
|
|
|
|
-- ============================================================
|
|
-- 触发器: 新用户注册时自动创建 profile
|
|
-- (仅在使用 Supabase Auth 时生效)
|
|
-- ============================================================
|
|
CREATE OR REPLACE FUNCTION handle_new_user()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
INSERT INTO profiles (id, email, nickname, role, membership_tier, credits_balance, daily_quota_limit)
|
|
VALUES (
|
|
NEW.id,
|
|
NEW.email,
|
|
COALESCE(NEW.raw_user_meta_data->>'nickname', split_part(NEW.email, '@', 1)),
|
|
'user',
|
|
'free',
|
|
10, -- 新用户赠送 10 积分
|
|
5 -- 每日配额 5 次
|
|
)
|
|
ON CONFLICT (id) DO NOTHING;
|
|
-- 记录注册赠送积分
|
|
INSERT INTO credit_transactions (user_id, amount, balance_after, type, description)
|
|
SELECT NEW.id, 10, 10, 'gift', '新用户注册奖励'
|
|
WHERE NOT EXISTS (
|
|
SELECT 1 FROM credit_transactions
|
|
WHERE user_id = NEW.id AND type = 'gift' AND description = '新用户注册奖励'
|
|
);
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
|
|
DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users;
|
|
|
|
CREATE TRIGGER on_auth_user_created
|
|
AFTER INSERT ON auth.users
|
|
FOR EACH ROW EXECUTE FUNCTION handle_new_user();
|
|
|
|
-- ============================================================
|
|
-- 初始化管理员账户 (可选)
|
|
-- 请在注册管理员后,手动执行以下 SQL 将角色设为 admin:
|
|
-- UPDATE profiles SET role = 'admin' WHERE email = 'your-admin@example.com';
|
|
-- ============================================================
|
|
|
|
-- ============================================================
|
|
-- 原子递增访问量的 SQL 函数
|
|
-- ============================================================
|
|
CREATE OR REPLACE FUNCTION increment_visits()
|
|
RETURNS BIGINT AS $$
|
|
DECLARE
|
|
new_count BIGINT;
|
|
BEGIN
|
|
UPDATE site_stats SET total_visits = total_visits + 1, updated_at = now() WHERE id = 1
|
|
RETURNING total_visits INTO new_count;
|
|
RETURN new_count;
|
|
END;
|
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
|
|
-- 完成
|
|
SELECT 'Database initialization completed successfully!' AS status;
|