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
@@ -321,12 +321,14 @@ public class AdminController {
@Get
@Mapping("/migrate")
public Object migratePage(@Param(defaultValue = "") String msg,
@Param(defaultValue = "") String imported) {
@Param(defaultValue = "") String imported,
@Param(defaultValue = "") String migrated) {
ModelAndView mv = view("migrate.ftl", "数据迁移", "migrate");
mv.put("tasks", migrateService.listTasks());
mv.put("apps", sourceAppService.list());
mv.put("msg", msg);
mv.put("imported", imported);
mv.put("migrated", migrated);
return mv;
}
@@ -357,6 +359,71 @@ public class AdminController {
ctx.redirect(basePath + "/admin/migrate?msg=done&imported=" + task.getProcessedCount());
}
/**
* 群迁移:选老应用 → getAppidGroups 拉群 → 逐群拉成员 → 加前缀建群 → 导入成员 → 写 group_mapping
*/
@Post
@Mapping("/migrate/groups")
public void migrateGroups(@Param Long sourceAppId, Context ctx) throws Throwable {
if (sourceAppId == null) {
ctx.redirect(basePath + "/admin/migrate?msg=noapp");
return;
}
MigrateTask task = migrateService.migrateGroups(sourceAppId);
ctx.redirect(basePath + "/admin/migrate?msg=gdone&migrated=" + task.getProcessedCount());
}
/**
* 单聊消息迁移:上传会话清单(每行 from,to)或粘贴 → getRoamMsg 拉 → 加前缀 → importMsg
*/
@Post
@Mapping("/migrate/messages")
public void migrateMessages(@Param Long sourceAppId,
@Param(defaultValue = "") String convText,
Context ctx) throws Throwable {
UploadedFile file = ctx.file("file");
String content = file != null ? new String(file.getContentAsBytes(), StandardCharsets.UTF_8) : convText;
List<String[]> pairs = new ArrayList<>();
parseConvPairs(content, pairs);
if (sourceAppId == null) {
ctx.redirect(basePath + "/admin/migrate?msg=noapp");
return;
}
if (pairs.isEmpty()) {
ctx.redirect(basePath + "/admin/migrate?msg=empty");
return;
}
MigrateTask task = migrateService.migrateMessages(sourceAppId, pairs);
ctx.redirect(basePath + "/admin/migrate?msg=mdone&imported=" + task.getProcessedCount());
}
/**
* 关系链迁移:上传用户清单(每行一个 UserID)或粘贴 → 逐用户拉好友 → 加前缀 → importFriend
*/
@Post
@Mapping("/migrate/roster")
public void migrateRoster(@Param Long sourceAppId,
@Param(defaultValue = "") String userText,
Context ctx) throws Throwable {
UploadedFile file = ctx.file("file");
List<String> userIds = new ArrayList<>();
if (file != null) {
parseUserIds(new String(file.getContentAsBytes(), StandardCharsets.UTF_8), userIds);
} else if (!userText.isEmpty()) {
parseUserIds(userText, userIds);
}
if (sourceAppId == null) {
ctx.redirect(basePath + "/admin/migrate?msg=noapp");
return;
}
if (userIds.isEmpty()) {
ctx.redirect(basePath + "/admin/migrate?msg=empty");
return;
}
MigrateTask task = migrateService.migrateRoster(sourceAppId, userIds);
ctx.redirect(basePath + "/admin/migrate?msg=rdone&imported=" + task.getProcessedCount());
}
/** 解析用户清单:每行一个 UserID,空行与 # 注释跳过 */
private void parseUserIds(String content, List<String> out) {
for (String line : content.split("\\r?\\n")) {
@@ -367,6 +434,23 @@ public class AdminController {
}
}
/** 解析会话清单:每行 from,to(逗号或空格分隔),空行与 # 注释跳过 */
private void parseConvPairs(String content, List<String[]> out) {
if (content == null || content.isEmpty()) {
return;
}
for (String line : content.split("\\r?\\n")) {
line = line.trim();
if (line.isEmpty() || line.startsWith("#")) {
continue;
}
String[] parts = line.split("[,\\s]+");
if (parts.length >= 2) {
out.add(new String[]{parts[0], parts[1]});
}
}
}
// ==================== 公共:构造页面模型 ====================
/**