feat(db): purchase_task 新增 created_by/created_source 迁移 (#382)

只新增可空 created_by(索引)与非空默认空串的 created_source,不回填历史行;
迁移按列/索引存在性守卫,可重复执行。

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
This commit is contained in:
QiuSW
2026-10-12 17:14:50 +08:00
co-authored by Claude Sonnet 5.5
parent 16e3d35b8f
commit 6cc3bb98d0
3 changed files with 103 additions and 2 deletions
+7 -2
View File
@@ -170,8 +170,13 @@ type PurchaseTask struct {
CancelReason *string `json:"cancelReason" gorm:"size:500"`
ErrorCode *string `json:"errorCode" gorm:"size:64;index"`
ErrorMessage *string `json:"errorMessage" gorm:"size:1000"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
// CreatedBy/CreatedSource 只在新建任务时写入一次(#382):admin=管理端账号,
// client_api=客户端密钥的授权人,agent=设备重试(CreatedBy 为空);空串表示
// 迁移前的历史任务,不回填。
CreatedBy *uint64 `json:"createdBy" gorm:"index"`
CreatedSource string `json:"createdSource" gorm:"size:16;not null;default:''"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (PurchaseTask) TableName() string { return "purchase_task" }
@@ -0,0 +1,37 @@
package version_local
import (
"runtime"
"go-admin/app/goauto/models"
"go-admin/cmd/migrate/migration"
common "go-admin/common/models"
"gorm.io/gorm"
)
func init() {
_, file, _, _ := runtime.Caller(0)
migration.Migrate.SetVersion(migration.GetFilename(file), MigratePurchaseTaskCreator)
}
// MigratePurchaseTaskCreator only adds the #382 creator columns to
// purchase_task: nullable created_by (indexed) and created_source defaulting
// to the empty string. Existing rows are not rewritten or backfilled. Each step is guarded so
// a partial run (MySQL DDL commits implicitly) can be re-run safely.
func MigratePurchaseTaskCreator(db *gorm.DB, version string) error {
m := db.Migrator()
for _, column := range []string{"CreatedBy", "CreatedSource"} {
if !m.HasColumn(&models.PurchaseTask{}, column) {
if err := m.AddColumn(&models.PurchaseTask{}, column); err != nil {
return err
}
}
}
if !m.HasIndex(&models.PurchaseTask{}, "idx_purchase_task_created_by") {
if err := m.CreateIndex(&models.PurchaseTask{}, "CreatedBy"); err != nil {
return err
}
}
return db.Where("version = ?", version).FirstOrCreate(&common.Migration{Version: version}).Error
}
@@ -0,0 +1,59 @@
package version_local
import (
"testing"
"go-admin/app/goauto/models"
common "go-admin/common/models"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
func TestMigratePurchaseTaskCreatorAddsColumnsKeepsLegacyRowsAndIsIdempotent(t *testing.T) {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
if err != nil {
t.Fatal(err)
}
conn, _ := db.DB()
defer conn.Close()
if err = db.AutoMigrate(&common.Migration{}); err != nil {
t.Fatal(err)
}
if err = db.Exec("CREATE TABLE purchase_task (id integer primary key, status text NOT NULL)").Error; err != nil {
t.Fatal(err)
}
if err = db.Exec("INSERT INTO purchase_task(id,status) VALUES(1,'failed')").Error; err != nil {
t.Fatal(err)
}
for i := 0; i < 2; i++ {
if err = MigratePurchaseTaskCreator(db, "test_382"); err != nil {
t.Fatal(err)
}
}
for _, column := range []string{"CreatedBy", "CreatedSource"} {
if !db.Migrator().HasColumn(&models.PurchaseTask{}, column) {
t.Fatalf("missing column %s", column)
}
}
if !db.Migrator().HasIndex(&models.PurchaseTask{}, "idx_purchase_task_created_by") {
t.Fatal("missing created_by index")
}
var row struct {
Status string
CreatedBy *uint64
CreatedSource string
}
if err = db.Table("purchase_task").Select("status, created_by, created_source").Where("id = 1").Scan(&row).Error; err != nil {
t.Fatal(err)
}
if row.Status != "failed" || row.CreatedBy != nil || row.CreatedSource != "" {
t.Fatalf("legacy row changed: %+v", row)
}
var count int64
db.Model(&common.Migration{}).Where("version = ?", "test_382").Count(&count)
if count != 1 {
t.Fatal("version not idempotent")
}
}