Files
raisfast/migrations/postgres/schema.postgres.sql
T

582 lines
24 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- ============================================================
-- raisfast 完整数据库 Schema — PostgreSQL(BUILTIN_TENANTABLE=false 默认模式)
-- 由所有 migration 文件合并而成,用于新部署一键初始化
-- 生成日期:2026-05-07
--
-- 注意:此 schema 不含 tenant_id 列。
-- 若需多租户支持,设置 BUILTIN_TENANTABLE=true 后迁移 026 会自动添加。
-- ============================================================
-- ── 平台基础层(永不禁用) ──────────────────────────────────
-- 用户
CREATE TABLE IF NOT EXISTS users (
id BIGSERIAL PRIMARY KEY,
document_id VARCHAR(36) NOT NULL UNIQUE,
email VARCHAR(255) UNIQUE NOT NULL,
username VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
role VARCHAR(50) NOT NULL DEFAULT 'reader',
avatar VARCHAR(500),
bio TEXT,
website VARCHAR(500),
phone VARCHAR(50),
email_verified BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_phone ON users(phone) WHERE phone IS NOT NULL;
-- Refresh Tokens
CREATE TABLE IF NOT EXISTS refresh_tokens (
id BIGSERIAL PRIMARY KEY,
document_id VARCHAR(36) NOT NULL UNIQUE,
user_id BIGINT NOT NULL REFERENCES users(id),
token VARCHAR(500) UNIQUE NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user ON refresh_tokens(user_id);
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_token ON refresh_tokens(token);
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_expires_at ON refresh_tokens(expires_at);
-- 站点配置
CREATE TABLE IF NOT EXISTS options (
id BIGSERIAL PRIMARY KEY,
document_id VARCHAR(36) NOT NULL UNIQUE,
option_key VARCHAR(255) NOT NULL,
value TEXT NOT NULL,
type VARCHAR(50) NOT NULL DEFAULT 'text',
group_name VARCHAR(100) NOT NULL DEFAULT 'general',
label VARCHAR(255) NOT NULL DEFAULT '',
description TEXT,
validation JSONB,
is_public BOOLEAN NOT NULL DEFAULT FALSE,
autoload BOOLEAN NOT NULL DEFAULT TRUE,
sort_order INTEGER NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(option_key)
);
-- RBAC 角色
CREATE TABLE IF NOT EXISTS roles (
id BIGSERIAL PRIMARY KEY,
document_id VARCHAR(36) NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL UNIQUE,
description TEXT,
is_system BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- RBAC 权限
CREATE TABLE IF NOT EXISTS permissions (
id BIGSERIAL PRIMARY KEY,
document_id VARCHAR(36) NOT NULL UNIQUE,
role_id BIGINT NOT NULL REFERENCES roles(id),
action VARCHAR(255) NOT NULL,
subject VARCHAR(255) NOT NULL,
fields JSONB,
conditions JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_permissions_role_action_subject
ON permissions(role_id, action, subject);
-- 租户
CREATE TABLE IF NOT EXISTS tenants (
id BIGSERIAL PRIMARY KEY,
document_id VARCHAR(36) NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL,
domain VARCHAR(255) UNIQUE,
config JSONB NOT NULL DEFAULT '{}',
status VARCHAR(50) NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 审计日志
CREATE TABLE IF NOT EXISTS audit_log (
id BIGSERIAL PRIMARY KEY,
document_id VARCHAR(36) NOT NULL UNIQUE,
actor_id BIGINT,
actor_role VARCHAR(50),
action VARCHAR(255) NOT NULL,
subject VARCHAR(255) NOT NULL,
subject_id VARCHAR(36),
detail TEXT,
ip_address VARCHAR(45),
user_agent TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_audit_log_action ON audit_log(action);
CREATE INDEX IF NOT EXISTS idx_audit_log_actor ON audit_log(actor_id);
CREATE INDEX IF NOT EXISTS idx_audit_log_created ON audit_log(created_at);
-- API Token
CREATE TABLE IF NOT EXISTS api_tokens (
id BIGSERIAL PRIMARY KEY,
document_id VARCHAR(36) NOT NULL UNIQUE,
user_id BIGINT NOT NULL REFERENCES users(id),
name VARCHAR(255) NOT NULL,
token_hash VARCHAR(255) UNIQUE NOT NULL,
token_prefix VARCHAR(50) NOT NULL,
scopes JSONB NOT NULL DEFAULT '["read","write"]',
last_used_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_api_tokens_user_id ON api_tokens(user_id);
CREATE INDEX IF NOT EXISTS idx_api_tokens_token_hash ON api_tokens(token_hash);
-- Webhook 订阅
CREATE TABLE IF NOT EXISTS webhook_subscriptions (
id BIGSERIAL PRIMARY KEY,
document_id VARCHAR(36) NOT NULL UNIQUE,
url VARCHAR(1024) NOT NULL,
secret VARCHAR(255) NOT NULL,
events JSONB NOT NULL DEFAULT '[]',
enabled BOOLEAN NOT NULL DEFAULT TRUE,
description TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_webhook_subscriptions_enabled ON webhook_subscriptions(enabled);
-- 插件 KV 存储
CREATE TABLE IF NOT EXISTS plugin_storage (
plugin_id VARCHAR(100) NOT NULL,
storage_key VARCHAR(255) NOT NULL,
value TEXT NOT NULL,
expires_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (plugin_id, storage_key)
);
CREATE INDEX IF NOT EXISTS idx_plugin_storage_plugin ON plugin_storage(plugin_id);
-- 内容版本历史
CREATE TABLE IF NOT EXISTS content_revisions (
id BIGSERIAL PRIMARY KEY,
document_id VARCHAR(36) NOT NULL UNIQUE,
content_type VARCHAR(100) NOT NULL,
record_id VARCHAR(36) NOT NULL,
revision_number INTEGER NOT NULL,
snapshot JSONB NOT NULL,
created_by BIGINT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(content_type, record_id, revision_number)
);
CREATE INDEX IF NOT EXISTS idx_revisions_ct_record
ON content_revisions(content_type, record_id);
CREATE INDEX IF NOT EXISTS idx_revisions_ct_record_rev
ON content_revisions(content_type, record_id, revision_number DESC);
-- OAuth 账号绑定
CREATE TABLE IF NOT EXISTS oauth_accounts (
id BIGSERIAL PRIMARY KEY,
document_id VARCHAR(36) NOT NULL UNIQUE,
user_id BIGINT NOT NULL REFERENCES users(id),
provider VARCHAR(50) NOT NULL,
provider_user_id VARCHAR(255) NOT NULL,
email VARCHAR(255),
display_name VARCHAR(255),
avatar_url VARCHAR(500),
access_token VARCHAR(1024),
refresh_token VARCHAR(1024),
token_expires_at TIMESTAMPTZ,
profile JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(provider, provider_user_id)
);
CREATE INDEX IF NOT EXISTS idx_oauth_accounts_user ON oauth_accounts(user_id);
CREATE INDEX IF NOT EXISTS idx_oauth_accounts_provider ON oauth_accounts(provider, provider_user_id);
-- OAuth 短期 state 存储(PKCE)
CREATE TABLE IF NOT EXISTS oauth_states (
id BIGSERIAL PRIMARY KEY,
document_id VARCHAR(36) NOT NULL UNIQUE,
provider VARCHAR(50) NOT NULL,
code_verifier VARCHAR(255) NOT NULL,
user_id BIGINT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_oauth_states_expires ON oauth_states(expires_at);
-- 密码重置令牌
CREATE TABLE IF NOT EXISTS password_reset_tokens (
id BIGSERIAL PRIMARY KEY,
document_id VARCHAR(36) NOT NULL UNIQUE,
user_id BIGINT NOT NULL REFERENCES users(id),
token VARCHAR(255) NOT NULL UNIQUE,
expires_at TIMESTAMPTZ NOT NULL,
used_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_token ON password_reset_tokens(token);
CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_user_id ON password_reset_tokens(user_id);
CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_expires_at ON password_reset_tokens(expires_at);
-- 短信验证码
CREATE TABLE IF NOT EXISTS sms_codes (
id BIGSERIAL PRIMARY KEY,
document_id VARCHAR(36) NOT NULL UNIQUE,
phone VARCHAR(50) NOT NULL,
code VARCHAR(20) NOT NULL,
purpose VARCHAR(50) NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
verified_at TIMESTAMPTZ,
attempts INTEGER NOT NULL DEFAULT 0,
ip_address VARCHAR(45),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_sms_codes_phone ON sms_codes(phone);
CREATE INDEX IF NOT EXISTS idx_sms_codes_expires ON sms_codes(expires_at);
-- 邮箱验证令牌
CREATE TABLE IF NOT EXISTS email_verification_tokens (
id BIGSERIAL PRIMARY KEY,
document_id VARCHAR(36) NOT NULL UNIQUE,
user_id BIGINT NOT NULL REFERENCES users(id),
token VARCHAR(255) NOT NULL UNIQUE,
email VARCHAR(255) NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
verified_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_email_verification_tokens_token ON email_verification_tokens(token);
CREATE INDEX IF NOT EXISTS idx_email_verification_tokens_user_id ON email_verification_tokens(user_id);
CREATE INDEX IF NOT EXISTS idx_email_verification_tokens_expires ON email_verification_tokens(expires_at);
-- 后台任务队列
CREATE TABLE IF NOT EXISTS jobs (
id BIGSERIAL PRIMARY KEY,
document_id VARCHAR(36) NOT NULL UNIQUE,
job_type VARCHAR(100) NOT NULL,
payload TEXT NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'pending',
attempts INTEGER NOT NULL DEFAULT 0,
max_attempts INTEGER NOT NULL DEFAULT 3,
run_after TIMESTAMPTZ,
error TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status);
CREATE INDEX IF NOT EXISTS idx_jobs_run_after ON jobs(run_after) WHERE status = 'pending';
CREATE INDEX IF NOT EXISTS idx_jobs_type ON jobs(job_type);
-- 定时任务调度
CREATE TABLE IF NOT EXISTS cron_schedules (
id BIGSERIAL PRIMARY KEY,
document_id VARCHAR(36) NOT NULL UNIQUE,
label VARCHAR(255) NOT NULL,
job_type VARCHAR(100) NOT NULL,
payload TEXT,
cron_expr VARCHAR(100) NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
last_run_at TIMESTAMPTZ,
next_run_at TIMESTAMPTZ NOT NULL,
plugin_id VARCHAR(100),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_cron_enabled ON cron_schedules(enabled);
CREATE INDEX IF NOT EXISTS idx_cron_next_run ON cron_schedules(next_run_at) WHERE enabled = TRUE;
CREATE INDEX IF NOT EXISTS idx_cron_plugin ON cron_schedules(plugin_id);
-- Cron 执行历史
CREATE TABLE IF NOT EXISTS cron_execution_log (
id BIGSERIAL PRIMARY KEY,
document_id VARCHAR(36) NOT NULL UNIQUE,
schedule_id BIGINT NOT NULL,
job_type VARCHAR(100) NOT NULL,
label VARCHAR(255) NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'running',
duration_ms INTEGER,
error TEXT,
started_at TIMESTAMPTZ NOT NULL,
finished_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_cron_log_schedule ON cron_execution_log(schedule_id);
CREATE INDEX IF NOT EXISTS idx_cron_log_status ON cron_execution_log(status);
CREATE INDEX IF NOT EXISTS idx_cron_log_started ON cron_execution_log(started_at);
-- ── 内置模块:Blog(BUILTIN_BLOG=true) ──────────────────
-- 分类
CREATE TABLE IF NOT EXISTS categories (
id BIGSERIAL PRIMARY KEY,
document_id VARCHAR(36) NOT NULL UNIQUE,
name VARCHAR(255) UNIQUE NOT NULL,
slug VARCHAR(255) UNIQUE NOT NULL,
description TEXT,
parent_id BIGINT REFERENCES categories(id),
sort_order INTEGER NOT NULL DEFAULT 0,
created_by BIGINT,
updated_by BIGINT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 标签
CREATE TABLE IF NOT EXISTS tags (
id BIGSERIAL PRIMARY KEY,
document_id VARCHAR(36) NOT NULL UNIQUE,
name VARCHAR(255) UNIQUE NOT NULL,
slug VARCHAR(255) UNIQUE NOT NULL,
created_by BIGINT,
updated_by BIGINT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 文章
CREATE TABLE IF NOT EXISTS posts (
id BIGSERIAL PRIMARY KEY,
document_id VARCHAR(36) NOT NULL UNIQUE,
title TEXT NOT NULL,
slug VARCHAR(255) UNIQUE NOT NULL,
content TEXT NOT NULL,
excerpt TEXT,
cover_image VARCHAR(500),
status VARCHAR(50) NOT NULL DEFAULT 'draft',
created_by BIGINT NOT NULL REFERENCES users(id),
updated_by BIGINT,
category_id BIGINT REFERENCES categories(id),
view_count INTEGER NOT NULL DEFAULT 0,
is_pinned BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
published_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_posts_slug ON posts(slug);
CREATE INDEX IF NOT EXISTS idx_posts_status ON posts(status);
CREATE INDEX IF NOT EXISTS idx_posts_author ON posts(created_by);
CREATE INDEX IF NOT EXISTS idx_posts_category ON posts(category_id);
CREATE INDEX IF NOT EXISTS idx_posts_created ON posts(created_at);
CREATE INDEX IF NOT EXISTS idx_posts_status_created
ON posts(status, is_pinned DESC, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_posts_status_category
ON posts(status, category_id);
CREATE INDEX IF NOT EXISTS idx_posts_status_author
ON posts(status, created_by);
-- 文章-标签(多对多)
CREATE TABLE IF NOT EXISTS posts_tags (
post_id BIGINT NOT NULL REFERENCES posts(id),
tag_id BIGINT NOT NULL REFERENCES tags(id),
PRIMARY KEY (post_id, tag_id)
);
CREATE INDEX IF NOT EXISTS idx_posts_tags_tag_id ON posts_tags(tag_id);
-- 评论
CREATE TABLE IF NOT EXISTS comments (
id BIGSERIAL PRIMARY KEY,
document_id VARCHAR(36) NOT NULL UNIQUE,
post_id BIGINT NOT NULL REFERENCES posts(id),
created_by BIGINT REFERENCES users(id),
updated_by BIGINT,
nickname VARCHAR(100),
email VARCHAR(255),
content TEXT NOT NULL,
parent_id BIGINT REFERENCES comments(id),
status VARCHAR(50) NOT NULL DEFAULT 'pending',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_comments_post ON comments(post_id);
CREATE INDEX IF NOT EXISTS idx_comments_status ON comments(status);
CREATE INDEX IF NOT EXISTS idx_comments_post_status
ON comments(post_id, status);
CREATE INDEX IF NOT EXISTS idx_comments_parent_id
ON comments(parent_id);
-- ── 内置模块:Pages(BUILTIN_PAGES=true) ────────────────
CREATE TABLE IF NOT EXISTS pages (
id BIGSERIAL PRIMARY KEY,
document_id VARCHAR(36) NOT NULL UNIQUE,
title TEXT NOT NULL,
slug VARCHAR(255) NOT NULL UNIQUE,
content TEXT,
blocks JSONB,
meta_title VARCHAR(255),
meta_description VARCHAR(500),
og_image VARCHAR(500),
template VARCHAR(100) NOT NULL DEFAULT 'default',
parent_id BIGINT REFERENCES pages(id),
sort_order INTEGER NOT NULL DEFAULT 0,
status VARCHAR(50) NOT NULL DEFAULT 'draft',
created_by BIGINT NOT NULL REFERENCES users(id),
updated_by BIGINT,
cover_image VARCHAR(500),
published_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_pages_slug ON pages(slug);
CREATE INDEX IF NOT EXISTS idx_pages_status ON pages(status);
CREATE INDEX IF NOT EXISTS idx_pages_parent ON pages(parent_id);
CREATE INDEX IF NOT EXISTS idx_pages_author ON pages(created_by);
CREATE TABLE IF NOT EXISTS reusable_blocks (
id BIGSERIAL PRIMARY KEY,
document_id VARCHAR(36) NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL,
block_type TEXT NOT NULL,
content TEXT NOT NULL,
description TEXT,
created_by BIGINT,
updated_by BIGINT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ── 内置模块:Media(BUILTIN_MEDIA=true) ────────────────
CREATE TABLE IF NOT EXISTS media (
id BIGSERIAL PRIMARY KEY,
document_id VARCHAR(36) NOT NULL UNIQUE,
user_id BIGINT NOT NULL REFERENCES users(id),
filename VARCHAR(255) NOT NULL,
filepath VARCHAR(500) NOT NULL,
mimetype VARCHAR(100) NOT NULL,
size INTEGER NOT NULL,
width INTEGER,
height INTEGER,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_media_user_created
ON media(user_id, created_at DESC);
-- ── 内置模块:Workflow(BUILTIN_WORKFLOW=true) ──────────
CREATE TABLE IF NOT EXISTS workflow_definitions (
id BIGSERIAL PRIMARY KEY,
document_id VARCHAR(36) NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL,
description TEXT,
steps JSONB NOT NULL,
initial_step VARCHAR(100) NOT NULL,
version INTEGER NOT NULL DEFAULT 1,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS workflow_instances (
id BIGSERIAL PRIMARY KEY,
document_id VARCHAR(36) NOT NULL UNIQUE,
definition_id BIGINT NOT NULL REFERENCES workflow_definitions(id),
status VARCHAR(50) NOT NULL DEFAULT 'running',
current_step VARCHAR(100),
context JSONB NOT NULL DEFAULT '{}',
triggered_by BIGINT,
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
completed_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_wf_instances_definition ON workflow_instances(definition_id);
CREATE INDEX IF NOT EXISTS idx_wf_instances_status ON workflow_instances(status);
CREATE TABLE IF NOT EXISTS workflow_step_logs (
id BIGSERIAL PRIMARY KEY,
document_id VARCHAR(36) NOT NULL UNIQUE,
instance_id BIGINT NOT NULL REFERENCES workflow_instances(id),
step_id VARCHAR(100) NOT NULL,
step_name VARCHAR(255) NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'running',
input TEXT,
output TEXT,
error TEXT,
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
completed_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_wf_step_logs_instance ON workflow_step_logs(instance_id);
-- ============================================================
-- 预置数据
-- ============================================================
-- 默认租户
INSERT INTO tenants (document_id, name, domain, config, status, created_at, updated_at) VALUES
('default', 'Default', NULL, '{}', 'active', NOW(), NOW())
ON CONFLICT (document_id) DO NOTHING;
-- 系统角色
INSERT INTO roles (document_id, name, description, is_system, created_at, updated_at) VALUES
('role-admin', 'admin', '超级管理员', TRUE, NOW(), NOW()),
('role-editor', 'editor', '编辑', FALSE, NOW(), NOW()),
('role-author', 'author', '作者', FALSE, NOW(), NOW()),
('role-reader', 'reader', '读者', TRUE, NOW(), NOW())
ON CONFLICT (document_id) DO NOTHING;
-- admin 全局权限
INSERT INTO permissions (document_id, role_id, action, subject, fields, conditions, created_at) VALUES
('perm-admin-all', (SELECT id FROM roles WHERE document_id = 'role-admin'), '*', '*', '["*"]', NULL, NOW())
ON CONFLICT (document_id) DO NOTHING;
-- editor 权限
INSERT INTO permissions (document_id, role_id, action, subject, fields, conditions, created_at) VALUES
('perm-editor-ct-create', (SELECT id FROM roles WHERE document_id = 'role-editor'), 'content-type::*.*', 'content-type::*', '["*"]', NULL, NOW())
ON CONFLICT (document_id) DO NOTHING;
-- author 权限
INSERT INTO permissions (document_id, role_id, action, subject, fields, conditions, created_at) VALUES
('perm-author-post-create', (SELECT id FROM roles WHERE document_id = 'role-author'), 'content-type::post.create', 'content-type::post', '["*"]', NULL, NOW()),
('perm-author-post-read', (SELECT id FROM roles WHERE document_id = 'role-author'), 'content-type::post.read', 'content-type::post', '["*"]', NULL, NOW()),
('perm-author-post-update', (SELECT id FROM roles WHERE document_id = 'role-author'), 'content-type::post.update', 'content-type::post', '["*"]', '{"author_id":"$user.id"}', NOW()),
('perm-author-post-delete', (SELECT id FROM roles WHERE document_id = 'role-author'), 'content-type::post.delete', 'content-type::post', '["*"]', '{"author_id":"$user.id"}', NOW())
ON CONFLICT (document_id) DO NOTHING;
-- reader 权限
INSERT INTO permissions (document_id, role_id, action, subject, fields, conditions, created_at) VALUES
('perm-reader-post-read', (SELECT id FROM roles WHERE document_id = 'role-reader'), 'content-type::post.read', 'content-type::post', '["title","slug","content","excerpt","status"]', NULL, NOW()),
('perm-reader-comment-create', (SELECT id FROM roles WHERE document_id = 'role-reader'), 'content-type::comment.create', 'content-type::comment', '["content","nickname","email"]', NULL, NOW())
ON CONFLICT (document_id) DO NOTHING;
-- 站点配置
INSERT INTO options (document_id, option_key, value, type, group_name, label, description, validation, is_public, autoload, sort_order, updated_at) VALUES
('opt-site-title', 'site_title', '"My Blog"', 'text', 'general', '站点标题', '显示在浏览器标题栏和页面头部', '{"max_length":100}', TRUE, TRUE, 1, NOW()),
('opt-site-desc', 'site_description', '""', 'text', 'general', '站点描述', '简短描述站点用途', '{"max_length":500}', TRUE, TRUE, 2, NOW()),
('opt-site-url', 'site_url', '""', 'url', 'general', '站点 URL', '如 https://example.com', NULL, TRUE, TRUE, 3, NOW()),
('opt-admin-email', 'admin_email', '""', 'email', 'general', '管理员邮箱', NULL, NULL, FALSE, TRUE, 4, NOW()),
('opt-timezone', 'timezone', '"UTC"', 'select', 'general', '时区', NULL, '{"values":["UTC","Asia/Shanghai","Asia/Tokyo","US/Eastern","US/Pacific","Europe/London","Europe/Berlin"]}', TRUE, TRUE, 5, NOW()),
('opt-date-fmt', 'date_format', '"%Y-%m-%d"', 'select', 'general', '日期格式', NULL, '{"values":["%Y-%m-%d","%d/%m/%Y","%m/%d/%Y","%Y年%m月%d日"]}', TRUE, TRUE, 6, NOW()),
('opt-per-page', 'posts_per_page', '10', 'integer', 'reading', '每页文章数', NULL, '{"min":1,"max":100}', TRUE, TRUE, 10, NOW()),
('opt-rss-items', 'rss_items', '20', 'integer', 'reading', 'RSS 条目数', NULL, '{"min":1,"max":100}', TRUE, TRUE, 11, NOW()),
('opt-permalink', 'permalink_structure', '"/:year/:month/:slug"', 'select', 'reading', 'URL 结构', NULL, '{"values":["/:year/:month/:slug","/:slug","/posts/:slug"]}', TRUE, TRUE, 12, NOW()),
('opt-comment-mod', 'comment_moderation', 'true', 'boolean', 'discussion', '评论需审核', '开启后新评论需管理员审批', NULL, FALSE, TRUE, 20, NOW()),
('opt-comment-order', 'comment_order', '"asc"', 'select', 'discussion', '评论排序', NULL, '{"values":["asc","desc"]}', TRUE, TRUE, 21, NOW()),
('opt-default-role', 'default_role', '"reader"', 'select', 'discussion', '新用户默认角色', NULL, '{"values":["reader","author"]}', FALSE, TRUE, 22, NOW()),
('opt-theme', 'theme', '"default"', 'select', 'appearance', '当前主题', NULL, '{"values":["default","corporate","minimal","warm"]}', TRUE, TRUE, 30, NOW()),
('opt-maintenance', 'maintenance_mode', 'false', 'boolean', 'appearance', '维护模式', '开启后前台显示维护页面', NULL, TRUE, TRUE, 31, NOW())
ON CONFLICT (document_id) DO NOTHING;