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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user