Files
doodle-store/apps/server/prisma/sql/002-create-tables.sql
T

409 lines
22 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,
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_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 (
status varchar(16) NOT NULL DEFAULT 'draft',
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_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 (
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', 'user'))
);
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 是完整内容收藏;逻辑关联,缺失/下架不清空,不退化为功能收藏';
COMMIT;