Files
zt-closet-backend/.codebuddy/plans/基于_golang-migrate_的数据表迁移_50fc3756.md
T
2026-09-11 01:41:52 +08:00

12 KiB
Raw Blame History

name, overview, todos
name overview todos
基于 golang-migrate 的数据表迁移 使用 github.com/golang-migrate/migrate/v4 在 pkg/migration 下建立版本化迁移:22 张表的 up/down SQL(按业务域分组)+ embed 加载 + cmd/migrate 独立命令入口,并同步更新 go.mod 与 README。
id content status
setup-deps 提升 go.mod 依赖:golang-migrate/v4 与 go-sql-driver/mysql 改为直接依赖并 tidy completed
id content status dependencies
write-migration-pkg 实现 pkg/migration/migration.go:embed 源、DSN 拼装、Up/Down/Steps/Version/Force completed
setup-deps
id content status dependencies
write-sql 编写 22 张表的版本化 up/down 迁移 SQL 与全部索引 completed
setup-deps
id content status dependencies
write-cmd 实现 cmd/migrate 独立命令入口(up/down/steps/version/force) completed
write-migration-pkg
id content status dependencies
verify-schema 用 [subagent:code-explorer] 核对表字段与索引并修正差异 completed
write-sql
id content status dependencies
update-readme 更新 README 目录树与数据库准备章节的迁移说明 completed
write-cmd
id content status dependencies
build-verify 执行 go build ./... 与 go vet 验证编译通过 completed
verify-schema
update-readme

产品概述

为 zt-closet-backend 引入基于 github.com/golang-migrate/migrate/v4 的版本化数据库迁移能力:把 internal/mysqlmodel 中 22 张表的结构固化为可执行、可回滚、可版本管理的 SQL 迁移脚本,并提供独立命令入口手动执行,服务启动流程保持不变。

核心功能

  • 建表迁移脚本:按业务域分组的版本化 SQL(up 建表 + 索引,down 为 DROP TABLE),覆盖全部 22 张表,字段/类型/注释与 GORM 模型严格一致
  • 索引完整性:补齐 GORM tag 声明的全部唯一索引(幂等索引、联合唯一索引),并补充列表查询所需的普通索引
  • 迁移加载:go:embed 将 SQL 打包进二进制,通过 iofs 作为 golang-migrate 数据源,部署无需额外携带 SQL 目录
  • 独立命令入口:cmd/migrate 支持 up / down / steps N / version / force <version>,仅读取 Mysql 配置段,服务启动时完全不触发迁移
  • 幂等与容错:migrate.ErrNoChange 视为成功;提供 force 修复脏库(dirty)状态;失败以非 0 退出码返回
  • 文档同步:改写 README 中「未内置建表 SQL,也不会自动建表」及占位目录说明,给出迁移命令用法

技术栈

  • 语言/框架:Go 1.27,模块 git.lzcwy.cn/zhengchiliu/zt-closet-backend
  • 迁移库:github.com/golang-migrate/migrate/v4 v4.19.1(当前为 indirect,需提升为直接依赖),配套 database/mysql 驱动、iofs 源
  • SQL 驱动:github.com/go-sql-driver/mysql v1.10.0(当前 indirect,需提升为直接依赖)
  • 配置:pkg/setting(viper 只读 configs/config.yaml)
  • 数据库:MySQL 5.7+/8.0,InnoDB + utf8mb4

实现方案

采用「嵌入 SQL + 独立命令执行」模式:迁移脚本作为版本化资产随代码评审与发布,运行时由 cmd/migrate 显式触发,服务主进程不接触 DDL,避免多实例并发迁移与启动期锁表风险。

关键决策与取舍:

  1. embed 而非 file://:迁移二进制可单独拷贝执行,避免工作目录/漏拷导致的版本漂移;切换 file:// 仅需替换源构造的 2 行代码,保留改造空间。
  2. 独立命令而非启动自动迁移:主服务多实例部署时自动迁移会产生并发 DDL 与脏库风险;独立命令让 DDL 成为发布流程中的显式步骤。
  3. 自建 DSN 而非复用 gorm 连接:gorm DSN 无 multiStatements=true,而单个迁移文件含多条 CREATE TABLE/CREATE INDEX,必须开启该参数;故在迁移包内按 setting.MysqlSettingS 拼装专用 DSN 并 sql.Open("mysql", dsn),不改动既有 NewMysqlEngine。
  4. 按业务域分组版本(约 14 对文件)而非一表一版本:22 张表一文件一对会导致文件过碎、回滚步骤过多;单文件全量则回滚粒度太粗。按域分组(用户/衣物/穿搭/衣柜分享/额度/商品/订单/退款/VIP/权益任务/AI 日志/审核日志/互动/通知/运势)在可读性与回滚粒度间平衡。
  5. down 为 DROP TABLE:本项目为初始建库,down 语义即整域回滚;按依赖倒序删除(明细表先于主表)。

性能与可靠性:

  • 迁移为一次性 DDL,非热路径;唯一风险点是 multiStatements 与脏库。通过 DSN 参数与 force 子命令覆盖。
  • 索引设计直接映射 GORM 唯一索引(保障 upsert 幂等:collects/likes/quota_flow_logs/vip_flow_logs/benefit_grant_tasks/horoscopes/user_quotas/user_daily_usages)与列表查询条件(user_id + 状态/分类),避免全表扫描。
  • migrate.ErrNoChange 与 os.ErrNotExist(无版本记录)均按成功处理,保证命令可重复执行。

架构设计

flowchart LR
    A["cmd/migrate/main.go<br/>解析子命令"] --> B["pkg/setting<br/>读取 Mysql 段"]
    B --> C["pkg/migration.Run<br/>拼 DSN(multiStatements=true)<br/>sql.Open"]
    C --> D["iofs.New(embed FS, migrations)"]
    C --> E["mysql.WithInstance(*sql.DB)"]
    D --> F["migrate.NewWithSourceInstance"]
    E --> F
    F --> G["Up/Down/Steps/Version/Force"]
    G --> H["schema_migrations 版本表"]

组件关系:

  • cmd/migrate:命令层,只做参数解析、配置读取与结果输出,不含业务逻辑
  • pkg/migration:能力层,封装 embed FS、DSN 构造、migrate 实例创建与 Up/Down/Steps/Version/Force
  • pkg/migration/migrations/*.sql:数据资产层,版本化 DDL
  • 依赖方向单向:cmd/migrate → pkg/migration → pkg/setting,主服务 main.go 不依赖迁移包

目录结构

pkg/migration/
├── migration.go              # [NEW] 迁移能力封装://go:embed migrations/*.sql、iofs 源构造、DSN 拼装(含 multiStatements=true)、
│                             #        Run/Up/Down/Steps/Version/Force 导出函数;ErrNoChange 视为成功
└── migrations/               # [NEW] 版本化 SQL,命名 000001_xxx.up.sql / 000001_xxx.down.sql,约 14 对
    ├── 000001_create_users.{up,down}.sql
    ├── 000002_create_clothes.{up,down}.sql
    ├── 000003_create_outfits.{up,down}.sql            # outfits + outfit_items
    ├── 000004_create_closet_shares.{up,down}.sql      # closet_shares + closet_share_items
    ├── 000005_create_user_quotas.{up,down}.sql        # user_quotas + user_daily_usages
    ├── 000006_create_quota_flow_logs.{up,down}.sql
    ├── 000007_create_goods.{up,down}.sql
    ├── 000008_create_orders.{up,down}.sql
    ├── 000009_create_refund_flow_logs.{up,down}.sql
    ├── 000010_create_vip_flow_logs.{up,down}.sql
    ├── 000011_create_benefit_grant_tasks.{up,down}.sql
    ├── 000012_create_ai_flow_logs.{up,down}.sql       # ai_recommend_flow_logs + ai_horoscope_flow_logs
    ├── 000013_create_interactions.{up,down}.sql       # collects + likes
    ├── 000014_create_user_notifications.{up,down}.sql
    ├── 000015_create_horoscopes.{up,down}.sql
    └── 000016_create_audit_logs.{up,down}.sql         # closet_audit_logs + outfit_audit_logs
cmd/
└── migrate/
    └── main.go               # [NEW] 独立迁移命令入口:子命令 up/down/steps N/version/force <version>;
                              #       仅 setting.NewSetting() + ReadSection("Mysql"),不调用 main.go 的 setupSetting()
go.mod                        # [MODIFY] golang-migrate/v4、go-sql-driver/mysql 由 indirect 提升为直接依赖
go.sum                        # [MODIFY] 随 go mod tidy 更新(不手工编造哈希)
README.md                     # [MODIFY] 目录树新增 pkg/migration、cmd/migrate;改写 sql/、migrations/ 占位说明;
                              #          「准备数据库」章节改为 go run ./cmd/migrate up 等命令用法

关键代码结构

// pkg/migration/migration.go(仅签名,不含实现)
package migration

//go:embed migrations/*.sql
var migrationFS embed.FS

// Run 执行迁移动作;action 取值 up/down/steps/version/force。
func Run(s *setting.MysqlSettingS, action string, arg int) error

// 语义化封装,供命令层直接调用
func Up(s *setting.MysqlSettingS) error          // 执行到最新版本;ErrNoChange 视为成功
func Down(s *setting.MysqlSettingS) error        // 全量回滚(按依赖倒序 DROP)
func Steps(s *setting.MysqlSettingS, n int) error
func Version(s *setting.MysqlSettingS) (uint, bool, error) // 返回版本号与 dirty 状态
func Force(s *setting.MysqlSettingS, version int) error    // 修复脏库

实现要点(防回归)

  • DSN 必须带 multiStatements=true,否则同文件多条语句会被 MySQL 拒绝;同时保留 charset=utf8mb4&parseTime=True&loc=Local。
  • 字段类型映射:字符串按用途取 VARCHAR(64)(UUID/编码)、VARCHAR(128/255)(名称/短描述)、TEXT(description、content、result_content、callback_raw、daily_text、remark);状态/类型 TINYINT;时间统一 BIGINT(秒级时间戳);金额与计数 BIGINT(分);deleted_at BIGINT NOT NULL DEFAULT 0(soft_delete 插件以 0 表示未删除)。
  • 易错字段:clothes 主色列名为 color(非 color_main),必须与模型一致,否则 GORM 查询报未知列。
  • 必须建立的唯一索引(与 GORM tag 一一对应,缺失会破坏 upsert 幂等):uk_user_wx_open_id、uk_user_quota(user_id,quota_type)、uk_daily_usage_user_date(user_id,stat_date)、uk_quota_flow_idem(order_no,quota_type,change_type)、uk_order_no、uk_refund_flow_no、uk_vip_flow_idem(order_no,change_type)、uk_benefit_task_idem(order_no,benefit_type)、uk_collect_user_target(collect_type,target_id,user_id)、uk_like_user_target(like_type,target_id,user_id)、uk_horoscope_date_zodiac(stat_date,zodiac)。
  • 补充普通索引(模型未声明但为查询条件):clothes(user_id,category_main)、clothes(user_id,image_process_status)、outfits(user_id,status)、outfit_items(outfit_id)、closet_shares(user_id,status)、closet_share_items(share_uuid)、各业务 UUID 列(clothes_uuid/outfit_uuid/share_uuid/notification_uuid/quota_flow_uuid)、user_notifications(user_id,is_read)、orders(user_id,order_status)。
  • 命令层不要复用 main.go 的 setupSetting()(会强制校验 JWT 等无关配置,导致迁移命令在只配了数据库的环境失败);也不要 import global 之外触发 Redis/COS 初始化。
  • 日志/输出:命令层用 fmt/slog 输出版本号与执行结果,失败 os.Exit(1);不打印 DSN 明文密码。
  • 本次不连接真实数据库(配置中的 MySQL 为内网地址),验证以 go build/go vet 为准。

验证清单

  • go build ./... 通过,go vet ./pkg/migration/... ./cmd/migrate/... 无告警
  • go run ./cmd/migrate version 能正确加载配置并输出版本(连接失败时应给出可读错误而非 panic)
  • 逐对检查 up/down 文件名配对、版本号连续无重复
  • 22 张表名、列名与 internal/mysqlmodel 模型逐一比对一致

Agent Extensions

SubAgent

  • code-explorer
  • Purpose:在迁移 SQL 编写完成后,交叉核对 internal/mysqlmodel/ 下 18 个模型文件与 pkg/migration/migrations/*.sql,逐表比对表名、列名、字段类型与索引是否一致,重点核查 22 张表无遗漏、唯一索引与 GORM tag 完全对应
  • Expected outcome:输出差异清单(缺失表/错误列名/缺失或多余索引),并据此修正 SQL,确保迁移结果与 GORM 模型零偏差