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:
@@ -0,0 +1,137 @@
|
||||
package com.imutil.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.imutil.common.BizException;
|
||||
import com.imutil.entity.MigrateTask;
|
||||
import com.imutil.entity.SourceApp;
|
||||
import com.imutil.entity.Tenant;
|
||||
import com.imutil.entity.UserMapping;
|
||||
import com.imutil.mapper.MigrateTaskMapper;
|
||||
import com.imutil.mapper.SourceAppMapper;
|
||||
import com.imutil.mapper.TenantMapper;
|
||||
import com.imutil.mapper.UserMappingMapper;
|
||||
import com.imutil.service.MigrateService;
|
||||
import com.imutil.tencent.TencentImClient;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.noear.solon.annotation.Component;
|
||||
import org.noear.solon.annotation.Inject;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 数据迁移服务实现(T14)
|
||||
*
|
||||
* @author imutil
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class MigrateServiceImpl implements MigrateService {
|
||||
|
||||
@Inject
|
||||
private SourceAppMapper sourceAppMapper;
|
||||
|
||||
@Inject
|
||||
private TenantMapper tenantMapper;
|
||||
|
||||
@Inject
|
||||
private UserMappingMapper userMappingMapper;
|
||||
|
||||
@Inject
|
||||
private MigrateTaskMapper migrateTaskMapper;
|
||||
|
||||
@Inject
|
||||
private TencentImClient tencentImClient;
|
||||
|
||||
@Override
|
||||
public MigrateTask migrateUsers(Long sourceAppId, List<String> oldUserIds) {
|
||||
SourceApp app = sourceAppMapper.selectById(sourceAppId);
|
||||
if (app == null) {
|
||||
throw new BizException("老应用不存在");
|
||||
}
|
||||
Tenant tenant = tenantMapper.selectById(app.getTenantId());
|
||||
if (tenant == null) {
|
||||
throw new BizException("关联租户不存在: " + app.getTenantId());
|
||||
}
|
||||
String prefix = tenant.getPrefixCode();
|
||||
|
||||
// 建迁移任务记录(running)
|
||||
MigrateTask task = new MigrateTask();
|
||||
task.setSourceAppId(sourceAppId);
|
||||
task.setObjectType("USER");
|
||||
task.setStatus(1);
|
||||
task.setTotalCount(oldUserIds == null ? 0L : (long) oldUserIds.size());
|
||||
task.setStartedAt(OffsetDateTime.now());
|
||||
migrateTaskMapper.insert(task);
|
||||
|
||||
try {
|
||||
// 老用户 ID 加租户前缀(对齐 user_mapping: im_user_id = prefix + '_' + biz_user_id)
|
||||
List<String> newImUserIds = new ArrayList<>();
|
||||
Map<String, String> oldToNew = new HashMap<>();
|
||||
if (oldUserIds != null) {
|
||||
for (String oldId : oldUserIds) {
|
||||
if (oldId == null || oldId.trim().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
String oldTrim = oldId.trim();
|
||||
String newId = prefix + "_" + oldTrim;
|
||||
newImUserIds.add(newId);
|
||||
oldToNew.put(oldTrim, newId);
|
||||
}
|
||||
}
|
||||
|
||||
// 批量导入主应用(走主应用密钥+限流;幂等,已存在账号亦成功)
|
||||
int imported = tencentImClient.batchImport(newImUserIds);
|
||||
|
||||
// 写 user_mapping(幂等:同 tenant+bizUserId 已存在则跳过)
|
||||
int mapped = 0;
|
||||
for (Map.Entry<String, String> e : oldToNew.entrySet()) {
|
||||
UserMapping exist = userMappingMapper.selectOne(Wrappers.<UserMapping>lambdaQuery()
|
||||
.eq(UserMapping::getTenantId, app.getTenantId())
|
||||
.eq(UserMapping::getBizUserId, e.getKey())
|
||||
.last("LIMIT 1"));
|
||||
if (exist != null) {
|
||||
continue;
|
||||
}
|
||||
UserMapping um = new UserMapping();
|
||||
um.setTenantId(app.getTenantId());
|
||||
um.setBizUserId(e.getKey());
|
||||
um.setImUserId(e.getValue());
|
||||
um.setIsDefault(false);
|
||||
um.setIsGlobal(false);
|
||||
um.setStatus(1);
|
||||
userMappingMapper.insert(um);
|
||||
mapped++;
|
||||
}
|
||||
|
||||
// 校验:老清单数 vs 导入成功数 vs 新建映射数
|
||||
task.setProcessedCount((long) imported);
|
||||
task.setStatus(3); // done
|
||||
task.setFinishedAt(OffsetDateTime.now());
|
||||
task.setVerifyResult(String.format(
|
||||
"{\"oldCount\":%d,\"imported\":%d,\"mapped\":%d}",
|
||||
oldToNew.size(), imported, mapped));
|
||||
migrateTaskMapper.updateById(task);
|
||||
log.info("用户迁移完成 sourceApp={} old={} imported={} mapped={}",
|
||||
sourceAppId, oldToNew.size(), imported, mapped);
|
||||
return task;
|
||||
} catch (Exception e) {
|
||||
task.setStatus(4); // failed
|
||||
task.setErrorMsg(e.getClass().getSimpleName() + ": " + e.getMessage());
|
||||
task.setFinishedAt(OffsetDateTime.now());
|
||||
migrateTaskMapper.updateById(task);
|
||||
log.error("用户迁移失败 sourceApp={}", sourceAppId, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MigrateTask> listTasks() {
|
||||
return migrateTaskMapper.selectList(Wrappers.<MigrateTask>lambdaQuery()
|
||||
.orderByDesc(MigrateTask::getId)
|
||||
.last("LIMIT 100"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.imutil.service.impl;
|
||||
|
||||
import com.imutil.entity.SourceApp;
|
||||
import com.imutil.mapper.SourceAppMapper;
|
||||
import com.imutil.service.SourceAppService;
|
||||
import org.noear.solon.annotation.Component;
|
||||
import org.noear.solon.annotation.Inject;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 老应用配置服务实现
|
||||
*
|
||||
* @author imutil
|
||||
*/
|
||||
@Component
|
||||
public class SourceAppServiceImpl implements SourceAppService {
|
||||
|
||||
@Inject
|
||||
private SourceAppMapper sourceAppMapper;
|
||||
|
||||
@Override
|
||||
public List<SourceApp> list() {
|
||||
return sourceAppMapper.selectList(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SourceApp getById(Long id) {
|
||||
if (id == null) {
|
||||
return null;
|
||||
}
|
||||
return sourceAppMapper.selectById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(SourceApp app) {
|
||||
if (app.getId() == null) {
|
||||
if (app.getStatus() == null) {
|
||||
app.setStatus(1);
|
||||
}
|
||||
sourceAppMapper.insert(app);
|
||||
} else {
|
||||
sourceAppMapper.updateById(app);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toggle(Long id, Integer status) {
|
||||
SourceApp app = new SourceApp();
|
||||
app.setId(id);
|
||||
app.setStatus(status == null ? 0 : status);
|
||||
sourceAppMapper.updateById(app);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user