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
@@ -1,5 +1,6 @@
package com.imutil.tencent;
import com.imutil.common.BizException;
import com.imutil.common.Httpx;
import com.imutil.common.Jsons;
import com.imutil.common.LocalCache;
@@ -60,6 +61,15 @@ public class TencentImClient {
@Inject
private RateLimiter rateLimiter;
// ===== 数据迁移:消息导入限速重试(T14 V3)=====
/** 消息导入 429 限流重试次数 */
@Inject("${imutil.migrate.importMaxRetry:3}")
private int migrateImportMaxRetry;
/** 消息导入重试间隔(毫秒) */
@Inject("${imutil.migrate.importRetryMs:1000}")
private long migrateImportRetryMs;
@Inject
private TenantService tenantService;
@@ -283,6 +293,38 @@ public class TencentImClient {
return callApi("group_open_http_svc/group_msg_get_simple", Jsons.stringify(body));
}
/**
* 查单聊历史漫游消息(老应用拉取,用老密钥)— 数据迁移用
* <p>
* 与 {@link #getRoamMsg} 同语义,但用指定老应用密钥调 callApiAs(迁移拉取消耗老应用配额)。
*/
public String getRoamMsgAs(String fromAccount, String toAccount, int maxCnt, long minTime, long maxInterval,
long srcSdkAppId, String srcSecretKey) {
Map<String, Object> body = new HashMap<>();
body.put("From_Account", fromAccount);
body.put("To_Account", toAccount);
body.put("MaxCnt", maxCnt);
body.put("MinTime", minTime);
body.put("MaxTimeInterval", maxInterval);
return callApiAs("openim_admin/get_roam_msg", Jsons.stringify(body), srcSdkAppId, srcSecretKey);
}
/**
* 查群历史消息(老应用拉取,用老密钥,支持 ReqMsgSeq 分页)— 数据迁移用
*
* @param reqMsgSeq 分页游标(上次最旧消息 seq;≤0 表示从最新开始)
*/
public String getGroupMsgAs(String groupId, int reqMsgNumber, long reqMsgSeq,
long srcSdkAppId, String srcSecretKey) {
Map<String, Object> body = new HashMap<>();
body.put("GroupId", groupId);
body.put("ReqMsgNumber", reqMsgNumber);
if (reqMsgSeq > 0) {
body.put("ReqMsgSeq", reqMsgSeq);
}
return callApiAs("group_open_http_svc/group_msg_get_simple", Jsons.stringify(body), srcSdkAppId, srcSecretKey);
}
// ==================== 群迁移相关(T14 S3 ====================
/**
@@ -384,8 +426,20 @@ public class TencentImClient {
+ "\",\"To_Account\":\"" + toAccount
+ "\",\"MsgTimeStamp\":" + msgTimeStamp
+ ",\"MsgBody\":" + (msgBodyJson == null ? "[]" : msgBodyJson) + "}";
String resp = callApi("openim_http_svc/import_msg", body);
return isOk(resp);
// V3: 消息导入密集调用易触主应用限流,捕获 429 限速重试;耗尽或非 429 返回 false 跳过该条
for (int attempt = 0; attempt <= migrateImportMaxRetry; attempt++) {
try {
String resp = callApi("openim_http_svc/import_msg", body);
return isOk(resp);
} catch (BizException e) {
if (e.getCode() != 429 || attempt == migrateImportMaxRetry) {
log.warn("导入单聊消息失败 from={} to={} ts={} : {}", fromAccount, toAccount, msgTimeStamp, e.getMessage());
return false;
}
sleepRetry();
}
}
return false;
}
/**
@@ -400,8 +454,33 @@ public class TencentImClient {
+ "\",\"From_Account\":\"" + (fromAccount == null ? "" : fromAccount)
+ "\",\"MsgTimeStamp\":" + msgTimeStamp
+ ",\"SyncFromOldSystem\":1,\"MsgBody\":" + (msgBodyJson == null ? "[]" : msgBodyJson) + "}";
String resp = callApi("group_open_http_svc/import_group_msg", body);
return isOk(resp);
// V3: 消息导入密集调用易触主应用限流,捕获 429 限速重试;耗尽或非 429 返回 false 跳过该条
for (int attempt = 0; attempt <= migrateImportMaxRetry; attempt++) {
try {
String resp = callApi("group_open_http_svc/import_group_msg", body);
return isOk(resp);
} catch (BizException e) {
if (e.getCode() != 429 || attempt == migrateImportMaxRetry) {
log.warn("导入群消息失败 group={} from={} ts={} : {}", groupId, fromAccount, msgTimeStamp, e.getMessage());
return false;
}
sleepRetry();
}
}
return false;
}
/**
* 消息导入限速重试间隔(T14 V3)
* <p>
* 429 配额超限时 sleep 后重试;线程被中断则提前返回(中断状态已恢复,由上层决定是否继续)。
*/
private void sleepRetry() {
try {
Thread.sleep(migrateImportRetryMs);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
// ==================== 关系链迁移相关(T14 S5 ====================