feat: 完成分类、worksheet 接口开发,资源迁移到七牛
This commit is contained in:
@@ -1,7 +1,28 @@
|
||||
# 数据库结构与初始化 SQL
|
||||
# 数据库结构与 Prisma 迁移
|
||||
|
||||
已提供 [v3.5 初始化 SQL](./sql/README.md),包含建库、13 张表的最新基线结构、配置初始化和 004~008 增量升级。2026-10-14 已核对设计文档的 135 个字段,最新基线、完整旧库升级链及本地 doodle_dev 的字段、约束、索引和注释一致。开发库已执行至 008;原用户、题型、收藏及配置的保留规则通过临时库验证。本地 PostgreSQL 数据库由 [infra](../../../infra/README.md) 的 Docker 配置提供。
|
||||
2026-10-15 已接入 Prisma 7.10.0、PostgreSQL adapter 和服务端连接池,Schema 反向映射现有 13 张表、135 个字段。Prisma 模型使用 PascalCase,标量字段 camelCase,通过 map 保留数据库表名和列名;原字符串 ID、字段类型、可空性与默认值不变。客户端仅服务端使用,不向公共契约包或前端导出。
|
||||
|
||||
数据库已确认使用 PostgreSQL + Prisma;具体兼容版本在实施时验证并锁定,保留原用户和题型的字符串 ID。目标设计见 [v3.5 数据库设计文档](../../../docs/v3.5/数据库设计文档.md)。当前手工 SQL 基线和增量升级已执行;Prisma 尚未安装,尚未创建 Schema、登记 Prisma 迁移历史或接入应用数据库查询。
|
||||
## 基线与已有数据
|
||||
|
||||
本目录及未来 Prisma Client 只能由服务端使用,不能作为公共契约包被 admin、web 或小程序导入。
|
||||
`migrations/0_v35_baseline/migration.sql` 来自已验证的最新 SQL 002/003,合为一个事务,保留 CHECK、C 排序规则、原外键删除规则、两种收藏部分唯一索引及初始配置。CHECK、排序规则及注释不能仅从 Prisma Schema 重建;部分唯一索引由 introspection 加入 partialIndexes 预览声明,实际约束仍以 SQL 为准。
|
||||
|
||||
- 新空库:配置正确的 DATABASE_URL,再从项目根运行 `pnpm --filter @doodle/server db:deploy`。
|
||||
- 已按 002/003 或完整 004~008 链升级的库:先逐项核对实际结构、配置和数据,再执行 `pnpm --filter @doodle/server exec prisma migrate resolve --applied 0_v35_baseline`;不重跑建表。
|
||||
- 检查状态:`pnpm --filter @doodle/server db:status`。
|
||||
- 后续变更:新增迁移,保持已应用基线不变。不要对现有开发库使用 db push/reset,也不要在最新基线之上重复执行历史 004~008。
|
||||
|
||||
本地开发库已在完整结构核对后登记基线,新增 `_prisma_migrations` 技术表,仍为 13 张业务表;已有两条初始配置保留。后续 Prisma 迁移只通过正式新增 migration 变更结构,原 SQL 目录作为历史及设计参考保留。
|
||||
|
||||
## 连接、生成与测试
|
||||
|
||||
CLI 由 `prisma.config.ts` 加载服务端 `.env`;应用从已验证的 ServerConfig 读取连接串,不搜索根 Compose `.env`。用户密码等信息不得写入 Schema、迁移或日志。
|
||||
|
||||
`pnpm --filter @doodle/server prisma:generate` 生成忽略提交的 `src/generated/prisma`;dev/build/typecheck/test 脚本均会先生成。使用 CommonJS 模式匹配现有 NestJS 工程,不引入前端数据库客户端。
|
||||
|
||||
`pnpm test:db` 自动创建随机命名的本机临时库,执行正式迁移,验证微信登录和真实账户鉴权,随后删除临时库;不会清空或写入开发库业务数据。默认 `pnpm check` 不需要真实数据库。
|
||||
|
||||
范围见 [服务端登录接入说明](../../../docs/v3.5/服务端登录接入说明.md),完整模型见 [数据库设计](../../../docs/v3.5/数据库设计文档.md),旧 SQL 执行档案见 [sql/README](./sql/README.md)。
|
||||
|
||||
## 分类发布状态(2026-10-16)
|
||||
|
||||
保持已应用的 0_v35_baseline 不变,新增 20261016000000_category_publication。当前 Schema 为 13 张业务表、136 个字段。既有分类迁移为 active,后续创建默认 draft,增加状态 CHECK 与索引;题型和资源复用现有状态列。本地已 deploy 并核对全部业务数据未改变(分类新增状态除外)。详见 [内容发布与可见性](../../../docs/v3.5/内容发布与可见性.md)。
|
||||
|
||||
@@ -0,0 +1,419 @@
|
||||
-- v3.5 已有结构的 Prisma 基线;保留原始 SQL 的 C 排序规则、CHECK 和部分唯一索引。
|
||||
-- 已初始化数据库先核对结构再 migrate resolve --applied,不重建已有业务表。
|
||||
BEGIN;
|
||||
-- v3.5 PostgreSQL 基线结构。连接目标空数据库后执行。
|
||||
-- 单事务创建;失败全部回滚;不使用 IF NOT EXISTS 掩盖既有结构差异。
|
||||
-- 主键由服务端生成或使用旧 CloudBase _id,SQL 不重新生成旧 ID。
|
||||
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,
|
||||
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_openid_key UNIQUE (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', 'worksheet_source', '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,
|
||||
generation_mode varchar(16) NOT NULL DEFAULT 'procedural',
|
||||
renderer_key varchar(64) COLLATE "C",
|
||||
render_config jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
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',
|
||||
first_published_at timestamptz(3),
|
||||
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_generation_mode_check CHECK (generation_mode IN ('procedural', 'resource')),
|
||||
CONSTRAINT worksheets_renderer_key_check CHECK (
|
||||
renderer_key IS NULL OR length(btrim(renderer_key)) > 0
|
||||
),
|
||||
CONSTRAINT worksheets_render_config_object_check CHECK (jsonb_typeof(render_config) = 'object'),
|
||||
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);
|
||||
|
||||
-- 年级与内容分类独立;同一 worksheet 可适用于多个(含不连续)年级。
|
||||
-- 不从年龄或旧 grade 自动生成关系,避免把推算值当作已核实的适用范围。
|
||||
CREATE TABLE public.worksheet_grades (
|
||||
worksheet_id varchar(128) COLLATE "C" NOT NULL,
|
||||
grade smallint NOT NULL,
|
||||
CONSTRAINT worksheet_grades_pkey PRIMARY KEY (worksheet_id, grade),
|
||||
CONSTRAINT worksheet_grades_worksheet_id_fkey FOREIGN KEY (worksheet_id)
|
||||
REFERENCES public.worksheets (id) ON UPDATE RESTRICT ON DELETE RESTRICT,
|
||||
CONSTRAINT worksheet_grades_grade_check CHECK (grade BETWEEN -4 AND 6)
|
||||
);
|
||||
|
||||
-- 联合主键支持查询功能的适用年级;反向索引用于年级菜单及筛选。
|
||||
CREATE INDEX worksheet_grades_grade_worksheet_idx
|
||||
ON public.worksheet_grades (grade, worksheet_id);
|
||||
|
||||
-- 资源条目代表一份完整可选择内容,不等于单个文件,也不是独立模板引擎。
|
||||
CREATE TABLE public.worksheet_resources (
|
||||
id varchar(128) COLLATE "C" PRIMARY KEY,
|
||||
worksheet_id varchar(128) COLLATE "C" NOT NULL,
|
||||
title varchar(100) NOT NULL,
|
||||
preview_asset_id varchar(128) COLLATE "C" NOT NULL,
|
||||
config_override jsonb,
|
||||
status varchar(16) NOT NULL DEFAULT 'draft',
|
||||
sort_order integer NOT NULL DEFAULT 0,
|
||||
downloads bigint NOT NULL DEFAULT 0,
|
||||
first_published_at timestamptz(3),
|
||||
created_at timestamptz(3) NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz(3) NOT NULL DEFAULT now(),
|
||||
CONSTRAINT worksheet_resources_worksheet_id_fkey FOREIGN KEY (worksheet_id)
|
||||
REFERENCES public.worksheets (id) ON UPDATE RESTRICT ON DELETE RESTRICT,
|
||||
CONSTRAINT worksheet_resources_preview_asset_id_fkey FOREIGN KEY (preview_asset_id)
|
||||
REFERENCES public.assets (id) ON UPDATE RESTRICT ON DELETE RESTRICT,
|
||||
CONSTRAINT worksheet_resources_downloads_check CHECK (downloads >= 0),
|
||||
CONSTRAINT worksheet_resources_status_check CHECK (status IN ('draft', 'active', 'hidden')),
|
||||
CONSTRAINT worksheet_resources_config_object_check CHECK (
|
||||
config_override IS NULL OR jsonb_typeof(config_override) = 'object'
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX worksheet_resources_worksheet_status_sort_idx
|
||||
ON public.worksheet_resources (worksheet_id, status, sort_order, id);
|
||||
CREATE INDEX worksheet_resources_preview_asset_idx ON public.worksheet_resources (preview_asset_id);
|
||||
|
||||
-- 首期仅支持线框和参考图;允许多个同用途文件,排序并列时按 id 确定顺序。
|
||||
-- 文件格式由 assets 描述;是否打印、如何排列由 renderer 决定。
|
||||
CREATE TABLE public.worksheet_resource_assets (
|
||||
id varchar(128) COLLATE "C" PRIMARY KEY,
|
||||
worksheet_resource_id varchar(128) COLLATE "C" NOT NULL,
|
||||
asset_id varchar(128) COLLATE "C" NOT NULL,
|
||||
role varchar(16) NOT NULL,
|
||||
sort_order integer NOT NULL DEFAULT 0,
|
||||
CONSTRAINT worksheet_resource_assets_resource_id_fkey FOREIGN KEY (worksheet_resource_id)
|
||||
REFERENCES public.worksheet_resources (id) ON UPDATE RESTRICT ON DELETE RESTRICT,
|
||||
CONSTRAINT worksheet_resource_assets_asset_id_fkey FOREIGN KEY (asset_id)
|
||||
REFERENCES public.assets (id) ON UPDATE RESTRICT ON DELETE RESTRICT,
|
||||
CONSTRAINT worksheet_resource_assets_role_check CHECK (role IN ('outline', 'reference')),
|
||||
CONSTRAINT worksheet_resource_assets_resource_role_asset_key
|
||||
UNIQUE (worksheet_resource_id, role, asset_id)
|
||||
);
|
||||
|
||||
CREATE INDEX worksheet_resource_assets_resource_role_sort_idx
|
||||
ON public.worksheet_resource_assets (worksheet_resource_id, role, sort_order, id);
|
||||
CREATE INDEX worksheet_resource_assets_asset_idx ON public.worksheet_resource_assets (asset_id);
|
||||
|
||||
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,
|
||||
worksheet_resource_id varchar(128) COLLATE "C",
|
||||
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_resource_id_check CHECK (
|
||||
worksheet_resource_id IS NULL OR length(btrim(worksheet_resource_id)) > 0
|
||||
)
|
||||
);
|
||||
|
||||
-- NULL 表示功能收藏;非 NULL 表示完整内容收藏,不另加 type 字段。
|
||||
-- 部分唯一索引分别去重,允许同用户收藏一个功能及其中多个资源。
|
||||
CREATE UNIQUE INDEX favorites_user_worksheet_key
|
||||
ON public.favorites (user_id, worksheet_id)
|
||||
WHERE worksheet_resource_id IS NULL;
|
||||
CREATE UNIQUE INDEX favorites_user_resource_key
|
||||
ON public.favorites (user_id, worksheet_resource_id)
|
||||
WHERE worksheet_resource_id IS NOT NULL;
|
||||
|
||||
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,
|
||||
worksheet_resource_id varchar(128) COLLATE "C",
|
||||
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,按 OpenID 唯一;AppID 由服务端配置';
|
||||
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.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.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 '管理变更审计;操作者为逻辑关联,注销需主动去关联';
|
||||
|
||||
COMMENT ON COLUMN public.worksheets.generation_mode IS 'procedural 算法生成;resource 完整内容资源驱动;旧数据默认 procedural,导入逐项核对';
|
||||
COMMENT ON COLUMN public.worksheets.renderer_key IS '客户端绘制实现标识;多题型可共用;NULL 仅供旧版 ID/path 映射兼容,新功能发布必须明确';
|
||||
COMMENT ON COLUMN public.worksheets.render_config IS '共用绘制参数;仅允许已定义参数,不存执行代码或密钥';
|
||||
COMMENT ON TABLE public.worksheet_resources IS 'worksheet 下可选择的完整内容;独立预览及轻量配置覆盖';
|
||||
COMMENT ON COLUMN public.worksheet_resources.preview_asset_id IS '内容列表预览图;与绘制参考图用途独立,允许引用同一资产';
|
||||
COMMENT ON COLUMN public.worksheet_resources.config_override IS '可选参数覆盖;应用按 renderer 白名单校验,不支持任意深度合并';
|
||||
COMMENT ON TABLE public.worksheet_resource_assets IS '完整内容的文件输入;outline 线框,reference 对照图;不保存文件级配置';
|
||||
COMMENT ON COLUMN public.worksheet_resource_assets.role IS '业务用途,不是文件格式;用途对应的文件数量、类型和是否打印由 renderer 校验';
|
||||
COMMENT ON COLUMN public.worksheet_resource_assets.sort_order IS '同用途文件的处理顺序,并列按 id 排序;不等于通用图层 z-index';
|
||||
COMMENT ON COLUMN public.download_logs.worksheet_resource_id IS '可空资源条目逻辑关联;历史和算法生成可空;新写入校验属于同一 worksheet';
|
||||
|
||||
COMMENT ON TABLE public.worksheet_grades IS '功能适用年级的唯一权威关系;与内容分类独立,不从年龄自动推算';
|
||||
COMMENT ON COLUMN public.worksheet_grades.grade IS '-4 托班、-3 小班、-2 中班、-1 大班、0 幼小衔接、1至6 小学年级;无关系表示未配置,不表示全年级';
|
||||
COMMENT ON COLUMN public.worksheets.grade IS '旧版单年级兼容值;可能由年龄推算;保留原值,新年级筛选使用 worksheet_grades';
|
||||
COMMENT ON TABLE public.categories IS '内容分类树,与适用年级独立;多层循环需由服务端事务检查';
|
||||
|
||||
COMMENT ON COLUMN public.worksheets.first_published_at IS '首次公开发布时间;新功能由发布事务写入,重发/新增资源不重置;旧时间未知保持 NULL';
|
||||
COMMENT ON COLUMN public.worksheets.is_new IS '旧版新品兼容标记;新功能新品由首次发布时间及时间窗口派生,不作为新业务权威值';
|
||||
COMMENT ON COLUMN public.worksheets.is_hot IS '功能级人工运营推荐标记,不是下载排行榜计算结果,不自动传递给资源';
|
||||
COMMENT ON COLUMN public.worksheets.downloads IS '功能累计成功保存次数;保留旧累计值,资源下载也计入,不等于资源计数总和';
|
||||
COMMENT ON COLUMN public.worksheet_resources.first_published_at IS '内容首次公开发布时间;用于资源新品判断,草稿/重发不重置,未知旧时间保持 NULL';
|
||||
COMMENT ON COLUMN public.worksheet_resources.downloads IS '内容累计成功保存次数;仅有可信资源归属的新事件递增,不分摊功能旧计数,不因个人日志删除回减';
|
||||
|
||||
COMMENT ON TABLE public.favorites IS '功能和完整内容的当前收藏;取消即删除;仅功能收藏增减 worksheets.likes';
|
||||
COMMENT ON COLUMN public.favorites.worksheet_id IS '稳定功能逻辑关联;内容收藏的功能归属由服务端查询确定,历史缺失时保留';
|
||||
COMMENT ON COLUMN public.favorites.worksheet_resource_id IS 'NULL 是功能收藏,非 NULL 是完整内容收藏;逻辑关联,缺失/下架不清空,不退化为功能收藏';
|
||||
|
||||
|
||||
-- 初始配置锁行;可重复执行,绝不覆盖已经导入的页面、版本或 enabled=false。
|
||||
-- 不插入假用户、假题型或生产管理员;不启动任何调度器。
|
||||
SET LOCAL TIME ZONE 'UTC';
|
||||
|
||||
INSERT INTO public.page_configs (id, home, category, age, version)
|
||||
VALUES ('current', NULL, NULL, NULL, 0)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO public.settings (id, value, version)
|
||||
VALUES ('homeAutoRefresh', '{"enabled": true}'::jsonb, 0)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,9 @@
|
||||
-- 既有分类保留当前公开行为;以后新建分类默认草稿。不要重跑已成功的增量迁移。
|
||||
BEGIN;
|
||||
ALTER TABLE public.categories ADD COLUMN status varchar(16) NOT NULL DEFAULT 'draft';
|
||||
UPDATE public.categories SET status = 'active';
|
||||
ALTER TABLE public.categories ADD CONSTRAINT categories_status_check
|
||||
CHECK (status IN ('draft', 'active', 'hidden'));
|
||||
CREATE INDEX categories_status_parent_sort_idx ON public.categories (status, parent_id, sort_order, id);
|
||||
COMMENT ON COLUMN public.categories.status IS '发布状态:draft 开发测试,active 上线,hidden 下架;公开访问还需所有上级分类 active';
|
||||
COMMIT;
|
||||
@@ -0,0 +1 @@
|
||||
provider = "postgresql"
|
||||
@@ -0,0 +1,293 @@
|
||||
// 由现有 v3.5 PostgreSQL 结构反向映射。
|
||||
// CHECK、C 排序规则与部分唯一索引以 migrations 中保留的原始 SQL 为准。
|
||||
generator client {
|
||||
provider = "prisma-client"
|
||||
output = "../src/generated/prisma"
|
||||
previewFeatures = ["partialIndexes"]
|
||||
importFileExtension = "js"
|
||||
moduleFormat = "cjs"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
}
|
||||
|
||||
/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info.
|
||||
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
|
||||
model AdminAuditLog {
|
||||
id String @id @db.VarChar(128)
|
||||
actorUserId String? @map("actor_user_id") @db.VarChar(128)
|
||||
actorType String @map("actor_type") @db.VarChar(16)
|
||||
action String @db.VarChar(64)
|
||||
resourceType String @map("resource_type") @db.VarChar(64)
|
||||
resourceId String? @map("resource_id") @db.VarChar(128)
|
||||
requestId String @map("request_id") @db.VarChar(128)
|
||||
beforeData Json? @map("before_data")
|
||||
afterData Json? @map("after_data")
|
||||
reason String?
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
|
||||
@@index([actorUserId, createdAt(sort: Desc)], map: "admin_audit_logs_actor_created_at_idx")
|
||||
@@index([requestId])
|
||||
@@index([resourceType, resourceId, createdAt(sort: Desc)], map: "admin_audit_logs_resource_created_at_idx")
|
||||
@@map("admin_audit_logs")
|
||||
}
|
||||
|
||||
/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info.
|
||||
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
|
||||
model Asset {
|
||||
id String @id @db.VarChar(128)
|
||||
sourceRef String? @map("source_ref")
|
||||
sourceRefHash String? @unique @map("source_ref_hash") @db.Char(64)
|
||||
provider String @default("qiniu") @db.VarChar(16)
|
||||
bucket String? @db.VarChar(128)
|
||||
objectKey String? @map("object_key")
|
||||
objectRefHash String? @unique @map("object_ref_hash") @db.Char(64)
|
||||
externalUrl String? @map("external_url")
|
||||
purpose String @db.VarChar(32)
|
||||
ownerUserId String? @map("owner_user_id") @db.VarChar(128)
|
||||
visibility String @db.VarChar(16)
|
||||
mimeType String? @map("mime_type") @db.VarChar(128)
|
||||
sizeBytes BigInt? @map("size_bytes")
|
||||
checksum String? @db.Char(64)
|
||||
status String @default("pending") @db.VarChar(16)
|
||||
lastError String? @map("last_error")
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @default(now()) @map("updated_at") @db.Timestamptz(3)
|
||||
usersAssetsOwnerUserIdTousers User? @relation("assets_owner_user_idTousers", fields: [ownerUserId], references: [id], onUpdate: Restrict)
|
||||
categories Category[]
|
||||
usersUsersAvatarAssetIdToassets User[] @relation("users_avatar_asset_idToassets")
|
||||
worksheetResourceAssets WorksheetResourceAsset[]
|
||||
worksheetResources WorksheetResource[]
|
||||
worksheets Worksheet[]
|
||||
|
||||
@@index([ownerUserId, purpose, status], map: "assets_owner_purpose_status_idx")
|
||||
@@index([status, updatedAt])
|
||||
@@map("assets")
|
||||
}
|
||||
|
||||
/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info.
|
||||
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
|
||||
model Category {
|
||||
status String @default("draft") @db.VarChar(16)
|
||||
id String @id @db.VarChar(128)
|
||||
name String @db.VarChar(100)
|
||||
icon String @db.VarChar(1024)
|
||||
iconAssetId String? @map("icon_asset_id") @db.VarChar(128)
|
||||
color String @db.VarChar(32)
|
||||
sortOrder Int @default(0) @map("sort_order")
|
||||
parentId String? @map("parent_id") @db.VarChar(128)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @default(now()) @map("updated_at") @db.Timestamptz(3)
|
||||
assets Asset? @relation(fields: [iconAssetId], references: [id], onDelete: Restrict, onUpdate: Restrict)
|
||||
categories Category? @relation("categoriesTocategories", fields: [parentId], references: [id], onDelete: Restrict, onUpdate: Restrict)
|
||||
otherCategories Category[] @relation("categoriesTocategories")
|
||||
worksheets Worksheet[]
|
||||
|
||||
@@index([parentId, sortOrder, id], map: "categories_parent_sort_idx")
|
||||
@@index([status, parentId, sortOrder, id], map: "categories_status_parent_sort_idx")
|
||||
@@map("categories")
|
||||
}
|
||||
|
||||
/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info.
|
||||
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
|
||||
model DownloadLog {
|
||||
id String @id @db.VarChar(128)
|
||||
userId String @map("user_id") @db.VarChar(128)
|
||||
worksheetId String @map("worksheet_id") @db.VarChar(128)
|
||||
exportId String? @map("export_id") @db.VarChar(128)
|
||||
source String @default("mini") @db.VarChar(16)
|
||||
params Json?
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
worksheetResourceId String? @map("worksheet_resource_id") @db.VarChar(128)
|
||||
users User @relation(fields: [userId], references: [id], onUpdate: Restrict)
|
||||
|
||||
@@unique([userId, exportId], map: "download_logs_user_export_key")
|
||||
@@index([userId, createdAt(sort: Desc), id(sort: Desc)], map: "download_logs_user_created_at_idx")
|
||||
@@index([worksheetId, createdAt(sort: Desc), id(sort: Desc)], map: "download_logs_worksheet_created_at_idx")
|
||||
@@map("download_logs")
|
||||
}
|
||||
|
||||
/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info.
|
||||
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
|
||||
model Favorite {
|
||||
id String @id @db.VarChar(128)
|
||||
userId String @map("user_id") @db.VarChar(128)
|
||||
worksheetId String @map("worksheet_id") @db.VarChar(128)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
worksheetResourceId String? @map("worksheet_resource_id") @db.VarChar(128)
|
||||
users User @relation(fields: [userId], references: [id], onUpdate: Restrict)
|
||||
|
||||
@@unique([userId, worksheetResourceId], map: "favorites_user_resource_key", where: raw("(worksheet_resource_id IS NOT NULL)"))
|
||||
@@unique([userId, worksheetId], map: "favorites_user_worksheet_key", where: raw("(worksheet_resource_id IS NULL)"))
|
||||
@@index([userId, createdAt(sort: Desc), id(sort: Desc)], map: "favorites_user_created_at_idx")
|
||||
@@map("favorites")
|
||||
}
|
||||
|
||||
/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info.
|
||||
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
|
||||
model JobRun {
|
||||
id String @id @db.VarChar(128)
|
||||
jobName String @map("job_name") @db.VarChar(64)
|
||||
runKey String @map("run_key") @db.VarChar(128)
|
||||
triggerType String @map("trigger_type") @db.VarChar(16)
|
||||
status String @default("pending") @db.VarChar(16)
|
||||
scheduledAt DateTime? @map("scheduled_at") @db.Timestamptz(3)
|
||||
startedAt DateTime? @map("started_at") @db.Timestamptz(3)
|
||||
finishedAt DateTime? @map("finished_at") @db.Timestamptz(3)
|
||||
attempt Int @default(0)
|
||||
lockToken String? @map("lock_token") @db.VarChar(128)
|
||||
lockedUntil DateTime? @map("locked_until") @db.Timestamptz(3)
|
||||
result Json?
|
||||
errorMessage String? @map("error_message")
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @default(now()) @map("updated_at") @db.Timestamptz(3)
|
||||
|
||||
@@unique([jobName, runKey], map: "job_runs_job_run_key")
|
||||
@@index([jobName, createdAt(sort: Desc)], map: "job_runs_job_created_at_idx")
|
||||
@@index([status, lockedUntil])
|
||||
@@map("job_runs")
|
||||
}
|
||||
|
||||
/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info.
|
||||
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
|
||||
model PageConfig {
|
||||
id String @id @db.VarChar(128)
|
||||
home Json?
|
||||
category Json?
|
||||
age Json?
|
||||
version BigInt @default(0)
|
||||
updatedAt DateTime @default(now()) @map("updated_at") @db.Timestamptz(3)
|
||||
publishedVersion BigInt? @map("published_version")
|
||||
publishedAt DateTime? @map("published_at") @db.Timestamptz(3)
|
||||
|
||||
@@map("page_configs")
|
||||
}
|
||||
|
||||
/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info.
|
||||
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
|
||||
model Setting {
|
||||
id String @id @db.VarChar(128)
|
||||
value Json
|
||||
version BigInt @default(0)
|
||||
updatedAt DateTime @default(now()) @map("updated_at") @db.Timestamptz(3)
|
||||
|
||||
@@map("settings")
|
||||
}
|
||||
|
||||
/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info.
|
||||
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
|
||||
model User {
|
||||
id String @id @db.VarChar(128)
|
||||
openid String @unique @db.VarChar(128)
|
||||
unionid String? @db.VarChar(128)
|
||||
nickName String? @map("nick_name") @db.VarChar(100)
|
||||
avatarAssetId String? @map("avatar_asset_id") @db.VarChar(128)
|
||||
totalDownloads BigInt @default(0) @map("total_downloads")
|
||||
role String @default("user") @db.VarChar(16)
|
||||
status String @default("active") @db.VarChar(16)
|
||||
authVersion Int @default(1) @map("auth_version")
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
lastActiveAt DateTime @default(now()) @map("last_active_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @default(now()) @map("updated_at") @db.Timestamptz(3)
|
||||
assetsAssetsOwnerUserIdTousers Asset[] @relation("assets_owner_user_idTousers")
|
||||
downloadLogs DownloadLog[]
|
||||
favorites Favorite[]
|
||||
assetsUsersAvatarAssetIdToassets Asset? @relation("users_avatar_asset_idToassets", fields: [avatarAssetId], references: [id], onDelete: Restrict, onUpdate: Restrict)
|
||||
|
||||
@@index([unionid])
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info.
|
||||
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
|
||||
model WorksheetGrade {
|
||||
worksheetId String @map("worksheet_id") @db.VarChar(128)
|
||||
grade Int @db.SmallInt
|
||||
worksheets Worksheet @relation(fields: [worksheetId], references: [id], onUpdate: Restrict)
|
||||
|
||||
@@id([worksheetId, grade])
|
||||
@@index([grade, worksheetId], map: "worksheet_grades_grade_worksheet_idx")
|
||||
@@map("worksheet_grades")
|
||||
}
|
||||
|
||||
/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info.
|
||||
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
|
||||
model WorksheetResourceAsset {
|
||||
id String @id @db.VarChar(128)
|
||||
worksheetResourceId String @map("worksheet_resource_id") @db.VarChar(128)
|
||||
assetId String @map("asset_id") @db.VarChar(128)
|
||||
role String @db.VarChar(16)
|
||||
sortOrder Int @default(0) @map("sort_order")
|
||||
assets Asset @relation(fields: [assetId], references: [id], onUpdate: Restrict)
|
||||
worksheetResources WorksheetResource @relation(fields: [worksheetResourceId], references: [id], onUpdate: Restrict, map: "worksheet_resource_assets_resource_id_fkey")
|
||||
|
||||
@@unique([worksheetResourceId, role, assetId], map: "worksheet_resource_assets_resource_role_asset_key")
|
||||
@@index([assetId], map: "worksheet_resource_assets_asset_idx")
|
||||
@@index([worksheetResourceId, role, sortOrder, id], map: "worksheet_resource_assets_resource_role_sort_idx")
|
||||
@@map("worksheet_resource_assets")
|
||||
}
|
||||
|
||||
/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info.
|
||||
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
|
||||
model WorksheetResource {
|
||||
id String @id @db.VarChar(128)
|
||||
worksheetId String @map("worksheet_id") @db.VarChar(128)
|
||||
title String @db.VarChar(100)
|
||||
previewAssetId String @map("preview_asset_id") @db.VarChar(128)
|
||||
configOverride Json? @map("config_override")
|
||||
status String @default("draft") @db.VarChar(16)
|
||||
sortOrder Int @default(0) @map("sort_order")
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @default(now()) @map("updated_at") @db.Timestamptz(3)
|
||||
firstPublishedAt DateTime? @map("first_published_at") @db.Timestamptz(3)
|
||||
downloads BigInt @default(0)
|
||||
worksheetResourceAssets WorksheetResourceAsset[]
|
||||
assets Asset @relation(fields: [previewAssetId], references: [id], onUpdate: Restrict)
|
||||
worksheets Worksheet @relation(fields: [worksheetId], references: [id], onUpdate: Restrict)
|
||||
|
||||
@@index([previewAssetId], map: "worksheet_resources_preview_asset_idx")
|
||||
@@index([worksheetId, status, sortOrder, id], map: "worksheet_resources_worksheet_status_sort_idx")
|
||||
@@map("worksheet_resources")
|
||||
}
|
||||
|
||||
/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info.
|
||||
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
|
||||
model Worksheet {
|
||||
id String @id @db.VarChar(128)
|
||||
title String @db.VarChar(100)
|
||||
subtitle String @db.VarChar(500)
|
||||
categoryId String @map("category_id") @db.VarChar(128)
|
||||
subcategory String? @db.VarChar(100)
|
||||
path String @db.VarChar(1024)
|
||||
ageMin Int @map("age_min") @db.SmallInt
|
||||
ageMax Int @map("age_max") @db.SmallInt
|
||||
grade Int @default(0) @db.SmallInt
|
||||
difficulty Int @db.SmallInt
|
||||
previewAssetId String @map("preview_asset_id") @db.VarChar(128)
|
||||
tags Json @default("[]")
|
||||
isNew Boolean @default(false) @map("is_new")
|
||||
isHot Boolean @default(false) @map("is_hot")
|
||||
sortOrder Int @default(0) @map("sort_order")
|
||||
downloads BigInt @default(0)
|
||||
likes BigInt @default(0)
|
||||
downloadsSeed BigInt @default(0) @map("downloads_seed")
|
||||
likesSeed BigInt @default(0) @map("likes_seed")
|
||||
status String @default("draft") @db.VarChar(16)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
contentUpdatedAt DateTime? @map("content_updated_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @default(now()) @map("updated_at") @db.Timestamptz(3)
|
||||
generationMode String @default("procedural") @map("generation_mode") @db.VarChar(16)
|
||||
rendererKey String? @map("renderer_key") @db.VarChar(64)
|
||||
renderConfig Json @default("{}") @map("render_config")
|
||||
firstPublishedAt DateTime? @map("first_published_at") @db.Timestamptz(3)
|
||||
worksheetGrades WorksheetGrade[]
|
||||
worksheetResources WorksheetResource[]
|
||||
categories Category @relation(fields: [categoryId], references: [id], onUpdate: Restrict)
|
||||
assets Asset @relation(fields: [previewAssetId], references: [id], onUpdate: Restrict)
|
||||
|
||||
@@index([categoryId, status, sortOrder, updatedAt(sort: Desc), id], map: "worksheets_category_status_sort_idx")
|
||||
@@index([status, ageMin, ageMax], map: "worksheets_status_age_idx")
|
||||
@@index([status, sortOrder, updatedAt(sort: Desc), id], map: "worksheets_status_sort_idx")
|
||||
@@map("worksheets")
|
||||
}
|
||||
@@ -94,6 +94,7 @@ ALTER TABLE public.users
|
||||
REFERENCES public.assets (id) ON UPDATE RESTRICT ON DELETE RESTRICT;
|
||||
|
||||
CREATE TABLE public.categories (
|
||||
status varchar(16) NOT NULL DEFAULT 'draft',
|
||||
id varchar(128) COLLATE "C" PRIMARY KEY,
|
||||
name varchar(100) NOT NULL,
|
||||
icon varchar(1024) NOT NULL,
|
||||
@@ -107,9 +108,13 @@ CREATE TABLE public.categories (
|
||||
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_status_check CHECK (status IN ('draft', 'active', 'hidden')),
|
||||
CONSTRAINT categories_parent_not_self_check CHECK (parent_id IS NULL OR parent_id <> id)
|
||||
);
|
||||
|
||||
CREATE INDEX categories_status_parent_sort_idx ON public.categories (status, parent_id, sort_order, id);
|
||||
COMMENT ON COLUMN public.categories.status IS '发布状态:draft 开发测试,active 上线,hidden 下架;公开访问还需所有上级分类 active';
|
||||
|
||||
CREATE INDEX categories_parent_sort_idx ON public.categories (parent_id, sort_order, id);
|
||||
|
||||
CREATE TABLE public.worksheets (
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
-- 既有分类保留当前公开行为;以后新建分类默认草稿。不要重跑已成功的增量迁移。
|
||||
BEGIN;
|
||||
ALTER TABLE public.categories ADD COLUMN status varchar(16) NOT NULL DEFAULT 'draft';
|
||||
UPDATE public.categories SET status = 'active';
|
||||
ALTER TABLE public.categories ADD CONSTRAINT categories_status_check
|
||||
CHECK (status IN ('draft', 'active', 'hidden'));
|
||||
CREATE INDEX categories_status_parent_sort_idx ON public.categories (status, parent_id, sort_order, id);
|
||||
COMMENT ON COLUMN public.categories.status IS '发布状态:draft 开发测试,active 上线,hidden 下架;公开访问还需所有上级分类 active';
|
||||
COMMIT;
|
||||
@@ -1,5 +1,7 @@
|
||||
# v3.5 PostgreSQL 初始化 SQL
|
||||
|
||||
> 2026-10-15:已接入 Prisma Schema 与 `0_v35_baseline`,本地现有库仅登记基线,没有重建业务表。下文保留原手工 SQL 的执行路径和历史核查;新的空库优先使用 Prisma migrate deploy,后续升级新增迁移。详见 [Prisma 说明](../README.md)。
|
||||
|
||||
基于 [数据库设计](../../../../docs/v3.5/数据库设计文档.md),面向 PostgreSQL 18。缩进统一 4 个空格;文件是普通 PostgreSQL SQL,可使用 psql 或数据库客户端执行。
|
||||
|
||||
2026-10-12 已在本地 PostgreSQL 18.6 临时数据库执行 001/002/003,并重复执行 003 验证不覆盖 enabled=false;10 张表与 19 项关键约束检查通过,历史 NULL 事件、标识大小写及资产所有者删除行为符合设计。验证库已删除;随后已在用户授权下正式初始化 doodle_dev,10 张表与两条配置记录核对通过。
|
||||
@@ -28,7 +30,7 @@ Docker 已在首次启动时创建 doodle_dev,**当前本地跳过 001**。其
|
||||
| 已执行部分增量的库 | 按编号顺序执行尚未应用的后续文件 |
|
||||
| 已执行 008 的库 | 无需再次执行 DDL |
|
||||
|
||||
004~008 是历史结构的升级文件,不能用于已执行最新 002 的空库基线。下文各阶段记录解释该次变更,不表示只完成其中某一步就已到最新结构。Prisma 迁移历史尚未接入,执行前依据实际结构及执行记录确认当前版本。
|
||||
004~008 是历史结构的升级文件,不能用于已执行最新 002 的空库基线。下文各阶段记录解释该次变更,不表示只完成其中某一步就已到最新结构。当前已接入 Prisma 迁移基线,旧手工库在登记前依据实际结构及执行记录确认当前版本。
|
||||
|
||||
## 空库初始化示例
|
||||
|
||||
@@ -64,7 +66,7 @@ SELECT id, value FROM public.settings;
|
||||
- 下载 NULL exportId 只允许 import 来源;非空事件按 userId + exportId 去重。计数不会通过触发器自动更新,由后续业务事务实现。
|
||||
- tags 元素类型、分类多层循环、任务租约/锁、JWT 失效与管理员权限由后续业务实现;SQL 不代表业务 API 已完成。
|
||||
- updatedAt 和 contentUpdatedAt 由服务显式维护,不添加把两者混为一谈的更新时间触发器。
|
||||
- Prisma 尚未安装,Schema 和迁移历史尚未接入。后续接入时将本 SQL 纳入正式基线;已有表的库须采用基线登记流程,不能直接再次执行同一 DDL。
|
||||
- Prisma Schema 和迁移历史已接入,本 SQL 已纳入正式基线;已有表的库须先核对再登记基线,不能直接再次执行同一 DDL。
|
||||
|
||||
## 单小程序身份简化(2026-10-14)
|
||||
|
||||
@@ -180,3 +182,7 @@ docker compose --env-file .env -f infra/compose.yaml exec -T postgres \
|
||||
- 所有临时核查库已清理;开发库只读比对,无样本写入。本次只修正文档说明,无需新增 DDL 或再次升级开发库。
|
||||
|
||||
业务归属/权限、首次发布、计数和审计事务、任务调度等仍需服务端接入;SQL 验证不代替业务 API 验收。
|
||||
|
||||
## 分类发布状态(2026-10-16)
|
||||
|
||||
最新 002/003 包含分类 status,空库不再执行 009。原 004~008 及 Prisma 旧基线不改写;已有结构新增 009(或使用对应 Prisma 迁移,二者不能重复)。009 新增 status 默认 draft、三态 CHECK 和状态/父级索引,同时将既有分类置为 active 保持兼容。当前为 13 张表、136 个字段。新后端公开接口同时检查分类祖先状态,管理员状态修改使数据库页面快照失效。
|
||||
|
||||
Reference in New Issue
Block a user