mirror of
https://github.com/RaisFast/raisfast.git
synced 2026-09-25 16:02:41 +00:00
957 lines
40 KiB
SQL
957 lines
40 KiB
SQL
-- ============================================================
|
||
-- raisfast 完整数据库 Schema(含多租户支持)
|
||
-- 由所有 migration 文件合并而成,用于新部署一键初始化
|
||
-- 生成日期:2026-05-07
|
||
-- ============================================================
|
||
|
||
-- ── 平台基础层(永不禁用) ──────────────────────────────────
|
||
|
||
-- 租户表
|
||
CREATE TABLE IF NOT EXISTS tenants (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
document_id TEXT NOT NULL UNIQUE,
|
||
name TEXT NOT NULL,
|
||
domain TEXT UNIQUE,
|
||
config TEXT NOT NULL DEFAULT '{}',
|
||
status TEXT NOT NULL DEFAULT 'active',
|
||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
|
||
);
|
||
|
||
-- 默认租户
|
||
INSERT OR IGNORE INTO tenants (document_id, name, domain, config, status, created_at, updated_at) VALUES
|
||
('default', 'Default', NULL, '{}', 'active', strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), strftime('%Y-%m-%dT%H:%M:%SZ', 'now'));
|
||
|
||
-- 用户
|
||
CREATE TABLE IF NOT EXISTS users (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
document_id TEXT NOT NULL UNIQUE,
|
||
tenant_id TEXT NOT NULL DEFAULT 'default',
|
||
username TEXT UNIQUE NOT NULL,
|
||
role TEXT NOT NULL DEFAULT 'reader',
|
||
avatar TEXT,
|
||
bio TEXT,
|
||
website TEXT,
|
||
status TEXT NOT NULL DEFAULT 'active',
|
||
registered_via TEXT NOT NULL,
|
||
display_name TEXT,
|
||
slug TEXT UNIQUE,
|
||
locale TEXT,
|
||
social_links TEXT,
|
||
metadata TEXT,
|
||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', '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);
|
||
|
||
-- 用户凭据
|
||
CREATE TABLE IF NOT EXISTS user_credentials (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
document_id TEXT NOT NULL UNIQUE,
|
||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||
auth_type TEXT NOT NULL,
|
||
identifier TEXT NOT NULL,
|
||
credential_data TEXT NOT NULL,
|
||
verified INTEGER NOT NULL DEFAULT 0,
|
||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', '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 账号绑定
|
||
CREATE TABLE IF NOT EXISTS oauth_accounts (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
document_id TEXT NOT NULL UNIQUE,
|
||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||
provider TEXT NOT NULL,
|
||
provider_user_id TEXT NOT NULL,
|
||
email TEXT,
|
||
display_name TEXT,
|
||
avatar_url TEXT,
|
||
access_token TEXT,
|
||
refresh_token TEXT,
|
||
token_expires_at TEXT,
|
||
profile TEXT,
|
||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', '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 INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
document_id TEXT NOT NULL UNIQUE,
|
||
provider TEXT NOT NULL,
|
||
code_verifier TEXT NOT NULL,
|
||
user_id INTEGER,
|
||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
expires_at TEXT NOT NULL
|
||
);
|
||
|
||
CREATE INDEX IF NOT EXISTS idx_oauth_states_expires ON oauth_states(expires_at);
|
||
|
||
-- 币种配置
|
||
CREATE TABLE IF NOT EXISTS currencies (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
document_id TEXT NOT NULL UNIQUE,
|
||
code TEXT 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 INTEGER NOT NULL DEFAULT 1,
|
||
version INTEGER NOT NULL DEFAULT 1,
|
||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
|
||
);
|
||
|
||
CREATE UNIQUE INDEX IF NOT EXISTS idx_currencies_code ON currencies(code);
|
||
|
||
-- 用户钱包(每用户每币种一个)
|
||
CREATE TABLE IF NOT EXISTS wallets (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
document_id TEXT NOT NULL UNIQUE,
|
||
tenant_id TEXT NOT NULL DEFAULT 'default',
|
||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||
currency TEXT NOT NULL,
|
||
balance INTEGER NOT NULL DEFAULT 0 CHECK(balance >= 0),
|
||
version INTEGER NOT NULL DEFAULT 1,
|
||
status TEXT NOT NULL DEFAULT 'active',
|
||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', '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 INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
document_id TEXT NOT NULL UNIQUE,
|
||
tenant_id TEXT NOT NULL DEFAULT 'default',
|
||
wallet_id INTEGER NOT NULL REFERENCES wallets(id),
|
||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||
entry_type TEXT NOT NULL,
|
||
amount INTEGER NOT NULL CHECK(amount > 0),
|
||
balance_after INTEGER NOT NULL CHECK(balance_after >= 0),
|
||
tx_type TEXT NOT NULL,
|
||
currency TEXT NOT NULL,
|
||
transaction_no TEXT NOT NULL UNIQUE,
|
||
related_tx_id INTEGER,
|
||
reference_type TEXT,
|
||
reference_id TEXT,
|
||
counterparty_wallet_id INTEGER,
|
||
metadata TEXT,
|
||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', '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 INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
document_id TEXT NOT NULL UNIQUE,
|
||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||
token TEXT UNIQUE NOT NULL,
|
||
expires_at TEXT NOT NULL,
|
||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', '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 INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
document_id TEXT NOT NULL UNIQUE,
|
||
tenant_id TEXT NOT NULL DEFAULT 'default',
|
||
option_key TEXT NOT NULL,
|
||
value TEXT NOT NULL,
|
||
type TEXT NOT NULL DEFAULT 'text',
|
||
group_name TEXT NOT NULL DEFAULT 'general',
|
||
label TEXT NOT NULL DEFAULT '',
|
||
description TEXT,
|
||
validation TEXT,
|
||
is_public BOOLEAN NOT NULL DEFAULT 0,
|
||
autoload BOOLEAN NOT NULL DEFAULT 1,
|
||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
UNIQUE(option_key)
|
||
);
|
||
|
||
CREATE INDEX IF NOT EXISTS idx_options_tenant_option_key ON options(tenant_id, option_key);
|
||
|
||
-- RBAC 角色
|
||
CREATE TABLE IF NOT EXISTS roles (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
document_id TEXT NOT NULL UNIQUE,
|
||
tenant_id TEXT NOT NULL DEFAULT 'default',
|
||
name TEXT NOT NULL UNIQUE,
|
||
description TEXT,
|
||
is_system BOOLEAN NOT NULL DEFAULT 0,
|
||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
|
||
);
|
||
|
||
CREATE INDEX IF NOT EXISTS idx_roles_tenant ON roles(tenant_id);
|
||
|
||
-- RBAC 权限
|
||
CREATE TABLE IF NOT EXISTS permissions (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
document_id TEXT NOT NULL UNIQUE,
|
||
tenant_id TEXT NOT NULL DEFAULT 'default',
|
||
role_id INTEGER NOT NULL REFERENCES roles(id),
|
||
action TEXT NOT NULL,
|
||
subject TEXT NOT NULL,
|
||
fields TEXT,
|
||
conditions TEXT,
|
||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', '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);
|
||
|
||
-- 审计日志
|
||
CREATE TABLE IF NOT EXISTS audit_log (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
document_id TEXT NOT NULL UNIQUE,
|
||
tenant_id TEXT NOT NULL DEFAULT 'default',
|
||
actor_id INTEGER,
|
||
actor_role TEXT,
|
||
action TEXT NOT NULL,
|
||
subject TEXT NOT NULL,
|
||
subject_id TEXT,
|
||
detail TEXT,
|
||
ip_address TEXT,
|
||
user_agent TEXT,
|
||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', '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 INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
document_id TEXT NOT NULL UNIQUE,
|
||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||
name TEXT NOT NULL,
|
||
token_hash TEXT UNIQUE NOT NULL,
|
||
token_prefix TEXT NOT NULL,
|
||
scopes TEXT NOT NULL DEFAULT '["read","write"]',
|
||
last_used_at TEXT,
|
||
expires_at TEXT,
|
||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', '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 INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
document_id TEXT NOT NULL UNIQUE,
|
||
tenant_id TEXT NOT NULL DEFAULT 'default',
|
||
url TEXT NOT NULL,
|
||
secret TEXT NOT NULL,
|
||
events TEXT NOT NULL DEFAULT '[]',
|
||
enabled INTEGER NOT NULL DEFAULT 1,
|
||
description TEXT,
|
||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', '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);
|
||
|
||
-- 插件 KV 存储
|
||
CREATE TABLE IF NOT EXISTS plugin_storage (
|
||
plugin_id TEXT NOT NULL,
|
||
storage_key TEXT NOT NULL,
|
||
value TEXT NOT NULL,
|
||
expires_at TEXT,
|
||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', '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 INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
document_id TEXT NOT NULL UNIQUE,
|
||
content_type TEXT NOT NULL,
|
||
record_id TEXT NOT NULL,
|
||
revision_number INTEGER NOT NULL,
|
||
snapshot TEXT NOT NULL,
|
||
created_by INTEGER,
|
||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', '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);
|
||
|
||
-- 密码重置令牌
|
||
CREATE TABLE IF NOT EXISTS password_reset_tokens (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
document_id TEXT NOT NULL UNIQUE,
|
||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||
token TEXT NOT NULL UNIQUE,
|
||
expires_at TEXT NOT NULL,
|
||
used_at TEXT,
|
||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', '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 INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
document_id TEXT NOT NULL UNIQUE,
|
||
phone TEXT NOT NULL,
|
||
code TEXT NOT NULL,
|
||
purpose TEXT NOT NULL,
|
||
expires_at TEXT NOT NULL,
|
||
verified_at TEXT,
|
||
attempts INTEGER NOT NULL DEFAULT 0,
|
||
ip_address TEXT,
|
||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', '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 INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
document_id TEXT NOT NULL UNIQUE,
|
||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||
token TEXT NOT NULL UNIQUE,
|
||
email TEXT NOT NULL,
|
||
expires_at TEXT NOT NULL,
|
||
verified_at TEXT,
|
||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', '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 INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
document_id TEXT NOT NULL UNIQUE,
|
||
job_type TEXT NOT NULL,
|
||
payload TEXT NOT NULL,
|
||
status TEXT NOT NULL DEFAULT 'pending',
|
||
attempts INTEGER NOT NULL DEFAULT 0,
|
||
max_attempts INTEGER NOT NULL DEFAULT 3,
|
||
run_after TEXT,
|
||
error TEXT,
|
||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', '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 INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
document_id TEXT NOT NULL UNIQUE,
|
||
label TEXT NOT NULL,
|
||
job_type TEXT NOT NULL,
|
||
payload TEXT,
|
||
cron_expr TEXT NOT NULL,
|
||
enabled INTEGER NOT NULL DEFAULT 1,
|
||
last_run_at TEXT,
|
||
next_run_at TEXT NOT NULL,
|
||
plugin_id TEXT,
|
||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', '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 = 1;
|
||
CREATE INDEX IF NOT EXISTS idx_cron_plugin ON cron_schedules(plugin_id);
|
||
|
||
-- Cron 执行历史
|
||
CREATE TABLE IF NOT EXISTS cron_execution_log (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
document_id TEXT NOT NULL UNIQUE,
|
||
schedule_id INTEGER NOT NULL,
|
||
job_type TEXT NOT NULL,
|
||
label TEXT NOT NULL,
|
||
status TEXT NOT NULL DEFAULT 'running',
|
||
duration_ms INTEGER,
|
||
error TEXT,
|
||
started_at TEXT NOT NULL,
|
||
finished_at TEXT
|
||
);
|
||
|
||
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 INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
document_id TEXT NOT NULL UNIQUE,
|
||
tenant_id TEXT NOT NULL DEFAULT 'default',
|
||
name TEXT UNIQUE NOT NULL,
|
||
slug TEXT UNIQUE NOT NULL,
|
||
description TEXT,
|
||
parent_id INTEGER REFERENCES categories(id),
|
||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||
created_by INTEGER,
|
||
updated_by INTEGER,
|
||
cover_image TEXT,
|
||
meta_title TEXT,
|
||
meta_description TEXT,
|
||
og_title TEXT,
|
||
og_description TEXT,
|
||
og_image TEXT,
|
||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
|
||
);
|
||
|
||
CREATE INDEX IF NOT EXISTS idx_categories_tenant ON categories(tenant_id);
|
||
|
||
-- 标签
|
||
CREATE TABLE IF NOT EXISTS tags (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
document_id TEXT NOT NULL UNIQUE,
|
||
tenant_id TEXT NOT NULL DEFAULT 'default',
|
||
name TEXT UNIQUE NOT NULL,
|
||
slug TEXT UNIQUE NOT NULL,
|
||
created_by INTEGER,
|
||
updated_by INTEGER,
|
||
description TEXT,
|
||
cover_image TEXT,
|
||
meta_title TEXT,
|
||
meta_description TEXT,
|
||
og_title TEXT,
|
||
og_description TEXT,
|
||
og_image TEXT,
|
||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
|
||
);
|
||
|
||
CREATE INDEX IF NOT EXISTS idx_tags_tenant ON tags(tenant_id);
|
||
|
||
-- 文章
|
||
CREATE TABLE IF NOT EXISTS posts (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
document_id TEXT NOT NULL UNIQUE,
|
||
tenant_id TEXT NOT NULL DEFAULT 'default',
|
||
title TEXT NOT NULL,
|
||
slug TEXT UNIQUE NOT NULL,
|
||
content TEXT NOT NULL,
|
||
excerpt TEXT,
|
||
cover_image TEXT,
|
||
status TEXT NOT NULL DEFAULT 'draft',
|
||
created_by INTEGER NOT NULL REFERENCES users(id),
|
||
updated_by INTEGER,
|
||
category_id INTEGER REFERENCES categories(id),
|
||
view_count INTEGER NOT NULL DEFAULT 0,
|
||
is_pinned INTEGER NOT NULL DEFAULT 0,
|
||
password TEXT,
|
||
comment_status TEXT NOT NULL DEFAULT 'open',
|
||
format TEXT NOT NULL DEFAULT 'standard',
|
||
template TEXT NOT NULL DEFAULT 'default',
|
||
meta_title TEXT,
|
||
meta_description TEXT,
|
||
og_title TEXT,
|
||
og_description TEXT,
|
||
og_image TEXT,
|
||
canonical_url TEXT,
|
||
reading_time INTEGER NOT NULL DEFAULT 0,
|
||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
published_at TEXT
|
||
);
|
||
|
||
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);
|
||
|
||
-- 文章-标签(多对多)
|
||
CREATE TABLE IF NOT EXISTS posts_tags (
|
||
post_id INTEGER NOT NULL REFERENCES posts(id),
|
||
tag_id INTEGER 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 INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
document_id TEXT NOT NULL UNIQUE,
|
||
tenant_id TEXT NOT NULL DEFAULT 'default',
|
||
post_id INTEGER NOT NULL REFERENCES posts(id),
|
||
created_by INTEGER REFERENCES users(id),
|
||
updated_by INTEGER,
|
||
nickname TEXT,
|
||
email TEXT,
|
||
content TEXT NOT NULL,
|
||
parent_id INTEGER REFERENCES comments(id),
|
||
status TEXT NOT NULL DEFAULT 'pending',
|
||
author_ip TEXT,
|
||
author_url TEXT,
|
||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', '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);
|
||
|
||
-- ── 内置模块:Pages(BUILTIN_PAGES=true) ────────────────
|
||
|
||
CREATE TABLE IF NOT EXISTS pages (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
document_id TEXT NOT NULL UNIQUE,
|
||
tenant_id TEXT NOT NULL DEFAULT 'default',
|
||
title TEXT NOT NULL,
|
||
slug TEXT NOT NULL UNIQUE,
|
||
content TEXT,
|
||
blocks TEXT,
|
||
meta_title TEXT,
|
||
meta_description TEXT,
|
||
og_image TEXT,
|
||
template TEXT NOT NULL DEFAULT 'default',
|
||
parent_id INTEGER REFERENCES pages(id),
|
||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||
status TEXT NOT NULL DEFAULT 'draft',
|
||
created_by INTEGER NOT NULL REFERENCES users(id),
|
||
updated_by INTEGER,
|
||
cover_image TEXT,
|
||
published_at TEXT,
|
||
password TEXT,
|
||
comment_status TEXT NOT NULL DEFAULT 'closed',
|
||
og_title TEXT,
|
||
og_description TEXT,
|
||
canonical_url TEXT,
|
||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', '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 INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
document_id TEXT NOT NULL UNIQUE,
|
||
tenant_id TEXT NOT NULL DEFAULT 'default',
|
||
name TEXT NOT NULL,
|
||
block_type TEXT NOT NULL,
|
||
content TEXT NOT NULL,
|
||
description TEXT,
|
||
created_by INTEGER,
|
||
updated_by INTEGER,
|
||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
|
||
);
|
||
|
||
CREATE INDEX IF NOT EXISTS idx_reusable_blocks_tenant ON reusable_blocks(tenant_id);
|
||
|
||
-- ── 内置模块:Media(BUILTIN_MEDIA=true) ────────────────
|
||
|
||
CREATE TABLE IF NOT EXISTS media (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
document_id TEXT NOT NULL UNIQUE,
|
||
tenant_id TEXT NOT NULL DEFAULT 'default',
|
||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||
filename TEXT NOT NULL,
|
||
filepath TEXT NOT NULL,
|
||
mimetype TEXT NOT NULL,
|
||
size INTEGER NOT NULL,
|
||
width INTEGER,
|
||
height INTEGER,
|
||
title TEXT,
|
||
alt_text TEXT,
|
||
caption TEXT,
|
||
description TEXT,
|
||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', '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);
|
||
|
||
-- ── 内置模块:Workflow(BUILTIN_WORKFLOW=true) ──────────
|
||
|
||
CREATE TABLE IF NOT EXISTS workflow_definitions (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
document_id TEXT NOT NULL UNIQUE,
|
||
name TEXT NOT NULL,
|
||
description TEXT,
|
||
steps TEXT NOT NULL,
|
||
initial_step TEXT NOT NULL,
|
||
version INTEGER NOT NULL DEFAULT 1,
|
||
enabled INTEGER NOT NULL DEFAULT 1,
|
||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS workflow_instances (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
document_id TEXT NOT NULL UNIQUE,
|
||
definition_id INTEGER NOT NULL REFERENCES workflow_definitions(id),
|
||
status TEXT NOT NULL DEFAULT 'running',
|
||
current_step TEXT,
|
||
context TEXT NOT NULL DEFAULT '{}',
|
||
triggered_by INTEGER,
|
||
started_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
completed_at TEXT,
|
||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', '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 INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
document_id TEXT NOT NULL UNIQUE,
|
||
instance_id INTEGER NOT NULL REFERENCES workflow_instances(id),
|
||
step_id TEXT NOT NULL,
|
||
step_name TEXT NOT NULL,
|
||
status TEXT NOT NULL DEFAULT 'running',
|
||
input TEXT,
|
||
output TEXT,
|
||
error TEXT,
|
||
started_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
completed_at TEXT
|
||
);
|
||
|
||
CREATE INDEX IF NOT EXISTS idx_wf_step_logs_instance ON workflow_step_logs(instance_id);
|
||
|
||
-- ============================================================
|
||
-- 预置数据
|
||
-- ============================================================
|
||
|
||
-- 系统角色
|
||
INSERT OR IGNORE INTO roles (document_id, tenant_id, name, description, is_system, created_at, updated_at) VALUES
|
||
('role-admin', 'default', 'admin', '超级管理员', 1, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
('role-editor', 'default', 'editor', '编辑', 0, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
('role-author', 'default', 'author', '作者', 0, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
('role-reader', 'default', 'reader', '读者', 1, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), strftime('%Y-%m-%dT%H:%M:%SZ', 'now'));
|
||
|
||
-- admin 全局权限
|
||
INSERT OR IGNORE INTO permissions (document_id, tenant_id, role_id, action, subject, fields, conditions, created_at) VALUES
|
||
('perm-admin-all', 'default', (SELECT id FROM roles WHERE document_id = 'role-admin'), '*', '*', '["*"]', NULL, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'));
|
||
|
||
-- editor 权限
|
||
INSERT OR IGNORE INTO permissions (document_id, tenant_id, role_id, action, subject, fields, conditions, created_at) VALUES
|
||
('perm-editor-ct-create', 'default', (SELECT id FROM roles WHERE document_id = 'role-editor'), 'content-type::*.*', 'content-type::*', '["*"]', NULL, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'));
|
||
|
||
-- author 权限
|
||
INSERT OR IGNORE INTO permissions (document_id, tenant_id, role_id, action, subject, fields, conditions, created_at) VALUES
|
||
('perm-author-post-create', 'default', (SELECT id FROM roles WHERE document_id = 'role-author'), 'content-type::post.create', 'content-type::post', '["*"]', NULL, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
('perm-author-post-read', 'default', (SELECT id FROM roles WHERE document_id = 'role-author'), 'content-type::post.read', 'content-type::post', '["*"]', NULL, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
('perm-author-post-update', 'default', (SELECT id FROM roles WHERE document_id = 'role-author'), 'content-type::post.update', 'content-type::post', '["*"]', '{"author_id":"$user.id"}', strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
('perm-author-post-delete', 'default', (SELECT id FROM roles WHERE document_id = 'role-author'), 'content-type::post.delete', 'content-type::post', '["*"]', '{"author_id":"$user.id"}', strftime('%Y-%m-%dT%H:%M:%SZ', 'now'));
|
||
|
||
-- reader 权限
|
||
INSERT OR IGNORE INTO permissions (document_id, tenant_id, role_id, action, subject, fields, conditions, created_at) VALUES
|
||
('perm-reader-post-read', 'default', (SELECT id FROM roles WHERE document_id = 'role-reader'), 'content-type::post.read', 'content-type::post', '["title","slug","content","excerpt","status"]', NULL, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
('perm-reader-comment-create', 'default', (SELECT id FROM roles WHERE document_id = 'role-reader'), 'content-type::comment.create', 'content-type::comment', '["content","nickname","email"]', NULL, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'));
|
||
|
||
-- 站点配置
|
||
INSERT OR IGNORE INTO options (document_id, tenant_id, option_key, value, type, group_name, label, description, validation, is_public, autoload, sort_order, updated_at) VALUES
|
||
('opt-site-title', 'default', 'site_title', '"My Blog"', 'text', 'general', '站点标题', '显示在浏览器标题栏和页面头部', '{"max_length":100}', 1, 1, 1, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
('opt-site-desc', 'default', 'site_description', '""', 'text', 'general', '站点描述', '简短描述站点用途', '{"max_length":500}', 1, 1, 2, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
('opt-site-url', 'default', 'site_url', '""', 'url', 'general', '站点 URL', '如 https://example.com', NULL, 1, 1, 3, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
('opt-admin-email', 'default', 'admin_email', '""', 'email', 'general', '管理员邮箱', NULL, NULL, 0, 1, 4, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
('opt-timezone', 'default', 'timezone', '"UTC"', 'select', 'general', '时区', NULL, '{"values":["UTC","Asia/Shanghai","Asia/Tokyo","US/Eastern","US/Pacific","Europe/London","Europe/Berlin"]}', 1, 1, 5, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
('opt-date-fmt', 'default', 'date_format', '"%Y-%m-%d"', 'select', 'general', '日期格式', NULL, '{"values":["%Y-%m-%d","%d/%m/%Y","%m/%d/%Y","%Y年%m月%d日"]}', 1, 1, 6, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
('opt-per-page', 'default', 'posts_per_page', '10', 'integer', 'reading', '每页文章数', NULL, '{"min":1,"max":100}', 1, 1, 10, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
('opt-rss-items', 'default', 'rss_items', '20', 'integer', 'reading', 'RSS 条目数', NULL, '{"min":1,"max":100}', 1, 1, 11, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
('opt-permalink', 'default', 'permalink_structure', '"/:year/:month/:slug"', 'select', 'reading', 'URL 结构', NULL, '{"values":["/:year/:month/:slug","/:slug","/posts/:slug"]}', 1, 1, 12, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
('opt-comment-mod', 'default', 'comment_moderation', 'true', 'boolean', 'discussion', '评论需审核', '开启后新评论需管理员审批', NULL, 0, 1, 20, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
('opt-comment-order', 'default', 'comment_order', '"asc"', 'select', 'discussion', '评论排序', NULL, '{"values":["asc","desc"]}', 1, 1, 21, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
('opt-default-role', 'default', 'default_role', '"reader"', 'select', 'discussion', '新用户默认角色', NULL, '{"values":["reader","author"]}', 0, 1, 22, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
('opt-theme', 'default', 'theme', '"default"', 'select', 'appearance', '当前主题', NULL, '{"values":["default","corporate","minimal","warm"]}', 1, 1, 30, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
('opt-maintenance', 'default', 'maintenance_mode', 'false', 'boolean', 'appearance', '维护模式', '开启后前台显示维护页面', NULL, 1, 1, 31, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'));
|
||
|
||
-- Products
|
||
CREATE TABLE IF NOT EXISTS products (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
document_id TEXT NOT NULL UNIQUE,
|
||
tenant_id TEXT NOT NULL DEFAULT 'default',
|
||
category_id INTEGER REFERENCES categories(id),
|
||
title TEXT NOT NULL,
|
||
description TEXT,
|
||
cover_url TEXT,
|
||
product_type TEXT NOT NULL DEFAULT 'custom',
|
||
fulfillment_type TEXT NOT NULL DEFAULT 'digital',
|
||
delivery_hook TEXT,
|
||
weight INTEGER,
|
||
shipping_template_id INTEGER,
|
||
price INTEGER NOT NULL CHECK(price >= 0),
|
||
currency TEXT NOT NULL DEFAULT 'USD',
|
||
status TEXT NOT NULL DEFAULT 'draft',
|
||
attributes TEXT,
|
||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||
version INTEGER NOT NULL DEFAULT 1,
|
||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
slug TEXT UNIQUE,
|
||
content TEXT,
|
||
image_ids TEXT,
|
||
original_price INTEGER,
|
||
specs TEXT,
|
||
unit TEXT 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 TEXT,
|
||
meta_description TEXT,
|
||
published_at TEXT,
|
||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', '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);
|
||
|
||
-- Orders
|
||
CREATE TABLE IF NOT EXISTS orders (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
document_id TEXT NOT NULL UNIQUE,
|
||
tenant_id TEXT NOT NULL DEFAULT 'default',
|
||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||
order_no TEXT NOT NULL UNIQUE,
|
||
subtotal INTEGER NOT NULL DEFAULT 0,
|
||
discount_amount INTEGER NOT NULL DEFAULT 0,
|
||
shipping_amount INTEGER NOT NULL DEFAULT 0,
|
||
total_amount INTEGER NOT NULL CHECK(total_amount >= 0),
|
||
currency TEXT NOT NULL DEFAULT 'USD',
|
||
status TEXT NOT NULL DEFAULT 'pending',
|
||
buyer_name TEXT,
|
||
buyer_phone TEXT,
|
||
buyer_email TEXT,
|
||
shipping_address TEXT,
|
||
tracking_no TEXT,
|
||
carrier TEXT,
|
||
remark TEXT,
|
||
admin_remark TEXT,
|
||
delivery_data TEXT,
|
||
paid_at TEXT,
|
||
completed_at TEXT,
|
||
cancelled_at TEXT,
|
||
refunding_at TEXT,
|
||
refunded_at TEXT,
|
||
expired_at TEXT,
|
||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', '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 INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
document_id TEXT NOT NULL UNIQUE,
|
||
tenant_id TEXT NOT NULL DEFAULT 'default',
|
||
order_id INTEGER NOT NULL REFERENCES orders(id),
|
||
product_id INTEGER REFERENCES products(id),
|
||
title TEXT NOT NULL,
|
||
description TEXT,
|
||
unit_price INTEGER NOT NULL CHECK(unit_price >= 0),
|
||
quantity INTEGER NOT NULL CHECK(quantity > 0),
|
||
subtotal INTEGER NOT NULL,
|
||
cover_url TEXT,
|
||
attributes TEXT,
|
||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', '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);
|
||
|
||
-- Payment Channels
|
||
CREATE TABLE IF NOT EXISTS payment_channels (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
document_id TEXT NOT NULL UNIQUE,
|
||
tenant_id TEXT NOT NULL DEFAULT 'default',
|
||
provider TEXT NOT NULL,
|
||
name TEXT NOT NULL,
|
||
is_live INTEGER NOT NULL DEFAULT 0,
|
||
credentials TEXT NOT NULL,
|
||
webhook_secret TEXT,
|
||
settings TEXT,
|
||
is_active INTEGER NOT NULL DEFAULT 1,
|
||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||
version INTEGER NOT NULL DEFAULT 1,
|
||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', '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 INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
document_id TEXT NOT NULL UNIQUE,
|
||
tenant_id TEXT NOT NULL DEFAULT 'default',
|
||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||
order_id TEXT,
|
||
title TEXT NOT NULL,
|
||
amount INTEGER NOT NULL,
|
||
currency TEXT NOT NULL DEFAULT 'USD',
|
||
channel_id INTEGER NOT NULL REFERENCES payment_channels(id),
|
||
provider TEXT NOT NULL,
|
||
provider_order_id TEXT,
|
||
provider_method TEXT,
|
||
status TEXT NOT NULL DEFAULT 'pending',
|
||
reference_type TEXT,
|
||
reference_id TEXT,
|
||
return_url TEXT,
|
||
idempotency_key TEXT NOT NULL UNIQUE,
|
||
version INTEGER NOT NULL DEFAULT 1,
|
||
provider_data TEXT,
|
||
client_ip TEXT,
|
||
client_language TEXT,
|
||
client_country TEXT,
|
||
client_user_agent TEXT,
|
||
channel_selected_by TEXT,
|
||
metadata TEXT,
|
||
paid_at TEXT,
|
||
cancelled_at TEXT,
|
||
expired_at TEXT,
|
||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', '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 INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
document_id TEXT NOT NULL UNIQUE,
|
||
tenant_id TEXT NOT NULL DEFAULT 'default',
|
||
payment_order_id INTEGER NOT NULL REFERENCES payment_orders(id),
|
||
order_id TEXT,
|
||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||
tx_type TEXT NOT NULL,
|
||
amount INTEGER NOT NULL,
|
||
currency TEXT NOT NULL,
|
||
provider_tx_id TEXT NOT NULL UNIQUE,
|
||
status TEXT NOT NULL DEFAULT 'pending',
|
||
raw_payload TEXT,
|
||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', '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 INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
document_id TEXT NOT NULL UNIQUE,
|
||
tenant_id TEXT NOT NULL DEFAULT 'default',
|
||
payment_order_id INTEGER NOT NULL REFERENCES payment_orders(id),
|
||
order_id TEXT,
|
||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||
amount INTEGER NOT NULL,
|
||
currency TEXT NOT NULL,
|
||
reason TEXT,
|
||
provider_refund_id TEXT,
|
||
status TEXT NOT NULL DEFAULT 'pending',
|
||
payment_tx_id INTEGER REFERENCES payment_transactions(id),
|
||
metadata TEXT,
|
||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', '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 INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
document_id TEXT NOT NULL UNIQUE,
|
||
tenant_id TEXT NOT NULL DEFAULT 'default',
|
||
user_id INTEGER NOT NULL,
|
||
currency TEXT NOT NULL,
|
||
amount INTEGER 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 TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', '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);
|