fix(database): 资源上传幂等 - 补齐生产库 upload_key 迁移

This commit is contained in:
ddCat committed 2026-09-01 20:55:04 +08:00
1 parent a6fa105689
commit 9c9b9cf88d
5 files changed
+69 -3

No files matched your search

+18 -1
View File
@@ -10,11 +10,12 @@ import (
"gorm.io/gorm"
)
const CurrentSchemaVersion int64 = 3
const CurrentSchemaVersion int64 = 4
const baselineSchemaChecksum = "sha256:open-ai-canvas-schema-v1-20260830"
const schemaMigrationAppliedAtIndexChecksum = "sha256:schema-migrations-applied-at-index-v2-20260830"
const assetTaxonomyCandidateIdentityChecksum = "sha256:asset-taxonomy-candidate-identity-v3-20260831-r1"
const resourceUploadKeyChecksum = "sha256:resource-upload-key-v4-20260901"
const postgresSchemaMigrationLockID int64 = 73123910420260830
@@ -44,6 +45,7 @@ var schemaMigrations = []migration{
{version: 1, name: "baseline_gorm_schema", checksum: baselineSchemaChecksum, apply: migrateSchemaV1},
{version: 2, name: "schema_migrations_applied_at_index", checksum: schemaMigrationAppliedAtIndexChecksum, apply: migrateSchemaV2},
{version: 3, name: "asset_taxonomy_candidate_identity", checksum: assetTaxonomyCandidateIdentityChecksum, apply: migrateSchemaV3},
{version: 4, name: "resource_upload_key", checksum: resourceUploadKeyChecksum, apply: migrateSchemaV4},
}
func migrateSchemaV2(tx *gorm.DB) error {
@@ -89,6 +91,21 @@ func migrateSchemaV3(tx *gorm.DB) error {
return tx.Exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_project_asset_candidates_pending_identity ON project_asset_candidates(project_id, category, name_key) WHERE status = 'pending_confirmation' AND name_key <> ''").Error
}
func migrateSchemaV4(tx *gorm.DB) error {
if !tx.Migrator().HasTable(&model.Resource{}) {
return fmt.Errorf("资源表不存在")
}
if !tx.Migrator().HasColumn(&model.Resource{}, "upload_key") {
if err := tx.Migrator().AddColumn(&model.Resource{}, "UploadKey"); err != nil {
return fmt.Errorf("增加资源上传幂等列:%w", err)
}
}
if err := tx.Exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_resources_user_upload_key ON resources (user_id, upload_key)").Error; err != nil {
return fmt.Errorf("创建资源上传幂等索引:%w", err)
}
return nil
}
func MigrateSchema(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
if tx.Dialector.Name() == "postgres" {
@@ -4,6 +4,7 @@ import (
"errors"
"strings"
"testing"
"time"
"infinite-canvas/backend/internal/model"
@@ -88,6 +89,50 @@ func TestMigrateSchemaV3NormalizesLegacyAccessoryCategory(t *testing.T) {
}
}
func TestMigrateSchemaV4AddsResourceUploadKeyToExistingSchema(t *testing.T) {
db, err := Open(Config{Driver: "sqlite", DSN: "file:migration-resource-upload-key?mode=memory&cache=shared"})
if err != nil {
t.Fatal(err)
}
if err := db.Exec(`CREATE TABLE resources (id TEXT PRIMARY KEY, user_id TEXT NOT NULL)`).Error; err != nil {
t.Fatal(err)
}
if err := db.AutoMigrate(&schemaMigration{}); err != nil {
t.Fatal(err)
}
for _, item := range schemaMigrations[:3] {
if err := db.Create(&schemaMigration{Version: item.version, Name: item.name, Checksum: item.checksum, AppliedAt: time.Now().UTC()}).Error; err != nil {
t.Fatal(err)
}
}
if err := MigrateSchema(db); err != nil {
t.Fatalf("migrate existing schema: %v", err)
}
if !db.Migrator().HasColumn(&model.Resource{}, "upload_key") {
t.Fatal("resource upload_key column was not added")
}
if !db.Migrator().HasIndex(&model.Resource{}, "idx_resources_user_upload_key") {
t.Fatal("resource upload key index was not added")
}
var status SchemaStatus
status, err = ReadSchemaStatus(db)
if err != nil {
t.Fatal(err)
}
if !status.Ready || status.Current != 4 {
t.Fatalf("unexpected schema status: %#v", status)
}
firstKey := "same-upload"
if err := db.Exec(`INSERT INTO resources (id, user_id, upload_key) VALUES (?, ?, ?)`, "resource-1", "user-1", firstKey).Error; err != nil {
t.Fatal(err)
}
if err := db.Exec(`INSERT INTO resources (id, user_id, upload_key) VALUES (?, ?, ?)`, "resource-2", "user-1", firstKey).Error; err == nil {
t.Fatal("duplicate resource upload key should be rejected")
}
}
func TestMigrateSchemaRollsBackFailedMigration(t *testing.T) {
db, err := Open(Config{Driver: "sqlite", DSN: "file:migration-rollback?mode=memory&cache=shared"})
if err != nil {