1
This commit is contained in:
@@ -64,7 +64,10 @@ func main() {
|
||||
|
||||
// 幂等:已有方案则只重试效果图(主方案 → select-main → 等 3 张 done)
|
||||
var mainPlan *entity.OutfitPlan
|
||||
existing, _ := dao.OutfitPlan.ListByUser(ctx, user.Id)
|
||||
existing, err := dao.OutfitPlan.ListByUser(ctx, user.Id)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("读取方案列表失败: %v", err))
|
||||
}
|
||||
for _, p := range existing {
|
||||
if p.MainFlag == 1 {
|
||||
mainPlan = p
|
||||
@@ -103,7 +106,10 @@ func main() {
|
||||
|
||||
waitEffects(ctx, mainPlan.Id)
|
||||
|
||||
all, _ := dao.OutfitPlan.ListByUser(ctx, user.Id)
|
||||
all, err := dao.OutfitPlan.ListByUser(ctx, user.Id)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("读取方案列表失败: %v", err))
|
||||
}
|
||||
fmt.Printf("完成!wenwu901 现有 %d 套方案:\n", len(all))
|
||||
for _, p := range all {
|
||||
fmt.Printf(" - plan %d: %s(评分 %d,%s)\n", p.Id, p.Title, p.Score, p.Source)
|
||||
@@ -185,7 +191,10 @@ func waitTaskDone(ctx context.Context, taskId, userId int64) {
|
||||
// waitEffects 等主方案的 3 张效果图(正面/侧面/背面)全部 done
|
||||
func waitEffects(ctx context.Context, planId int64) {
|
||||
for i := 0; i < 20; i++ {
|
||||
images, _ := dao.PlanEffectImage.ListByPlan(ctx, planId)
|
||||
images, err := dao.PlanEffectImage.ListByPlan(ctx, planId)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("读取效果图列表失败: %v", err))
|
||||
}
|
||||
done := 0
|
||||
for _, im := range images {
|
||||
if im.Status == consts.EffectStatusDone {
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
// 一次性迁移工具:把单文件 slogan.db 拆分为 4 个 SQLite 库
|
||||
//
|
||||
// slogan.db 主库(用户域 + 低频配置)
|
||||
// slogan_plan.db 穿搭方案域
|
||||
// slogan_pay.db 会员/支付域
|
||||
// slogan_cps.db CPS 联盟域
|
||||
//
|
||||
// 用法:在 slogan-agent 目录执行 `go run ./scripts/split_db`
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
|
||||
)
|
||||
|
||||
const mainDB = "slogan.db"
|
||||
|
||||
var groups = []struct {
|
||||
file string
|
||||
tables []string
|
||||
}{
|
||||
{"slogan_plan.db", []string{
|
||||
"slogan_outfit_generation_task", "slogan_outfit_plan", "slogan_plan_outfit_item",
|
||||
"slogan_plan_effect_image", "slogan_plan_review", "slogan_hairstyle_asset",
|
||||
}},
|
||||
{"slogan_pay.db", []string{
|
||||
"slogan_member_plan", "slogan_user_member", "slogan_payment_order",
|
||||
"slogan_pay_notify_log", "slogan_ad_reward_log",
|
||||
}},
|
||||
{"slogan_cps.db", []string{
|
||||
"slogan_cps_category", "slogan_cps_product", "slogan_cps_click_log",
|
||||
"slogan_scene_category_map",
|
||||
}},
|
||||
}
|
||||
|
||||
func main() {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
fatal("getwd: %v", err)
|
||||
}
|
||||
mainPath := filepath.Join(dir, mainDB)
|
||||
if _, err := os.Stat(mainPath); err != nil {
|
||||
fatal("slogan.db 不存在(请在 slogan-agent 目录执行): %v", err)
|
||||
}
|
||||
backup := filepath.Join(dir, "slogan_backup_"+time.Now().Format("20060102_150405")+".db")
|
||||
if err := copyFile(mainPath, backup); err != nil {
|
||||
fatal("备份失败: %v", err)
|
||||
}
|
||||
fmt.Printf("已备份 -> %s\n", backup)
|
||||
|
||||
src, err := sql.Open("sqlite", mainPath)
|
||||
if err != nil {
|
||||
fatal("open %s: %v", mainDB, err)
|
||||
}
|
||||
src.SetMaxOpenConns(1)
|
||||
defer src.Close()
|
||||
|
||||
ddl, err := loadDDL(src)
|
||||
if err != nil {
|
||||
fatal("读取 DDL: %v", err)
|
||||
}
|
||||
srcAbs, err := filepath.Abs(mainPath)
|
||||
if err != nil {
|
||||
fatal("abs: %v", err)
|
||||
}
|
||||
|
||||
failed := false
|
||||
for _, g := range groups {
|
||||
if err := migrateGroup(dir, src, srcAbs, g.file, g.tables, ddl); err != nil {
|
||||
fmt.Printf("❌ %s 迁移失败: %v\n", g.file, err)
|
||||
failed = true
|
||||
}
|
||||
}
|
||||
if failed {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 主库删除已迁出的表(连带索引),清理自增序列残留
|
||||
for _, g := range groups {
|
||||
for _, t := range g.tables {
|
||||
if _, err := src.Exec("DROP TABLE IF EXISTS " + t); err != nil {
|
||||
fatal("DROP %s: %v", t, err)
|
||||
}
|
||||
}
|
||||
names := quoteList(g.tables)
|
||||
if _, err := src.Exec("DELETE FROM sqlite_sequence WHERE name IN (" + names + ")"); err != nil {
|
||||
fmt.Printf("⚠ 清理 sqlite_sequence 失败(可忽略): %v\n", err)
|
||||
}
|
||||
}
|
||||
fmt.Printf("✅ %s 主库已清理,剩余表:\n", mainDB)
|
||||
if err := listTables(src, mainDB); err != nil {
|
||||
fatal("list: %v", err)
|
||||
}
|
||||
fmt.Println("✅ 拆分完成")
|
||||
}
|
||||
|
||||
// migrateGroup 新建目标库文件并拷贝表 + 索引 + 校验行数
|
||||
func migrateGroup(dir string, src *sql.DB, srcAbs, file string, tables []string, ddl map[string][]string) error {
|
||||
dstPath := filepath.Join(dir, file)
|
||||
_ = os.Remove(dstPath) // 覆盖上次失败残留
|
||||
dst, err := sql.Open("sqlite", dstPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dst.SetMaxOpenConns(1)
|
||||
defer dst.Close()
|
||||
|
||||
if _, err := dst.Exec(fmt.Sprintf("ATTACH DATABASE %q AS src", srcAbs)); err != nil {
|
||||
return fmt.Errorf("attach: %w", err)
|
||||
}
|
||||
defer dst.Exec("DETACH DATABASE src")
|
||||
|
||||
for _, t := range tables {
|
||||
createDDL, ok := firstByType(ddl[t], "table")
|
||||
if !ok {
|
||||
return fmt.Errorf("表 %s 未找到建表 DDL", t)
|
||||
}
|
||||
if _, err := dst.Exec(createDDL); err != nil {
|
||||
return fmt.Errorf("create %s: %w", t, err)
|
||||
}
|
||||
if _, err := dst.Exec(fmt.Sprintf("INSERT INTO %s SELECT * FROM src.%s", t, t)); err != nil {
|
||||
return fmt.Errorf("copy %s: %w", t, err)
|
||||
}
|
||||
}
|
||||
for _, t := range tables {
|
||||
for _, idx := range ddl[t] {
|
||||
if !strings.HasPrefix(idx, "CREATE INDEX") && !strings.HasPrefix(idx, "CREATE UNIQUE INDEX") {
|
||||
continue
|
||||
}
|
||||
if _, err := dst.Exec(idx); err != nil {
|
||||
return fmt.Errorf("index %s: %w", idx, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 校验行数
|
||||
for _, t := range tables {
|
||||
srcN, dstN, err := countPair(src, dst, t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if srcN != dstN {
|
||||
return fmt.Errorf("%s 行数不一致: src=%d dst=%d", t, srcN, dstN)
|
||||
}
|
||||
fmt.Printf("✅ %-24s %8d 行\n", t, dstN)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadDDL(db *sql.DB) (map[string][]string, error) {
|
||||
rows, err := db.Query("SELECT type, name, sql FROM sqlite_master WHERE sql IS NOT NULL AND name NOT LIKE 'sqlite_%' AND type IN ('table','index') ORDER BY type DESC")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := map[string][]string{}
|
||||
for rows.Next() {
|
||||
var typ, name, sqlText string
|
||||
if err := rows.Scan(&typ, &name, &sqlText); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if strings.HasPrefix(sqlText, "CREATE TABLE") {
|
||||
out[name] = append([]string{sqlText}, out[name]...) // table 放最前
|
||||
} else {
|
||||
out[name] = append(out[name], sqlText)
|
||||
}
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func firstByType(ddls []string, prefix string) (string, bool) {
|
||||
for _, d := range ddls {
|
||||
if strings.HasPrefix(d, "CREATE TABLE") {
|
||||
return d, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func countPair(src, dst *sql.DB, table string) (int, int, error) {
|
||||
srcN, err := count(src, table)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
dstN, err := count(dst, table)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return srcN, dstN, nil
|
||||
}
|
||||
|
||||
func count(db *sql.DB, table string) (int, error) {
|
||||
var n int
|
||||
err := db.QueryRow("SELECT COUNT(*) FROM " + table).Scan(&n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func listTables(db *sql.DB, file string) error {
|
||||
rows, err := db.Query("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var name string
|
||||
if err := rows.Scan(&name); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf(" %s\n", name)
|
||||
}
|
||||
return rows.Err()
|
||||
}
|
||||
|
||||
func quoteList(items []string) string {
|
||||
q := make([]string, len(items))
|
||||
for i, s := range items {
|
||||
q[i] = "'" + s + "'"
|
||||
}
|
||||
return strings.Join(q, ",")
|
||||
}
|
||||
|
||||
func copyFile(src, dst string) error {
|
||||
data, err := os.ReadFile(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(dst, data, 0o644)
|
||||
}
|
||||
|
||||
func fatal(format string, args ...any) {
|
||||
fmt.Printf("❌ "+format+"\n", args...)
|
||||
os.Exit(1)
|
||||
}
|
||||
Reference in New Issue
Block a user