feat(t14-s345): 群迁移 + 消息迁移 + 关系链迁移

T14 S3/S4/S5 阶段:
- S3 群迁移:getAppidGroups/getGroupInfo(老密钥拉)+ createGroup/addGroupMember(主密钥导入)+ migrateGroups
- S4 消息迁移:importMsg/importGroupMsg + migrateMessages(会话清单+断点续传,v1 最近7天/100条)
- S5 关系链:importFriend/friendGetList(sns 系列)+ migrateRoster
- 后台 /admin/migrate/{groups,messages,roster} 触发 + migrate.ftl 三表单

S1/S2 完整联调通过;S3/S4/S5 代码完成+通路验证(腾讯返业务响应),
核心导入待真实老应用(10003 群组/60008 消息/60002 关系链)。

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
yixiong
2026-07-09 12:12:26 +08:00
co-authored by Claude
parent 7b1385e160
commit 460a31b52f
5 changed files with 603 additions and 1 deletions
@@ -17,6 +17,7 @@ import org.noear.solon.annotation.Inject;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -282,6 +283,168 @@ public class TencentImClient {
return callApi("group_open_http_svc/group_msg_get_simple", Jsons.stringify(body));
}
// ==================== 群迁移相关(T14 S3 ====================
/**
* 列出 SDKAppId 下所有群(老应用拉群清单,用老密钥)
* <p>
* 命令字 group_open_http_svc/get_appid_groups,返回 GroupIdListGroupId/Type)。
* 与用户清单不同,群清单可从腾讯 API 直接拉取。
*
* @return 腾讯响应原始 JSON(含 GroupIdList),由调用方解析
*/
public String getAppidGroups(long srcSdkAppId, String srcSecretKey) {
Map<String, Object> body = new HashMap<>();
body.put("Limit", 10000);
return callApiAs("group_open_http_svc/get_appid_groups", Jsons.stringify(body), srcSdkAppId, srcSecretKey);
}
/**
* 查群资料 + 成员列表(老应用拉群详情,用老密钥)
* <p>
* 命令字 group_open_http_svc/get_group_info,返回 GroupInfo[].MemberList。
*
* @return 腾讯响应原始 JSON,由调用方解析
*/
public String getGroupInfo(String groupId, long srcSdkAppId, String srcSecretKey) {
Map<String, Object> body = new HashMap<>();
body.put("GroupId", List.of(groupId));
return callApiAs("group_open_http_svc/get_group_info", Jsons.stringify(body), srcSdkAppId, srcSecretKey);
}
/**
* 建群(主应用,可带初始成员 ≤ 100)
* <p>
* 命令字 group_open_http_svc/create_group。迁移时 newGroupId = 前缀 + 老 GroupId。
* 成员账号需已存在于主应用(用户迁移已完成)。
*
* @return ActionStatus 是否 OK
*/
public boolean createGroup(String groupId, String type, String name, String ownerAccount, List<String> memberAccounts) {
Map<String, Object> body = new HashMap<>();
body.put("Type", type == null || type.isEmpty() ? "Public" : type);
body.put("GroupId", groupId);
body.put("Name", name == null ? groupId : name);
if (ownerAccount != null && !ownerAccount.isEmpty()) {
body.put("Owner_Account", ownerAccount);
}
if (memberAccounts != null && !memberAccounts.isEmpty()) {
List<Map<String, String>> ml = new ArrayList<>();
for (String m : memberAccounts) {
Map<String, String> item = new HashMap<>();
item.put("Member_Account", m);
item.put("MemberRole", "Member");
ml.add(item);
}
body.put("MemberList", ml);
}
String resp = callApi("group_open_http_svc/create_group", Jsons.stringify(body));
return isOk(resp);
}
/**
* 增加群成员(主应用,成员 > 100 时分批补充;成员账号需已存在)
* <p>
* 命令字 group_open_http_svc/add_group_memberSilence=1 静默加入。
*
* @return ActionStatus 是否 OK
*/
public boolean addGroupMember(String groupId, List<String> memberAccounts) {
if (memberAccounts == null || memberAccounts.isEmpty()) {
return true;
}
Map<String, Object> body = new HashMap<>();
body.put("GroupId", groupId);
List<Map<String, String>> ml = new ArrayList<>();
for (String m : memberAccounts) {
Map<String, String> item = new HashMap<>();
item.put("Member_Account", m);
ml.add(item);
}
body.put("MemberList", ml);
body.put("Silence", 1);
String resp = callApi("group_open_http_svc/add_group_member", Jsons.stringify(body));
return isOk(resp);
}
// ==================== 消息迁移相关(T14 S4 ====================
/**
* 导入单聊消息(历史消息,不计未读,主应用)
* <p>
* 命令字 openim_http_svc/import_msgSyncFromOldSystem=1 标记为旧系统迁移消息(不计未读数)。
* From/To 需已存在于主应用(用户迁移已完成)。MsgBody 透传(来自 getRoamMsg 的原始消息体 JSON)。
*
* @param msgBodyJson MsgBody 原始 JSON 数组字符串(来自漫游消息,直接透传)
* @return ActionStatus 是否 OK
*/
public boolean importMsg(String fromAccount, String toAccount, long msgTimeStamp, String msgBodyJson) {
// 手动拼 bodyMsgBody 直接嵌入(已是合法 JSON 数组),from/to 为安全 IM ID 无需转义
String body = "{\"SyncFromOldSystem\":1,\"From_Account\":\"" + fromAccount
+ "\",\"To_Account\":\"" + toAccount
+ "\",\"MsgTimeStamp\":" + msgTimeStamp
+ ",\"MsgBody\":" + (msgBodyJson == null ? "[]" : msgBodyJson) + "}";
String resp = callApi("openim_http_svc/import_msg", body);
return isOk(resp);
}
/**
* 导入群消息(历史消息,主应用)
* <p>
* 命令字 group_open_http_svc/import_group_msg。GroupId/From 需已迁移(群迁移 + 用户迁移已完成)。
*
* @return ActionStatus 是否 OK
*/
public boolean importGroupMsg(String groupId, String fromAccount, long msgTimeStamp, String msgBodyJson) {
String body = "{\"GroupId\":\"" + groupId
+ "\",\"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);
}
// ==================== 关系链迁移相关(T14 S5 ====================
/**
* 导入好友关系(主应用,历史关系链迁移)
* <p>
* 命令字 sns/openim_http_svc/import_friend。From/To 需已存在于主应用(用户迁移已完成)。
*
* @return ActionStatus 是否 OK
*/
public boolean importFriend(String fromAccount, List<String> friendAccounts) {
if (friendAccounts == null || friendAccounts.isEmpty()) {
return true;
}
Map<String, Object> body = new HashMap<>();
body.put("From_Account", fromAccount);
List<Map<String, String>> fl = new ArrayList<>();
for (String f : friendAccounts) {
Map<String, String> item = new HashMap<>();
item.put("To_Account", f);
item.put("AddSource", "AddSource_Type_System");
fl.add(item);
}
body.put("FriendList", fl);
String resp = callApi("sns/openim_http_svc/import_friend", Jsons.stringify(body));
return isOk(resp);
}
/**
* 拉取用户好友列表(老应用拉关系链,用老密钥)
* <p>
* 命令字 sns/openim_http_svc/friend_get_list,返回 FriendList。
*
* @return 腾讯响应原始 JSON,由调用方解析
*/
public String friendGetList(String account, long srcSdkAppId, String srcSecretKey) {
Map<String, Object> body = new HashMap<>();
body.put("From_Account", account);
body.put("StartSequence", 0L);
return callApiAs("sns/openim_http_svc/friend_get_list", Jsons.stringify(body), srcSdkAppId, srcSecretKey);
}
/**
* 判断腾讯 API 返回是否成功
*/