Files
doodle-store/apps/server/prisma/sql/002-create-tables.sql
T
2026-10-12 16:56:42 +08:00

295 lines
14 KiB
PL/PgSQL
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.
-- 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;