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

178 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: 基于 golang-migrate 的数据表迁移
overview: 使用 github.com/golang-migrate/migrate/v4 在 pkg/migration 下建立版本化迁移:22 张表的 up/down SQL(按业务域分组)+ embed 加载 + cmd/migrate 独立命令入口,并同步更新 go.mod 与 README。
todos:
- id: setup-deps
content: 提升 go.mod 依赖:golang-migrate/v4 与 go-sql-driver/mysql 改为直接依赖并 tidy
status: completed
- id: write-migration-pkg
content: 实现 pkg/migration/migration.go:embed 源、DSN 拼装、Up/Down/Steps/Version/Force
status: completed
dependencies:
- setup-deps
- id: write-sql
content: 编写 22 张表的版本化 up/down 迁移 SQL 与全部索引
status: completed
dependencies:
- setup-deps
- id: write-cmd
content: 实现 cmd/migrate 独立命令入口(up/down/steps/version/force)
status: completed
dependencies:
- write-migration-pkg
- id: verify-schema
content: 用 [subagent:code-explorer] 核对表字段与索引并修正差异
status: completed
dependencies:
- write-sql
- id: update-readme
content: 更新 README 目录树与数据库准备章节的迁移说明
status: completed
dependencies:
- write-cmd
- id: build-verify
content: 执行 go build ./... 与 go vet 验证编译通过
status: completed
dependencies:
- 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`(无版本记录)均按成功处理,保证命令可重复执行。
## 架构设计
```mermaid
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 模型零偏差