# PocketBase 到 Supabase 迁移映射

## 迁移原则

- 保留 PocketBase 旧 ID 到 `legacy_id`，新系统主键统一用 UUID。
- 所有租户内业务表都带 `tenant_id`。
- 旧表原始 JSON 进入 `pb_raw_records` 前默认脱敏，保证可追溯但不泄露密钥。
- 旧集合里不符合商用规范的字段不得直接映射到正式表，必须经过清洗和拆表。
- 第一阶段标准化导入覆盖核心商用链路：租户、用户、题库目录、题目、订单、支付、权益、激活码、优惠券、词库、手册、运营内容、分数线、视频解析、CRM 和推广关系。未覆盖集合先 raw import，并在 `pb_import_issues` 里记录风险。
- `users.isSvip`、`svipExpiry`、`svipRegions` 最终迁移为 `entitlements`。
- `users.stats.favorites`、`wrongBook` 最终迁移为 `favorite_questions`、`wrong_questions`。
- `settings` 中的密钥类配置必须进入 `app_private.tenant_secrets` 或外部 Vault，不进入 `public` schema。

## 核心映射

| PocketBase | PostgreSQL | 说明 |
| --- | --- | --- |
| `tenant_config` | `tenants` / `tenant_branding` / `tenant_settings` | 从单实例配置升级为平台租户配置 |
| `users` | `platform_users` / `tenant_memberships` / `student_profiles` / `user_identities` | 用户身份、租户角色、学生资料拆分 |
| `regions` | `regions` | 增加 `tenant_id` |
| `region_modules` | `region_modules` | 增加 `tenant_id` |
| `module_nodes` | `module_nodes` | 作为后续题库层级主结构 |
| `subjects` | `subjects` | 保留旧结构兼容，逐步和 `module_nodes` 对齐 |
| `categories` | `categories` | 保留旧结构兼容 |
| `questions` | `questions` / `question_versions` | 题目实体和题目内容版本拆分 |
| `orders` | `orders` / `order_items` / `payments` / `payment_events` | 订单与支付流水分离 |
| `svip_plans` | `svip_plans` | 金额改为分 |
| `codes` | `activation_codes` | 激活码表 |
| `code_batches` | `code_batches` | 批次表 |
| `coupons` | `coupons` | 优惠券 |
| `coupon_redemptions` | `coupon_redemptions` | 兑换流水 |
| `vocabulary_units` | `vocabulary_units` | 背单词单元 |
| `vocabulary` | `vocabulary_words` | 单词表 |
| `handbook_*` | `handbook_*` | 手册内容 |
| `banners` / `faqs` / `announcements` | 同名表 | 运营内容 |
| `settings` | `tenant_settings` / `tenant_payment_accounts` | 拆分公开配置、私密配置、支付配置 |
| `crm_config.secret` | `app_private.tenant_secrets` | 公共表只保留 `secret_ref` |
| `users.stats.favorites` | `favorite_questions` | 迁移为关系表 |
| `users.stats.wrongBook` | `wrong_questions` | 迁移为关系表 |
| `users.isSvip` / `svipExpiry` / `svipRegions` | `entitlements` | 迁移为租户/地区范围权益 |
| `user_word_progress` / `user_word_favorites` | 同名规范表 | 关联到 `platform_users` 与 `vocabulary_words` |
| `app_assets` / `images` | `content_assets` | 统一素材索引 |

## 导入命令

先安装导入器依赖：

```bash
cd scripts/import-pocketbase
copy .env.example .env
npm install
```

查看 schema 摘要：

```bash
npm run schema:summary
npm run schema:risk
```

把 PocketBase 导出的集合 JSON 放到 `pb_export`：

```text
pb_export/
  users.json
  regions.json
  questions.json
```

先执行不写数据库的 dry-run：

```bash
npm run import:dry-run
```

根目录也可以执行：

```bash
npm run pb:import:dry-run
```

dry-run 会检查：

- 导出目录和 JSON 文件格式。
- 核心集合是否缺失。
- 旧记录 `id` 是否缺失或重复。
- 旧 `pb_schema.json` 中的 relation 是否能在导出数据里找到目标记录。
- `settings`、`crm_config`、用户资料等位置是否存在敏感字段。
- 哪些集合当前已有规范化 mapper，哪些集合仍未映射。

存在 `blocker` 时不要执行正式导入；先修导出、补 mapper 或记录业务处理结论。

执行正式导入：

```bash
npm run import:json
```

导入后执行验证：

```bash
npm run import:validate
```

根目录也可以执行：

```bash
npm run pb:import:dry-run
npm run pb:import:json
npm run pb:import:validate
```

导入器会先把所有 JSON 放入 `pb_raw_records`，并默认对敏感字段脱敏；然后按依赖顺序标准化导入业务表。上线前必须处理 `FAIL` 项；`WARN` 项通常表示旧数据关系缺失，例如旧题目引用了不存在的章节，需要业务确认是否可接受。
