feat(t14-s1s2): 数据迁移基础设施 + 用户迁移
T14 数据迁移工具 S1/S2 阶段: S1 基础设施: - source_app/migrate_task 建表 + 实体 + Mapper - TencentImClient 双密钥 callApiAs(老应用拉取,不走主应用限流,审计记 migrate) - 后台「老应用配置」页(CRUD + 测试连通) S2 用户迁移: - batchImport(multiaccount_import,每批≤100) - MigrateService.migrateUsers(前缀转换+导入+映射+校验,幂等) - 后台「数据迁移」页(上传清单文件/粘贴列表 + 任务记录) 联调通过:3用户清单→加sa前缀→腾讯导入OK→user_mapping→校验3/3/3。 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -66,14 +66,7 @@ public class TencentImClient {
|
||||
private LocalCache localCache;
|
||||
|
||||
/**
|
||||
* 生成管理员 UserSig(长效,用于调后台 API)
|
||||
*/
|
||||
private String genAdminSig() {
|
||||
return UserSigUtil.genSig(sdkAppId, secretKey, adminUserId, 30L * 86400);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用 IM REST API
|
||||
* 调用主应用 IM REST API(用主应用密钥,走出站限流)
|
||||
*
|
||||
* @param command 命令路径,如 im_open_login_svc/account_import
|
||||
* @param bodyJson 请求体 JSON
|
||||
@@ -83,16 +76,38 @@ public class TencentImClient {
|
||||
// 租户来源:当前请求上下文,无则记 system(admin 后台调用等无租户上下文场景)
|
||||
String tenantId = TenantContext.get();
|
||||
String tid = (tenantId == null || tenantId.isEmpty()) ? "system" : tenantId;
|
||||
|
||||
// 出站腾讯 API 限流:按租户配额 + 全局共享配额(对齐设计 3.2),超限抛 429
|
||||
// system(无租户上下文)用默认配额;补拉等后台任务超限由调用方 catch 跳过本轮
|
||||
rateLimiter.checkApiLimit(tid, resolveTenantQps(tid));
|
||||
return doCallApi(command, bodyJson, sdkAppId, secretKey, tid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用老应用 IM REST API(数据迁移拉取用,指定老应用密钥)
|
||||
* <p>
|
||||
* 老应用拉取消耗的是老应用自身套餐配额(非主应用),故不走主应用限流;
|
||||
* 审计 tid 记 "migrate" 便于与业务调用区分。管理员 identifier 默认 administrator
|
||||
* (腾讯各应用默认管理员账号),如某老应用管理员不同,后续扩展 source_app 字段。
|
||||
*
|
||||
* @param srcSdkAppId 老应用 SDKAppId
|
||||
* @param srcSecretKey 老应用密钥
|
||||
* @return 响应 JSON 字符串
|
||||
*/
|
||||
public String callApiAs(String command, String bodyJson, long srcSdkAppId, String srcSecretKey) {
|
||||
return doCallApi(command, bodyJson, srcSdkAppId, srcSecretKey, "migrate");
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用腾讯 API 的公共实现:生成管理员 UserSig + 拼 URL + POST + 审计落库
|
||||
* <p>
|
||||
* 限流由调用方决定:主应用 callApi 走限流,老应用 callApiAs 不走(消耗老应用配额)。
|
||||
*/
|
||||
private String doCallApi(String command, String bodyJson, long appid, String key, String tid) {
|
||||
String result;
|
||||
try {
|
||||
String adminSig = genAdminSig();
|
||||
String adminSig = UserSigUtil.genSig(appid, key, adminUserId, 30L * 86400);
|
||||
String url = "https://" + apiHost + "/v4/" + command
|
||||
+ "?sdkappid=" + sdkAppId
|
||||
+ "?sdkappid=" + appid
|
||||
+ "&identifier=" + URLEncoder.encode(adminUserId, StandardCharsets.UTF_8)
|
||||
+ "&usersig=" + URLEncoder.encode(adminSig, StandardCharsets.UTF_8)
|
||||
+ "&contenttype=json&platform=10&apn=1";
|
||||
@@ -182,6 +197,35 @@ public class TencentImClient {
|
||||
return isOk(resp);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量导入账号(multiaccount_import,每批 ≤ 100,主应用密钥)
|
||||
* <p>
|
||||
* 幂等:已存在账号亦成功。数据迁移用户导入用:老应用 UserID 加租户前缀后批量导入主应用。
|
||||
* 分批调用,返回成功导入数(整批 ActionStatus=OK 记为全部成功;个别非法 ID 场景后续可解析 RetItemList 精确化)。
|
||||
*
|
||||
* @param imUserIds 已加前缀的 IM 用户 ID 列表
|
||||
* @return 成功导入数
|
||||
*/
|
||||
public int batchImport(List<String> imUserIds) {
|
||||
if (imUserIds == null || imUserIds.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
int success = 0;
|
||||
int batchSize = 100; // 腾讯 multiaccount_import 单批上限 100
|
||||
for (int i = 0; i < imUserIds.size(); i += batchSize) {
|
||||
List<String> sub = imUserIds.subList(i, Math.min(i + batchSize, imUserIds.size()));
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("Accounts", sub);
|
||||
String resp = callApi("im_open_login_svc/multiaccount_import", Jsons.stringify(body));
|
||||
if (isOk(resp)) {
|
||||
success += sub.size();
|
||||
} else {
|
||||
log.warn("批量导入失败批次 {} size={} resp={}", i / batchSize, sub.size(), resp);
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查账号状态(只读)
|
||||
* <p>
|
||||
|
||||
Reference in New Issue
Block a user