迁移手册:导入 → 双跑 → 切换

从其它 IM 迁入本平台,正确路线是三阶段:先导入用户/群/关系(不动流量),再双跑(旧新并行、以新为准逐项验证),最后切换(停旧写、清尾差、切 Token 签发、保留回滚窗口)。本页给出每一步的真实接口、幂等键、校验点与回滚方案。

历史消息不迁移

本平台没有“导入指定时间戳历史消息”的接口;message.private.publish / message.group.publish 只能以当前时间入库。

如果必须保留旧历史,请把旧消息归档到你的业务系统(只读查询),IM 内从切换时刻开始新历史;不要用循环补发伪造时间戳,会造成未读、推送与审计混乱。

阶段 0:评估与映射(1–3 天)​

项要做的事产出
身份映射旧 userId → 新 userId(建议新 ID 统一前缀,避免与历史冲突)映射表(唯一索引、可回滚)
会话映射单聊/群/聊天室/系统会话按 ConversationType 归类;群 ID 重新分配或沿用会话映射表
消息类型映射旧自定义消息 → 本项目 content.type(text/image/file/voice/location/custom/recall/system/combine)类型映射表 + 不支持的降级方案
能力差异对照功能矩阵确认长连接、推送、审核、翻译等能力边界差异清单与产品决策
容量按峰值连接/消息/群成员做容量估算容量与配额申请单

阶段 1:导入(不停旧系统)​

导入只写新平台,不切流量。所有写接口都要可重复执行:重复导入不能产生重复用户/群/关系。

顺序数据接口幂等 / 限制
1用户user.registeruserId 幂等(已存在则更新 name/avatarUrl)
2用户校验user.batch.get / user.get批量 ≤100;缺失在 missing 返回
3Token(可选)user.token.issue有效期 7 天;封禁/注销用户 1016
4群/超级群group.import单次 ≤50,逐条幂等;返回 imported/failed
5群成员group.members.add批量拉人;普通群上限 3000
6好友关系friend.add双向直接建立,幂等;不经过申请流程
7标签/订阅(可选)user.tag.batch.set / user.subscription.set标签 ≤20×32;订阅 ≤100
8抽样验证逐项对比用户/群/好友数量与关键字段差异必须为 0 才进入双跑
JavaScript
import { AppClient } from '@ni-paas-im/server-sdk';

const app = new AppClient({ appKey: process.env.IM_APP_KEY, appSecret: process.env.IM_APP_SECRET, baseUrl: 'http://127.0.0.1:8080' });

// 用户:分批导入,单批 ≤100(与 batch.get 上限一致,便于逐批校验)
for (const batch of chunk(legacyUsers, 100)) {
  for (const user of batch) {
    await app.registerUser({ userId: user.newId, name: user.nickname, avatarUrl: user.avatar });
  }
}
const check = await app.batchGetUsers({ userIds: legacyUsers.slice(0, 100).map((u) => u.newId) });
if (check.missing.length) throw new Error(`missing users: ${check.missing.join(',')}`);

// 群:单次 ≤50,逐条幂等;失败明细保留重试
for (const batch of chunk(legacyGroups, 50)) {
  const result = await app.openApi('group.import', {
    groups: batch.map((g) => ({
      groupId: g.newGroupId,
      name: g.name,
      owner: g.ownerNewId,
      members: g.memberIds,
      groupType: g.memberCount > 3000 ? 'super' : 'normal',
    })),
  });
  if (result.failed?.length) console.warn('group import failed', result.failed);
}

// 好友:直接建立双向关系(幂等)
for (const rel of legacyFriends) {
  await app.openApi('friend.add', { userId: rel.a, friendId: rel.b });
}
导入期间的注意事项

不要给导入用户触发真实推送/短信;导入只是建数据。

群人数超过 3000 必须建为超级群(groupType:"super"),否则成员添加会 1015。

导入完成先做只读抽样(用户/群/好友/会话),不要提前让客户端登录新平台。

阶段 2:双跑(旧新并行)​

双跑期旧系统仍是主链路,新平台接收影子流量并验证一致性。目标是让“切换”变成一次低风险操作,而不是第一次真跑。

动作做法验证
影子写入业务服务在写旧 IM 的同时,把同一业务事件写新平台(消息用固定 clientMsgId)两边消息数、会话数、未读对账
消息镜像旧消息事件 → 业务服务转成新平台 message.*.publish(仅用于验证/归档,不伪造历史时间)抽样对比内容、顺序、会话归属
回调镜像新平台回调写入影子库,与旧回调做事件级对账(按 eventId/业务键)回调成功率、重复率、延迟
客户端灰度内部员工/小流量客户端连接新平台(独立 Token),不切换全量连接成功率、重连率、关键页面耗时
差异修复对账差异按“缺用户/缺群/缺关系/消息不一致”分类修复后重跑差异收敛到 0

双跑至少连续 3–7 天无差异,且覆盖高峰时段,才进入切换。

阶段 3:切换与回滚​

  1. 切换前冻结

    停止旧系统新写入(或进入只读);记录切换时间点 T0 与旧系统最后游标。

  2. 清尾差

    把 T0 前未同步的用户/群/关系做最后一轮导入;消息不迁移,业务侧记录“新历史从 T0 开始”。

  3. 签发新 Token

    客户端登录改为向业务服务申请新平台 Token(user.token.issue);旧 Token 停止下发。

  4. 验证

    登录→连接→会话→发消息→离线补发→多端→回调→审核,按上线检查清单逐项打勾。

  5. 回滚预案

    保留旧系统只读与旧 Token 校验 7–14 天;若出现阻断级问题,客户端切回旧配置,新平台停止写入并保留数据。

  6. 旧系统下线

    观察期满、业务确认无回滚需求后,再回收旧系统资源与数据(按合规保留期)。

时间点事项
T-14 天完成映射与容量评估;启动导入
T-7 天导入完成 + 抽样校验;开始双跑
T-3 天双跑差异归零;灰度客户端验证
T0停旧写、清尾差、切 Token
T0 + 1 天全量观察;处理首日问题
T0 + 7~14 天保留回滚窗口
T0 + 14 天后旧系统下线与数据归档

不可迁移 / 需要业务决策的项​

项现状建议
历史消息无导入接口,只有当前时间入库旧历史归档只读;新历史从 T0 开始
好友申请历史只有当前关系;friend.add 直接建立申请记录留业务库
用户密码/凭证IM 只管 Token,不管密码账号体系仍由业务/IdP 负责
群公告/群文件无对应导入接口公告转成首条系统消息或业务侧展示
聊天室历史聊天室本身无历史无需迁移
推送 token设备需重新注册到新平台切换后客户端启动时重新注册

相关页面​