first commit

This commit is contained in:
2026-09-11 01:41:52 +08:00
commit 9f22e6ccae
263 changed files with 28935 additions and 0 deletions
@@ -0,0 +1,178 @@
---
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 模型零偏差