feat: 数据库导出文档
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
-- 由文档中的打包步骤在事务和 COPY 数据段之后拼接;不要单独执行此文件。
|
||||
DO $$
|
||||
DECLARE
|
||||
payload jsonb;
|
||||
actual_migrations jsonb;
|
||||
actual_columns jsonb;
|
||||
BEGIN
|
||||
IF current_database() <> 'doodle_prod'
|
||||
AND current_database() !~ '^doodle_migration_[0-9]{8}(_[a-z0-9]+)?$' THEN
|
||||
RAISE EXCEPTION '目标只能是 doodle_prod 或独立的 doodle_migration_YYYYMMDD 库';
|
||||
END IF;
|
||||
IF (SELECT count(*) FROM content_import_encoded) <> 1 THEN
|
||||
RAISE EXCEPTION '导入包必须只有一份内容快照';
|
||||
END IF;
|
||||
SELECT convert_from(decode(body, 'base64'), 'UTF8')::jsonb
|
||||
INTO STRICT payload FROM content_import_encoded;
|
||||
IF payload->>'format' IS DISTINCT FROM 'doodle-content-v1'
|
||||
OR payload->>'source_database' IS DISTINCT FROM 'doodle_dev'
|
||||
OR jsonb_typeof(payload->'assets') IS DISTINCT FROM 'array'
|
||||
OR jsonb_typeof(payload->'categories') IS DISTINCT FROM 'array'
|
||||
OR jsonb_typeof(payload->'worksheets') IS DISTINCT FROM 'array'
|
||||
OR payload->'excluded_relations' IS DISTINCT FROM
|
||||
'{"worksheet_grades":0,"worksheet_resources":0,"worksheet_resource_assets":0}'::jsonb THEN
|
||||
RAISE EXCEPTION '导入包格式错误,或已有年级/资源关联需要扩展本批范围';
|
||||
END IF;
|
||||
IF jsonb_array_length(payload->'categories') = 0
|
||||
OR jsonb_array_length(payload->'worksheets') = 0 THEN
|
||||
RAISE EXCEPTION '本批分类和题型不能为空';
|
||||
END IF;
|
||||
IF EXISTS (SELECT 1 FROM public._prisma_migrations
|
||||
WHERE finished_at IS NULL AND rolled_back_at IS NULL) THEN
|
||||
RAISE EXCEPTION '目标存在未完成迁移';
|
||||
END IF;
|
||||
SELECT COALESCE(jsonb_agg(jsonb_build_object(
|
||||
'name', migration_name, 'checksum', checksum
|
||||
) ORDER BY migration_name), '[]'::jsonb)
|
||||
INTO actual_migrations FROM public._prisma_migrations
|
||||
WHERE finished_at IS NOT NULL AND rolled_back_at IS NULL;
|
||||
IF actual_migrations IS DISTINCT FROM payload->'migration_history' THEN
|
||||
RAISE EXCEPTION '源库与目标的迁移名称/校验和不一致';
|
||||
END IF;
|
||||
SELECT jsonb_agg(jsonb_build_object(
|
||||
'table', c.relname, 'column', a.attname,
|
||||
'type', format_type(a.atttypid, a.atttypmod), 'not_null', a.attnotnull
|
||||
) ORDER BY c.relname, a.attname)
|
||||
INTO actual_columns
|
||||
FROM pg_attribute a
|
||||
JOIN pg_class c ON c.oid = a.attrelid
|
||||
JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||
WHERE n.nspname = 'public' AND c.relname IN ('assets', 'categories', 'worksheets')
|
||||
AND a.attnum > 0 AND NOT a.attisdropped;
|
||||
IF actual_columns IS DISTINCT FROM payload->'column_signature' THEN
|
||||
RAISE EXCEPTION '源库与目标字段结构不一致,停止以免丢失字段';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE TEMP TABLE content_import_payload ON COMMIT DROP AS
|
||||
SELECT convert_from(decode(body, 'base64'), 'UTF8')::jsonb AS body
|
||||
FROM content_import_encoded;
|
||||
CREATE TEMP TABLE content_import_assets ON COMMIT DROP AS
|
||||
SELECT a.* FROM content_import_payload p,
|
||||
LATERAL jsonb_populate_recordset(NULL::public.assets, p.body->'assets') a;
|
||||
CREATE TEMP TABLE content_import_categories ON COMMIT DROP AS
|
||||
SELECT c.* FROM content_import_payload p,
|
||||
LATERAL jsonb_populate_recordset(NULL::public.categories, p.body->'categories') c;
|
||||
CREATE TEMP TABLE content_import_worksheets ON COMMIT DROP AS
|
||||
SELECT w.* FROM content_import_payload p,
|
||||
LATERAL jsonb_populate_recordset(NULL::public.worksheets, p.body->'worksheets') w;
|
||||
|
||||
-- 这一步只初始化内容。真实运营计数之后由 CloudBase 最新快照赋值,不复制开发计数。
|
||||
UPDATE content_import_worksheets SET downloads = 0, likes = 0;
|
||||
LOCK TABLE public.assets, public.categories, public.worksheets IN SHARE ROW EXCLUSIVE MODE;
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM public.assets)
|
||||
OR EXISTS (SELECT 1 FROM public.categories)
|
||||
OR EXISTS (SELECT 1 FROM public.worksheets) THEN
|
||||
RAISE EXCEPTION '目标三张表必须为空;本流程不覆盖、清空或重复合并已有内容';
|
||||
END IF;
|
||||
IF EXISTS (SELECT 1 FROM content_import_assets
|
||||
WHERE provider IS DISTINCT FROM 'qiniu' OR visibility IS DISTINCT FROM 'public'
|
||||
OR status IS DISTINCT FROM 'ready'
|
||||
OR purpose NOT IN ('preview', 'category_icon')) THEN
|
||||
RAISE EXCEPTION '本批只允许就绪的七牛公开预览/分类图标资产';
|
||||
END IF;
|
||||
IF EXISTS (SELECT 1 FROM content_import_assets a
|
||||
WHERE a.owner_user_id IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM public.users u WHERE u.id = a.owner_user_id)) THEN
|
||||
RAISE EXCEPTION '内容资产含未迁移的用户归属,须先处理用户映射;不自动清空 owner';
|
||||
END IF;
|
||||
IF EXISTS (SELECT 1 FROM content_import_worksheets w
|
||||
WHERE w.generation_mode <> 'procedural') THEN
|
||||
RAISE EXCEPTION '资源型题型需要同时迁移完整资源关联,本批不能只导题型父表';
|
||||
END IF;
|
||||
IF EXISTS (SELECT 1 FROM content_import_worksheets w
|
||||
WHERE NOT EXISTS (SELECT 1 FROM content_import_categories c WHERE c.id = w.category_id)
|
||||
OR NOT EXISTS (SELECT 1 FROM content_import_assets a
|
||||
WHERE a.id = w.preview_asset_id AND a.purpose = 'preview'))
|
||||
OR EXISTS (SELECT 1 FROM content_import_categories c
|
||||
WHERE (c.parent_id IS NOT NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM content_import_categories p WHERE p.id = c.parent_id))
|
||||
OR (c.icon_asset_id IS NOT NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM content_import_assets a WHERE a.id = c.icon_asset_id
|
||||
AND a.purpose = 'category_icon'))) THEN
|
||||
RAISE EXCEPTION '分类父级、题型分类或资产引用缺失/用途不匹配';
|
||||
END IF;
|
||||
IF EXISTS (SELECT 1 FROM content_import_assets a
|
||||
WHERE NOT EXISTS (SELECT 1 FROM content_import_worksheets w WHERE w.preview_asset_id = a.id)
|
||||
AND NOT EXISTS (SELECT 1 FROM content_import_categories c WHERE c.icon_asset_id = a.id)) THEN
|
||||
RAISE EXCEPTION '导入包含本批内容未引用的资产';
|
||||
END IF;
|
||||
IF EXISTS (
|
||||
WITH RECURSIVE ancestors AS (
|
||||
SELECT id, parent_id, ARRAY[id::text] AS path, false AS cycle
|
||||
FROM content_import_categories
|
||||
UNION ALL
|
||||
SELECT a.id, c.parent_id, a.path || c.id::text, c.id = ANY(a.path)
|
||||
FROM ancestors a JOIN content_import_categories c ON c.id = a.parent_id
|
||||
WHERE NOT a.cycle
|
||||
) SELECT 1 FROM ancestors WHERE cycle
|
||||
) THEN
|
||||
RAISE EXCEPTION '分类存在循环父子关系';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 单条分类 INSERT 包含所有父子行;语句结束时检查外键,无需关闭约束或触发器。
|
||||
INSERT INTO public.assets SELECT * FROM content_import_assets;
|
||||
INSERT INTO public.categories SELECT * FROM content_import_categories;
|
||||
INSERT INTO public.worksheets SELECT * FROM content_import_worksheets;
|
||||
|
||||
-- 提交前逐字段核对;统计字段与已经归零的临时数据比较。
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT * FROM public.assets EXCEPT SELECT * FROM content_import_assets)
|
||||
OR EXISTS (SELECT * FROM content_import_assets EXCEPT SELECT * FROM public.assets)
|
||||
OR EXISTS (SELECT * FROM public.categories EXCEPT SELECT * FROM content_import_categories)
|
||||
OR EXISTS (SELECT * FROM content_import_categories EXCEPT SELECT * FROM public.categories)
|
||||
OR EXISTS (SELECT * FROM public.worksheets EXCEPT SELECT * FROM content_import_worksheets)
|
||||
OR EXISTS (SELECT * FROM content_import_worksheets EXCEPT SELECT * FROM public.worksheets) THEN
|
||||
RAISE EXCEPTION '写入结果与导入包不一致,整批回滚';
|
||||
END IF;
|
||||
END $$;
|
||||
INSERT INTO public.admin_audit_logs (
|
||||
id, actor_type, action, resource_type, request_id, after_data, reason
|
||||
)
|
||||
SELECT
|
||||
'content-import-' || current_setting('doodle.content_import_sha256'),
|
||||
'migration', 'content.import', 'content-batch',
|
||||
'content-import-' || current_setting('doodle.content_import_sha256'),
|
||||
jsonb_build_object(
|
||||
'assets', (SELECT count(*) FROM content_import_assets),
|
||||
'categories', (SELECT count(*) FROM content_import_categories),
|
||||
'worksheets', (SELECT count(*) FROM content_import_worksheets),
|
||||
'sourceDatabase', body->>'source_database',
|
||||
'exportedAt', body->>'exported_at',
|
||||
'sourceSha256', current_setting('doodle.content_import_sha256'),
|
||||
'statisticsState', 'pending-cloudbase'
|
||||
),
|
||||
'本地内容初始化;资产与业务 ID 保留,downloads/likes 初始化为 0,待 CloudBase 最新统计覆盖。'
|
||||
FROM content_import_payload;
|
||||
COMMIT;
|
||||
\echo CONTENT_IMPORT_COMMITTED
|
||||
@@ -0,0 +1,65 @@
|
||||
\set ON_ERROR_STOP on
|
||||
BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY;
|
||||
SET LOCAL TIME ZONE 'UTC';
|
||||
|
||||
-- 只读导出本地内容,防止误把另一环境的数据当作本次内容来源。
|
||||
DO $$
|
||||
BEGIN
|
||||
IF current_database() <> 'doodle_dev' THEN
|
||||
RAISE EXCEPTION '导出只允许读取 doodle_dev';
|
||||
END IF;
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM public._prisma_migrations
|
||||
WHERE finished_at IS NULL AND rolled_back_at IS NULL
|
||||
) THEN
|
||||
RAISE EXCEPTION '源库存在未完成迁移,请先核查';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
WITH asset_ids AS (
|
||||
SELECT preview_asset_id AS id FROM public.worksheets
|
||||
UNION
|
||||
SELECT icon_asset_id FROM public.categories WHERE icon_asset_id IS NOT NULL
|
||||
)
|
||||
SELECT jsonb_build_object(
|
||||
'format', 'doodle-content-v1',
|
||||
'source_database', current_database(),
|
||||
'exported_at', transaction_timestamp(),
|
||||
'migration_history', (
|
||||
SELECT COALESCE(jsonb_agg(jsonb_build_object(
|
||||
'name', migration_name, 'checksum', checksum
|
||||
) ORDER BY migration_name), '[]'::jsonb)
|
||||
FROM public._prisma_migrations
|
||||
WHERE finished_at IS NOT NULL AND rolled_back_at IS NULL
|
||||
),
|
||||
-- 按字段名比较签名,兼容经旧 SQL 初始化、字段物理顺序不同的本地库。
|
||||
'column_signature', (
|
||||
SELECT jsonb_agg(jsonb_build_object(
|
||||
'table', c.relname, 'column', a.attname,
|
||||
'type', format_type(a.atttypid, a.atttypmod), 'not_null', a.attnotnull
|
||||
) ORDER BY c.relname, a.attname)
|
||||
FROM pg_attribute a
|
||||
JOIN pg_class c ON c.oid = a.attrelid
|
||||
JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||
WHERE n.nspname = 'public' AND c.relname IN ('assets', 'categories', 'worksheets')
|
||||
AND a.attnum > 0 AND NOT a.attisdropped
|
||||
),
|
||||
'excluded_relations', jsonb_build_object(
|
||||
'worksheet_grades', (SELECT count(*) FROM public.worksheet_grades),
|
||||
'worksheet_resources', (SELECT count(*) FROM public.worksheet_resources),
|
||||
'worksheet_resource_assets', (SELECT count(*) FROM public.worksheet_resource_assets)
|
||||
),
|
||||
'assets', (
|
||||
SELECT COALESCE(jsonb_agg(to_jsonb(a) ORDER BY a.id), '[]'::jsonb)
|
||||
FROM public.assets a JOIN asset_ids r ON r.id = a.id
|
||||
),
|
||||
'categories', (
|
||||
SELECT COALESCE(jsonb_agg(to_jsonb(c) ORDER BY c.id), '[]'::jsonb)
|
||||
FROM public.categories c
|
||||
),
|
||||
'worksheets', (
|
||||
SELECT COALESCE(jsonb_agg(to_jsonb(w) ORDER BY w.id), '[]'::jsonb)
|
||||
FROM public.worksheets w
|
||||
)
|
||||
);
|
||||
COMMIT;
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user