240 lines
9.8 KiB
Go
240 lines
9.8 KiB
Go
// +======================================================================
|
||
// | 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)
|
||
}
|