签入版本

This commit is contained in:
小蚂蚁云
2026-10-20 19:08:49 +08:00
commit 1f248e89e4
890 changed files with 119227 additions and 0 deletions
+239
View File
@@ -0,0 +1,239 @@
// +======================================================================
// | EasyGoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2019-2026 半城风雨团队 版权所有
// +----------------------------------------------------------------------
// | 开源协议: Apache-2.0
// | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。
// +----------------------------------------------------------------------
// | 官方网站: https://www.easygoadmin.vip
// +----------------------------------------------------------------------
// | 研发团队: 小蚂蚁云团队 荣誉出品
// +======================================================================
// | 版权和免责声明:
// | 一、知识产权声明
// | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、
// | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权,
// | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。
// | 二、使用限制与禁止条款
// | 您理解并同意,禁止将本产品用于以下任何行为:
// | 1. 任何违反中华人民共和国法律法规的用途;
// | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为;
// | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。
// | 三、责任豁免
// | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失
// | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三
// | 方提起索赔或行政机关进行调查,您应独立承担全部责任。
// | 四、违约与法律责任
// | 如发现任何违反本声明的行为,本团队有权:
// | 1. 无条件配合公安机关及其他有权机关的调查取证工作;
// | 2. 依法追究相关方的民事责任,并要求赔偿全部损失;
// | 3. 保留进一步采取刑事控告等法律措施的权利。
// | 五、许可范围说明
// | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包
// | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通
// | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、
// | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造
// | 成的一切经济损失及商誉损害。
// | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件
// | 许可协议》附件为准。
// +======================================================================
package migrate
import (
"context"
"fmt"
"time"
"github.com/gogf/gf/v2/database/gdb"
)
// ============================== 整库迁移主流程 ==============================
// banner 迁移起止分隔线
const banner = "=============================================================="
// Run 整库迁移主流程
//
// 共五个阶段:建目标库 → 建表结构 → 逐表迁移数据 → 重置自增序列 → 逐表行数校验。
// progress 为进度输出回调,CLI 传入格式化输出函数,为 nil 时静默。
func Run(ctx context.Context, opt Options, progress func(format string, args ...any)) (*Report, error) {
report := &Report{}
logf := func(format string, args ...any) {
if progress != nil {
progress(format, args...)
}
}
start := time.Now()
src, err := openConn(ctx, "migrate_src", opt.Src)
if err != nil {
return nil, err
}
tables, err := listTables(ctx, src, opt.Tables)
if err != nil {
return nil, fmt.Errorf("读取源库表清单失败: %v", err)
}
if len(tables) == 0 {
return nil, fmt.Errorf("源库无表(或白名单未命中任何表)")
}
report.TableTotal = len(tables)
renderer, err := NewRenderer(opt.Dst.Type)
if err != nil {
return nil, err
}
// 抬头
logf("%s\n", banner)
logf("[迁移开始] 源: %s:%s → 目标: %s:%s\n", opt.Src.Type, opt.Src.Database, opt.Dst.Type, opt.Dst.Database)
logf("[迁移开始] 待迁移 %d 张表\n", len(tables))
logf("%s\n", banner)
// ── 1/5 建目标库 ──
phase := time.Now()
logf("[·] [1/5] 建目标库...\n")
if err = ensureDatabase(ctx, opt.Dst); err != nil {
return nil, err
}
logf(" · 目标库 \"%s\" 已就绪\n", opt.Dst.Database)
logf("[✓] [1/5] 建目标库 完成(耗时 %s)\n", dur(time.Since(phase)))
dst, err := openConn(ctx, "migrate_dst", opt.Dst)
if err != nil {
return nil, err
}
// ── 2/5 建表结构 ──
phase = time.Now()
logf("[·] [2/5] 建表结构(%d 张表)...\n", len(tables))
if opt.DropTargetFirst {
if err = renderer.DropAllTables(ctx, dst); err != nil {
return nil, fmt.Errorf("清空目标库失败: %v", err)
}
}
metas := make([]*TableMeta, 0, len(tables))
totalIndexes := 0
for _, table := range tables {
meta, e := introspectTable(ctx, src, opt.Src.Type, table)
if e != nil {
report.TableFailed++
report.Failed = append(report.Failed, FailedTable{Table: table, Err: "内省失败: " + e.Error()})
logf(" [✗ %s] 内省失败: %v\n", table, e)
continue
}
// 非清空模式下目标已有同名表则该表跳过(建表用 IF NOT EXISTS,数据会追加,须前置拦截)
if !opt.DropTargetFirst {
if exists, e2 := tableExists(ctx, dst, table); e2 == nil && exists {
report.TableFailed++
report.Failed = append(report.Failed, FailedTable{Table: table, Err: "目标库已存在同名表"})
logf(" [✗ %s] 目标库已存在同名表\n", table)
continue
}
}
if _, e = dst.Exec(ctx, renderer.CreateTable(meta)); e != nil {
report.TableFailed++
report.Failed = append(report.Failed, FailedTable{Table: table, Err: "建表失败: " + e.Error()})
logf(" [✗ %s] 建表失败: %v\n", table, e)
continue
}
for _, idx := range meta.Indexes {
if _, e = dst.Exec(ctx, renderer.CreateIndex(meta, idx)); e != nil {
report.TableFailed++
report.Failed = append(report.Failed, FailedTable{Table: table, Err: "建索引 " + idx.Name + " 失败: " + e.Error()})
logf(" [✗ %s] 建索引 %s 失败: %v\n", table, idx.Name, e)
break
}
}
totalIndexes += len(meta.Indexes)
metas = append(metas, meta)
}
logf("[✓] [2/5] 建表结构 完成(耗时 %s),%d 张表,%d 个索引,0 个外键\n",
dur(time.Since(phase)), len(metas), totalIndexes)
// ── 3/5 逐表迁移数据 ──
phase = time.Now()
logf("[·] [3/5] 逐表迁移数据...\n")
totalRows := int64(0)
for i, meta := range metas {
logf(" · [%d/%d] 正在迁移 %s...\n", i+1, len(metas), meta.Name)
tableStart := time.Now()
rows, e := copyTable(ctx, src, dst, renderer, meta, opt.BatchSize, func(batch, total int64) {
logf(" [%s] +%d(累计 %d)\n", meta.Name, batch, total)
})
if e != nil {
report.TableFailed++
report.Failed = append(report.Failed, FailedTable{Table: meta.Name, Err: "数据搬运失败: " + e.Error()})
logf(" [✗ %s] 失败: %v\n", meta.Name, e)
continue
}
report.TableOK++
totalRows += rows
logf(" [✓ %s] 共 %d 行(耗时 %s)\n", meta.Name, rows, dur(time.Since(tableStart)))
}
report.RowTotal = totalRows
logf("[✓] [3/5] 逐表迁移数据 完成(耗时 %s),共 %d 行\n", dur(time.Since(phase)), totalRows)
// ── 4/5 重置自增序列 ──
phase = time.Now()
logf("[·] [4/5] 重置自增序列...\n")
for _, meta := range metas {
if e := renderer.FixSequence(ctx, dst, meta); e != nil {
report.Failed = append(report.Failed, FailedTable{Table: meta.Name, Err: "序列修复失败: " + e.Error()})
}
}
logf("[✓] [4/5] 重置自增序列 完成(耗时 %s)\n", dur(time.Since(phase)))
// ── 5/5 逐表行数校验 ──
phase = time.Now()
logf("[·] [5/5] 逐表行数校验...\n")
allPass := true
for _, meta := range metas {
srcCount, e1 := countRows(ctx, src, meta.Name)
dstCount, e2 := countRows(ctx, dst, meta.Name)
if e1 != nil || e2 != nil || srcCount != dstCount {
allPass = false
logf(" [%s] source=%d target=%d FAIL\n", meta.Name, srcCount, dstCount)
continue
}
logf(" [%s] source=%d target=%d PASS\n", meta.Name, srcCount, dstCount)
}
if allPass {
logf("[✓] [5/5] 逐表行数校验 完成(耗时 %s),%d 张表全部 PASS\n", dur(time.Since(phase)), len(metas))
} else {
logf("[✓] [5/5] 逐表行数校验 完成(耗时 %s),存在 FAIL\n", dur(time.Since(phase)))
}
// 落款
logf("%s\n", banner)
logf("[迁移完成] 成功:%d 行 / %d 张表 / 总耗时 %s\n", totalRows, report.TableOK, dur(time.Since(start)))
logf("%s\n", banner)
if report.TableFailed > 0 {
return report, fmt.Errorf("%d 张表迁移失败", report.TableFailed)
}
if !allPass {
return report, fmt.Errorf("存在行数校验不一致的表")
}
return report, nil
}
// countRows 统计表行数
func countRows(ctx context.Context, db gdb.DB, table string) (int64, error) {
n, err := db.Ctx(ctx).Model(table).Count()
return int64(n), err
}
// dur 时长格式化
//
// 不足 1 秒输出毫秒,不足 1 分输出秒,否则输出分秒
func dur(d time.Duration) string {
if d < time.Second {
return fmt.Sprintf("%dms", d.Milliseconds())
}
if d < time.Minute {
return fmt.Sprintf("%ds", int(d.Seconds()))
}
return fmt.Sprintf("%dm%ds", int(d.Minutes()), int(d.Seconds())%60)
}