Initial miaojingAI project with image resolution guard

This commit is contained in:
FengLee
2026-05-09 11:32:34 +08:00
commit d499020d4e
264 changed files with 54160 additions and 0 deletions

663
scripts/init-database.sql Normal file
View File

@@ -0,0 +1,663 @@
-- ============================================================
-- 妙境 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;