295 lines
14 KiB
PL/PgSQL
295 lines
14 KiB
PL/PgSQL
-- v3.5 PostgreSQL 基线结构。连接目标空数据库后执行。
|
||
-- 单事务创建;失败全部回滚;不使用 IF NOT EXISTS 掩盖既有结构差异。
|
||
-- 主键由服务端生成或使用旧 CloudBase _id,SQL 不重新生成旧 ID。
|
||
BEGIN;
|
||
SET LOCAL TIME ZONE 'UTC';
|
||
SET LOCAL lock_timeout = '5s';
|
||
SET LOCAL search_path = public, pg_catalog;
|
||
|
||
CREATE TABLE public.users (
|
||
id varchar(128) COLLATE "C" PRIMARY KEY,
|
||
mini_app_id varchar(64) COLLATE "C" NOT NULL,
|
||
openid varchar(128) COLLATE "C" NOT NULL,
|
||
unionid varchar(128) COLLATE "C",
|
||
nick_name varchar(100),
|
||
avatar_asset_id varchar(128) COLLATE "C",
|
||
total_downloads bigint NOT NULL DEFAULT 0,
|
||
role varchar(16) NOT NULL DEFAULT 'user',
|
||
status varchar(16) NOT NULL DEFAULT 'active',
|
||
auth_version integer NOT NULL DEFAULT 1,
|
||
created_at timestamptz(3) NOT NULL DEFAULT now(),
|
||
last_active_at timestamptz(3) NOT NULL DEFAULT now(),
|
||
updated_at timestamptz(3) NOT NULL DEFAULT now(),
|
||
CONSTRAINT users_mini_app_openid_key UNIQUE (mini_app_id, openid),
|
||
CONSTRAINT users_role_check CHECK (role IN ('user', 'admin')),
|
||
CONSTRAINT users_status_check CHECK (status IN ('active', 'disabled')),
|
||
CONSTRAINT users_total_downloads_check CHECK (total_downloads >= 0),
|
||
CONSTRAINT users_auth_version_check CHECK (auth_version >= 0)
|
||
);
|
||
|
||
CREATE INDEX users_unionid_idx ON public.users (unionid);
|
||
|
||
CREATE TABLE public.assets (
|
||
id varchar(128) COLLATE "C" PRIMARY KEY,
|
||
source_ref text COLLATE "C",
|
||
source_ref_hash char(64) COLLATE "C",
|
||
provider varchar(16) NOT NULL DEFAULT 'qiniu',
|
||
bucket varchar(128) COLLATE "C",
|
||
object_key text COLLATE "C",
|
||
object_ref_hash char(64) COLLATE "C",
|
||
external_url text COLLATE "C",
|
||
purpose varchar(32) NOT NULL,
|
||
owner_user_id varchar(128) COLLATE "C",
|
||
visibility varchar(16) NOT NULL,
|
||
mime_type varchar(128),
|
||
size_bytes bigint,
|
||
checksum char(64) COLLATE "C",
|
||
status varchar(16) NOT NULL DEFAULT 'pending',
|
||
last_error text,
|
||
created_at timestamptz(3) NOT NULL DEFAULT now(),
|
||
updated_at timestamptz(3) NOT NULL DEFAULT now(),
|
||
CONSTRAINT assets_source_ref_hash_key UNIQUE (source_ref_hash),
|
||
CONSTRAINT assets_object_ref_hash_key UNIQUE (object_ref_hash),
|
||
CONSTRAINT assets_owner_user_id_fkey FOREIGN KEY (owner_user_id)
|
||
REFERENCES public.users (id) ON UPDATE RESTRICT ON DELETE SET NULL,
|
||
CONSTRAINT assets_provider_check CHECK (provider IN ('qiniu', 'external')),
|
||
CONSTRAINT assets_purpose_check CHECK (
|
||
purpose IN ('avatar', 'preview', 'category_icon', 'font', 'share', 'page_config', 'other')
|
||
),
|
||
CONSTRAINT assets_visibility_check CHECK (visibility IN ('public', 'private')),
|
||
CONSTRAINT assets_avatar_visibility_check CHECK (purpose <> 'avatar' OR visibility = 'private'),
|
||
CONSTRAINT assets_status_check CHECK (status IN ('pending', 'ready', 'failed', 'deleting', 'deleted')),
|
||
CONSTRAINT assets_size_bytes_check CHECK (size_bytes IS NULL OR size_bytes >= 0),
|
||
CONSTRAINT assets_source_ref_hash_check CHECK (
|
||
source_ref_hash IS NULL OR source_ref_hash ~ '^[0-9a-f]{64}$'
|
||
),
|
||
CONSTRAINT assets_object_ref_hash_check CHECK (
|
||
object_ref_hash IS NULL OR object_ref_hash ~ '^[0-9a-f]{64}$'
|
||
),
|
||
CONSTRAINT assets_checksum_check CHECK (checksum IS NULL OR checksum ~ '^[0-9a-f]{64}$'),
|
||
CONSTRAINT assets_ready_reference_check CHECK (
|
||
status <> 'ready'
|
||
OR (
|
||
provider = 'qiniu'
|
||
AND bucket IS NOT NULL AND length(bucket) > 0
|
||
AND object_key IS NOT NULL AND length(object_key) > 0
|
||
AND object_ref_hash IS NOT NULL
|
||
AND mime_type IS NOT NULL AND length(mime_type) > 0
|
||
AND size_bytes IS NOT NULL
|
||
AND checksum IS NOT NULL
|
||
)
|
||
OR (
|
||
provider = 'external'
|
||
AND external_url IS NOT NULL
|
||
AND external_url ~ '^https://[^/[:space:]]+'
|
||
)
|
||
)
|
||
);
|
||
|
||
CREATE INDEX assets_owner_purpose_status_idx ON public.assets (owner_user_id, purpose, status);
|
||
CREATE INDEX assets_status_updated_at_idx ON public.assets (status, updated_at);
|
||
|
||
-- users 与 assets 双向关联:先建立两张表,再补头像外键。
|
||
ALTER TABLE public.users
|
||
ADD CONSTRAINT users_avatar_asset_id_fkey FOREIGN KEY (avatar_asset_id)
|
||
REFERENCES public.assets (id) ON UPDATE RESTRICT ON DELETE RESTRICT;
|
||
|
||
CREATE TABLE public.categories (
|
||
id varchar(128) COLLATE "C" PRIMARY KEY,
|
||
name varchar(100) NOT NULL,
|
||
icon varchar(1024) NOT NULL,
|
||
icon_asset_id varchar(128) COLLATE "C",
|
||
color varchar(32) NOT NULL,
|
||
sort_order integer NOT NULL DEFAULT 0,
|
||
parent_id varchar(128) COLLATE "C",
|
||
created_at timestamptz(3) NOT NULL DEFAULT now(),
|
||
updated_at timestamptz(3) NOT NULL DEFAULT now(),
|
||
CONSTRAINT categories_parent_id_fkey FOREIGN KEY (parent_id)
|
||
REFERENCES public.categories (id) ON UPDATE RESTRICT ON DELETE RESTRICT,
|
||
CONSTRAINT categories_icon_asset_id_fkey FOREIGN KEY (icon_asset_id)
|
||
REFERENCES public.assets (id) ON UPDATE RESTRICT ON DELETE RESTRICT,
|
||
CONSTRAINT categories_parent_not_self_check CHECK (parent_id IS NULL OR parent_id <> id)
|
||
);
|
||
|
||
CREATE INDEX categories_parent_sort_idx ON public.categories (parent_id, sort_order, id);
|
||
|
||
CREATE TABLE public.worksheets (
|
||
id varchar(128) COLLATE "C" PRIMARY KEY,
|
||
title varchar(100) NOT NULL,
|
||
subtitle varchar(500) NOT NULL,
|
||
category_id varchar(128) COLLATE "C" NOT NULL,
|
||
subcategory varchar(100),
|
||
path varchar(1024) NOT NULL,
|
||
age_min smallint NOT NULL,
|
||
age_max smallint NOT NULL,
|
||
grade smallint NOT NULL DEFAULT 0,
|
||
difficulty smallint NOT NULL,
|
||
preview_asset_id varchar(128) COLLATE "C" NOT NULL,
|
||
tags jsonb NOT NULL DEFAULT '[]'::jsonb,
|
||
is_new boolean NOT NULL DEFAULT false,
|
||
is_hot boolean NOT NULL DEFAULT false,
|
||
sort_order integer NOT NULL DEFAULT 0,
|
||
downloads bigint NOT NULL DEFAULT 0,
|
||
likes bigint NOT NULL DEFAULT 0,
|
||
downloads_seed bigint NOT NULL DEFAULT 0,
|
||
likes_seed bigint NOT NULL DEFAULT 0,
|
||
status varchar(16) NOT NULL DEFAULT 'draft',
|
||
created_at timestamptz(3) NOT NULL DEFAULT now(),
|
||
content_updated_at timestamptz(3),
|
||
updated_at timestamptz(3) NOT NULL DEFAULT now(),
|
||
CONSTRAINT worksheets_category_id_fkey FOREIGN KEY (category_id)
|
||
REFERENCES public.categories (id) ON UPDATE RESTRICT ON DELETE RESTRICT,
|
||
CONSTRAINT worksheets_preview_asset_id_fkey FOREIGN KEY (preview_asset_id)
|
||
REFERENCES public.assets (id) ON UPDATE RESTRICT ON DELETE RESTRICT,
|
||
CONSTRAINT worksheets_age_check CHECK (age_min >= 0 AND age_max >= age_min),
|
||
CONSTRAINT worksheets_difficulty_check CHECK (difficulty BETWEEN 1 AND 4),
|
||
CONSTRAINT worksheets_status_check CHECK (status IN ('draft', 'active', 'hidden')),
|
||
CONSTRAINT worksheets_counts_check CHECK (
|
||
downloads >= 0 AND likes >= 0 AND downloads_seed >= 0 AND likes_seed >= 0
|
||
),
|
||
CONSTRAINT worksheets_tags_array_check CHECK (jsonb_typeof(tags) = 'array')
|
||
);
|
||
|
||
CREATE INDEX worksheets_category_status_sort_idx
|
||
ON public.worksheets (category_id, status, sort_order, updated_at DESC, id);
|
||
CREATE INDEX worksheets_status_sort_idx
|
||
ON public.worksheets (status, sort_order, updated_at DESC, id);
|
||
CREATE INDEX worksheets_status_age_idx ON public.worksheets (status, age_min, age_max);
|
||
|
||
CREATE TABLE public.favorites (
|
||
id varchar(128) COLLATE "C" PRIMARY KEY,
|
||
user_id varchar(128) COLLATE "C" NOT NULL,
|
||
worksheet_id varchar(128) COLLATE "C" NOT NULL,
|
||
created_at timestamptz(3) NOT NULL DEFAULT now(),
|
||
CONSTRAINT favorites_user_id_fkey FOREIGN KEY (user_id)
|
||
REFERENCES public.users (id) ON UPDATE RESTRICT ON DELETE RESTRICT,
|
||
CONSTRAINT favorites_user_worksheet_key UNIQUE (user_id, worksheet_id)
|
||
);
|
||
|
||
CREATE INDEX favorites_user_created_at_idx
|
||
ON public.favorites (user_id, created_at DESC, id DESC);
|
||
|
||
CREATE TABLE public.download_logs (
|
||
id varchar(128) COLLATE "C" PRIMARY KEY,
|
||
user_id varchar(128) COLLATE "C" NOT NULL,
|
||
worksheet_id varchar(128) COLLATE "C" NOT NULL,
|
||
export_id varchar(128) COLLATE "C",
|
||
source varchar(16) NOT NULL DEFAULT 'mini',
|
||
params jsonb,
|
||
created_at timestamptz(3) NOT NULL DEFAULT now(),
|
||
CONSTRAINT download_logs_user_id_fkey FOREIGN KEY (user_id)
|
||
REFERENCES public.users (id) ON UPDATE RESTRICT ON DELETE RESTRICT,
|
||
-- 默认 NULLS DISTINCT 语义,历史 NULL export_id 可保存多条。
|
||
CONSTRAINT download_logs_user_export_key UNIQUE (user_id, export_id),
|
||
CONSTRAINT download_logs_source_check CHECK (source IN ('mini', 'legacy', 'import')),
|
||
CONSTRAINT download_logs_export_id_check CHECK (
|
||
(source = 'import' AND export_id IS NULL)
|
||
OR (export_id IS NOT NULL AND length(btrim(export_id)) > 0)
|
||
)
|
||
);
|
||
|
||
CREATE INDEX download_logs_user_created_at_idx
|
||
ON public.download_logs (user_id, created_at DESC, id DESC);
|
||
CREATE INDEX download_logs_worksheet_created_at_idx
|
||
ON public.download_logs (worksheet_id, created_at DESC, id DESC);
|
||
|
||
CREATE TABLE public.page_configs (
|
||
id varchar(128) COLLATE "C" PRIMARY KEY,
|
||
home jsonb,
|
||
category jsonb,
|
||
age jsonb,
|
||
version bigint NOT NULL DEFAULT 0,
|
||
updated_at timestamptz(3) NOT NULL DEFAULT now(),
|
||
published_version bigint,
|
||
published_at timestamptz(3),
|
||
CONSTRAINT page_configs_version_check CHECK (version >= 0),
|
||
CONSTRAINT page_configs_published_version_check CHECK (
|
||
published_version IS NULL OR (published_version >= 0 AND published_version <= version)
|
||
),
|
||
CONSTRAINT page_configs_home_object_check CHECK (home IS NULL OR jsonb_typeof(home) = 'object'),
|
||
CONSTRAINT page_configs_category_object_check CHECK (
|
||
category IS NULL OR jsonb_typeof(category) = 'object'
|
||
),
|
||
CONSTRAINT page_configs_age_object_check CHECK (age IS NULL OR jsonb_typeof(age) = 'object')
|
||
);
|
||
|
||
CREATE TABLE public.settings (
|
||
id varchar(128) COLLATE "C" PRIMARY KEY,
|
||
value jsonb NOT NULL,
|
||
version bigint NOT NULL DEFAULT 0,
|
||
updated_at timestamptz(3) NOT NULL DEFAULT now(),
|
||
CONSTRAINT settings_value_object_check CHECK (jsonb_typeof(value) = 'object'),
|
||
CONSTRAINT settings_version_check CHECK (version >= 0)
|
||
);
|
||
|
||
CREATE TABLE public.job_runs (
|
||
id varchar(128) COLLATE "C" PRIMARY KEY,
|
||
job_name varchar(64) COLLATE "C" NOT NULL,
|
||
run_key varchar(128) COLLATE "C" NOT NULL,
|
||
trigger_type varchar(16) NOT NULL,
|
||
status varchar(16) NOT NULL DEFAULT 'pending',
|
||
scheduled_at timestamptz(3),
|
||
started_at timestamptz(3),
|
||
finished_at timestamptz(3),
|
||
attempt integer NOT NULL DEFAULT 0,
|
||
lock_token varchar(128) COLLATE "C",
|
||
locked_until timestamptz(3),
|
||
result jsonb,
|
||
error_message text,
|
||
created_at timestamptz(3) NOT NULL DEFAULT now(),
|
||
updated_at timestamptz(3) NOT NULL DEFAULT now(),
|
||
CONSTRAINT job_runs_job_run_key UNIQUE (job_name, run_key),
|
||
CONSTRAINT job_runs_trigger_type_check CHECK (trigger_type IN ('scheduled', 'manual', 'recovery')),
|
||
CONSTRAINT job_runs_status_check CHECK (
|
||
status IN ('pending', 'running', 'succeeded', 'failed', 'skipped')
|
||
),
|
||
CONSTRAINT job_runs_attempt_check CHECK (attempt >= 0)
|
||
);
|
||
|
||
CREATE INDEX job_runs_status_locked_until_idx ON public.job_runs (status, locked_until);
|
||
CREATE INDEX job_runs_job_created_at_idx ON public.job_runs (job_name, created_at DESC);
|
||
|
||
CREATE TABLE public.admin_audit_logs (
|
||
id varchar(128) COLLATE "C" PRIMARY KEY,
|
||
actor_user_id varchar(128) COLLATE "C",
|
||
actor_type varchar(16) NOT NULL,
|
||
action varchar(64) NOT NULL,
|
||
resource_type varchar(64) NOT NULL,
|
||
resource_id varchar(128) COLLATE "C",
|
||
request_id varchar(128) COLLATE "C" NOT NULL,
|
||
before_data jsonb,
|
||
after_data jsonb,
|
||
reason text,
|
||
created_at timestamptz(3) NOT NULL DEFAULT now(),
|
||
CONSTRAINT admin_audit_logs_actor_type_check CHECK (actor_type IN ('admin', 'system', 'migration'))
|
||
);
|
||
|
||
CREATE INDEX admin_audit_logs_resource_created_at_idx
|
||
ON public.admin_audit_logs (resource_type, resource_id, created_at DESC);
|
||
CREATE INDEX admin_audit_logs_actor_created_at_idx
|
||
ON public.admin_audit_logs (actor_user_id, created_at DESC);
|
||
CREATE INDEX admin_audit_logs_request_id_idx ON public.admin_audit_logs (request_id);
|
||
|
||
COMMENT ON TABLE public.users IS '微信用户;沿用 CloudBase 字符串 ID,按 AppID + OpenID 唯一';
|
||
COMMENT ON COLUMN public.users.total_downloads IS '旧字段迁移兼容值;个人下载数量以 download_logs 查询为准';
|
||
COMMENT ON COLUMN public.users.auth_version IS '应用凭证版本;服务端鉴权校验,不由 SQL 自动递增';
|
||
COMMENT ON TABLE public.assets IS '七牛文件引用与旧资源迁移映射;不保存临时签名 URL';
|
||
COMMENT ON COLUMN public.assets.object_ref_hash IS '规范序列化 [provider,bucket,objectKey] 的 SHA-256;服务端生成并核对原引用';
|
||
COMMENT ON COLUMN public.assets.checksum IS '文件内容 SHA-256,不是七牛 ETag;非唯一';
|
||
COMMENT ON TABLE public.categories IS '分类树;多层循环需由服务端事务检查';
|
||
COMMENT ON TABLE public.worksheets IS '题型内容与真实/种子计数;资源通过 assets 引用';
|
||
COMMENT ON COLUMN public.worksheets.content_updated_at IS '仅内容修改时更新;不随计数变化;历史缺失保留 NULL';
|
||
COMMENT ON COLUMN public.worksheets.updated_at IS '由服务显式更新;包含计数、状态、内容变更';
|
||
COMMENT ON COLUMN public.worksheets.tags IS '字符串数组;数据库限制顶层 array,服务端校验元素';
|
||
COMMENT ON TABLE public.favorites IS '当前收藏关系;取消收藏删除关系并由业务事务扣减 likes';
|
||
COMMENT ON COLUMN public.favorites.worksheet_id IS '题型逻辑关联,不建外键,允许保留缺失题型的历史收藏';
|
||
COMMENT ON TABLE public.download_logs IS '成功保存历史;同用户 exportId 唯一,日志与计数由业务事务同步';
|
||
COMMENT ON COLUMN public.download_logs.worksheet_id IS '题型逻辑关联,不建外键,历史查询使用 LEFT JOIN';
|
||
COMMENT ON COLUMN public.download_logs.export_id IS '历史 import 可为 NULL;mini/legacy 必须提供非空事件 ID';
|
||
COMMENT ON TABLE public.page_configs IS '页面配置权威源;JSON 内持久化 assetId,DTO 解析资源地址';
|
||
COMMENT ON TABLE public.settings IS '运行配置;value 保留原文档业务字段';
|
||
COMMENT ON TABLE public.job_runs IS '任务逻辑去重、租约与执行结果;内容更新仍需业务行锁';
|
||
COMMENT ON TABLE public.admin_audit_logs IS '管理变更审计;操作者为逻辑关联,注销需主动去关联';
|
||
|
||
COMMIT;
|