From 9c9b9cf88df4f48f35c88cea83781673adee38b4 Mon Sep 17 00:00:00 2001 From: ddCat <2695092045@qq.com> Date: Tue, 1 Sep 2026 20:55:04 +0800 Subject: [PATCH] =?UTF-8?q?fix(database):=20=E8=B5=84=E6=BA=90=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=E5=B9=82=E7=AD=89=20-=20=E8=A1=A5=E9=BD=90=E7=94=9F?= =?UTF-8?q?=E4=BA=A7=E5=BA=93=20upload=5Fkey=20=E8=BF=81=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 4 ++ VERSION | 2 +- backend/internal/database/migrations.go | 19 +++++++- backend/internal/database/migrations_test.go | 45 +++++++++++++++++++ .../content/docs/backend/backend-database.mdx | 2 +- 5 files changed, 69 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38051b2de..a1794f16d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## v1.2.4.1 + +- 修复资源上传幂等字段未纳入独立数据库迁移的问题,升级已有 v3 数据库时自动补齐 `resources.upload_key` 及用户维度唯一索引,避免生成内容写入资源存储失败。 + ## v1.2.4 - 升级声明式模型协议和官方插件目录,完善生成任务恢复、渠道能力保护与运行时协议校验。 diff --git a/VERSION b/VERSION index c7cd5b267..2a5284688 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v1.2.4 +v1.2.4.1 diff --git a/backend/internal/database/migrations.go b/backend/internal/database/migrations.go index e744a341a..ea1e3368b 100644 --- a/backend/internal/database/migrations.go +++ b/backend/internal/database/migrations.go @@ -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" { diff --git a/backend/internal/database/migrations_test.go b/backend/internal/database/migrations_test.go index 5a5e03ce4..806df6995 100644 --- a/backend/internal/database/migrations_test.go +++ b/backend/internal/database/migrations_test.go @@ -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 { diff --git a/docs/content/docs/backend/backend-database.mdx b/docs/content/docs/backend/backend-database.mdx index 213ff3f64..d1a86c782 100644 --- a/docs/content/docs/backend/backend-database.mdx +++ b/docs/content/docs/backend/backend-database.mdx @@ -15,7 +15,7 @@ description: 影策后端核心持久化表与状态边界 容器内命令 `migrate-schema up` 应用缺失迁移,`migrate-schema status` 输出当前和期望版本,`migrate-schema verify` 只检查兼容性。`CANVAS_AUTO_MIGRATE=true` 仅用于开发和旧部署兼容;受管生产部署设置为 `false`,并由 Compose 的一次性 `migrate` 服务先完成迁移。后续迁移必须新增版本,不得修改已经发布迁移的实现或校验和。 -当前结构版本为 3。版本 2 为 `schema_migrations.applied_at` 增加 `idx_schema_migrations_applied_at` 索引;版本 3 `asset_taxonomy_candidate_identity` 为资产候选增加规范化名称和来源,收敛资产分类,并为待确认候选建立项目内部分唯一索引。 +当前结构版本为 4。版本 2 为 `schema_migrations.applied_at` 增加 `idx_schema_migrations_applied_at` 索引;版本 3 `asset_taxonomy_candidate_identity` 为资产候选增加规范化名称和来源,收敛资产分类,并为待确认候选建立项目内部分唯一索引;版本 4 `resource_upload_key` 为资源上传幂等补充 `resources.upload_key` 列和用户维度唯一索引。 版本 3 会将 `wardrobe`、`weapon`、`accessory` 迁移为 `prop`,将 `style` 迁移为 `material`,并把原先归入 `other` 的图片、视频、音频和 3D 模型迁移为 `material`。迁移只合并重复的待确认候选:保留最早记录,其余标记为 `ignored`;已经确认的资产不会被自动删除或归并。