Files
gongxue-base/docs/refactor/pocketbase-real-data-migration-runbook.md
2026-06-30 13:03:38 +08:00

21 KiB
Raw Blame History

PocketBase 真实数据迁移验收 Runbook

更新时间2026-06-30

这份文档用于把旧 PocketBase 生产数据迁移到新的 Supabase/PostgreSQL 多租户题库 SaaS。目标不是简单把 JSON 塞进新库,而是证明旧系统的用户、题库、订单、权益、学习数据和运营配置进入新模型后仍能支撑业务上线。

适用范围

本流程适用于正式切换前的迁移演练、预生产验收和最终切换。旧 PocketBase/React 项目只作为导出来源和功能参照,新系统以 apps/apiapps/workersupabase/migrationsapps/taro 为准。

迁移覆盖:

  • 用户、租户成员、学生资料、身份映射。
  • 地区、入口、分类、科目、题目、题目版本、题库集合、练习蓝图。
  • 错题、收藏、最近练习、背单词进度和收藏。
  • 单词、知识手册、分数线、题目视频、资料资源台账。
  • 订单、支付、权益、SVIP 套餐、激活码、优惠券。
  • Banner、公告、FAQ、考试日期、勋章、CRM、推广关系和统计数据。

迁移原则

  • 不直接复用 PocketBase 的不规范字段作为长期模型,全部经过导入器规范化。
  • id 必须保留到 legacy_id 或相关 legacy 字段,方便追溯和二次修复。
  • 所有租户内数据必须带 tenant_id,不得出现跨租户共享业务行。
  • 密码、token、secret、openid、unionid、短信验证码、支付密钥等不得进入 public schema。
  • 私有资料、PDF、题图、视频不能变成长效 URL必须进入 content_assets 台账并由后端签名。
  • 导入前 dry-run 有 blocker 时禁止正式导入;上线切换前 pb:import:validate 不能有 FAIL。

环境准备

本地或预生产环境需要:

  • Node.js 20+
  • Docker Desktop
  • Supabase CLI
  • 已执行最新数据库迁移

建议使用独立迁移验收库,不要混用日常开发 smoke 数据库。迁移演练开始前:

npm install
npm run supabase:start
npm run supabase:reset

如果要验证 API 主链路,可以在真实数据导入后再补充最小管理账号或临时测试账号。不要在真实迁移验收库中先跑 npm run db:smoke-seed,否则统计和抽样验收会被模拟数据污染。

导出目录

当前工作区已有旧 PocketBase SQLite 数据参考:

F:\project\参考\旧题库数据库文件
  data.db
  auxiliary.db
  storage/

data.dbauxiliary.db 必须按只读源处理,不能直接在旧库上执行修复 SQL。当前已补 npm run pb:export:sqlite,会用只读 SQLite 连接把 PocketBase collection 导出为规范 JSON再进入下面的 dry-run/import 管线;storage/ 中的附件、题图、PDF、视频封面等资源会同步生成资源迁移清单最终进入 content_assets 和对象存储,不允许把旧本地路径或长效 URL 直接写给前端。

把 PocketBase 导出的集合 JSON 放到仓库根目录:

pb_export/
  users.json
  regions.json
  region_modules.json
  module_nodes.json
  subjects.json
  categories.json
  questions.json
  orders.json
  svip_plans.json
  codes.json
  code_batches.json
  vocabulary_units.json
  vocabulary.json
  handbook_subjects.json
  handbook_chapters.json
  handbook_entries.json
  scoreline_schools.json
  scoreline_majors.json
  scoreline_fields.json
  scoreline_records.json
  video_explanations.json
  question_videos.json

可以用 PB_EXPORT_DIR 指向其他目录:

$env:PB_EXPORT_DIR="F:\migration\pb_export_20260630"
npm run pb:import:dry-run

如果输入源仍是 SQLite而不是 JSON 目录,先执行 scripts/import-pocketbase 的 SQLite 只读导出命令,建议输出到仓库外或 .gitignore 覆盖的 pb_export/

$env:PB_SQLITE_DIR="F:\project\参考\旧题库数据库文件"
$env:PB_EXPORT_DIR="F:\project\pb_export"
npm run pb:export:sqlite

导出脚本行为:

  • 使用只读 SQLite 连接,禁止修改 data.dbauxiliary.dbstorage/
  • 每个 PocketBase collection 输出一个 JSON 文件,并保留旧 idcreatedupdated、relation 字段和文件字段。
  • passwordtokenKeysecretwechatSessionKey、短信验证码等密钥类字段默认不会写入普通 JSONopenid/unionid 默认也会移除,只在 sqlite-export-manifest.json 里记录字段和数量。后续如果确实要迁移第三方登录身份,必须走单独的加密身份迁移链路,不混入普通 pb_export
  • 手机号、邮箱、业务旧 ID、订单、题目、学习记录等迁移必需字段会保留用于自营 ToC 租户的数据归属和售后追溯。
  • 资源清单输出到 storage-manifest.json包含相对路径、hash、size、mime 和旧 collection/record/file 三元组,后续由对象存储迁移步骤转为 content_assets
  • auxiliary.db 默认只写 _logs 的数量和 level 摘要,不导出完整日志正文,避免把 UA、请求参数或历史 token 放进迁移数据包。

可选环境变量:

$env:PB_SQLITE_DATA_DB="F:\project\参考\旧题库数据库文件\data.db"
$env:PB_SQLITE_AUX_DB="F:\project\参考\旧题库数据库文件\auxiliary.db"
$env:PB_SQLITE_STORAGE_DIR="F:\project\参考\旧题库数据库文件\storage"
$env:PB_SQLITE_COLLECTIONS="users,regions,subjects,categories,questions"
$env:PB_SQLITE_BATCH_SIZE="2000"

正常迁移不要设置 PB_SQLITE_INCLUDE_SENSITIVE_IDENTITIES=true。这个开关只允许在加密迁移工作区临时使用,并且导出的身份文件不得提交 Git。

当前真实库只读导出基线:

data.db: 58 个业务 collection248555 条记录
questions: 74102
users: 3670
orders: 636
user_answer_records: 85442
vocabulary: 3500
handbook_entries: 2676
storage-manifest: 9 个原始资源文件
auxiliary.db: _logs 121715 条,仅摘要

最近一次 production dry-run 仍有 2 个真实数据 blocker需要在正式切换前处理

  • orders.userId 覆盖率 95.3%30 个订单缺用户,其中 7 个为已支付订单。处理策略应优先从支付通知、手机号、订单号或人工售后记录找回用户;找不回的已支付订单必须进入人工异常台账,不允许静默开权益。
  • handbook_chapters.subjectId 覆盖率 94.4%22 个章节缺所属手册。处理策略应按章节标题和条目归属归并到正确 handbook_subjects,无法归并的放入迁移隔离手册并标记待人工复核。

当前导入器已经补齐下面 4 个旧集合的标准化 mapper

  • user_answer_records 85442 条会导入到 answer_records,并按错误记录重建 wrong_questions。旧用户为空或旧用户已不存在的答题记录会跳过并写 warning不作为财务/权益级 blocker。
  • mock_exam_configs 48 条会导入到 practice_blueprints(mode='mock_exam', assembly_type='filters')
  • referral_qrcodes 79 条会导入到 referral_qrcodes,并同步补 referral_codes
  • commission_settings 1 条会导入到 tenant_commission_settings

当前导入器还会把旧题库的树形导航整理成新 SaaS 题库导航:

  • region_modules 生成 content_entries(entry_type='question_practice')
  • module_nodessubjectscategories 生成 content_nodes,保留任意深度分类和考试意向标记。
  • questions.nodeId 会优先生成 module_node:*:direct 题目合集;旧库真实数据中 74102 道题都有 nodeId,因此这是迁移后的第一归属。
  • categoryId 只覆盖 27665 道题,作为补充分类合集处理,不再作为唯一前端入口。
  • 每个有效合集自动生成顺序刷题和随机刷题 practice_blueprints,旧 mock_exam_configs 继续生成全真模拟蓝图。
  • 每道已发布旧题都会回填 entry_idcontent_node_idprimary_collection_id,方便 Taro 前端直接按 content_entries/content_nodes/question_collections/practice_blueprints 对接。

导入器对两个真实 blocker 采用“隔离 + 审计”策略,不会静默丢数据:

  • 缺用户订单仍导入 orders/payments/order_itemsraw_payload.migration.reviewRequired=trueentitlementBlocked=true,并写入 pb_import_issues。缺用户订单不会自动开通权益。
  • subjectId 的手册章节会挂到 handbook_subjects.legacy_id='__migration_orphan_handbook_subject__' 的“迁移待复核手册”,章节 metadata.reviewRequired=true,并写入 pb_import_issues

production dry-run 还有这些 warning属于运营处理项

  • dashboard_cache、空 customer_messages/smscodes/tenant_config 可不作为正式数据源,必要时只保留审计摘要。
  • recent_practices 和少量 user_answer_records 有旧用户引用断裂,需要在导入后复核 pb_import_issues

最新真实导入演练结果:

环境:本地 Supabase先执行 npx supabase db reset
命令npm run pb:import:json
耗时:约 10 分 11 秒
导入后校验npm run pb:import:validate => 0 failures, 3 warnings

核心计数:
platform_users: 3670
questions: 74102
answer_records: 85199
wrong_questions: 38205
orders: 636
entitlements: 447
referral_qrcodes: 79
tenant_commission_settings: 1

新题库导航计数:
content_entries: 11
content_nodes: 2830
question_collections: 1597
question_collection_items: 82106
practice_blueprints: 3102
questions_missing_entry/node/collection: 0
node_direct_collections: 1518
hidden_review_entries: 1
orphan_review_collections: 5
orphan_review_questions: 103

最新导入 run 的 pb_import_issues 仍有 29 个 critical需要正式切换前人工复核

  • 7 个 orders 已支付订单缺用户:订单会进入财务复核,不自动开通权益。
  • 22 个 handbook_chapters 缺所属手册:章节会挂到“迁移待复核手册”。

导入后校验的 3 个 warnings 当前含义:

  • 2533 道题保留 legacy_category_id 但无法解析到旧 categories/module_nodes。其中能落到明确 nodeId 或公开科目的题会进入对应合集;确实断裂且不可公开归类的 103 道题进入隐藏的“迁移待复核题库”,不会出现在学生端目录。
  • 5298 条旧答题记录保留 legacy_question_id 但无法解析到题目,说明旧学习日志引用了已删除/未导出的题目;这些记录不会进入错题本有效组卷。
  • 最新 run 存在 29 个 critical import issues即上面的付费订单和手册章节人工复核项。

nodeId 断裂的题目分两类处理:

  • 103 道题:科目可解析,但旧节点和科目入口都无法恢复,进入隐藏入口 migration_review:orphan_questions、inactive 节点和 draft 合集,只给租户后台复核。
  • 129 道题:旧节点断裂,但科目/入口仍可公开解析,挂到对应公开科目 fallback 合集,学生端可正常看到。

性能注意:真实 pb:import:json 已能在约 10 分钟完成 248555 条真实记录导入并生成新导航。后续若题库规模继续增长,仍建议继续批量化 questions/question_versions、记录阶段耗时、增加失败恢复和断点重跑策略,并按 docs/refactor/performance-benchmark-runbook.md 做导入后 API 压测。

阶段 1静态 Dry-Run

先运行不写数据库的静态报告。默认是 development profile适合开发环境快速发现 JSON 形态、关系和敏感字段问题:

npm run pb:import:dry-run

生成机器可读报告:

npm run pb:import:dry-run -- --json > migration-dry-run-report.json

预生产验收、最终切换和 CI 必须使用 production profile并建议同时打开 warning 阻断:

npm run pb:import:dry-run -- --profile=production --json --fail-on-warnings

也可以用环境变量指定 profile

$env:PB_DRY_RUN_PROFILE="production"
npm run pb:import:dry-run -- --json --fail-on-warnings
Remove-Item Env:\PB_DRY_RUN_PROFILE

--profile 只接受 developmentproduction,拼写错误会 fail closed 并返回 blocker避免正式迁移时误用开发模式。

dry-run 会检查:

  • 导出目录是否存在。
  • JSON 是否可解析,集合是否是数组或 { items: [] } / { records: [] }
  • 核心集合是否缺失。
  • 旧记录 id 是否缺失或重复。
  • docs/pb_schema.json 中的 relation 是否能解析到导出数据。
  • 敏感字段是否出现在旧导出中。
  • 未映射集合是否需要补 mapper。
  • 用户、题目、订单、SVIP、激活码、单词、手册、分数线、视频等业务数量。
  • production profile 会额外检查生产迁移必需集合:usersquestionssubjectscategoriesorderssvip_planscodesvocabulary_unitsvocabularyhandbook_subjectshandbook_chaptershandbook_entries
  • migrationReadiness.criticalFieldCoverage 会统计关键字段覆盖率,例如 users.phonequestions.subjectId/categoryId/contentorders.userId/planId/statuscodes.code、单词和手册的归属字段;生产模式下低于阈值会变成 blocker。
  • 对 SQLite 导出,questions.categoryId 会把 nodeId 视为新架构有效归属,vocabulary.unitId 会兼容旧字段 unit

准入标准:

  • blockers = 0
  • 正式切换前建议 warnings = 0;如确有历史脏数据,需要记录处理结论、影响范围和接受人。
  • businessCounts 与旧后台统计口径差异必须能解释。
  • 生产切换前 migrationProfile 必须是 production,且 migrationReadiness.requiredCollections 不能有缺失或记录数不足。

阶段 2正式导入演练

确认目标租户:

$env:TENANT_ID="00000000-0000-0000-0000-000000000001"
$env:TENANT_SLUG="master"
$env:TENANT_NAME="工学题库主租户"

执行导入:

npm run pb:import:json

默认不会把旧系统密钥值写入新库,只会记录脱敏和问题项。只有在迁移受控密钥到 app_private.tenant_secrets 时才允许临时打开:

$env:IMPORT_SECRET_VALUES="true"
npm run pb:import:json
Remove-Item Env:\IMPORT_SECRET_VALUES

密钥导入后必须立即执行生产就绪检查和人工抽查,确保 public 表不含密钥明文。

阶段 3导入后校验

导入完成后运行:

npm run pb:import:validate

正式切换前建议使用 warning 阻断:

$env:FAIL_ON_WARNINGS="true"
npm run pb:import:validate
Remove-Item Env:\FAIL_ON_WARNINGS

必须通过的关键检查:

  • 租户存在。
  • 最新一次导入的核心 raw records 已规范化。
  • 每道题都有 current version。
  • question_versions.tenant_id 与题目一致。
  • 已支付订单有 payment 行。
  • 权益关联到有效用户。
  • platform_users.raw_profile 不含敏感身份字段。
  • tenant_settings.public_config 不含密钥。
  • crm_config.secret_ref 只引用 app_private.tenant_secrets

当前演练可接受但上线前必须确认的 warning

  • 已支付订单缺用户不能自动开权益,必须由财务/运营确认是否补绑用户、退款、作废或保留售后台账。
  • 手册章节缺归属需要内容负责人确认归并到正确手册,或保留在迁移待复核手册并在前端隐藏。
  • 已删除分类下的题目和已删除题目对应的学习记录,需要按运营口径决定是否建立“迁移待复核分类”、按科目批量归档,或仅保留 legacy trace。

阶段 4抽样验收

结构校验通过后,先运行只读业务抽样脚本:

npm run pb:import:sample

它和 pb:import:validate 的边界不同:

  • pb:import:validate 证明核心表关系、敏感字段和迁移隔离策略没有断。
  • pb:import:sample 证明真实迁移数据能被新 SaaS 业务模型消费,覆盖题库入口、内容节点、合集、练习蓝图、题目当前版本、答题记录、错题、收藏、单词、知识手册、分数线、订单、支付、权益、激活码、内容资源、视频和敏感字段泄露。
  • 脚本只读数据库不修复数据不生成测试数据FAIL 表示当前业务模型不能安全消费该部分迁移数据WARN 表示旧数据或上线配置需要人工复核。

常用参数:

$env:DATABASE_URL="postgresql://postgres:postgres@127.0.0.1:54322/postgres"
$env:PB_SAMPLE_TENANT_SLUG="master"
npm run pb:import:sample

# 需要保存本地报告时开启;报告目录已被 .gitignore 忽略
$env:PB_SAMPLE_WRITE_REPORT="true"
npm run pb:import:sample
Remove-Item Env:\PB_SAMPLE_WRITE_REPORT

当前真实迁移库最近一次抽样结果:

npm run pb:import:sample => 0 failures, 6 warnings, 1 skipped, 39 passed

其中 6 个 warning 分别对应:迁移待复核题目、部分旧答题记录引用已删除题、已支付缺用户订单、已使用但缺使用人的激活码、已知 critical import issues 人工复核项、最新导入 warning/error issue 留档项;videos.not_present 为 SKIP因为当前旧库没有题目视频记录。

每次演练都应至少抽样下面数据:

范围 抽样建议 验收点
用户 随机 20 个学生、5 个管理员/销售/教师 手机号、昵称、角色、禁用状态、地区/院校目标、会员状态
题库 每个地区至少 2 个入口,每个入口抽 2 条路径 入口、分类层级、考试意向标记、题目数量
题目 每种题型至少 10 道,含阅读理解/案例分析 题干、选项、答案、解析、子题、图片/公式、难度、标签
练习 顺序、随机、全真模拟各 3 次 组卷、答题、判分、错题、收藏、报告、复盘
单词 每个地区/科目抽 2 个单元 单词、音标、释义、例句、收藏、进度
知识手册 每个手册抽 2 个章节 Markdown、图片、公式、目录、权限
分数线 每个地区抽 2 所学校、2 个专业 年份、动态字段、趋势查询
视频 抽 20 道带视频题 权限、次数扣减、签名 URL、水印 traceId、播放日志
资料 抽 PDF/图片各 10 个 content_assets 台账、扫描状态、预览/下载短签名、水印
订单权益 抽 20 个付费订单、20 个激活码 订单状态、payment、entitlement、地区/范围、过期时间
营销 抽优惠券、激活码批次、勋章 规则、核销记录、发放记录
CRM/推广 抽销售、代理、自然流用户 首绑保护、推广来源、队列 payload、分佣归因

抽样结果建议保存到 docs/refactor/migration-reports/。真实数据报告可能包含业务敏感信息提交前必须脱敏如包含用户手机号、订单号、openid、支付流水号不要提交到 Git。

阶段 5API 与 Taro 联调验收

迁移数据通过校验后,再启动 API

npm run dev:api

前端联调至少跑通:

  • 学生 H5登录、首页、地区选择、题库入口、刷题、错题、收藏、背单词、知识手册、分数线、资料、视频、会员、订单、激活码、个人中心。
  • 租户后台 H5数据看板、学生管理、题库内容、导入、公共题库采纳/同步、营销、财务、主题、权限、成员。
  • 平台后台 H5租户、套餐、订阅、账单、用量、公共题库授权。

前端只允许通过 apps/api 获取业务数据和签名资源。不要在前端直接访问 Supabase 表或拼接对象存储私有 URL。

阶段 6最终切换前冻结

正式切换窗口建议:

  1. 公告维护窗口。
  2. 旧 PocketBase 进入只读或暂停写入。
  3. 导出最终 JSON。
  4. 执行 production strict dry-run。
  5. 重置目标生产库或清理目标租户迁移数据。
  6. 执行正式导入。
  7. 执行 pb:import:validate strict 模式。
  8. 抽样验收核心链路。
  9. 切换域名/API 配置。
  10. 保留旧系统只读快照,至少覆盖一个完整售后周期。

回滚策略

切换后如果发现阻断级问题:

  • 立即停止新系统写入或进入维护模式。
  • 保留新库快照和 API 日志,便于定位导入器或业务 API 问题。
  • 将域名/API 流量切回旧 PocketBase 只读或旧生产服务。
  • 修复 mapper 或数据清洗规则后重新跑 dry-run、导入和抽样验收。

不要在问题未定位时手工批量修改生产表。所有批量修复应沉淀为可重复脚本或 importer mapper 修复,并保留审计记录。

上线准出标准

满足以下条件后,才建议进入生产切换:

  • npm run pb:import:dry-run -- --profile=production --json --fail-on-warnings 通过,或全部 warning 有签字确认的处理结论。
  • npm run pb:import:validate 无 FAIL生产切换前 strict 模式无 WARN或 WARN 已确认。
  • 核心业务抽样通过,尤其是题目答案解析、会员权益、订单支付、错题收藏、资料视频权限。
  • npm run readiness:productionnpm run readiness:production:db 通过。
  • API/Taro 三端核心链路在迁移数据上跑通。
  • 对象存储、短信、OAuth、支付、CRM webhook 使用生产 provider 或明确标记为暂不开启。
  • 已准备数据库备份、旧系统快照、回滚步骤和负责人。