Files
gongxue-base/docs/refactor/pocketbase-real-data-migration-runbook.md
2026-06-30 12:47:27 +08:00

404 lines
19 KiB
Markdown
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.

# PocketBase 真实数据迁移验收 Runbook
更新时间2026-06-30
这份文档用于把旧 PocketBase 生产数据迁移到新的 Supabase/PostgreSQL 多租户题库 SaaS。目标不是简单把 JSON 塞进新库,而是证明旧系统的用户、题库、订单、权益、学习数据和运营配置进入新模型后仍能支撑业务上线。
## 适用范围
本流程适用于正式切换前的迁移演练、预生产验收和最终切换。旧 PocketBase/React 项目只作为导出来源和功能参照,新系统以 `apps/api``apps/worker``supabase/migrations``apps/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 数据库。迁移演练开始前:
```bash
npm install
npm run supabase:start
npm run supabase:reset
```
如果要验证 API 主链路,可以在真实数据导入后再补充最小管理账号或临时测试账号。不要在真实迁移验收库中先跑 `npm run db:smoke-seed`,否则统计和抽样验收会被模拟数据污染。
## 导出目录
当前工作区已有旧 PocketBase SQLite 数据参考:
```text
F:\project\参考\旧题库数据库文件
data.db
auxiliary.db
storage/
```
`data.db``auxiliary.db` 必须按只读源处理,不能直接在旧库上执行修复 SQL。当前已补 `npm run pb:export:sqlite`,会用只读 SQLite 连接把 PocketBase collection 导出为规范 JSON再进入下面的 dry-run/import 管线;`storage/` 中的附件、题图、PDF、视频封面等资源会同步生成资源迁移清单最终进入 `content_assets` 和对象存储,不允许把旧本地路径或长效 URL 直接写给前端。
把 PocketBase 导出的集合 JSON 放到仓库根目录:
```text
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` 指向其他目录:
```powershell
$env:PB_EXPORT_DIR="F:\migration\pb_export_20260630"
npm run pb:import:dry-run
```
如果输入源仍是 SQLite而不是 JSON 目录,先执行 `scripts/import-pocketbase` 的 SQLite 只读导出命令,建议输出到仓库外或 `.gitignore` 覆盖的 `pb_export/`
```powershell
$env:PB_SQLITE_DIR="F:\project\参考\旧题库数据库文件"
$env:PB_EXPORT_DIR="F:\project\pb_export"
npm run pb:export:sqlite
```
导出脚本行为:
- 使用只读 SQLite 连接,禁止修改 `data.db``auxiliary.db``storage/`
- 每个 PocketBase collection 输出一个 JSON 文件,并保留旧 `id``created``updated`、relation 字段和文件字段。
- `password``tokenKey``secret``wechatSessionKey`、短信验证码等密钥类字段默认不会写入普通 JSON`openid/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 放进迁移数据包。
可选环境变量:
```powershell
$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。
当前真实库只读导出基线:
```text
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_nodes``subjects``categories` 生成 `content_nodes`,保留任意深度分类和考试意向标记。
- `questions.nodeId` 会优先生成 `module_node:*:direct` 题目合集;旧库真实数据中 74102 道题都有 `nodeId`,因此这是迁移后的第一归属。
- `categoryId` 只覆盖 27665 道题,作为补充分类合集处理,不再作为唯一前端入口。
- 每个有效合集自动生成顺序刷题和随机刷题 `practice_blueprints`,旧 `mock_exam_configs` 继续生成全真模拟蓝图。
- 每道已发布旧题都会回填 `entry_id``content_node_id``primary_collection_id`,方便 Taro 前端直接按 `content_entries/content_nodes/question_collections/practice_blueprints` 对接。
导入器对两个真实 blocker 采用“隔离 + 审计”策略,不会静默丢数据:
- 缺用户订单仍导入 `orders/payments/order_items``raw_payload.migration.reviewRequired=true``entitlementBlocked=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`
最新真实导入演练结果:
```text
环境:本地 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 形态、关系和敏感字段问题:
```bash
npm run pb:import:dry-run
```
生成机器可读报告:
```bash
npm run pb:import:dry-run -- --json > migration-dry-run-report.json
```
预生产验收、最终切换和 CI 必须使用 `production` profile并建议同时打开 warning 阻断:
```bash
npm run pb:import:dry-run -- --profile=production --json --fail-on-warnings
```
也可以用环境变量指定 profile
```powershell
$env:PB_DRY_RUN_PROFILE="production"
npm run pb:import:dry-run -- --json --fail-on-warnings
Remove-Item Env:\PB_DRY_RUN_PROFILE
```
`--profile` 只接受 `development``production`,拼写错误会 fail closed 并返回 blocker避免正式迁移时误用开发模式。
dry-run 会检查:
- 导出目录是否存在。
- JSON 是否可解析,集合是否是数组或 `{ items: [] }` / `{ records: [] }`
- 核心集合是否缺失。
- 旧记录 `id` 是否缺失或重复。
- `docs/pb_schema.json` 中的 relation 是否能解析到导出数据。
- 敏感字段是否出现在旧导出中。
- 未映射集合是否需要补 mapper。
- 用户、题目、订单、SVIP、激活码、单词、手册、分数线、视频等业务数量。
- `production` profile 会额外检查生产迁移必需集合:`users``questions``subjects``categories``orders``svip_plans``codes``vocabulary_units``vocabulary``handbook_subjects``handbook_chapters``handbook_entries`
- `migrationReadiness.criticalFieldCoverage` 会统计关键字段覆盖率,例如 `users.phone``questions.subjectId/categoryId/content``orders.userId/planId/status``codes.code`、单词和手册的归属字段;生产模式下低于阈值会变成 blocker。
- 对 SQLite 导出,`questions.categoryId` 会把 `nodeId` 视为新架构有效归属,`vocabulary.unitId` 会兼容旧字段 `unit`
准入标准:
- `blockers = 0`
- 正式切换前建议 `warnings = 0`;如确有历史脏数据,需要记录处理结论、影响范围和接受人。
- `businessCounts` 与旧后台统计口径差异必须能解释。
- 生产切换前 `migrationProfile` 必须是 `production`,且 `migrationReadiness.requiredCollections` 不能有缺失或记录数不足。
## 阶段 2正式导入演练
确认目标租户:
```powershell
$env:TENANT_ID="00000000-0000-0000-0000-000000000001"
$env:TENANT_SLUG="master"
$env:TENANT_NAME="工学题库主租户"
```
执行导入:
```bash
npm run pb:import:json
```
默认不会把旧系统密钥值写入新库,只会记录脱敏和问题项。只有在迁移受控密钥到 `app_private.tenant_secrets` 时才允许临时打开:
```powershell
$env:IMPORT_SECRET_VALUES="true"
npm run pb:import:json
Remove-Item Env:\IMPORT_SECRET_VALUES
```
密钥导入后必须立即执行生产就绪检查和人工抽查,确保 public 表不含密钥明文。
## 阶段 3导入后校验
导入完成后运行:
```bash
npm run pb:import:validate
```
正式切换前建议使用 warning 阻断:
```powershell
$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抽样验收
每次演练都应至少抽样下面数据:
| 范围 | 抽样建议 | 验收点 |
| --- | --- | --- |
| 用户 | 随机 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
```bash
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:production``npm run readiness:production:db` 通过。
- API/Taro 三端核心链路在迁移数据上跑通。
- 对象存储、短信、OAuth、支付、CRM webhook 使用生产 provider 或明确标记为暂不开启。
- 已准备数据库备份、旧系统快照、回滚步骤和负责人。