Files
raisfast/migrations/postgres/schema.postgres.sql
T
2026-06-02 13:05:46 +08:00

1076 lines
43 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 complete database schema — PostgreSQL (with multi-tenant support)
-- Merged from all migration files for one-click initialization of new deployments
-- Generated date:2026-05-07
-- ============================================================
-- ── Platform foundation layer (always enabled) ──────────────────────────────────
-- Tenants
CREATE TABLE IF NOT EXISTS tenants (
id BIGINT PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE,
domain VARCHAR(255) UNIQUE,
config JSONB NOT NULL DEFAULT '{}',
status VARCHAR(50) NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW()
);
-- Default tenant
INSERT INTO tenants (name, domain, config, status, created_at, updated_at) VALUES
('Default', NULL, '{}', 'active', NOW(), NOW())
ON CONFLICT (name) DO NOTHING;
-- Users
CREATE TABLE IF NOT EXISTS users (
id BIGINT PRIMARY KEY,
tenant_id TEXT NOT NULL DEFAULT 'default',
username VARCHAR(255) UNIQUE NOT NULL,
role VARCHAR(50) NOT NULL DEFAULT 'reader',
avatar VARCHAR(500),
bio TEXT,
website VARCHAR(500),
status VARCHAR(50) NOT NULL DEFAULT 'active',
registered_via VARCHAR(100) NOT NULL,
display_name VARCHAR(100),
slug VARCHAR(100) UNIQUE,
locale VARCHAR(10),
social_links JSONB,
metadata JSONB,
created_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_slug ON users(slug) WHERE slug IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_users_tenant ON users(tenant_id);
-- User credentials
CREATE TABLE IF NOT EXISTS user_credentials (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
auth_type VARCHAR(100) NOT NULL,
identifier VARCHAR(500) NOT NULL,
credential_data TEXT NOT NULL,
verified BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW(),
UNIQUE(auth_type, identifier)
);
CREATE INDEX IF NOT EXISTS idx_user_credentials_user ON user_credentials(user_id);
CREATE INDEX IF NOT EXISTS idx_user_credentials_type_id ON user_credentials(auth_type, identifier);
CREATE INDEX IF NOT EXISTS idx_user_credentials_type ON user_credentials(auth_type);
-- OAuth account bindings
CREATE TABLE IF NOT EXISTS oauth_accounts (
id BIGINT PRIMARY KEY,
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(0) NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ(0) 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 short-lived state storage (PKCE)
CREATE TABLE IF NOT EXISTS oauth_states (
id BIGINT PRIMARY KEY,
provider VARCHAR(50) NOT NULL,
code_verifier VARCHAR(255) NOT NULL,
user_id BIGINT,
created_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_oauth_states_expires ON oauth_states(expires_at);
-- Currency configuration
CREATE TABLE IF NOT EXISTS currencies (
id BIGINT PRIMARY KEY,
code VARCHAR(10) NOT NULL UNIQUE CHECK(code = UPPER(code) AND LENGTH(code) BETWEEN 1 AND 10),
name TEXT NOT NULL,
decimals INTEGER NOT NULL DEFAULT 0 CHECK(decimals BETWEEN 0 AND 18),
is_active BOOLEAN NOT NULL DEFAULT TRUE,
version INTEGER NOT NULL DEFAULT 1,
created_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_currencies_code ON currencies(code);
CREATE TABLE IF NOT EXISTS wallets (
id BIGINT PRIMARY KEY,
tenant_id TEXT NOT NULL DEFAULT 'default',
user_id BIGINT NOT NULL REFERENCES users(id),
currency VARCHAR(50) NOT NULL,
balance BIGINT NOT NULL DEFAULT 0 CHECK(balance >= 0),
version BIGINT NOT NULL DEFAULT 1,
status VARCHAR(50) NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW(),
UNIQUE(user_id, currency)
);
CREATE INDEX IF NOT EXISTS idx_wallets_user ON wallets(user_id);
CREATE INDEX IF NOT EXISTS idx_wallets_currency ON wallets(currency);
CREATE INDEX IF NOT EXISTS idx_wallets_tenant ON wallets(tenant_id);
CREATE TABLE IF NOT EXISTS wallet_transactions (
id BIGINT PRIMARY KEY,
tenant_id TEXT NOT NULL DEFAULT 'default',
wallet_id BIGINT NOT NULL REFERENCES wallets(id),
user_id BIGINT NOT NULL REFERENCES users(id),
entry_type VARCHAR(10) NOT NULL,
amount BIGINT NOT NULL CHECK(amount > 0),
balance_after BIGINT NOT NULL CHECK(balance_after >= 0),
tx_type VARCHAR(50) NOT NULL,
currency VARCHAR(50) NOT NULL,
transaction_no VARCHAR(255) NOT NULL UNIQUE,
related_tx_id BIGINT,
reference_type VARCHAR(100),
reference_id VARCHAR(255),
counterparty_wallet_id BIGINT,
metadata JSONB,
created_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_wallet_tx_wallet ON wallet_transactions(wallet_id);
CREATE INDEX IF NOT EXISTS idx_wallet_tx_user ON wallet_transactions(user_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_wallet_tx_transaction_no ON wallet_transactions(transaction_no);
CREATE INDEX IF NOT EXISTS idx_wallet_tx_tx_type ON wallet_transactions(tx_type);
CREATE INDEX IF NOT EXISTS idx_wallet_tx_reference ON wallet_transactions(reference_type, reference_id);
CREATE INDEX IF NOT EXISTS idx_wallet_tx_created ON wallet_transactions(created_at);
CREATE INDEX IF NOT EXISTS idx_wallet_transactions_tenant ON wallet_transactions(tenant_id);
-- Refresh Tokens
CREATE TABLE IF NOT EXISTS refresh_tokens (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
token VARCHAR(500) UNIQUE NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ(0) 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);
-- Site options
CREATE TABLE IF NOT EXISTS options (
id BIGINT PRIMARY KEY,
tenant_id TEXT NOT NULL DEFAULT 'default',
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(0) NOT NULL DEFAULT NOW(),
UNIQUE(option_key)
);
CREATE INDEX IF NOT EXISTS idx_options_tenant_option_key ON options(tenant_id, option_key);
-- RBAC roles
CREATE TABLE IF NOT EXISTS roles (
id BIGINT PRIMARY KEY,
tenant_id TEXT NOT NULL DEFAULT 'default',
name VARCHAR(255) NOT NULL UNIQUE,
description TEXT,
is_system BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_roles_tenant ON roles(tenant_id);
-- RBAC permissions
CREATE TABLE IF NOT EXISTS permissions (
id BIGINT PRIMARY KEY,
tenant_id TEXT NOT NULL DEFAULT 'default',
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(0) NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_permissions_role_action_subject
ON permissions(role_id, action, subject);
CREATE INDEX IF NOT EXISTS idx_permissions_tenant ON permissions(tenant_id);
-- Audit log
CREATE TABLE IF NOT EXISTS audit_log (
id BIGINT PRIMARY KEY,
tenant_id TEXT NOT NULL DEFAULT 'default',
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(0) 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);
CREATE INDEX IF NOT EXISTS idx_audit_log_tenant ON audit_log(tenant_id);
-- API Token
CREATE TABLE IF NOT EXISTS api_tokens (
id BIGINT PRIMARY KEY,
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(0) 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 subscriptions
CREATE TABLE IF NOT EXISTS webhook_subscriptions (
id BIGINT PRIMARY KEY,
tenant_id TEXT NOT NULL DEFAULT 'default',
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(0) NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_webhook_subscriptions_enabled ON webhook_subscriptions(enabled);
CREATE INDEX IF NOT EXISTS idx_webhook_subscriptions_tenant ON webhook_subscriptions(tenant_id);
-- Plugin KV storage
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(0) NOT NULL DEFAULT NOW(),
PRIMARY KEY (plugin_id, storage_key)
);
CREATE INDEX IF NOT EXISTS idx_plugin_storage_plugin ON plugin_storage(plugin_id);
-- Content revision history
CREATE TABLE IF NOT EXISTS content_revisions (
id BIGINT PRIMARY KEY,
content_type VARCHAR(100) NOT NULL,
record_id BIGINT NOT NULL,
revision_number INTEGER NOT NULL,
snapshot JSONB NOT NULL,
created_by BIGINT,
created_at TIMESTAMPTZ(0) 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);
-- Password reset tokens
CREATE TABLE IF NOT EXISTS password_reset_tokens (
id BIGINT PRIMARY KEY,
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(0) 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);
-- SMS verification codes
CREATE TABLE IF NOT EXISTS sms_codes (
id BIGINT PRIMARY KEY,
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(0) 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);
-- Email verification tokens
CREATE TABLE IF NOT EXISTS email_verification_tokens (
id BIGINT PRIMARY KEY,
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(0) 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);
-- Background job queue
CREATE TABLE IF NOT EXISTS jobs (
id BIGINT PRIMARY KEY,
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(0) NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ(0) 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);
-- Cron job schedules
CREATE TABLE IF NOT EXISTS cron_schedules (
id BIGINT PRIMARY KEY,
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(0) NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ(0) 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 execution log
CREATE TABLE IF NOT EXISTS cron_execution_log (
id BIGINT PRIMARY KEY,
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);
-- ── Built-in module: Blog (BUILTIN_BLOG=true) ──────────────────
-- Categories
CREATE TABLE IF NOT EXISTS categories (
id BIGINT PRIMARY KEY,
tenant_id TEXT NOT NULL DEFAULT 'default',
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,
cover_image VARCHAR(500),
meta_title VARCHAR(255),
meta_description VARCHAR(500),
og_title VARCHAR(255),
og_description VARCHAR(500),
og_image VARCHAR(500),
created_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_categories_tenant ON categories(tenant_id);
-- Product categories
CREATE TABLE IF NOT EXISTS product_categories (
id BIGINT PRIMARY KEY,
tenant_id TEXT NOT NULL DEFAULT 'default',
name VARCHAR(255) UNIQUE NOT NULL,
slug VARCHAR(255) UNIQUE NOT NULL,
description TEXT,
cover_image VARCHAR(500),
parent_id BIGINT REFERENCES product_categories(id),
sort_order INTEGER NOT NULL DEFAULT 0,
created_by BIGINT,
updated_by BIGINT,
created_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_product_categories_tenant ON product_categories(tenant_id);
-- Tags
CREATE TABLE IF NOT EXISTS tags (
id BIGINT PRIMARY KEY,
tenant_id TEXT NOT NULL DEFAULT 'default',
name VARCHAR(255) UNIQUE NOT NULL,
slug VARCHAR(255) UNIQUE NOT NULL,
description TEXT,
cover_image VARCHAR(500),
meta_title VARCHAR(255),
meta_description VARCHAR(500),
og_title VARCHAR(255),
og_description VARCHAR(500),
og_image VARCHAR(500),
created_by BIGINT,
updated_by BIGINT,
created_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_tags_tenant ON tags(tenant_id);
-- Posts
CREATE TABLE IF NOT EXISTS posts (
id BIGINT PRIMARY KEY,
tenant_id TEXT NOT NULL DEFAULT 'default',
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,
password VARCHAR(255),
comment_status VARCHAR(20) NOT NULL DEFAULT 'open',
format VARCHAR(20) NOT NULL DEFAULT 'standard',
template VARCHAR(100) NOT NULL DEFAULT 'default',
meta_title VARCHAR(255),
meta_description VARCHAR(500),
og_title VARCHAR(255),
og_description VARCHAR(500),
og_image VARCHAR(500),
canonical_url VARCHAR(1024),
reading_time INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ(0) 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 INDEX IF NOT EXISTS idx_posts_tenant ON posts(tenant_id);
-- Posts-Tags (many-to-many)
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);
-- Comments
CREATE TABLE IF NOT EXISTS comments (
id BIGINT PRIMARY KEY,
tenant_id TEXT NOT NULL DEFAULT 'default',
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',
author_ip VARCHAR(45),
author_url VARCHAR(500),
created_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ(0) 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);
CREATE INDEX IF NOT EXISTS idx_comments_tenant ON comments(tenant_id);
-- ── Built-in module: Pages (BUILTIN_PAGES=true) ────────────────
CREATE TABLE IF NOT EXISTS pages (
id BIGINT PRIMARY KEY,
tenant_id TEXT NOT NULL DEFAULT 'default',
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,
password VARCHAR(255),
comment_status VARCHAR(20) NOT NULL DEFAULT 'closed',
og_title VARCHAR(255),
og_description VARCHAR(500),
canonical_url VARCHAR(1024),
created_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ(0) 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 INDEX IF NOT EXISTS idx_pages_tenant_slug ON pages(tenant_id, slug);
CREATE INDEX IF NOT EXISTS idx_pages_tenant_status ON pages(tenant_id, status);
CREATE TABLE IF NOT EXISTS reusable_blocks (
id BIGINT PRIMARY KEY,
tenant_id TEXT NOT NULL DEFAULT 'default',
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(0) NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_reusable_blocks_tenant ON reusable_blocks(tenant_id);
-- ── Built-in module: Media (BUILTIN_MEDIA=true) ────────────────
CREATE TABLE IF NOT EXISTS media (
id BIGINT PRIMARY KEY,
tenant_id TEXT NOT NULL DEFAULT 'default',
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,
title VARCHAR(255),
alt_text VARCHAR(255),
caption TEXT,
description TEXT,
updated_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW(),
created_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_media_user_created
ON media(user_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_media_tenant ON media(tenant_id);
-- ── Built-in module: Workflow (BUILTIN_WORKFLOW=true) ──────────
CREATE TABLE IF NOT EXISTS workflow_definitions (
id BIGINT PRIMARY KEY,
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(0) NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS workflow_instances (
id BIGINT PRIMARY KEY,
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(0) NOT NULL DEFAULT NOW(),
completed_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ(0) 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 BIGINT PRIMARY KEY,
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(0) NOT NULL DEFAULT NOW(),
completed_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_wf_step_logs_instance ON workflow_step_logs(instance_id);
-- Products
CREATE TABLE IF NOT EXISTS products (
id BIGINT PRIMARY KEY,
tenant_id TEXT NOT NULL DEFAULT 'default',
category_id BIGINT REFERENCES product_categories(id),
title TEXT NOT NULL,
description TEXT,
cover_url VARCHAR(500),
product_type VARCHAR(50) NOT NULL DEFAULT 'custom',
fulfillment_type VARCHAR(50) NOT NULL DEFAULT 'digital',
delivery_hook VARCHAR(255),
weight INTEGER,
shipping_template_id BIGINT,
price BIGINT NOT NULL CHECK(price >= 0),
currency VARCHAR(50) NOT NULL DEFAULT 'USD',
status VARCHAR(50) NOT NULL DEFAULT 'draft',
attributes JSONB,
sort_order INTEGER NOT NULL DEFAULT 0,
version INTEGER NOT NULL DEFAULT 1,
created_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW(),
slug VARCHAR(255) UNIQUE,
content TEXT,
image_ids JSONB,
original_price BIGINT,
specs JSONB,
unit VARCHAR(50) NOT NULL DEFAULT 'piece',
min_purchase INTEGER NOT NULL DEFAULT 1,
max_purchase INTEGER,
total_sales INTEGER NOT NULL DEFAULT 0,
virtual_sales INTEGER NOT NULL DEFAULT 0,
meta_title VARCHAR(255),
meta_description VARCHAR(500),
published_at TIMESTAMPTZ,
stock INTEGER NOT NULL DEFAULT 0,
cost_price BIGINT,
sale_price BIGINT,
has_variants BOOLEAN NOT NULL DEFAULT FALSE,
updated_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_products_status ON products(status);
CREATE INDEX IF NOT EXISTS idx_products_type ON products(product_type);
CREATE INDEX IF NOT EXISTS idx_products_slug ON products(slug);
CREATE INDEX IF NOT EXISTS idx_products_tenant ON products(tenant_id);
-- Product Variants
CREATE TABLE IF NOT EXISTS product_variants (
id BIGINT PRIMARY KEY,
tenant_id TEXT NOT NULL DEFAULT 'default',
product_id BIGINT NOT NULL REFERENCES products(id),
sku VARCHAR(100) UNIQUE,
title TEXT NOT NULL,
price BIGINT NOT NULL CHECK(price >= 0),
original_price BIGINT,
stock INTEGER NOT NULL DEFAULT 0,
attributes JSONB,
image_url TEXT,
weight INTEGER,
sort_order INTEGER NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_product_variants_product ON product_variants(product_id);
CREATE INDEX IF NOT EXISTS idx_product_variants_sku ON product_variants(sku);
CREATE INDEX IF NOT EXISTS idx_product_variants_tenant ON product_variants(tenant_id);
-- User Addresses
CREATE TABLE IF NOT EXISTS user_addresses (
id BIGINT PRIMARY KEY,
tenant_id TEXT NOT NULL DEFAULT 'default',
user_id BIGINT NOT NULL REFERENCES users(id),
label VARCHAR(100) NOT NULL DEFAULT '',
recipient_name VARCHAR(200) NOT NULL,
phone VARCHAR(50) NOT NULL,
country VARCHAR(10) NOT NULL DEFAULT 'CN',
province VARCHAR(100) NOT NULL DEFAULT '',
city VARCHAR(100) NOT NULL DEFAULT '',
district VARCHAR(100) NOT NULL DEFAULT '',
address_line1 TEXT NOT NULL,
address_line2 TEXT,
postal_code VARCHAR(20),
is_default BOOLEAN NOT NULL DEFAULT FALSE,
address_type VARCHAR(20) NOT NULL DEFAULT 'shipping',
created_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_user_addresses_user ON user_addresses(user_id);
CREATE INDEX IF NOT EXISTS idx_user_addresses_tenant ON user_addresses(tenant_id);
-- Orders
CREATE TABLE IF NOT EXISTS orders (
id BIGINT PRIMARY KEY,
tenant_id TEXT NOT NULL DEFAULT 'default',
user_id BIGINT NOT NULL REFERENCES users(id),
order_no VARCHAR(255) NOT NULL UNIQUE,
subtotal BIGINT NOT NULL DEFAULT 0,
discount_amount BIGINT NOT NULL DEFAULT 0,
shipping_amount BIGINT NOT NULL DEFAULT 0,
total_amount BIGINT NOT NULL CHECK(total_amount >= 0),
currency VARCHAR(50) NOT NULL DEFAULT 'USD',
status VARCHAR(50) NOT NULL DEFAULT 'pending',
buyer_name VARCHAR(255),
buyer_phone VARCHAR(50),
buyer_email VARCHAR(255),
shipping_address TEXT,
tracking_no VARCHAR(255),
carrier VARCHAR(100),
remark TEXT,
admin_remark TEXT,
delivery_data JSONB,
tax_amount BIGINT NOT NULL DEFAULT 0,
coupon_id BIGINT,
shipping_address_id BIGINT,
billing_address_id BIGINT,
paid_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
cancelled_at TIMESTAMPTZ,
refunding_at TIMESTAMPTZ,
refunded_at TIMESTAMPTZ,
expired_at TIMESTAMPTZ,
created_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_orders_user ON orders(user_id);
CREATE INDEX IF NOT EXISTS idx_orders_status ON orders(status);
CREATE INDEX IF NOT EXISTS idx_orders_order_no ON orders(order_no);
CREATE INDEX IF NOT EXISTS idx_orders_tenant ON orders(tenant_id);
-- Order Items
CREATE TABLE IF NOT EXISTS order_items (
id BIGINT PRIMARY KEY,
tenant_id TEXT NOT NULL DEFAULT 'default',
order_id BIGINT NOT NULL REFERENCES orders(id),
product_id BIGINT REFERENCES products(id),
variant_id BIGINT REFERENCES product_variants(id),
title TEXT NOT NULL,
description TEXT,
sku VARCHAR(100),
unit_price BIGINT NOT NULL CHECK(unit_price >= 0),
quantity INTEGER NOT NULL CHECK(quantity > 0),
subtotal BIGINT NOT NULL,
tax_amount BIGINT NOT NULL DEFAULT 0,
cover_url VARCHAR(500),
attributes JSONB,
created_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_order_items_order ON order_items(order_id);
CREATE INDEX IF NOT EXISTS idx_order_items_tenant ON order_items(tenant_id);
CREATE TABLE IF NOT EXISTS cart_items (
id BIGINT PRIMARY KEY,
tenant_id TEXT NOT NULL DEFAULT 'default',
user_id BIGINT NOT NULL REFERENCES users(id),
product_id BIGINT NOT NULL REFERENCES products(id),
variant_id BIGINT REFERENCES product_variants(id),
quantity INTEGER NOT NULL DEFAULT 1,
attributes JSONB,
created_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW(),
UNIQUE(user_id, product_id, variant_id)
);
CREATE INDEX IF NOT EXISTS idx_cart_items_tenant ON cart_items(tenant_id);
CREATE TABLE IF NOT EXISTS payment_channels (
id BIGINT PRIMARY KEY,
tenant_id TEXT NOT NULL DEFAULT 'default',
provider VARCHAR(50) NOT NULL,
name VARCHAR(200) NOT NULL,
is_live BOOLEAN NOT NULL DEFAULT FALSE,
credentials TEXT NOT NULL,
webhook_secret TEXT,
settings JSONB,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
sort_order INTEGER NOT NULL DEFAULT 0,
version INTEGER NOT NULL DEFAULT 1,
created_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW(),
UNIQUE(provider, name)
);
CREATE INDEX IF NOT EXISTS idx_payment_channels_provider ON payment_channels(provider);
CREATE INDEX IF NOT EXISTS idx_payment_channels_active ON payment_channels(is_active);
CREATE INDEX IF NOT EXISTS idx_payment_channels_tenant ON payment_channels(tenant_id);
-- Payment Orders
CREATE TABLE IF NOT EXISTS payment_orders (
id BIGINT PRIMARY KEY,
tenant_id TEXT NOT NULL DEFAULT 'default',
user_id BIGINT NOT NULL REFERENCES users(id),
order_id VARCHAR(36),
title TEXT NOT NULL,
amount BIGINT NOT NULL,
currency VARCHAR(10) NOT NULL DEFAULT 'USD',
channel_id BIGINT NOT NULL REFERENCES payment_channels(id),
provider VARCHAR(50) NOT NULL,
provider_order_id VARCHAR(200),
provider_method VARCHAR(50),
status VARCHAR(50) NOT NULL DEFAULT 'pending',
reference_type VARCHAR(50),
reference_id VARCHAR(200),
return_url VARCHAR(500),
idempotency_key VARCHAR(200) NOT NULL UNIQUE,
version INTEGER NOT NULL DEFAULT 1,
provider_data JSONB,
client_ip VARCHAR(45),
client_language VARCHAR(50),
client_country VARCHAR(2),
client_user_agent VARCHAR(512),
channel_selected_by VARCHAR(20),
metadata JSONB,
paid_at TIMESTAMPTZ,
cancelled_at TIMESTAMPTZ,
expired_at TIMESTAMPTZ,
created_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_payment_orders_user ON payment_orders(user_id);
CREATE INDEX IF NOT EXISTS idx_payment_orders_status ON payment_orders(status);
CREATE INDEX IF NOT EXISTS idx_payment_orders_provider ON payment_orders(provider_order_id);
CREATE INDEX IF NOT EXISTS idx_payment_orders_order_id ON payment_orders(order_id);
CREATE INDEX IF NOT EXISTS idx_payment_orders_tenant ON payment_orders(tenant_id);
-- Payment Transactions
CREATE TABLE IF NOT EXISTS payment_transactions (
id BIGINT PRIMARY KEY,
tenant_id TEXT NOT NULL DEFAULT 'default',
payment_order_id BIGINT NOT NULL REFERENCES payment_orders(id),
order_id VARCHAR(36),
user_id BIGINT NOT NULL REFERENCES users(id),
tx_type VARCHAR(50) NOT NULL,
amount BIGINT NOT NULL,
currency VARCHAR(10) NOT NULL,
provider_tx_id VARCHAR(200) NOT NULL UNIQUE,
status VARCHAR(50) NOT NULL DEFAULT 'pending',
raw_payload JSONB,
created_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_payment_tx_order ON payment_transactions(payment_order_id);
CREATE INDEX IF NOT EXISTS idx_payment_tx_order_id ON payment_transactions(order_id);
CREATE INDEX IF NOT EXISTS idx_payment_transactions_tenant ON payment_transactions(tenant_id);
-- Payment Refunds
CREATE TABLE IF NOT EXISTS payment_refunds (
id BIGINT PRIMARY KEY,
tenant_id TEXT NOT NULL DEFAULT 'default',
payment_order_id BIGINT NOT NULL REFERENCES payment_orders(id),
order_id VARCHAR(36),
user_id BIGINT NOT NULL REFERENCES users(id),
amount BIGINT NOT NULL,
currency VARCHAR(10) NOT NULL,
reason VARCHAR(200),
provider_refund_id VARCHAR(200),
status VARCHAR(50) NOT NULL DEFAULT 'pending',
payment_tx_id BIGINT REFERENCES payment_transactions(id),
metadata JSONB,
created_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_payment_refunds_order ON payment_refunds(payment_order_id);
CREATE INDEX IF NOT EXISTS idx_payment_refunds_order_id ON payment_refunds(order_id);
CREATE INDEX IF NOT EXISTS idx_payment_refunds_tenant ON payment_refunds(tenant_id);
-- Wallet Outbox (ensures wallet operations are never lost)
CREATE TABLE IF NOT EXISTS wallet_outbox (
id BIGINT PRIMARY KEY,
tenant_id TEXT NOT NULL DEFAULT 'default',
user_id BIGINT NOT NULL,
currency TEXT NOT NULL,
amount BIGINT NOT NULL,
entry_type TEXT NOT NULL,
tx_type TEXT NOT NULL,
transaction_no TEXT NOT NULL,
reference_type TEXT,
reference_id TEXT,
metadata TEXT,
status TEXT NOT NULL DEFAULT 'pending',
attempts INTEGER NOT NULL DEFAULT 0,
max_attempts INTEGER NOT NULL DEFAULT 5,
last_error TEXT,
created_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ(0) NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_wallet_outbox_status ON wallet_outbox(status);
CREATE INDEX IF NOT EXISTS idx_wallet_outbox_transaction_no ON wallet_outbox(transaction_no);
CREATE INDEX IF NOT EXISTS idx_wallet_outbox_tenant ON wallet_outbox(tenant_id);
-- Product Comments (reviews/ratings)
CREATE TABLE IF NOT EXISTS product_comments (
id BIGINT PRIMARY KEY,
tenant_id VARCHAR(64) NOT NULL DEFAULT 'default',
product_id BIGINT NOT NULL REFERENCES products(id),
order_id BIGINT NOT NULL REFERENCES orders(id),
user_id BIGINT NOT NULL REFERENCES users(id),
rating INT NOT NULL DEFAULT 5,
title VARCHAR(255),
content TEXT NOT NULL,
images TEXT,
status VARCHAR(32) NOT NULL DEFAULT 'approved',
admin_reply TEXT,
admin_replied_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_product_comments_unique ON product_comments(product_id, order_id, user_id);
CREATE INDEX IF NOT EXISTS idx_product_comments_product ON product_comments(product_id);
CREATE INDEX IF NOT EXISTS idx_product_comments_user ON product_comments(user_id);
CREATE INDEX IF NOT EXISTS idx_product_comments_status ON product_comments(status);
CREATE INDEX IF NOT EXISTS idx_product_comments_tenant ON product_comments(tenant_id);
-- ============================================================
-- Seed data
-- ============================================================
-- System roles
INSERT INTO roles (tenant_id, name, description, is_system, created_at, updated_at) VALUES
('default', 'admin', 'Super administrator', TRUE, NOW(), NOW()),
('default', 'editor', 'Editor', FALSE, NOW(), NOW()),
('default', 'author', 'Author', FALSE, NOW(), NOW()),
('default', 'reader', 'Reader', TRUE, NOW(), NOW())
ON CONFLICT (name) DO NOTHING;
-- Admin global permissions
INSERT INTO permissions (tenant_id, role_id, action, subject, fields, conditions, created_at) VALUES
('default', (SELECT id FROM roles WHERE name = 'admin'), '*', '*', '["*"]', NULL, NOW())
ON CONFLICT (role_id, action, subject) DO NOTHING;
-- Editor permissions
INSERT INTO permissions (tenant_id, role_id, action, subject, fields, conditions, created_at) VALUES
('default', (SELECT id FROM roles WHERE name = 'editor'), 'content-type::*.*', 'content-type::*', '["*"]', NULL, NOW())
ON CONFLICT (role_id, action, subject) DO NOTHING;
-- Author permissions
INSERT INTO permissions (tenant_id, role_id, action, subject, fields, conditions, created_at) VALUES
('default', (SELECT id FROM roles WHERE name = 'author'), 'content-type::post.create', 'content-type::post', '["*"]', NULL, NOW()),
('default', (SELECT id FROM roles WHERE name = 'author'), 'content-type::post.read', 'content-type::post', '["*"]', NULL, NOW()),
('default', (SELECT id FROM roles WHERE name = 'author'), 'content-type::post.update', 'content-type::post', '["*"]', '{"author_id":"$user.id"}', NOW()),
('default', (SELECT id FROM roles WHERE name = 'author'), 'content-type::post.delete', 'content-type::post', '["*"]', '{"author_id":"$user.id"}', NOW())
ON CONFLICT (role_id, action, subject) DO NOTHING;
-- Reader permissions
INSERT INTO permissions (tenant_id, role_id, action, subject, fields, conditions, created_at) VALUES
('default', (SELECT id FROM roles WHERE name = 'reader'), 'content-type::post.read', 'content-type::post', '["title","slug","content","excerpt","status"]', NULL, NOW()),
('default', (SELECT id FROM roles WHERE name = 'reader'), 'content-type::comment.create', 'content-type::comment', '["content","nickname","email"]', NULL, NOW())
ON CONFLICT (role_id, action, subject) DO NOTHING;
-- Site options
INSERT INTO options (tenant_id, option_key, value, type, group_name, label, description, validation, is_public, autoload, sort_order, updated_at) VALUES
('default', 'site_title', '"My Blog"', 'text', 'general', 'Site title', 'Displayed in browser title bar and page header', '{"max_length":100}', TRUE, TRUE, 1, NOW()),
('default', 'site_description', '""', 'text', 'general', 'Site description', 'Brief description of the site purpose', '{"max_length":500}', TRUE, TRUE, 2, NOW()),
('default', 'site_url', '""', 'url', 'general', 'Site URL', 'e.g. https://example.com', NULL, TRUE, TRUE, 3, NOW()),
('default', 'admin_email', '""', 'email', 'general', 'Admin email', NULL, NULL, FALSE, TRUE, 4, NOW()),
('default', 'timezone', '"UTC"', 'select', 'general', 'Timezone', NULL, '{"values":["UTC","Asia/Shanghai","Asia/Tokyo","US/Eastern","US/Pacific","Europe/London","Europe/Berlin"]}', TRUE, TRUE, 5, NOW()),
('default', 'date_format', '"%Y-%m-%d"', 'select', 'general', 'Date format', NULL, '{"values":["%Y-%m-%d","%d/%m/%Y","%m/%d/%Y","%Y年%m月%d日"]}', TRUE, TRUE, 6, NOW()),
('default', 'posts_per_page', '10', 'integer', 'reading', 'Posts per page', NULL, '{"min":1,"max":100}', TRUE, TRUE, 10, NOW()),
('default', 'rss_items', '20', 'integer', 'reading', 'RSS item count', NULL, '{"min":1,"max":100}', TRUE, TRUE, 11, NOW()),
('default', 'permalink_structure', '"/:year/:month/:slug"', 'select', 'reading', 'URL structure', NULL, '{"values":["/:year/:month/:slug","/:slug","/posts/:slug"]}', TRUE, TRUE, 12, NOW()),
('default', 'comment_moderation', 'true', 'boolean', 'discussion', 'Comments require moderation', 'When enabled, new comments require admin approval', NULL, FALSE, TRUE, 20, NOW()),
('default', 'comment_order', '"asc"', 'select', 'discussion', 'Comment order', NULL, '{"values":["asc","desc"]}', TRUE, TRUE, 21, NOW()),
('default', 'default_role', '"reader"', 'select', 'discussion', 'Default role for new users', NULL, '{"values":["reader","author"]}', FALSE, TRUE, 22, NOW()),
('default', 'theme', '"default"', 'select', 'appearance', 'Current theme', NULL, '{"values":["default","corporate","minimal","warm"]}', TRUE, TRUE, 30, NOW()),
('default', 'maintenance_mode', 'false', 'boolean', 'appearance', 'Maintenance mode', 'When enabled, a maintenance page is shown to visitors', NULL, TRUE, TRUE, 31, NOW())
ON CONFLICT (option_key) DO NOTHING;
-- Coupons
CREATE TABLE IF NOT EXISTS coupons (
id BIGINT PRIMARY KEY,
tenant_id VARCHAR(64) NOT NULL DEFAULT 'default',
code VARCHAR(64) NOT NULL UNIQUE,
title VARCHAR(255) NOT NULL,
coupon_type VARCHAR(32) NOT NULL DEFAULT 'percent',
value BIGINT NOT NULL,
min_order BIGINT NOT NULL DEFAULT 0,
max_uses INT NOT NULL DEFAULT 0,
used_count INT NOT NULL DEFAULT 0,
max_uses_per_user INT NOT NULL DEFAULT 1,
starts_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ,
status VARCHAR(32) NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_coupons_code ON coupons(code);
CREATE INDEX IF NOT EXISTS idx_coupons_status ON coupons(status);
CREATE INDEX IF NOT EXISTS idx_coupons_tenant ON coupons(tenant_id);
-- Shipping Templates
CREATE TABLE IF NOT EXISTS shipping_templates (
id BIGINT PRIMARY KEY,
tenant_id TEXT NOT NULL DEFAULT 'default',
name TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'weight',
first_unit INTEGER NOT NULL DEFAULT 1,
first_price INTEGER NOT NULL DEFAULT 0,
additional_unit INTEGER NOT NULL DEFAULT 1,
additional_price INTEGER NOT NULL DEFAULT 0,
free_shipping_amount INTEGER NOT NULL DEFAULT 0,
regions TEXT NOT NULL DEFAULT '[]',
status TEXT NOT NULL DEFAULT 'active',
created_at TEXT NOT NULL DEFAULT (NOW()),
updated_at TEXT NOT NULL DEFAULT (NOW())
);
CREATE INDEX IF NOT EXISTS idx_shipping_templates_tenant ON shipping_templates(tenant_id);
CREATE INDEX IF NOT EXISTS idx_shipping_templates_status ON shipping_templates(status);