From 6cc3bb98d099683aeab8e5ab5e7cfb340cb79f17 Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Mon, 12 Oct 2026 17:14:50 +0800 Subject: [PATCH] =?UTF-8?q?feat(db):=20purchase=5Ftask=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=20created=5Fby/created=5Fsource=20=E8=BF=81=E7=A7=BB?= =?UTF-8?q?=20(#382)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 只新增可空 created_by(索引)与非空默认空串的 created_source,不回填历史行; 迁移按列/索引存在性守卫,可重复执行。 Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F --- server/app/goauto/models/purchase.go | 9 ++- .../1791700000000_purchase_task_creator.go | 37 ++++++++++++ ...791700000000_purchase_task_creator_test.go | 59 +++++++++++++++++++ 3 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 server/cmd/migrate/migration/version-local/1791700000000_purchase_task_creator.go create mode 100644 server/cmd/migrate/migration/version-local/1791700000000_purchase_task_creator_test.go diff --git a/server/app/goauto/models/purchase.go b/server/app/goauto/models/purchase.go index 9a57f4a..c0e83d4 100644 --- a/server/app/goauto/models/purchase.go +++ b/server/app/goauto/models/purchase.go @@ -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" } diff --git a/server/cmd/migrate/migration/version-local/1791700000000_purchase_task_creator.go b/server/cmd/migrate/migration/version-local/1791700000000_purchase_task_creator.go new file mode 100644 index 0000000..37f49a1 --- /dev/null +++ b/server/cmd/migrate/migration/version-local/1791700000000_purchase_task_creator.go @@ -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 +} diff --git a/server/cmd/migrate/migration/version-local/1791700000000_purchase_task_creator_test.go b/server/cmd/migrate/migration/version-local/1791700000000_purchase_task_creator_test.go new file mode 100644 index 0000000..4af85c1 --- /dev/null +++ b/server/cmd/migrate/migration/version-local/1791700000000_purchase_task_creator_test.go @@ -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") + } +}