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:
@@ -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,返回 GroupIdList(GroupId/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_member,Silence=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_msg,SyncFromOldSystem=1 标记为旧系统迁移消息(不计未读数)。
|
||||
* From/To 需已存在于主应用(用户迁移已完成)。MsgBody 透传(来自 getRoamMsg 的原始消息体 JSON)。
|
||||
*
|
||||
* @param msgBodyJson MsgBody 原始 JSON 数组字符串(来自漫游消息,直接透传)
|
||||
* @return ActionStatus 是否 OK
|
||||
*/
|
||||
public boolean importMsg(String fromAccount, String toAccount, long msgTimeStamp, String msgBodyJson) {
|
||||
// 手动拼 body:MsgBody 直接嵌入(已是合法 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 返回是否成功
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user