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:
@@ -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]});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 公共:构造页面模型 ====================
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user