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:
yixiong
2026-07-09 11:12:25 +08:00
co-authored by Claude
parent b4c6652f66
commit 7b1385e160
14 changed files with 754 additions and 11 deletions
@@ -0,0 +1,94 @@
<#import "_macros.ftl" as m>
<@m.layout active="sourceapp" title="老应用配置">
<#if msg?has_content>
<div class="alert">
<#if msg=='saved'>保存成功
<#elseif msg=='toggled'>状态已切换
<#elseif msg=='test_ok'>✅ 连通成功(SDKAppId/密钥/sig 正确)
<#elseif msg=='test_sigfail'>❌ 连通失败:UserSig 错误(70003SDKAppId/密钥不匹配)
<#elseif msg=='test_fail'>❌ 连通失败(详见日志)
<#elseif msg=='sdkappid_required'>SDKAppID 必填
<#elseif msg=='tenant_required'>对应租户必填
<#elseif msg=='notfound'>老应用不存在
<#else>${msg}
</#if>
</div>
</#if>
<p class="muted">数据迁移源:每个老应用对应主应用一个租户(决定 UserID 前缀)。迁移时用其 SDKAppID/密钥调腾讯 API 拉取存量数据,加前缀后导入主应用。密钥仅本工具持有。</p>
<div style="margin-bottom:14px">
<button class="btn btn-success" onclick="document.getElementById('addModal').classList.add('show')"> 新增老应用</button>
</div>
<#if apps?has_content>
<table class="data-table">
<thead>
<tr>
<th>ID</th><th>应用名称</th><th>SDKAppID</th><th>密钥</th><th>对应租户</th>
<th>状态</th><th>操作</th>
</tr>
</thead>
<tbody>
<#list apps as a>
<tr>
<td>${a.id!}</td>
<td>${a.appName!}</td>
<td>${a.sdkAppId!}</td>
<td class="muted">••••••••</td>
<td>${a.tenantId!}</td>
<td>
<#if a.status?? && a.status == 1>
<span class="badge badge-ok">启用</span>
<#else>
<span class="badge badge-no">停用</span>
</#if>
</td>
<td>
<form method="post" action="${basePath}/admin/sourceapp/toggle" style="display:inline">
<input type="hidden" name="id" value="${a.id!}">
<#if a.status?? && a.status == 1>
<input type="hidden" name="status" value="0">
<button class="btn btn-sm btn-secondary">停用</button>
<#else>
<input type="hidden" name="status" value="1">
<button class="btn btn-sm btn-success">启用</button>
</#if>
</form>
<form method="post" action="${basePath}/admin/sourceapp/test" style="display:inline">
<input type="hidden" name="id" value="${a.id!}">
<button class="btn btn-sm btn-primary">测试连通</button>
</form>
</td>
</tr>
</#list>
</tbody>
</table>
<#else>
<div class="empty">暂无老应用配置,点击「新增老应用」添加</div>
</#if>
<div id="addModal" class="modal" onclick="if(event.target===this)this.classList.remove('show')">
<div class="modal-content">
<div class="modal-title">新增老应用</div>
<form method="post" action="${basePath}/admin/sourceapp/save">
<div class="modal-body">
<div class="form-group"><label>应用名称</label><input name="appName" required></div>
<div class="form-group"><label>SDKAppID(老应用)</label><input name="sdkAppId" required></div>
<div class="form-group"><label>SecretKey(老应用密钥)</label><input name="secretKey" required></div>
<div class="form-group">
<label>对应租户(决定 UserID 前缀)</label>
<select name="tenantId" required>
<option value="">请选择租户</option>
<#list tenants as t>
<option value="${t.tenantId!}">${t.tenantId!}${t.tenantName!},前缀 ${t.prefixCode!}</option>
</#list>
</select>
</div>
<div class="form-group"><label>备注</label><input name="remark" placeholder="如:原XX系统IM应用"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="document.getElementById('addModal').classList.remove('show')">取消</button>
<button type="submit" class="btn btn-primary">保存</button>
</div>
</form>
</div>
</div>
</@m.layout>