# Staging 迁移执行手册 — AIGNE Hub (Media Kit + Payment Kit 部分)

> **受众**：Shijun
> **目标**：把 staging AIGNE Hub 的 Media Kit 和 Payment Kit 从 AWS Blocklet Server 完整迁移到 Cloudflare Workers
> **范围**：本手册**只覆盖** Media Kit 和 Payment Kit。blocklet-service（DID）部分由 DID 团队负责，假设执行本手册前 **staging blocklet-service 已经部署在 CF**。
> **预计时间**：3~5 小时（首次执行）
> **前置阅读**：`README.md`（部署架构）、`MIGRATION-RUNBOOK.md`（迁移原理）、`MIGRATION-CHALLENGES.md`（已知坑）

---

## 📋 执行前必须确认

在动手前**逐项确认**下面 6 件事，任何一项不满足都不要开始：

| # | 确认项 | 确认方法 | 不满足怎么办 |
|---|--------|---------|-------------|
| 1 | AWS staging 访问权限 | `ssh <TODO: AWS_STAGING_HOST>` 能连上 | 找运维要跳板机权限 |
| 2 | staging blocklet-service 已在 CF 上 | `wrangler deployments list --name <TODO: STAGING_BLOCKLET_SERVICE_WORKER_NAME>` 有记录 | **停止**，等 DID 团队通知 |
| 3 | blocklet-service 的 `BlockletServiceRPC` entrypoint 可用 | DID 团队确认 `registerApp` / `getAppEk` / `resolveIdentity` 方法已实现 | **停止**，等 DID 团队通知 |
| 4 | Cloudflare 账号已登录 | `wrangler whoami` 返回预期账号 | `wrangler login` |
| 5 | Payment Kit 仓库已 clone 且 CF 分支已构建过 | `cd blocklets/core/cloudflare && node run-build.js` 本地能跑通 | 参考 README.md Phase 4 |
| 6 | Stripe staging 账号有权限改 webhook endpoint | 能登录 Stripe Dashboard staging 账号 | 找 Stripe 账号管理者 |

**确认完毕**后再进入 Phase 0。

---

## Phase 0：AWS staging 信息收集（10 min）

SSH 上 AWS，拿下面这些值，记到本地 **staging-migration-secrets.txt**（不要提交）：

```bash
ssh <TODO: AWS_STAGING_HOST>

echo $BLOCKLET_APP_ID   # APP_DID
echo $BLOCKLET_APP_PID  # APP_PID（后续 registerApp 用）
echo $BLOCKLET_APP_SK   # APP_SK（关键，后续 registerApp + CF secret 都用）
find $BLOCKLET_DATA_DIR -name "*.db"  # 定位 Payment Kit 数据库
```

如果 `$BLOCKLET_APP_SK` 为空，从 `~/.blocklet/core/server.db` 读：

```bash
sqlite3 ~/.blocklet/core/server.db \
  "SELECT appSk FROM blocklets WHERE appDid='$BLOCKLET_APP_ID'"
```

如果 `APP_DID ≠ APP_PID`（SK 曾轮转），额外记下 `$BLOCKLET_APP_PSK`。

## Phase 1：验证密钥派生一致性（5 min）

**关键**：如果导错 SK，后面全部白干。在本地机器上跑：

```bash
cd /tmp && mkdir did-verify && cd did-verify
npm init -y
npm install @ocap/wallet @ocap/mcrypto

node -e "
const { fromSecretKey } = require('@ocap/wallet');
const { types } = require('@ocap/mcrypto');
const sk = '<Phase 0 拿到的 APP_SK>';
const wallet = fromSecretKey(sk, {
  role: types.RoleType.ROLE_APPLICATION,
  pk: types.KeyType.ED25519,
  hash: types.HashType.SHA3,
});
console.log('Derived APP DID:', wallet.address);
console.log('Expected APP DID:', '<Phase 0 拿到的 APP_DID>');
console.log('Match:', wallet.address === '<APP_DID>');
"
```

- **`Match: true`** → 继续 Phase 2
- **`Match: false`** → **停止**。重新确认 `APP_SK` 是不是拿错了（可能拿到的是 server 的 SK，不是 blocklet 的）

## Phase 2：在 staging blocklet-service 上注册 Payment Kit（20 min）

这一步**必须在部署 Payment Kit Worker 之前**完成。blocklet-service 需要知道 Payment Kit 的身份，后续 Payment Kit Worker 启动时 `AUTH_SERVICE.getAppEk(APP_PID)` 才能拿到 EK 初始化 Stripe 加密链。

### 2.1 准备注册参数

```
APP_PID:       <Phase 0 拿到的 PID，复用 AWS 原值>
APP_SK:        <Phase 0 拿到的 hex>
APP_NAME:      Payment Kit
APP_DESC:      Payment Kit (staging)
APP_URL:       https://payment-kit-staging.<your-account>.workers.dev
               （先用 workers.dev，验证通过后再切自定义域名）
```

### 2.2 调用 registerApp

> **⚠️ Shijun 需要和 DID 团队确认注册方式**。下面是两种可能的路径，具体哪种由 blocklet-service 的实现决定：

**方式 A：通过 blocklet-service admin UI**

1. 访问 `https://<TODO: STAGING_BLOCKLET_SERVICE_URL>/admin`
2. 登录（DID 团队给的 admin 账号）
3. 找到 "Apps" / "Blocklets" 管理页
4. 点击 "Register App"，填入上面的参数
5. 上传 APP_SK（或粘贴 hex），确认 derived DID 与 APP_PID 一致

**方式 B：通过 RPC 调用**

```bash
# 如果 blocklet-service 暴露了注册 API：
curl -X POST https://<TODO: STAGING_BLOCKLET_SERVICE_URL>/admin/api/apps \
  -H "Authorization: Bearer <admin token>" \
  -H "Content-Type: application/json" \
  -d '{
    "appPid": "<APP_PID>",
    "appSk": "<APP_SK>",
    "name": "Payment Kit",
    "description": "Payment Kit (staging)",
    "url": "https://payment-kit-staging.<account>.workers.dev"
  }'
```

### 2.3 验证注册成功

```bash
# 假设 DID 团队提供了查询接口，或直接登录 admin UI 看列表
curl https://<TODO: STAGING_BLOCKLET_SERVICE_URL>/admin/api/apps/<APP_PID>
# 预期返回：{ pid, name, url, ek_initialized: true, ... }
```

**关键 1**：确认注册后该 APP_PID 对应的 **EK 已经初始化**。如果 EK 没生成，后续 Payment Kit Worker 首请求会打 `[Security] No APP_EK found`。

### 2.4 ⚠️ 和 DID 团队确认 EK 与 AWS 一致

> "我用 AWS 原 `APP_SK` + 原 `APP_PID` 注册后，`getAppEk(APP_PID)` 返回的值和 AWS Blocklet Server 上该 blocklet 的 EK 是同一个吗？"

**EK 必须一致**，否则 `payment_methods` 表里加密的 Stripe secret key 无法解密。如果 DID 团队的回答是 "不一致"，**停止迁移**，让他们把 AWS 原 EK 手动写入 staging blocklet-service 再继续。原 EK 取法：

```bash
ssh <AWS_STAGING_HOST>
echo $BLOCKLET_APP_EK
# 或: sqlite3 ~/.blocklet/core/server.db "SELECT appEk FROM blocklets WHERE appDid='$BLOCKLET_APP_ID'"
```

## Phase 3：部署 Media Kit staging（参考外部文档）

Media Kit 有自己的 CF Workers 部署方式：

1. **切到 media-kit 仓库**：`cd <blocklet/media-kit checkout>`
2. **参考该仓库的 `cloudflare/README.md`** 完成 staging 部署
3. **执行完成后，记录以下信息**：

```
MEDIA_KIT_WORKER_NAME=<例如: media-kit-staging>
MEDIA_KIT_URL=https://<media-kit-staging>.<account>.workers.dev
```

4. **验证 Media Kit staging 可访问**：

```bash
curl https://<MEDIA_KIT_URL>/
# 预期：200 或 Media Kit 的欢迎响应（非 522/404）
```

> **如果 Media Kit 仓库没有 staging 配置模板**，Shijun 需要基于 `wrangler.jsonc` 复制一份 `wrangler.staging.jsonc`，改 name 和资源 ID。具体以 Media Kit 仓库的 README 为准。

## Phase 4：Payment Kit — 创建 Cloudflare 资源（10 min）

```bash
cd <payment-kit-repo>/blocklets/core/cloudflare

# 4.1 D1 数据库
wrangler d1 create payment-kit-staging
#   记下返回的 database_id，记为 $D1_ID

# 4.2 KV Namespace（DID Connect session）
wrangler kv namespace create payment-kit-staging-didconnect
#   记下返回的 id，记为 $KV_ID

# 4.3 CF Queue 主队列 + DLQ
wrangler queues create payment-kit-staging-jobs
wrangler queues create payment-kit-staging-jobs-dlq
```

**本地记录资源 ID**：

```
D1_ID=<...>
KV_ID=<...>
```

## Phase 5：Payment Kit — 导出 AWS 数据（20 min）

### 5.1 在 AWS 上导出全表

```bash
ssh <AWS_STAGING_HOST>
PAYMENT_DB=<Phase 0 找到的路径>

mkdir -p /tmp/pk-mig/sql && cd /tmp/pk-mig

# 导出所有业务表为幂等 INSERT
for t in $(sqlite3 $PAYMENT_DB "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name != 'SequelizeMeta'"); do
  sqlite3 $PAYMENT_DB ".mode insert $t" "SELECT * FROM $t" \
    | sed 's/^INSERT INTO /INSERT OR IGNORE INTO /' > "sql/${t}.sql"
  echo "$t: $(grep -c '^INSERT' sql/${t}.sql)"
done > row-counts.txt

tar czf pk-data.tar.gz sql/ row-counts.txt
```

### 5.2 下载到本地

```bash
scp <AWS_STAGING_HOST>:/tmp/pk-mig/pk-data.tar.gz .
tar xzf pk-data.tar.gz
```

### 5.3 对大表分片

D1 单条 SQL 上限 100KB，超过 500 行 INSERT 的表要切片：

```bash
for t in credit_transactions invoice_items invoices meter_events payment_intents payment_stats price_quotes webhook_attempts; do
  F="sql/${t}.sql"
  [ -f "$F" ] && [ $(wc -l < "$F") -gt 500 ] && {
    split -l 500 -d "$F" "sql/${t}.part-"
    for p in sql/${t}.part-*; do mv "$p" "${p}.sql"; done
    rm "$F"
  }
done
```

### 5.4 替换内网地址

```bash
grep -rln '192\.168\.\|localhost\|127\.0\.0\.1\|10\.0\.' sql/
```

重点检查 `exchange_rate_providers.sql`、`webhook_endpoints.sql`、`settings.sql`。找到后手动编辑，把内网 URL 换成公网端点（比如汇率 API 换成 coingecko）。

## Phase 6：Payment Kit — 部署 D1 Schema（5 min）

在本地仓库执行：

```bash
cd <payment-kit-repo>/blocklets/core/cloudflare

# 6.1 应用 migration（表结构 + 索引 + 约束）
wrangler d1 migrations apply payment-kit-staging --remote

# 预期输出：0001_initial_schema.sql / 0002_indexes.sql / 0003_locks_and_constraints.sql 全部成功

# 6.2 验证表数量
wrangler d1 execute payment-kit-staging --remote \
  --command "SELECT COUNT(*) as cnt FROM sqlite_master WHERE type='table'"

# 预期：40 张左右（38 业务表 + _did_connect_tokens + d1_migrations）
```

## Phase 7：Payment Kit — 导入数据（1~2 hr）

按"基础表 → 业务表 → 交易/日志表"顺序导入，避开外键依赖问题。分片文件优先：

```bash
cd <payment-kit-repo>/blocklets/core/cloudflare
SQL_DIR=<Phase 5.2 解压的 sql 目录绝对路径>

import() {
  # 优先导入分片
  if ls "$SQL_DIR/$1.part-"*.sql 2>/dev/null | head -1 >/dev/null; then
    for p in "$SQL_DIR/$1.part-"*.sql; do
      wrangler d1 execute payment-kit-staging --remote --file="$p"
    done
  elif [ -f "$SQL_DIR/$1.sql" ]; then
    wrangler d1 execute payment-kit-staging --remote --file="$SQL_DIR/$1.sql"
  fi
}

# 基础表
for t in customers products prices meters coupons tax_rates payment_methods payment_currencies exchange_rate_providers settings; do import $t; done

# 业务表
for t in subscriptions subscription_items discounts promotion_codes product_vendors pricing_tables payment_links checkout_sessions setup_intents auto_recharge_configs credit_grants; do import $t; done

# 交易/日志表
for t in invoices invoice_items payment_intents price_quotes payment_stats credit_transactions meter_events webhook_endpoints webhook_attempts refunds payouts revenue_snapshots usage_records jobs archive_locks archive_metadata locks; do import $t; done
```

**行数校验**：

```bash
# 对几张关键表抽查即可
for t in customers invoices subscriptions payment_intents; do
  wrangler d1 execute payment-kit-staging --remote --command "SELECT COUNT(*) FROM $t"
done
```

结果和 Phase 5.1 的 `row-counts.txt` 对比。差异在几条以内可接受，大面积不一致回 Phase 7 重跑（INSERT OR IGNORE 幂等）。

## Phase 8：Payment Kit — 写 wrangler.staging.json（10 min）

```bash
cd <payment-kit-repo>/blocklets/core/cloudflare
cp wrangler.migration-test.json wrangler.staging.json
```

编辑 `wrangler.staging.json`，把关键字段改成 staging 值：

```jsonc
{
  "name": "payment-kit-staging",
  "main": "dist/worker.js",
  "compatibility_date": "2024-12-01",
  "compatibility_flags": ["nodejs_compat"],
  "placement": { "mode": "smart" },

  "assets": {
    "directory": "public",
    "binding": "ASSETS",
    "html_handling": "auto-trailing-slash",
    "not_found_handling": "single-page-application"
  },

  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "payment-kit-staging",
      "database_id": "<Phase 4.1 的 D1_ID>"
    }
  ],

  "kv_namespaces": [
    {
      "binding": "DID_CONNECT_KV",
      "id": "<Phase 4.2 的 KV_ID>"
    }
  ],

  "services": [
    {
      "binding": "MEDIA_KIT",
      "service": "<Phase 3 的 MEDIA_KIT_WORKER_NAME>"
    },
    {
      "binding": "AUTH_SERVICE",
      "service": "<TODO: STAGING_BLOCKLET_SERVICE_WORKER_NAME>",
      "entrypoint": "BlockletServiceRPC"
    }
  ],

  "vars": {
    "APP_NAME": "Payment Kit Staging",
    "APP_PID": "<Phase 0 的 APP_PID>",
    "COMPONENT_DID": "z2qaCNvKMv5GjouKdcDWexv6WqtHbpNPQDnAk",
    "MEDIA_KIT_URL": "<Phase 3 的 MEDIA_KIT_URL>"
  },

  "queues": {
    "producers": [
      { "binding": "JOB_QUEUE", "queue": "payment-kit-staging-jobs" }
    ],
    "consumers": [
      {
        "queue": "payment-kit-staging-jobs",
        "max_batch_size": 10,
        "max_batch_timeout": 5,
        "max_retries": 3,
        "dead_letter_queue": "payment-kit-staging-jobs-dlq"
      }
    ]
  },

  "triggers": {
    "crons": ["* * * * *"]
  }
}
```

## Phase 9：Payment Kit — 设置 Secrets（5 min）

```bash
cd <payment-kit-repo>/blocklets/core/cloudflare

wrangler secret put APP_SK --config wrangler.staging.json
# 粘贴 Phase 0 导出的 APP_SK hex
```

只需要设 `APP_SK`。`STRIPE_WEBHOOK_SECRET` 在 Phase 11 新建 Stripe endpoint 后再设；Stripe API key 走 DB + EK 解密，不设 env。

## Phase 10：Payment Kit — 构建 + 部署（15 min）

```bash
cd <payment-kit-repo>/blocklets/core

# 10.1 前端 SPA
npx vite build --config cloudflare/vite.config.ts --outDir cloudflare/public

# 10.2 Worker 入口
cd cloudflare
node run-build.js
# 预期：dist/worker.js 约 8~9MB

# 10.3 部署
npx wrangler deploy --config wrangler.staging.json

# 记下部署后的 URL，形如：
# https://payment-kit-staging.<account>.workers.dev
```

### 10.4 首请求日志验证

**立即**开启日志流，然后触发首请求：

```bash
# Terminal A
wrangler tail --config wrangler.staging.json --format pretty

# Terminal B
curl https://payment-kit-staging.<account>.workers.dev/__blocklet__.js
# 预期：返回 JS，包含 window.blocklet = { ... }

curl https://payment-kit-staging.<account>.workers.dev/api/checkout/sessions/status/test
# 预期：404 Checkout session not found（不是 500/503）
```

**Terminal A 应当看到**：

```
[Security] Initialized with EK from AUTH_SERVICE
```

如果看到以下任意一条 → **停止**排查：

| 日志 | 原因 | 处理 |
|---|---|---|
| `[Security] No APP_EK found for instance <APP_PID>` | Phase 2 registerApp 没注册成功或 EK 没生成 | 回 Phase 2 重新注册，或找 DID 团队确认 EK 状态 |
| `[Security] initFromAuthService failed: ...` | `AUTH_SERVICE` binding 错误或 RPC 不可用 | 检查 `wrangler.staging.json` 的 `services[].service` 和 `entrypoint` |
| `503 AUTH_SERVICE not configured` | `services` 配置缺失 | 检查 `wrangler.staging.json` |
| `500 no such table: customers` | Phase 6 schema 没应用 | 回 Phase 6 |

## Phase 11：Stripe Webhook 切换（10 min）

### 11.1 在 Stripe Dashboard 新建 webhook endpoint

1. 登录 Stripe Dashboard（**test mode**）
2. Developers → Webhooks → Add endpoint
3. Endpoint URL: `https://payment-kit-staging.<account>.workers.dev/api/stripe/webhook`
4. Events to send：勾选以下（或 `Select all events`）
   - `payment_intent.succeeded`
   - `payment_intent.payment_failed`
   - `payment_intent.canceled`
   - `invoice.payment_succeeded`
   - `invoice.payment_failed`
   - `invoice.finalized`
   - `customer.subscription.created`
   - `customer.subscription.updated`
   - `customer.subscription.deleted`
   - `checkout.session.completed`
5. 创建后点击 "Reveal" 拿到 `whsec_...`

### 11.2 用真 secret 覆盖

```bash
wrangler secret put STRIPE_WEBHOOK_SECRET --config wrangler.staging.json
# 粘贴 Phase 11.1 拿到的真 whsec_...
```

Worker 会在几秒内热更新 secret，不需要重新 deploy。

### 11.3 Stripe 侧测试 webhook

在 Stripe Dashboard 的新 endpoint 页面点 "Send test webhook"，选 `payment_intent.succeeded`，发送。

**Terminal A (`wrangler tail`) 预期**：

```
POST /api/stripe/webhook 200
```

如果是 `400 signature verification failed` → 说明 Phase 11.2 secret 放错了，重新 put 一次。

## Phase 12：端到端验证（30 min）

### 12.1 `__blocklet__.js` 检查

```bash
curl -s https://payment-kit-staging.<account>.workers.dev/__blocklet__.js
```

确认返回的 JS 里：
- `appName: 'Payment Kit Staging'`
- `appPid: '<APP_PID>'`
- `cloudflareWorker: true`

### 12.2 Admin UI 登录

1. 浏览器访问 `https://payment-kit-staging.<account>.workers.dev/admin`
2. 点击 "Connect DID" 或类似按钮
3. 用 DID Wallet 扫码登录
4. **期望**：能看到迁移过来的 customers / subscriptions / invoices 列表

### 12.3 Checkout 流程（Stripe test mode）

1. 打开前端创建一个 checkout session（用现有的测试商品）
2. 走完支付流程，用测试卡 `4242 4242 4242 4242`（任意未来日期 + 任意 CVC）
3. **验证**：
   - 支付成功页面正常
   - D1 里对应 checkout_session 的 `status='complete'`、`payment_status='paid'`
   - invoice 被创建（`SELECT * FROM invoices WHERE checkout_session_id = '<cs_xxx>'` 返回 1 行）
   - Stripe Dashboard 的 webhook logs 显示 200

### 12.4 订阅续费验证（如果 staging 数据里有活跃订阅）

```bash
# 检查 cron 触发器是否工作
wrangler tail --config wrangler.staging.json --format pretty
# 等 1~2 分钟，应该能看到定时任务日志
```

### 12.5 关键 SQL 抽查

```bash
wrangler d1 execute payment-kit-staging --remote --command \
  "SELECT COUNT(*) FROM customers; SELECT COUNT(*) FROM invoices WHERE status='paid'; SELECT COUNT(*) FROM subscriptions WHERE status='active';"
```

行数应当和 AWS 源端大致匹配（±少量差异可接受，取决于迁移期间是否有新写入）。

## Phase 13：回滚方案

**如果 Phase 10~12 任何一步发现致命问题**：

1. **停止切流**：staging 还在 `*.workers.dev`，AWS 源端仍然运行，无需回滚 DNS
2. **保留 CF 资源**：D1 / KV / Queue 不删，便于复现问题
3. **修复后重跑**：Phase 5~12 是幂等的（`INSERT OR IGNORE`），可以安全重跑
4. **确认失败原因**：查 `wrangler tail` 日志 + 对比 AWS 源端行为

**不要删除**：
- AWS staging（源数据是唯一真相）
- staging blocklet-service（DID 团队资产）
- 已注册的 APP_PID（重新注册会生成新 EK，导致 AWS 上的 Stripe 加密数据无法解密）

## 附录 A：常用命令速查

```bash
# 实时日志
wrangler tail --config wrangler.staging.json --format pretty

# D1 任意查询
wrangler d1 execute payment-kit-staging --remote --command "<SQL>"

# 某表行数
wrangler d1 execute payment-kit-staging --remote --command "SELECT COUNT(*) FROM <table>"

# 列出所有 secrets
wrangler secret list --config wrangler.staging.json

# 重新部署（代码改动后）
npx vite build --config cloudflare/vite.config.ts --outDir cloudflare/public && \
  cd cloudflare && node run-build.js && \
  npx wrangler deploy --config wrangler.staging.json
```

## 附录 B：Phase 对应所需时间

| Phase | 内容 | 预计耗时 | 可并行 |
|---|---|---|---|
| 前置确认 | 6 项环境就绪检查 | 10 min | - |
| Phase 0 | AWS 信息收集 | 15 min | - |
| Phase 1 | 密钥派生验证 | 5 min | - |
| Phase 2 | registerApp | 20 min | 与 Phase 3 并行 |
| Phase 3 | Media Kit staging 部署 | 30 min~1 hr | 与 Phase 2 并行 |
| Phase 4 | CF 资源创建 | 10 min | - |
| Phase 5 | 数据导出 + 处理 | 30 min | - |
| Phase 6 | Schema 部署 | 5 min | - |
| Phase 7 | 数据导入 | 1~2 hr | - |
| Phase 8 | wrangler.staging.json | 10 min | - |
| Phase 9 | 设置 secrets | 5 min | - |
| Phase 10 | 构建 + 部署 | 15 min | - |
| Phase 11 | Stripe webhook | 10 min | - |
| Phase 12 | 端到端验证 | 30 min | - |
| **合计** | | **约 3.5~5 hr** | |

## 附录 C：`<TODO:>` 占位清单

执行本手册前，把以下值补齐：

| 占位符 | 值 | 责任人 |
|---|---|---|
| `<TODO: AWS_STAGING_HOST>` | AWS staging SSH 目标 | Shijun（运维给） |
| `<TODO: STAGING_BLOCKLET_SERVICE_WORKER_NAME>` | staging blocklet-service Worker 名 | DID 团队给 |
| `<TODO: STAGING_BLOCKLET_SERVICE_URL>` | staging blocklet-service admin URL | DID 团队给 |
| `<TODO: media-kit-blocklet-name>` | AWS 上 Media Kit 的 blocklet name | Shijun（SSH 后看） |
| `<your-account>` (所有出现处) | Cloudflare account 域名前缀 | `wrangler whoami` 拿到 |

---

## 相关文档

- `README.md` — CF Workers 部署架构和环境变量总览
- `MIGRATION-RUNBOOK.md` — 通用迁移 runbook（本手册是它的 staging 特化版）
- `MIGRATION-CHALLENGES.md` — 迁移过程中的设计决策和踩坑记录
- `blocklet/media-kit` 仓库的 `cloudflare/README.md` — Media Kit CF 部署指南
- DID 仓库 `planning/29-blocklet-migration-runbook.md` — blocklet-service 迁移参考
