356 lines
9.9 KiB
Go
356 lines
9.9 KiB
Go
// 全量迁移:把业务表中的老 uid(yz_system_tenant_user.uid)统一替换为
|
||
// 认证中心的 identity_id(yz_auth_identity.id)。
|
||
//
|
||
// 背景:统一认证上线后,令牌里的 user_id 是 identity_id;而业务表的
|
||
// uid / user_id / create_user_id / owner_user_id / uploader_id 等字段存的仍是
|
||
// 老表 uid。不迁移的话用户会「找不到自己的数据」,因此做全量替换而非兼容层。
|
||
//
|
||
// 用法(需在 go/ 目录运行,读取 conf/app.conf):
|
||
//
|
||
// go run scripts/uidmigrate/migrate_uid.go -check 预检:列出受影响的表/列/行数
|
||
// go run scripts/uidmigrate/migrate_uid.go -apply 执行迁移(记录变更日志,可回滚)
|
||
// go run scripts/uidmigrate/migrate_uid.go -rollback 按变更日志回滚
|
||
//
|
||
// 说明:本脚本直接使用 database/sql,避免 beego ORM 的 Raw 取值差异。
|
||
// 执行前请先自行备份数据库(mysqldump)。
|
||
package main
|
||
|
||
import (
|
||
"database/sql"
|
||
"flag"
|
||
"fmt"
|
||
"log"
|
||
"os"
|
||
"strings"
|
||
"time"
|
||
|
||
beego "github.com/beego/beego/v2/server/web"
|
||
_ "github.com/go-sql-driver/mysql"
|
||
)
|
||
|
||
// tempOffset 中间值偏移:先把旧值搬到远离目标值的区间,避免新旧值重叠导致错改
|
||
const tempOffset = 1000000000
|
||
|
||
// 需要处理的列名
|
||
var uidColumns = []string{
|
||
"uid", "user_id", "create_user_id", "update_user_id",
|
||
"owner_user_id", "uploader_id", "operator_id", "tuid",
|
||
}
|
||
|
||
// 不参与迁移的表
|
||
var skipTables = map[string]bool{
|
||
"yz_system_tenant_user": true,
|
||
"yz_users": true,
|
||
"yz_uid_migration_log": true,
|
||
}
|
||
|
||
type colRef struct {
|
||
Table string
|
||
Col string
|
||
}
|
||
|
||
var db *sql.DB
|
||
|
||
func main() {
|
||
check := flag.Bool("check", false, "预检,不写数据")
|
||
apply := flag.Bool("apply", false, "执行迁移")
|
||
rollback := flag.Bool("rollback", false, "按变更日志回滚")
|
||
flag.Parse()
|
||
|
||
if !*check && !*apply && !*rollback {
|
||
fmt.Println("请指定 -check / -apply / -rollback")
|
||
os.Exit(1)
|
||
}
|
||
if err := beego.LoadAppConfig("ini", "conf/app.conf"); err != nil {
|
||
log.Printf("加载 conf/app.conf 失败(若已自动加载可忽略): %v", err)
|
||
}
|
||
|
||
var err error
|
||
db, err = openDB()
|
||
if err != nil {
|
||
log.Fatalf("连接数据库失败: %v", err)
|
||
}
|
||
defer db.Close()
|
||
|
||
switch {
|
||
case *check:
|
||
runCheck()
|
||
case *apply:
|
||
runApply()
|
||
case *rollback:
|
||
runRollback()
|
||
}
|
||
}
|
||
|
||
func openDB() (*sql.DB, error) {
|
||
user, _ := beego.AppConfig.String("mysqluser")
|
||
pass, _ := beego.AppConfig.String("mysqlpass")
|
||
urls, _ := beego.AppConfig.String("mysqlurls")
|
||
name, _ := beego.AppConfig.String("mysqldb")
|
||
if user == "" || urls == "" || name == "" {
|
||
return nil, fmt.Errorf("数据库配置(mysqluser/mysqlurls/mysqldb) 未正确设置")
|
||
}
|
||
dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", user, pass, urls, name)
|
||
return sql.Open("mysql", dsn)
|
||
}
|
||
|
||
// buildMapping 建立「老 uid → identity_id」映射:同企业(tid) 下账号/手机/邮箱 一致。
|
||
//
|
||
// 注意:老表是 utf8mb4_0900_ai_ci、新表是 utf8mb4_unicode_ci,直接 JOIN 比较字符串
|
||
// 会触发 Illegal mix of collations,因此改为在 Go 侧匹配。
|
||
func buildMapping() (map[uint64]uint64, error) {
|
||
legacy := make([]struct {
|
||
uid uint64
|
||
tid uint64
|
||
account, phone string
|
||
email string
|
||
}, 0)
|
||
rows, err := db.Query(
|
||
"SELECT uid, tid, IFNULL(account,''), IFNULL(phone,''), IFNULL(email,'') FROM yz_system_tenant_user WHERE delete_time IS NULL")
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
for rows.Next() {
|
||
var it struct {
|
||
uid uint64
|
||
tid uint64
|
||
account, phone string
|
||
email string
|
||
}
|
||
if err := rows.Scan(&it.uid, &it.tid, &it.account, &it.phone, &it.email); err != nil {
|
||
continue
|
||
}
|
||
legacy = append(legacy, it)
|
||
}
|
||
rows.Close()
|
||
|
||
index := map[string]uint64{}
|
||
rows2, err := db.Query(
|
||
"SELECT identity_id, tid, IFNULL(account,''), IFNULL(phone,''), IFNULL(email,'') FROM yz_auth_tenant_user WHERE delete_time IS NULL")
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
for rows2.Next() {
|
||
var identity, tid uint64
|
||
var account, phone, email string
|
||
if err := rows2.Scan(&identity, &tid, &account, &phone, &email); err != nil {
|
||
continue
|
||
}
|
||
if key := mergeKey(tid, account, phone, email); key != "" {
|
||
index[key] = identity
|
||
}
|
||
}
|
||
rows2.Close()
|
||
|
||
mapping := map[uint64]uint64{}
|
||
for _, it := range legacy {
|
||
key := mergeKey(it.tid, it.account, it.phone, it.email)
|
||
if key == "" {
|
||
continue
|
||
}
|
||
if id, ok := index[key]; ok && id > 0 {
|
||
mapping[it.uid] = id
|
||
}
|
||
}
|
||
return mapping, nil
|
||
}
|
||
|
||
// mergeKey 归并键:企业ID + (手机号 > 邮箱 > 账号)
|
||
func mergeKey(tid uint64, account, phone, email string) string {
|
||
base := fmt.Sprintf("%d:", tid)
|
||
if v := strings.TrimSpace(phone); v != "" {
|
||
return base + "p:" + v
|
||
}
|
||
if v := strings.TrimSpace(email); v != "" {
|
||
return base + "e:" + v
|
||
}
|
||
if v := strings.TrimSpace(account); v != "" {
|
||
return base + "a:" + v
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func findColumns() ([]colRef, error) {
|
||
name, _ := beego.AppConfig.String("mysqldb")
|
||
quoted := make([]string, 0, len(uidColumns))
|
||
for _, c := range uidColumns {
|
||
quoted = append(quoted, "'"+c+"'")
|
||
}
|
||
rows, err := db.Query(fmt.Sprintf(`
|
||
SELECT TABLE_NAME, COLUMN_NAME
|
||
FROM information_schema.COLUMNS
|
||
WHERE TABLE_SCHEMA = '%s' AND COLUMN_NAME IN (%s)
|
||
ORDER BY TABLE_NAME, COLUMN_NAME`, name, strings.Join(quoted, ",")))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
|
||
out := make([]colRef, 0)
|
||
total := 0
|
||
for rows.Next() {
|
||
var t, c string
|
||
if err := rows.Scan(&t, &c); err != nil {
|
||
continue
|
||
}
|
||
total++
|
||
if skipTables[t] || strings.Contains(t, "_bak") || strings.HasPrefix(t, "yz_auth_") {
|
||
continue
|
||
}
|
||
out = append(out, colRef{Table: t, Col: c})
|
||
}
|
||
log.Printf("扫描到 %d 个承载用户ID的列(跳过 %d 个)", len(out), total-len(out))
|
||
return out, rows.Err()
|
||
}
|
||
|
||
func runCheck() {
|
||
mapping, err := buildMapping()
|
||
if err != nil {
|
||
log.Fatalf("建立映射失败: %v", err)
|
||
}
|
||
if len(mapping) == 0 {
|
||
log.Fatal("没有建立任何 uid → identity_id 映射,请确认认证中心数据是否已迁移")
|
||
}
|
||
log.Printf("映射关系: %d 个老 uid", len(mapping))
|
||
for old, newUID := range mapping {
|
||
log.Printf(" uid %d → identity %d", old, newUID)
|
||
}
|
||
|
||
cols, err := findColumns()
|
||
if err != nil {
|
||
log.Fatalf("扫描列失败: %v", err)
|
||
}
|
||
|
||
in := joinKeys(mapping)
|
||
total := int64(0)
|
||
affected := 0
|
||
for _, c := range cols {
|
||
var cnt int64
|
||
err := db.QueryRow(fmt.Sprintf(
|
||
"SELECT COUNT(*) FROM `%s` WHERE `%s` IN (%s)", c.Table, c.Col, in)).Scan(&cnt)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
if cnt > 0 {
|
||
log.Printf(" %-42s %-18s %d 行", c.Table, c.Col, cnt)
|
||
total += cnt
|
||
affected++
|
||
}
|
||
}
|
||
log.Printf("合计:%d 张表的列需要更新,约 %d 行(预检完成,未写入任何数据)", affected, total)
|
||
}
|
||
|
||
func runApply() {
|
||
mapping, err := buildMapping()
|
||
if err != nil {
|
||
log.Fatalf("建立映射失败: %v", err)
|
||
}
|
||
cols, err := findColumns()
|
||
if err != nil {
|
||
log.Fatalf("扫描列失败: %v", err)
|
||
}
|
||
ensureLogTable()
|
||
|
||
batch := time.Now().Format("20060102150405")
|
||
changed := 0
|
||
for _, c := range cols {
|
||
for oldUID, newUID := range mapping {
|
||
if oldUID == newUID {
|
||
continue
|
||
}
|
||
// 阶段一:旧值 → 临时值;阶段二:临时值 → 新值
|
||
cnt := execUpdate(c.Table, c.Col, oldUID, oldUID+tempOffset)
|
||
if cnt == 0 {
|
||
continue
|
||
}
|
||
execUpdate(c.Table, c.Col, oldUID+tempOffset, newUID)
|
||
saveLog(batch, c.Table, c.Col, oldUID, newUID, cnt)
|
||
changed++
|
||
log.Printf(" %s.%s: %d → %d(%d 行)", c.Table, c.Col, oldUID, newUID, cnt)
|
||
}
|
||
}
|
||
log.Printf("迁移完成:%d 处替换,批次号 %s", changed, batch)
|
||
log.Println("如需回滚:go run scripts/uidmigrate/migrate_uid.go -rollback")
|
||
}
|
||
|
||
func runRollback() {
|
||
rows, err := db.Query(
|
||
"SELECT table_name, column_name, old_value, new_value FROM yz_uid_migration_log ORDER BY id DESC")
|
||
if err != nil {
|
||
log.Fatalf("读取变更日志失败: %v", err)
|
||
}
|
||
defer rows.Close()
|
||
|
||
type item struct {
|
||
table, col string
|
||
oldV, newV uint64
|
||
}
|
||
list := make([]item, 0)
|
||
for rows.Next() {
|
||
var it item
|
||
if err := rows.Scan(&it.table, &it.col, &it.oldV, &it.newV); err != nil {
|
||
continue
|
||
}
|
||
list = append(list, it)
|
||
}
|
||
if len(list) == 0 {
|
||
log.Println("没有可回滚的记录")
|
||
return
|
||
}
|
||
for _, it := range list {
|
||
// 反向:新值 → 临时值 → 旧值
|
||
execUpdate(it.table, it.col, it.newV, it.newV+tempOffset)
|
||
cnt := execUpdate(it.table, it.col, it.newV+tempOffset, it.oldV)
|
||
log.Printf(" 回滚 %s.%s: %d → %d(%d 行)", it.table, it.col, it.newV, it.oldV, cnt)
|
||
}
|
||
log.Println("回滚完成")
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 工具
|
||
|
||
func execUpdate(table, col string, from, to uint64) int64 {
|
||
res, err := db.Exec(fmt.Sprintf(
|
||
"UPDATE `%s` SET `%s` = ? WHERE `%s` = ?", table, col, col), to, from)
|
||
if err != nil {
|
||
log.Printf("更新失败 %s.%s (%d→%d): %v", table, col, from, to, err)
|
||
return 0
|
||
}
|
||
n, _ := res.RowsAffected()
|
||
return n
|
||
}
|
||
|
||
func saveLog(batch, table, col string, oldV, newV uint64, rows int64) {
|
||
_, err := db.Exec(
|
||
"INSERT INTO yz_uid_migration_log (batch_no, table_name, column_name, old_value, new_value, row_count, create_time) VALUES (?,?,?,?,?,?,NOW())",
|
||
batch, table, col, oldV, newV, rows)
|
||
if err != nil {
|
||
log.Printf("记录变更日志失败: %v", err)
|
||
}
|
||
}
|
||
|
||
func ensureLogTable() {
|
||
_, _ = db.Exec(`CREATE TABLE IF NOT EXISTS yz_uid_migration_log (
|
||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||
batch_no VARCHAR(32) NOT NULL,
|
||
table_name VARCHAR(128) NOT NULL,
|
||
column_name VARCHAR(64) NOT NULL,
|
||
old_value BIGINT UNSIGNED NOT NULL,
|
||
new_value BIGINT UNSIGNED NOT NULL,
|
||
row_count BIGINT NOT NULL DEFAULT 0,
|
||
create_time DATETIME NOT NULL,
|
||
PRIMARY KEY (id),
|
||
KEY idx_batch (batch_no)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`)
|
||
}
|
||
|
||
func joinKeys(m map[uint64]uint64) string {
|
||
parts := make([]string, 0, len(m))
|
||
for k := range m {
|
||
parts = append(parts, fmt.Sprintf("%d", k))
|
||
}
|
||
if len(parts) == 0 {
|
||
return "0"
|
||
}
|
||
return strings.Join(parts, ",")
|
||
}
|