Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.
 
 
 

18 KiB

海外已注册用户迁移到国内主站设计文档

日期:2026-06-02
状态:待评审
范围:将一批已注册于海外站点的历史用户迁移为“国内主账号 + 海外同步从账号”模型,保留原登录方式和海外入口,不迁移历史日志,不考虑订阅迁移。


背景与目标

当前项目已经具备基础的 region_sync 主从同步能力,核心方向是:

  • 主站创建用户后向从站推送同步用户
  • 从站上的 synced user 消费额度后回写主站
  • 从站周期性拉取主站额度用于本地 synced_quota

本次需求不是新增长期双主能力,而是把一批“历史上注册在海外站的用户”迁移为与现有同步用户一致的模型:

  • 国内站成为唯一主数据源
  • 海外站保留原用户入口,但该用户转为 synced user
  • 保留原密码登录能力
  • 保留原 OAuth 登录能力
  • 保留海外站入口可登录、可使用
  • 迁移用户的可用额度进入国内主账号
  • 不迁移调用日志、充值日志、普通操作日志

本次不考虑活动订阅用户,因为当前确认这批用户不存在订阅数据。


非目标

本次设计明确不做以下事情:

  • 不实现长期双向双主同步
  • 不共享国内和海外数据库
  • 不迁移 topupssubscription_orders、usage logs、quota logs
  • 不重构现有普通登录链路
  • 不删除海外原用户
  • 不修改项目受保护标识、品牌、模块路径

推荐方案

推荐方案为:单独迁移界面 + 后端迁移任务内核

方案说明

  • 国内站新增一个仅 root 管理员可见的“用户迁移中心”
  • 前端界面只负责:
    • 创建迁移批次
    • 触发预检查
    • 处理冲突
    • 触发迁移执行
    • 查看结果与验证
  • 真正的数据迁移逻辑全部放在后端任务内核中
  • 海外站与国内站之间通过受鉴权保护的内部 HTTP 接口通信
  • 迁移完成后:
    • 国内主账号持有最终主数据和额度
    • 海外原账号改为 source=synced
    • 海外原账号通过 remote_user_id 指向国内主账号

不选择其他方案的原因

  • 纯脚本:冲突处理、审计、幂等和回滚边界过弱
  • 纯界面前端直写:迁移逻辑分散,重试与恢复困难
  • 登录代理/远程鉴权:改动登录、会话、OAuth 主链路,范围过大
  • 长期双主:后续冲突处理成本过高,不适合本项目现状

核心设计原则

1. 国内唯一主账号

迁移完成后,国内站是该批用户唯一主账号归属地。之后额度主数据、用户主数据、后续运营动作都以国内站为准。

2. 海外原账号保留

不删除海外账号,不替换海外用户主键,不强制切登录入口。迁移后只是将其降级为同步从账号。

3. 冲突必须显式决策

usernameemail、OAuth 绑定冲突,不做隐式自动合并。允许自动推荐,但最终决策必须可审计。

4. 幂等优先

整个迁移链路按“可能中断、可能重试、可能部分成功”的现实场景设计,所有关键步骤必须可安全重跑。

5. 不做整批自动回滚

跨站点、带人工决策的迁移不适合整批一键回滚。系统只提供受限的单用户回退能力。


整体架构

新增一个独立迁移模块,由以下部分组成:

  • 国内站后端迁移服务
  • 海外站内部迁移接口
  • 国内站管理端迁移页面
  • 迁移批次表和迁移明细表

职责分工如下:

  • 国内站迁移服务:
    • 抓取海外候选用户快照
    • 执行国内侧冲突预检查
    • 记录批次与条目状态
    • 根据策略创建或合并国内主账号
    • 调用海外内部接口把原账号转换为 synced
    • 做迁移后验证
  • 海外内部接口:
    • 返回候选用户快照
    • 返回单用户最新快照
    • 执行“转换为 synced 用户”
  • 管理端页面:
    • 批次管理
    • 冲突处理
    • 迁移执行
    • 结果查看和验证

数据模型

本次不复用 quota_sync_logs,新增两张迁移专用表。

user_migration_batches

表示一次迁移批次。

建议字段:

  • id
  • name
  • source_region
  • target_region
  • status
    • draft
    • scanned
    • ready
    • running
    • completed
    • failed
    • cancelled
  • total_users
  • conflict_users
  • migrated_users
  • failed_users
  • operator_id
  • created_at
  • updated_at

user_migration_items

表示一个海外用户在某个批次中的迁移决议和执行结果。

建议字段:

  • id
  • batch_id
  • source_user_id
  • target_user_id
  • source_username
  • source_email
  • source_quota
  • conflict_flags
    • JSON 字符串
    • 记录 username/email/oauth/aff_code 等冲突
  • resolution_strategy
    • create_new
    • merge_into_existing
    • skip
  • status
    • pending_scan
    • conflict
    • ready
    • migrating
    • migrated
    • skipped
    • failed
  • error_message
  • snapshot
    • JSON 字符串
    • 保存预检查时的海外用户快照
  • migration_token
    • 用于额度合并和幂等保护
  • migrated_at
  • created_at
  • updated_at

唯一性建议

  • user_migration_items(batch_id, source_user_id) 唯一
  • 迁移执行时按 migration_token 保证不会重复做额度叠加

状态机

批次状态

  • draft:已创建,未扫描
  • scanned:预检查完成,但仍可能存在冲突项
  • ready:所有项都已变成 readyskipped
  • running:正在执行迁移
  • completed:全部可迁移项执行成功
  • failed:存在失败项,需要人工修正后重试
  • cancelled:人工取消

条目状态

  • pending_scan:尚未预检查
  • conflict:发现冲突,等待人工决策
  • ready:策略已明确,可以执行迁移
  • migrating:迁移进行中
  • migrated:迁移成功
  • skipped:人工跳过
  • failed:执行失败,允许单项重试

迁移范围

必迁数据

从海外站迁移以下主数据:

  • users 主字段
  • 密码哈希
  • display_name
  • email
  • status
  • role
  • group
  • created_at
  • setting
  • aff_code
  • 内置 OAuth 字段
    • github_id
    • discord_id
    • oidc_id
    • wechat_id
    • telegram_id
    • linux_do_id
  • user_oauth_bindings
  • 可用额度

不迁数据

  • topups
  • subscription_orders
  • quota_sync_logs
  • usage logs
  • 普通系统日志

预检查设计

海外候选用户快照

海外站为国内站提供用户快照接口,返回:

  • 用户主字段
  • 内置 OAuth 字段
  • 自定义 OAuth 绑定
  • quota
  • group
  • status
  • aff_code
  • created_at
  • 可选 setting
  • 可选 display_name

国内预检查规则

对每个海外用户,在国内检查:

  • username 是否已存在
  • email 是否已存在
  • 每个内置 OAuth ID 是否已被其他用户占用
  • 每个自定义 OAuth 绑定是否已被其他用户占用
  • aff_code 是否已存在

自定义 OAuth 绑定匹配规则

不能直接按 provider_id 比较,必须先通过 custom_oauth_providers.slug 建立海外与国内的 provider 映射,再按:

  • provider_slug
  • provider_user_id

进行冲突判断。

冲突分级

硬冲突

必须人工处理,不能自动迁移:

  • 用户名冲突且无法确认同一人
  • 邮箱冲突且无法确认同一人
  • OAuth 身份已绑定到另一个明确不同的国内用户
  • 自定义 OAuth 绑定指向多个不同国内候选目标

可合并冲突

可以由界面推荐合并,但仍需要人工确认:

  • 同邮箱,且无其他矛盾
  • 唯一 OAuth 身份指向同一个国内用户,且无其他矛盾

软冲突

不阻塞迁移,可自动处理:

  • aff_code 冲突
  • display_name 冲突

默认推荐策略

  • 无冲突:推荐 create_new
  • aff_code 冲突:推荐 create_new
  • 同邮箱且无其他矛盾、且目标国内用户尚未在海外存在其他 synced 副本:推荐 merge_into_existing
  • 任意 OAuth 多对一、一对多关系:强制人工处理

执行一致性约束

  • 预检查快照只用于冲突决策和界面展示,不作为最终额度结算依据。
  • 执行阶段必须重新拉取海外用户最新快照,并以该快照中的最终额度为准。
  • V1 版本要求在运维冻结窗口内执行批次。冻结窗口至少要覆盖待迁用户的充值、兑换码、额度转移和新请求入口,并等待已在途请求自然结束。
  • 如果执行时最新快照与预检查结论发生冲突性漂移(例如邮箱归属变化、OAuth 绑定目标变化、原本计划合并的国内账号已存在其他海外 synced 副本),该条目必须失败并要求重新扫描,不得静默沿用旧决议。

单用户迁移执行流程

执行顺序不能改变,必须先构建国内主账号,再将海外原账号降级。执行阶段读取的“最新快照”必须发生在冻结窗口内。

create_new

  1. 在冻结窗口内,从海外拉取该用户最新快照
  2. 在国内开启事务
  3. 通过“按哈希导入”的专用逻辑创建国内主账号
    • 不能走普通 Insert()
    • 否则密码会再次哈希
  4. 在同一个国内事务中完成以下动作:
    • 创建国内主账号
    • 复制主字段
    • 复制内置 OAuth 字段
    • 复制 user_oauth_bindings
    • 处理 aff_code 冲突
    • 回写 user_migration_items.target_user_id
  5. 如在执行阶段遇到新的唯一键冲突:
    • 直接将该迁移项标记为失败
    • 要求重新扫描或人工修正
    • 不允许按用户名/邮箱自动认领为“上次已创建成功”的账号
  6. 复制主字段:
    • username
    • password
    • email
    • display_name
    • status
    • role
    • group
    • created_at
    • setting
  7. 复制内置 OAuth ID 字段
  8. 复制 user_oauth_bindings
    • 先通过 slug 将海外 provider_id 映射为国内 provider_id
  9. 处理邀请码
    • 如冲突则生成新的 aff_code
  10. 将国内额度设置为“执行时最新快照中的最终额度”
  11. 国内事务提交,得到 cn_user_id
  12. 调海外内部接口,将原海外用户转换为 synced
    • source = synced
    • remote_user_id = cn_user_id
    • synced_quota = 国内提交后的最终额度
    • last_sync_at = now
  13. 更新迁移项为成功

merge_into_existing

  1. 在冻结窗口内拉取该用户最新快照
  2. 锁定目标国内账号
  3. 重新校验该用户仍应合并到这个目标
    • 同邮箱或唯一 OAuth 目标仍然成立
    • 目标国内账号在海外不存在其他 synced 副本
  4. 补齐国内为空、海外有值的字段
  5. 补齐国内缺失的 OAuth 绑定
  6. 额度合并规则:
    • 国内最终额度 = 国内原额度 + 执行时最新海外额度
    • 通过 migration_token 保证每个迁移项最多叠加一次
  7. 提交国内事务
  8. 调海外接口将原账号转换为 synced
  9. 更新迁移项为成功

为什么不删除海外原用户

  • 海外已有 session 兼容性更好
  • 海外已有 API token 兼容性更好
  • 现有 synced user 逻辑天然支持这种模型
  • 回退成本远低于删除账号再重建

海外用户转换接口

不建议国内直接操作海外数据库,建议由海外站提供内部接口执行切换。

推荐接口

  • GET /api/internal/migration/users
    • 分页返回候选用户快照
    • 仅返回 source=local 的海外用户
  • GET /api/internal/migration/users/:id
    • 返回单用户最新快照
  • GET /api/internal/migration/users/check-synced-copy?cn_user_id=:id
    • 检查某个国内用户是否已在海外存在 synced 副本
  • POST /api/internal/migration/users/:id/convert-to-synced
    • 将海外原用户降级为 synced

转换接口行为

接口需要做以下校验:

  • 仅允许内部受信任调用
  • 用户存在
  • 用户尚未迁移,或已迁移且目标一致
  • remote_user_id 不为空
  • 如果用户已是 syncedremote_user_id 与请求不一致,返回冲突错误

幂等语义:

  • 如果该用户已经是 source=synced
  • remote_user_id 与请求目标一致
  • 则直接返回成功

管理端页面设计

新增一个 root-only 页面:用户迁移中心

页面建议拆为四个区域:

1. 批次列表

展示:

  • 批次名
  • 状态
  • 总用户数
  • 冲突数
  • 已迁移数
  • 失败数
  • 创建人
  • 创建时间

2. 批次详情

展示用户级迁移项列表:

  • 海外用户 ID
  • 用户名
  • 邮箱
  • 海外额度
  • 冲突类型
  • 处理策略
  • 当前状态
  • 目标国内账号
  • 错误信息

3. 冲突处理面板

对单个冲突用户允许操作:

  • create_new
  • merge_into_existing
  • skip

并展示推荐策略及依据。

4. 执行与验证面板

提供以下操作:

  • 预检查
  • 开始迁移
  • 重试失败项
  • 迁移后验证
  • 导出失败项

后端 API 设计

新增一组管理端 API,不混入普通 region_sync

国内管理端 API

  • POST /api/user-migrations/batches
    • 创建批次
  • GET /api/user-migrations/batches
    • 批次列表
  • GET /api/user-migrations/batches/:id
    • 批次详情
  • POST /api/user-migrations/batches/:id/scan
    • 触发预检查
  • POST /api/user-migrations/items/:id/resolve
    • 设置处理策略
    • 成功后重新统计批次状态;若所有项都已变为 readyskipped,批次自动转为 ready
  • POST /api/user-migrations/batches/:id/execute
    • 执行迁移
  • POST /api/user-migrations/batches/:id/retry
    • 仅重试失败项
  • POST /api/user-migrations/batches/:id/verify
    • 执行迁移后校验

鉴权要求

  • 仅 root 管理员可访问
  • 不能开放给普通管理员

幂等要求

幂等是本设计的核心要求。

扫描幂等

  • 同一批次重复 scan
  • 不得重复插入相同 source_user_id
  • 只能刷新已有条目的快照与冲突分析结果

执行幂等

  • 批次级重试时,已成功的条目必须跳过
  • create_new 条目如已存在 target_user_id,不得再次创建账号
  • merge_into_existing 额度叠加必须有一次性保护

海外转换幂等

  • 已是 synced
  • remote_user_id 一致
  • 直接返回成功

OAuth 复制幂等

  • 已存在同一绑定则跳过
  • 不因重复插入报错

额度合并幂等

通过 migration_token 或同等级唯一标识保证:

  • 每个迁移项对国内额度的修改最多成功一次

冻结窗口设计

本次迁移不要求全站停机,但 V1 版本将“冻结窗口”定义为执行前的强制运维前置条件,而不是可选建议。

冻结范围应仅限该批次用户:

  • 禁止充值
  • 禁止兑换码
  • 禁止额度转移
  • 禁止发起新请求
  • 等待已在途请求自然结束

执行要求:

  • root 操作员在点击“开始迁移”前确认冻结窗口已经生效
  • 批次执行期间如果发现冻结条件被破坏,应立即停止后续条目并重新扫描

目的:

  • 稳定海外最终额度
  • 避免预检查和执行之间继续发生额度变化

回滚边界

本次设计不提供整批自动回滚,只提供有限的单用户回退能力。

create_new 的可回退场景

如果国内主账号刚创建成功,但海外尚未转换为 synced

  • 可直接删除国内主账号

如果海外已经完成降级:

  • 海外用户改回 source=local
  • 清空 remote_user_id
  • 将确认后的额度写回海外 quota
  • 国内新账号按策略禁用或删除

merge_into_existing 的回退限制

不建议自动回滚,因为:

  • 额度已合并进既有国内账号
  • 自动扣减可能伤及已发生的后续业务动作

这类只建议人工修正。

系统提供的回退能力

  • 查看失败现场
  • 导出失败项
  • create_new 类型执行受限回退

迁移后验证清单

系统至少应提供以下自动校验:

  • 国内目标主账号存在
  • 海外原账号已为 source=synced
  • 海外 remote_user_id 指向正确国内账号
  • 国内额度与迁移决议一致
  • 海外 synced_quota 与国内额度一致,或在允许误差范围内一致
  • 密码登录抽样验证可用
  • OAuth 绑定抽样验证存在
  • 冲突用户未被误迁
  • 重跑执行不会重复加额度

实施顺序

建议按以下顺序推进:

  1. 新增迁移数据模型与数据库迁移
  2. 实现国内迁移服务内核
  3. 实现海外内部迁移接口
  4. 实现国内管理端 API
  5. 实现管理端迁移页面
  6. 补充测试
    • 无冲突迁移
    • 邮箱冲突合并
    • OAuth 冲突阻断
    • 中断后重试
    • 已迁移项重复执行
    • 海外重复转换幂等
  7. 先在测试库验证
  8. 线上先跑 5-10 个用户的灰度批次
  9. 观察成功率和验证结果后再执行全量

关键实现注意事项

1. 密码导入必须是“导入哈希”

当前用户创建逻辑会对密码再次哈希。迁移实现必须提供专用导入路径,直接写入海外已有哈希值。

2. 自定义 OAuth provider 不能直接按 provider_id 迁移

必须按 slug 对齐国内外 provider 后再复制绑定,否则绑定会错位。

3. 不能直接复用现有 SyncUserCreate

现有 region_sync 的建用户请求字段不足,不能覆盖:

  • 完整冲突处理
  • 自定义 OAuth 绑定复制
  • 哈希密码导入控制
  • 合并已有国内用户

本次迁移需要单独实现迁移接口和迁移服务。

4. 与现有 synced user 模型兼容

本方案完成后,海外侧行为应回归现有同步用户路径:

  • 海外读取本地 synced_quota
  • 海外消费额度走批量回写主站
  • 海外周期性从主站拉取额度

结论

本次海外历史用户迁移的最佳路径是:

  • 不做双主
  • 不做纯脚本黑盒迁移
  • 使用“单独迁移界面 + 后端幂等迁移任务内核”
  • 将海外原用户保留并降级为 synced
  • 将国内账号作为唯一主账号

这条路径最符合当前代码库已有的 region_sync 架构,改动集中、风险可控、审计清晰,且能保留用户现有登录方式和海外站访问体验。