feat(skills): 技能管理 - 增加可选的平台子分类策展 (#693)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,7 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const CurrentSchemaVersion int64 = 43
|
||||
const CurrentSchemaVersion int64 = 46
|
||||
|
||||
const baselineSchemaChecksum = "sha256:open-ai-canvas-schema-v1-20260830"
|
||||
const schemaMigrationAppliedAtIndexChecksum = "sha256:schema-migrations-applied-at-index-v2-20260830"
|
||||
@@ -55,6 +55,7 @@ type migration struct {
|
||||
}
|
||||
|
||||
var schemaMigrations = []migration{
|
||||
// 44 and 45 are reserved for independent migrations.
|
||||
{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},
|
||||
@@ -145,6 +146,7 @@ var schemaMigrations = []migration{
|
||||
{version: 43, name: "topup_sale_strategies", checksum: "sha256:topup-sale-strategies-v43-20260929", apply: func(tx *gorm.DB) error {
|
||||
return tx.AutoMigrate(&model.TopupProduct{}, &model.PaymentOrder{})
|
||||
}},
|
||||
{version: 46, name: "skill_curation", checksum: "sha256:skill-curation-v46-20261005", apply: migrateSkillCuration},
|
||||
}
|
||||
|
||||
func migratePrefixedIDSequenceReconcile(tx *gorm.DB) error {
|
||||
|
||||
14
backend/internal/database/skill_curation.go
Normal file
14
backend/internal/database/skill_curation.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"infinite-canvas/backend/internal/model"
|
||||
)
|
||||
|
||||
func migrateSkillCuration(tx *gorm.DB) error {
|
||||
if err := tx.AutoMigrate(&model.SkillCurationSetting{}, &model.SkillCurationCategory{}, &model.SkillCurationAssignment{}); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&model.SkillCurationSetting{ID: 1}).Error
|
||||
}
|
||||
104
backend/internal/database/skill_curation_test.go
Normal file
104
backend/internal/database/skill_curation_test.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"infinite-canvas/backend/internal/model"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSkillCurationUpgradeAndRollback(t *testing.T) {
|
||||
db, err := gorm.Open(sqlite.Open(filepath.Join(t.TempDir(), "upgrade.db")), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sqlDB, _ := db.DB()
|
||||
defer sqlDB.Close()
|
||||
if err := db.AutoMigrate(&schemaMigration{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, m := range schemaMigrations {
|
||||
if m.version > 43 {
|
||||
break
|
||||
}
|
||||
if err := db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := m.apply(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Create(&schemaMigration{Version: m.version, Name: m.name, Checksum: m.checksum}).Error
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if db.Migrator().HasTable(&model.SkillCurationSetting{}) {
|
||||
t.Fatal("fixture is not an original schema")
|
||||
}
|
||||
if err := db.Create(&model.Skill{ID: "existing", Name: "Existing", Tag: "drama", Status: 1}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Exec(`CREATE TRIGGER reject_curation_migration BEFORE INSERT ON schema_migrations WHEN NEW.version = 46 BEGIN SELECT RAISE(ABORT, 'migration interrupted'); END`).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := MigrateSchema(db); err == nil {
|
||||
t.Fatal("forced migration failure accepted")
|
||||
}
|
||||
if db.Migrator().HasTable(&model.SkillCurationSetting{}) {
|
||||
t.Fatal("failed migration left partial tables")
|
||||
}
|
||||
if err := db.Exec("DROP TRIGGER reject_curation_migration").Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := MigrateSchema(db); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := MigrateSchema(db); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var skill model.Skill
|
||||
if err := db.First(&skill, "id = ?", "existing").Error; err != nil || skill.Tag != "drama" {
|
||||
t.Fatalf("existing skill changed: %#v %v", skill, err)
|
||||
}
|
||||
var setting model.SkillCurationSetting
|
||||
if err := db.First(&setting, 1).Error; err != nil || setting.Enabled {
|
||||
t.Fatalf("upgrade enabled curation: %#v %v", setting, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSkillCurationMigration(t *testing.T) {
|
||||
db, err := gorm.Open(sqlite.Open(filepath.Join(t.TempDir(), "migration.db")), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sqlDB, _ := db.DB()
|
||||
defer sqlDB.Close()
|
||||
for i := 0; i < 2; i++ {
|
||||
if err := db.Transaction(migrateSkillCuration); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
var state model.SkillCurationSetting
|
||||
if err := db.First(&state, 1).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if state.Enabled || state.Revision != 0 {
|
||||
t.Fatalf("migration must default off: %#v", state)
|
||||
}
|
||||
db.Model(&state).Updates(map[string]any{"enabled": true, "revision": 3})
|
||||
if err := db.Transaction(migrateSkillCuration); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
db.First(&state, 1)
|
||||
if !state.Enabled || state.Revision != 3 {
|
||||
t.Fatal("migration overwrote settings")
|
||||
}
|
||||
found := false
|
||||
for _, m := range schemaMigrations {
|
||||
if m.version == 46 {
|
||||
found = m.name == "skill_curation" && m.checksum == "sha256:skill-curation-v46-20261005"
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("migration 46 is not registered")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user