12 KiB
12 KiB
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。 |
|
产品概述
为 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,避免多实例并发迁移与启动期锁表风险。
关键决策与取舍:
- embed 而非 file://:迁移二进制可单独拷贝执行,避免工作目录/漏拷导致的版本漂移;切换 file:// 仅需替换源构造的 2 行代码,保留改造空间。
- 独立命令而非启动自动迁移:主服务多实例部署时自动迁移会产生并发 DDL 与脏库风险;独立命令让 DDL 成为发布流程中的显式步骤。
- 自建 DSN 而非复用 gorm 连接:gorm DSN 无
multiStatements=true,而单个迁移文件含多条CREATE TABLE/CREATE INDEX,必须开启该参数;故在迁移包内按setting.MysqlSettingS拼装专用 DSN 并sql.Open("mysql", dsn),不改动既有NewMysqlEngine。 - 按业务域分组版本(约 14 对文件)而非一表一版本:22 张表一文件一对会导致文件过碎、回滚步骤过多;单文件全量则回滚粒度太粗。按域分组(用户/衣物/穿搭/衣柜分享/额度/商品/订单/退款/VIP/权益任务/AI 日志/审核日志/互动/通知/运势)在可读性与回滚粒度间平衡。
- 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/Forcepkg/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 等无关配置,导致迁移命令在只配了数据库的环境失败);也不要 importglobal之外触发 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 模型零偏差