# 分类与题型首次导入操作手册 更新日期:2026-10-20。适用于把本地已整理的分类、题型及必要七牛资产映射,首次导入空的整合库或云端 `doodle_prod`。本文和配套模板不执行 CloudBase 用户数据迁移,也不代表生产库已经导入。 ## 1. 本次到底导入哪些数据 | 表 | 本次处理 | | ------------------ | --------------------------------------------------------------------------------------------- | | `categories` | 导入本地全部分类,保留 ID、层级、发布状态、排序、图标与时间 | | `worksheets` | 导入本地全部题型,保留 ID、内容、预览资产引用、发布状态、seed 与时间 | | `assets` | 仅导入上述两表引用的预览图和分类图标映射,保留资产 ID、bucket、对象 key、摘要、状态及原始时间 | | `admin_audit_logs` | 新增一条本批迁移审计;不复制本地历史操作日志 | | 其他业务表 | 本次不复制数据;正式迁移会先创建完整表结构 | **不能只导出前两张表。** `worksheets.preview_asset_id` 必须引用存在的 `assets.id`,分类的文件图标也可能引用资产。只导两张表会导致外键失败,或丢失已完成的七牛映射。 本次不会重新上传七牛文件。保存映射不等于重新验证对象可用;导入后还要检查资源域名及预览图访问。 **真实下载量和收藏量在本阶段初始化为 0。** 本地旧值保存在原始导出文件中供核对;`downloads_seed` / `likes_seed` 保留本地值。后续必须从 CloudBase 最新 `worksheets` 按 ID 赋值真实 `downloads` / `likes`,不与本地旧值相加,也不在导入历史记录时再次累加。本阶段完成不表示用户历史与统计已经迁移完毕。 本地只读核查记录(2026-10-20): | 项目 | 数量 | | -------------------------------- | ------ | | 分类 | 7 | | 题型 | 52 | | 本批引用资产 | 52 | | 本批含用户归属的资产 | 0 | | 本批非七牛 / 非公开 / 未就绪资产 | 0 | | 年级、内容资源、资源文件关联 | 均为 0 | 执行时以新导出的 `manifest.json` 和校验结果为准,不把以上数量写死为永久预期。 ## 2. 推荐操作路线 ```text 本机 doodle_dev(只读) → 导出 content.json → 打包 content-import.sql → 本机独立整合库试导、验证 → 上传文件到云服务器 → 在 1Panel 终端内执行 SQL → doodle_prod 验证、备份 ``` 第一次先导入 `doodle_migration_20261020` 进行试导。该库是一次性迁移整合库,不替代 `doodle_dev`,也无需常驻另一套服务器。 **云端 5432 不需要向公网放行。** 数据文件通过 1Panel 上传,SQL 在 PostgreSQL 容器内部执行。云端首次建表可按 [Adminer 建表指南](./云端数据库首次建表指南.md) 上传初始化 SQL,不依赖本机 SSH;下文另保留通过 SSH 隧道执行 Prisma 的备选方式。 现有旧 CloudBase 云函数、数据库、预拉取文件和任务持续保留。本次首次建库期间,不启动连接目标库的新 API 或后台任务,避免新写入影响对账。 ## 3. 配套文件、适用条件和限制 配套模板: - [只读导出 SQL](../../deploy/postgres/content/export.sql) - [导入事务模板](../../deploy/postgres/content/apply.sql) 模板已包含:目标库名保护、迁移校验和与字段签名核对、分类/资产依赖检查、分类循环检查、首次空表保护、事务回滚、逐字段对账及迁移审计。 执行前确认: 1. 本机 Node.js / pnpm 与项目要求一致,依赖已按锁文件安装;Docker 和本地 PostgreSQL 正常运行。 2. 源库已应用项目正式迁移,没有失败或未完成的迁移。 3. 目标存在完整的新后端表结构,且 `assets`、`categories`、`worksheets` 三张表都为空。本流程不会清空、覆盖或自动 upsert 已有数据。 4. 本批资产全部为已就绪的七牛公开预览或分类图标,不包含头像。 5. 若资产有用户归属,目标必须已有对应生产用户。找不到归属时模板停止,不自动置空 owner、不导入本地测试用户。需先完成该身份的 CloudBase 映射,或另行明确公共内容资产归属转换。 6. 如果源库已经有年级/完整内容资源关联,或者出现 `generation_mode=resource`,本模板会停止;需扩展为包含相应关联的内容迁移,不能把缺失资源的题型当成完整导入。 现有 `categories:import`、`worksheets:import`、`assets:map` 是本地导入/回填工具,限定 `doodle_dev`;本手册使用独立的 PostgreSQL 内容快照流程,不修改它们的目标保护。 ## 4. 本机:导出一致的内容快照 ### 4.1 进入项目并确认状态 以下本机命令在同一个终端中顺序执行,所有文件路径均以仓库根目录为基准。每一步出现错误就停止,不继续后续步骤。 ```bash cd /Users/joey-xd/sites/doodle/doodle-store node --version pnpm --version pnpm infra:status umask 077 doodle_batch_dir="$PWD/deploy/data/content-$(date -u +%Y%m%dT%H%M%SZ)" mkdir -m 700 "$doodle_batch_dir" ``` `deploy/data/` 已被 Git 忽略。导出文件、连接配置和真实密码不要提交仓库。不要通过 `pnpm format` 批量改写数据文件;原始快照的字节摘要用于本批对账。 ### 4.2 执行只读导出 ```bash docker compose --env-file .env -f infra/compose.yaml \ exec -T postgres sh -c \ 'exec psql -X -qAt -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d doodle_dev' \ < deploy/postgres/content/export.sql \ > "$doodle_batch_dir/content.json.partial" \ && mv "$doodle_batch_dir/content.json.partial" "$doodle_batch_dir/content.json" ``` 导出在一个只读、可重复读事务中完成,三份数组来自同一快照;不逐表分开导出,不修改开发库。导出包含所有状态的分类/题型,不会只筛选 active;资产范围自动按引用计算。 只有命令成功才生成正式的 `content.json`。若只剩 `.partial` 或文件为空,先排查错误,不打包导入。 ### 4.3 打包并生成摘要 以下命令只读取内容快照和仓库 SQL 模板,生成三个辅助文件。它不会连接数据库。 ```bash node --input-type=module - "$doodle_batch_dir" <<'JS' import { readFileSync, writeFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { createHash } from 'node:crypto'; const dir = resolve(process.argv[2]); const raw = readFileSync(resolve(dir, 'content.json')); const payload = JSON.parse(raw.toString('utf8')); if (payload.format !== 'doodle-content-v1' || payload.source_database !== 'doodle_dev' || !Array.isArray(payload.assets) || !Array.isArray(payload.categories) || !Array.isArray(payload.worksheets)) { throw new Error('内容快照格式不正确'); } const sha256 = (value) => createHash('sha256').update(value).digest('hex'); const digest = sha256(raw); const template = readFileSync('deploy/postgres/content/apply.sql', 'utf8'); const prefix = [ '\\set ON_ERROR_STOP on', 'BEGIN;', "SET LOCAL TIME ZONE 'UTC';", "SET LOCAL lock_timeout = '5s';", "SET LOCAL statement_timeout = '60s';", "SET LOCAL doodle.content_import_sha256 = '" + digest + "';", 'CREATE TEMP TABLE content_import_encoded (body text NOT NULL) ON COMMIT DROP;', 'COPY content_import_encoded (body) FROM STDIN;', ].join('\n') + '\n'; const sql = prefix + raw.toString('base64') + '\n\\.\n' + template; writeFileSync(resolve(dir, 'content-import.sql'), sql, { flag: 'wx', mode: 0o600 }); const manifest = { format: payload.format, exportedAt: payload.exported_at, sourceSha256: digest, assets: payload.assets.length, categories: payload.categories.length, worksheets: payload.worksheets.length, assetsWithOwner: payload.assets.filter((a) => a.owner_user_id !== null).length, excludedRelations: payload.excluded_relations, statisticsState: 'pending-cloudbase', }; writeFileSync(resolve(dir, 'manifest.json'), JSON.stringify(manifest, null, 4) + '\n', { flag: 'wx', mode: 0o600 }); writeFileSync(resolve(dir, 'SHA256SUMS'), digest + ' content.json\n' + sha256(sql) + ' content-import.sql\n', { flag: 'wx', mode: 0o600 }); console.log(manifest); JS ``` 打包后的目录: ```text content.json 原始内容及资产快照 content-import.sql 可直接交给 psql 的整批事务 manifest.json 本批时间、数量和源文件摘要 SHA256SUMS 上传前后校验文件完整性 ``` 原始 JSON 原封不动编码进 SQL,由 PostgreSQL 自己恢复 bigint 等字段。Node.js 只读取数组长度等元数据,不将解析后的数值重新序列化为导入数据,避免大整数精度损失。Base64 用于安全封装 JSON,不是加密。 打包路径已存在时会报错;保留上一批次,重新导出到新目录,不覆盖原始文件。 ## 5. 本机:建立独立整合库并试导 ### 5.1 创建空库 ```bash docker compose --env-file .env -f infra/compose.yaml \ exec -T postgres sh -c \ 'exec createdb -U "$POSTGRES_USER" --template=template0 --encoding=UTF8 --lc-collate=C --lc-ctype=C doodle_migration_20261020' ``` 库已存在时不要 drop 或清空。先查是否属于此前试导;新批次可改用 `doodle_migration_20261020_02`,并同步修改本节后续配置和命令。 ### 5.2 建立仅用于整合库的连接配置 以下脚本读取当前服务端连接地址,检查确实是本地 `doodle_dev`,只替换库名并生成独立文件。不会改动根 `.env` 或 `apps/server/.env`,也不打印密码。 ```bash node --input-type=module <<'JS' import { readFileSync, writeFileSync } from 'node:fs'; import { parseEnv } from 'node:util'; const source = parseEnv(readFileSync('apps/server/.env', 'utf8')); const url = new URL(source.DATABASE_URL); if (!['127.0.0.1', 'localhost'].includes(url.hostname) || url.pathname !== '/doodle_dev') { throw new Error('请先核对本地服务端连接配置'); } url.pathname = '/doodle_migration_20261020'; writeFileSync('.env.content-import', 'DATABASE_URL=' + url.href + '\n', { flag: 'wx', mode: 0o600 }); JS ``` 文件已存在时先核对用途,不覆盖。该文件受 `.env.*` 忽略规则保护。 ### 5.3 使用正式 Prisma 迁移创建结构 ```bash env -u DATABASE_URL node --env-file=.env.content-import --input-type=module <<'JS' import { spawnSync } from 'node:child_process'; const url = new URL(process.env.DATABASE_URL); if (!['127.0.0.1', 'localhost'].includes(url.hostname) || url.pathname !== '/doodle_migration_20261020') { throw new Error('连接目标不是本次本地整合库'); } for (const script of ['db:deploy', 'db:status']) { const result = spawnSync('pnpm', ['--filter', '@doodle/server', script], { stdio: 'inherit', env: process.env }); if (result.error || result.status !== 0) process.exit(1); } JS ``` 当前应成功登记三个迁移:`0_v35_baseline`、`20261016000000_category_publication`、`20261019000000_member_assets`。以后以仓库实际迁移为准,模板会核对源库和目标的迁移名称及校验和。 不要执行 `db push`、`migrate reset`、手工复制建表 SQL或仅手工写入迁移记录。迁移会创建完整业务表及必要默认配置,本阶段只填充内容数据。 ### 5.4 试导同一份 SQL ```bash docker compose --env-file .env -f infra/compose.yaml \ exec -T postgres sh -c \ 'exec psql -X -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d doodle_migration_20261020' \ < "$doodle_batch_dir/content-import.sql" ``` 以进程成功退出,并出现 `CONTENT_IMPORT_COMMITTED` 为成功标志。导入事务内已逐字段比较写入结果与快照;两项真实计数与已归零的临时数据比较,其余字段保持原值。 如出现错误,连接退出时整个业务导入事务回滚。不会留下半批分类/题型或审计。目标结构及之前单独完成的 Prisma 迁移仍保留。 使用第 8 节的 SQL 在整合库核对结果。**不要为测试重复执行而清空整合库;第二次导入会因三张表非空被拒绝,这是首次导入保护的预期行为。** ## 6. 云端:创建空库、备份并初始化结构 ### 6.1 在 1Panel 创建 doodle_prod 如已创建,核查后沿用,不重复新建: | 项目 | 值 | | --------------- | ----------------------------------------------- | | 数据库 | `doodle_prod` | | 所有者/迁移用户 | `doodle_prod_owner`,或此前为该库配置的专属账号 | | 超级用户 | 不勾选 | | 编码 | UTF-8 | | 密码 | 独立随机密码,只保存在受控配置中 | 在 1Panel「终端」执行以下命令,确认实际实例。容器名来自 2026-10-20 检查,若重装过实例先从容器列表核对。 ```bash doodle_pg_container="1Panel-postgresql-RqP6" docker exec "$doodle_pg_container" sh -c \ 'exec psql -X -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d doodle_prod -c "SELECT current_database(), current_user, version(); SELECT datname, pg_get_userbyid(datdba) AS owner, pg_encoding_to_char(encoding) AS encoding FROM pg_database WHERE datname = current_database();"' ``` 这里 `POSTGRES_USER` 在容器内部展开,代表实例初始化管理员,不是待部署 API 的运行账号。如果该实例未配置此变量,在 1Panel 连接信息中核对实际管理员用户名并替换命令中的 `-U` 参数,不输出或复制密码到聊天。 ### 6.2 创建导入前备份 即使刚创建空库,也记录导入前状态。以下命令在云服务器终端执行: ```bash umask 077 doodle_backup_dir="/opt/doodle/backups/content-$(date -u +%Y%m%dT%H%M%SZ)" mkdir -p "$doodle_backup_dir" docker exec "$doodle_pg_container" sh -c \ 'exec pg_dump -U "$POSTGRES_USER" -d doodle_prod --format=custom' \ > "$doodle_backup_dir/before-content.dump.partial" \ && mv "$doodle_backup_dir/before-content.dump.partial" "$doodle_backup_dir/before-content.dump" docker exec -i "$doodle_pg_container" pg_restore --list \ < "$doodle_backup_dir/before-content.dump" \ > "$doodle_backup_dir/before-content.list" ``` 检查两条命令均成功,且正式备份文件存在。`pg_restore --list` 只检查归档目录可读,不替代完整恢复验证。完整数据库备份不包含集群账号密码及服务端配置;记录所有者和权限配置以便恢复。 不要运行 `deploy/postgres/backup.sh`:它属于此前独立 Compose 方案,不适用于当前由 1Panel 管理的实例。 ### 6.3 用 SSH 隧道让本机 Prisma 连接云端 如果已按 [首次建表指南](./云端数据库首次建表指南.md) 完整恢复初始化 SQL,并核对 14 张表和三条迁移记录,本节可跳过,直接执行第 7 节内容导入。不要再次手工建表或登记相同迁移。 在**本机另一个终端**启动隧道: ```bash ssh -N -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 \ -L 127.0.0.1:15432:127.0.0.1:5432 \ -p 22 你的SSH用户名@175.24.138.234 ``` 替换真实 SSH 用户和端口;`22` 只是示例。使用你已有的 SSH 密钥/密码,不修改服务器登录配置。保持该终端运行。 连接路径:本机 `127.0.0.1:15432` → SSH → 云服务器 `127.0.0.1:5432` → PostgreSQL。前提是云端容器仍将 5432 映射到宿主机;若绑定或端口已调整,按实际配置修改转发目标。不要新增公网 5432 放行规则。 回到本机项目终端,新建权限为 600 的独立配置文件: ```bash touch .env.production-import chmod 600 .env.production-import ``` 使用编辑器填入以下内容。若文件已存在,先核对其中内容,避免误用别的环境;密码中的特殊字符需要 URL 编码。 ```dotenv DATABASE_URL=postgresql://doodle_prod_owner:填写URL编码后的密码@127.0.0.1:15432/doodle_prod?schema=public ``` 执行迁移: ```bash env -u DATABASE_URL node --env-file=.env.production-import --input-type=module <<'JS' import { spawnSync } from 'node:child_process'; const url = new URL(process.env.DATABASE_URL); if (url.hostname !== '127.0.0.1' || url.port !== '15432' || url.pathname !== '/doodle_prod') { throw new Error('目标不是经 SSH 隧道连接的 doodle_prod'); } for (const script of ['db:deploy', 'db:status']) { const result = spawnSync('pnpm', ['--filter', '@doodle/server', script], { stdio: 'inherit', env: process.env }); if (result.error || result.status !== 0) process.exit(1); } JS ``` 该命令只使用专属连接文件,不修改开发服务端配置。若目标已经完成相同迁移,`db:deploy` 检查后不会重建表。迁移失败先处理原因,不伪造 applied 状态。 如果暂时无法 SSH,不需要改用公网 5432:可以之后准备含锁定 Prisma 和迁移文件的迁移容器,在共享 Docker 网络内执行。项目当前尚无这个发布镜像,因此本文给出的可操作路线是 SSH 隧道,而不是假设云端已经安装 Node.js/pnpm。 ## 7. 云端:上传并导入经过试导的同一批次 ### 7.1 上传和校验 在 1Panel 文件管理中创建专属批次目录,例如: ```text /opt/doodle/imports/content-20261020-01/ ``` 上传本机**已经试导成功**的四个文件:`content.json`、`content-import.sql`、`manifest.json`、`SHA256SUMS`。不要上传 `.env`、密码、Mac 的 node_modules 或本地整库备份。 在 1Panel 终端中执行: ```bash cd /opt/doodle/imports/content-20261020-01 chmod 700 . chmod 600 content.json content-import.sql manifest.json SHA256SUMS sha256sum -c SHA256SUMS cat manifest.json ``` 两个摘要必须均为 `OK`,数量应与本机试导一致。上传后不要编辑 JSON 或 SQL;任何修改都生成新批次、重新试导和计算摘要。 摘要用于核对传输完整性,不是来源签名。文件必须来自本次受控导出,不能执行不明来源的 SQL。 ### 7.2 复核目标为空 在云端执行: ```bash doodle_pg_container="1Panel-postgresql-RqP6" docker exec "$doodle_pg_container" sh -c \ 'exec psql -X -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d doodle_prod -c "SELECT (SELECT count(*) FROM public.assets) AS assets, (SELECT count(*) FROM public.categories) AS categories, (SELECT count(*) FROM public.worksheets) AS worksheets;"' ``` 三项必须为 0。若不是 0,停止使用本手册的首次导入入口,核对已有内容及其来源;不要 `TRUNCATE`、删除或恢复旧整库覆盖它们。 ### 7.3 执行正式内容导入 仍在云端批次目录执行: ```bash docker exec -i "$doodle_pg_container" sh -c \ 'exec psql -X -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d doodle_prod' \ < content-import.sql ``` 必须同时满足:命令成功退出、出现 `CONTENT_IMPORT_COMMITTED`。只看到 INSERT 行数不表示整个事务已提交。 导入顺序为资产 → 分类 → 题型 → 对账 → 迁移审计 → 提交。外键和 CHECK 约束保持启用,不使用 `session_replication_role` 或关闭触发器来绕过错误。创建表的账号所有权不会因管理员执行数据插入而改变。 ## 8. 导入后验证 ### 8.1 在 SQL 客户端执行 本机整合库可通过容器 psql 或本机客户端运行;云端可使用 1Panel 终端的 psql 或 Adminer: ```sql -- 先确认正在检查的库。 SELECT current_database(); -- 数量与本批 manifest 对照。 SELECT (SELECT count(*) FROM public.assets) AS assets, (SELECT count(*) FROM public.categories) AS categories, (SELECT count(*) FROM public.worksheets) AS worksheets; SELECT status, count(*) FROM public.categories GROUP BY status ORDER BY status; SELECT status, count(*) FROM public.worksheets GROUP BY status ORDER BY status; -- 预期为 0:题型缺分类或缺就绪预览。 SELECT count(*) AS broken_worksheet_references FROM public.worksheets w LEFT JOIN public.categories c ON c.id = w.category_id LEFT JOIN public.assets a ON a.id = w.preview_asset_id WHERE c.id IS NULL OR a.id IS NULL OR a.status <> 'ready' OR a.provider <> 'qiniu' OR a.visibility <> 'public' OR a.purpose <> 'preview'; -- 第一阶段真实计数为 0,seed 保留本地值。 SELECT sum(downloads) AS downloads, sum(likes) AS likes, sum(downloads_seed) AS downloads_seed, sum(likes_seed) AS likes_seed FROM public.worksheets; -- 审计中应有本批源摘要、条数及 pending-cloudbase。 SELECT request_id, after_data, created_at FROM public.admin_audit_logs WHERE action = 'content.import' ORDER BY created_at DESC; -- 结构迁移全部完成;不应有未完成记录。 SELECT migration_name, checksum, finished_at, rolled_back_at FROM public._prisma_migrations ORDER BY migration_name; ``` 当前记录对应 52 资产、7 分类、52 题型;状态分布和 seed 总量以该批次源数据为准。导入模板提交前已经逐字段比较全部三表,不仅检查数量。 ### 8.2 核对资源访问 以实际服务端公开配置为准;当前已确认资源域名为 `https://cdn.joeyone.cn`,对象 key 自带 `doodle-assets/` 前缀。不要将该前缀重复拼接,也不要把 `source_ref` 的 cloud 地址直接当生产预览地址。 可在本机生成本批资源 URL 列表: ```bash node --input-type=module - "$doodle_batch_dir" <<'JS' import { readFileSync, writeFileSync } from 'node:fs'; import { resolve } from 'node:path'; const dir = resolve(process.argv[2]); const payload = JSON.parse(readFileSync(resolve(dir, 'content.json'), 'utf8')); const urls = payload.assets.map((a) => 'https://cdn.joeyone.cn/' + a.object_key.split('/').map(encodeURIComponent).join('/')); writeFileSync(resolve(dir, 'asset-urls.txt'), urls.join('\n') + '\n', { flag: 'wx', mode: 0o600 }); JS ``` 浏览器抽查新旧预览,或逐一请求列表中的 URL。要求无 403/404 且图片可正常显示。若发现同 key 对象内容被改变,应按原同步报告摘要重新核验;仅有 HTTP 200 不证明文件内容相同。 ### 8.3 Adminer 和后端验收边界 Adminer 选择 PostgreSQL,服务器填写 `1Panel-postgresql-RqP6:5432`,数据库为 `doodle_prod`,使用该库专属账号。不要填写默认 `db`;Adminer 自身容器内的 `127.0.0.1` 也不是 PostgreSQL。 API 尚未部署时,到此只能确认数据库与资源;不能声称小程序已联调成功。后端部署后再验证分类、题型、详情和预览接口。分类或其祖先为 draft/hidden 时,公开查询不返回是正常发布控制,不要为了通过验收批量改成 active。 后续 CloudBase 用户/统计整合、页面重建和正式切换完成前,不将本阶段归零的真实计数视为最终线上统计。 ## 9. 导入后备份、失败处理和回退 1. 成功后按第 6.2 节方法创建 `after-content.dump`,使用新的备份目录/文件名;保留本批原始导出、manifest、摘要、审计和导入前备份。 2. 在独立恢复验证库试恢复 `after-content.dump`,确认表结构、数据和迁移历史都可读。仅执行 `pg_restore --list` 不足以完成恢复演练。 3. 导入事务失败:本批数据和审计整批回滚。排查后可在仍为空的目标重试;不要通过删除约束绕过错误。 4. 导入成功但需要撤回:先停止连接新目标库的应用写入并保留当前备份,在另一个空库恢复导入前备份、对账后再安排目标切换。本文不提供自动删生产库、清表或 `--clean` 命令。 5. 生产库已经有新用户或用户操作后,不再用本阶段快照覆盖整库;改用受控的内容更新和后续 CloudBase 合并流程。 可在 1Panel 另建空的恢复验证库,再在云端运行以下命令;这里只验证备份,不覆盖 `doodle_prod`: ```bash docker exec -i "$doodle_pg_container" sh -c \ 'exec pg_restore -U "$POSTGRES_USER" --dbname doodle_restore_check_20261020 --no-owner --no-acl --exit-on-error' \ < /实际备份目录/after-content.dump ``` 验证库恢复时省略原所有权/ACL,便于检查归档内容;这不代表生产权限已经配置完成。真实恢复上线还需按目标所有者和应用账号恢复/核对权限。备份副本需要另存服务器之外。 ## 10. 常见错误 | 现象 | 处理 | | ------------------------------------- | --------------------------------------------------------------------------------------- | | `getaddrinfo for db failed` | Adminer 服务器填实际 PostgreSQL 容器名,且两者在可通信的 Docker 网络 | | 连接拒绝 / SSH 本机端口冲突 | 核对 SSH 登录、隧道是否仍运行、云端宿主机映射;改本机转发端口时同步改连接文件和保护检查 | | `permission denied for schema public` | 核对数据库 owner;Prisma 迁移使用数据库所有者,不使用只具备 DML 的运行账号 | | 迁移名称/校验和或字段签名不一致 | 先核对仓库版本和两端迁移状态,不跳过模板检查或伪造迁移记录 | | 目标三张表必须为空 | 可能已经成功导入,或有其他批次;核对审计及数据,不重复清空导入 | | 内容资产含未迁移用户归属 | 先核对生产用户映射;不要自动置空归属或顺带复制开发用户 | | 资产未就绪/用途不匹配 | 回到本地完成资源核验或修正引用后重新导出,不伪造 ready | | 已有年级/资源关联,或资源型题型 | 本手册的三表范围已不足,扩展完整内容批次后重新试导 | | 分类循环或引用缺失 | 在内容源修复并重新导出;不要关闭数据库约束 | | JSON 文件只有错误输出或为空 | 导出未成功,保留错误信息排查;不要继续打包 | | SQL 输出 INSERT 后仍报错 | 没有最终提交标记就不算成功;查询目标确认事务回滚状态 | ## 11. 实施与验证记录 本手册交付范围是文档及配套 SQL 模板。2026-10-20 验证了文档中 17 段 Bash 命令与 5 段 Node.js 代码的语法,并从文档直接提取打包代码进行本机隔离库验证: - 正常导入:52 条资产、7 条分类、52 条题型及 1 条迁移审计,提交前逐字段核对通过。 - 重复导入:被空表保护拒绝,已有数据保持不变。 - 错误目标库:被库名保护拒绝。 - 迁移校验和不一致:拒绝导入,业务表保持为空。 - 题型 CHECK 约束失败:之前已插入的资产和分类随事务全部回滚,未留下迁移审计。 - 计数:真实 downloads/likes 均为 0,downloads_seed=629、likes_seed=666,保留源值。 验证创建的三个隔离数据库已清理。源 doodle_dev 仅只读导出,未改动;未连接云端执行导入,也未在本次验证中请求七牛或测试 SSH/1Panel 上传。迁移快照、语法检查和回滚验证不能替代实际云端连接、生产权限和资源可访问性验收。 参考:[生产数据整合与导入方案](./生产数据整合与导入方案.md)、[Prisma 迁移说明](../../apps/server/prisma/README.md)、[PostgreSQL 18 psql](https://www.postgresql.org/docs/18/app-psql.html)、[PostgreSQL 18 JSON 函数](https://www.postgresql.org/docs/18/functions-json.html)、[Prisma 7 migrate deploy](https://www.prisma.io/docs/cli/v7/migrate/deploy)。