feat(t14-v1234): 消息迁移增强——C2C全量分页+群消息迁移+429限速重试+回溯天数可配

V1: migrateMessages 7天窗滚动 + Complete分页拉全量历史
V2: migrateGroupMessages + getGroupMsgAs/importGroupMsg + /migrate/groupmsg
V3: importMsg/importGroupMsg 捕获429重试 + app.yml imutil.migrate(msgLookbackDays/importMaxRetry/importRetryMs)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
yixiong
2026-07-09 12:57:54 +08:00
co-authored by Claude
parent 460a31b52f
commit f401e8d065
6 changed files with 265 additions and 17 deletions
@@ -52,6 +52,10 @@ public class MigrateServiceImpl implements MigrateService {
@Inject
private TencentImClient tencentImClient;
/** 消息迁移回溯天数,0=全量(T14 V3) */
@Inject("${imutil.migrate.msgLookbackDays:7}")
private int msgLookbackDays;
@Override
public MigrateTask migrateUsers(Long sourceAppId, List<String> oldUserIds) {
SourceApp app = sourceAppMapper.selectById(sourceAppId);
@@ -281,20 +285,42 @@ public class MigrateServiceImpl implements MigrateService {
String newFrom = prefix + "_" + oldFrom;
String newTo = prefix + "_" + oldTo;
try {
// v1:每会话拉最近 7 天内 100 条(腾讯 MaxTimeInterval 有限制;全量历史分页作为遗留
long maxInterval = 604800L; // 7 天
long minTime = nowSec - maxInterval;
String resp = tencentImClient.getRoamMsg(oldFrom, oldTo, 100, minTime, maxInterval);
ONode node = ONode.ofJson(resp);
ONode msgList = node.get("MsgList");
int n = msgList.size();
for (int i = 0; i < n; i++) {
ONode msg = msgList.get(i);
long ts = msg.get("MsgTimeStamp").getLong();
String msgBodyJson = msg.get("MsgBody").toString();
if (tencentImClient.importMsg(newFrom, newTo, ts, msgBodyJson)) {
imported++;
// 分页全量拉取:7 天窗滚动 + 窗内 Complete 分页(腾讯 MaxTimeInterval ≤ 604800
// V3: 回溯天数可配置(0=全量,受套餐云端保存时长限制)
long minTimeBound = (msgLookbackDays <= 0) ? 0 : nowSec - msgLookbackDays * 86400L;
long segEnd = nowSec;
while (segEnd > minTimeBound) {
long segStart = Math.max(minTimeBound, segEnd - 604800L);
long fetchMaxTime = segEnd;
// 窗内 Complete 分页(LastMsgTime 滚动至 Complete=1 或无消息)
int guard = 0;
while (guard++ < 1000) {
long interval = Math.min(604800L, fetchMaxTime - segStart);
if (interval <= 0) {
break;
}
String resp = tencentImClient.getRoamMsgAs(oldFrom, oldTo, 100, segStart, interval, oldSdkAppId, oldSecret);
ONode node = ONode.ofJson(resp);
ONode msgList = node.get("MsgList");
int n = msgList.size();
for (int i = 0; i < n; i++) {
ONode msg = msgList.get(i);
long ts = msg.get("MsgTimeStamp").getLong();
if (tencentImClient.importMsg(newFrom, newTo, ts, msg.get("MsgBody").toString())) {
imported++;
}
}
boolean complete = node.get("Complete").getLong() == 1;
if (complete || n == 0) {
break;
}
long nextLast = node.get("LastMsgTime").getLong();
if (nextLast <= 0 || nextLast >= fetchMaxTime) {
break; // 防死循环:LastMsgTime 必须向前(减小)
}
fetchMaxTime = nextLast;
}
segEnd = segStart; // 下一个 7 天窗
}
} catch (Exception ce) {
log.warn("单聊消息迁移会话失败 {}->{} : {}", oldFrom, oldTo, ce.getMessage());
@@ -320,6 +346,87 @@ public class MigrateServiceImpl implements MigrateService {
}
}
@Override
public MigrateTask migrateGroupMessages(Long sourceAppId, List<String> groupIds) {
SourceApp app = sourceAppMapper.selectById(sourceAppId);
if (app == null) {
throw new BizException("老应用不存在");
}
Tenant tenant = tenantMapper.selectById(app.getTenantId());
if (tenant == null) {
throw new BizException("关联租户不存在: " + app.getTenantId());
}
String prefix = tenant.getPrefixCode();
long oldSdkAppId = Long.parseLong(app.getSdkAppId());
String oldSecret = app.getSecretKey();
MigrateTask task = new MigrateTask();
task.setSourceAppId(sourceAppId);
task.setObjectType("GROUP_MSG");
task.setStatus(1);
int groups = groupIds == null ? 0 : groupIds.size();
task.setTotalCount((long) groups);
task.setStartedAt(OffsetDateTime.now());
migrateTaskMapper.insert(task);
try {
int imported = 0;
for (int gi = 0; gi < groups; gi++) {
String oldGroupId = groupIds.get(gi).trim();
if (oldGroupId.isEmpty()) {
continue;
}
String newGroupId = prefix + "_" + oldGroupId;
try {
// 分页拉取:ReqMsgSeq 滚动至 IsFinished=1 或无消息
long reqSeq = 0;
int guard = 0;
while (guard++ < 10000) {
String resp = tencentImClient.getGroupMsgAs(oldGroupId, 100, reqSeq, oldSdkAppId, oldSecret);
ONode node = ONode.ofJson(resp);
ONode rspList = node.get("RspMsgList");
int n = rspList.size();
long oldestSeq = 0;
for (int i = 0; i < n; i++) {
ONode msg = rspList.get(i);
long seq = msg.get("MsgSeq").getLong();
String from = msg.get("From_Account").getString();
long ts = msg.get("MsgTimeStamp").getLong();
String newFrom = (from != null && !from.isEmpty()) ? prefix + "_" + from : from;
if (tencentImClient.importGroupMsg(newGroupId, newFrom, ts, msg.get("MsgBody").toString())) {
imported++;
}
oldestSeq = seq;
}
boolean finished = node.get("IsFinished").getLong() == 1;
if (finished || n == 0 || oldestSeq <= 0) {
break;
}
reqSeq = oldestSeq; // 滚动到更旧
}
} catch (Exception ge) {
log.warn("群消息迁移单群失败 {} : {}", oldGroupId, ge.getMessage());
}
task.setProcessedCount((long) imported);
task.setPosCursor("group:" + (gi + 1));
migrateTaskMapper.updateById(task);
}
task.setStatus(3);
task.setFinishedAt(OffsetDateTime.now());
task.setVerifyResult(String.format("{\"groups\":%d,\"imported\":%d}", groups, imported));
migrateTaskMapper.updateById(task);
log.info("群消息迁移完成 sourceApp={} groups={} imported={}", sourceAppId, groups, imported);
return task;
} catch (Exception e) {
task.setStatus(4);
task.setErrorMsg(e.getClass().getSimpleName() + ": " + e.getMessage());
task.setFinishedAt(OffsetDateTime.now());
migrateTaskMapper.updateById(task);
log.error("群消息迁移失败 sourceApp={}", sourceAppId, e);
throw e;
}
}
@Override
public MigrateTask migrateRoster(Long sourceAppId, List<String> oldUserIds) {
SourceApp app = sourceAppMapper.selectById(sourceAppId);