@@ -0,0 +1,294 @@
-- 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 ;