package models import ( "encoding/json" "fmt" "strings" "time" "gorm.io/gorm" ) // SenlinAgentSchemaMigration 记录已完成的数据修复及审计摘要,确保升级可重入且可追溯。 type SenlinAgentSchemaMigration struct { Version int `gorm:"primaryKey;autoIncrement:false"` Name string `gorm:"not null"` Details string `gorm:"type:text;not null"` AppliedAt time.Time `gorm:"not null"` } func (SenlinAgentSchemaMigration) TableName() string { return "senlin_agent_schema_migrations" } type versionedMigration struct { version int name string run func(*gorm.DB) (map[string]int, error) } func runVersionedMigrations(database *gorm.DB) error { migrations := []versionedMigration{ {version: 1, name: "normalize_project_identifiers", run: normalizeLegacyProjectIdentifiers}, {version: 2, name: "deduplicate_project_tags", run: deduplicateLegacyProjectTags}, } for _, migration := range migrations { var applied int64 if err := database.Model(&SenlinAgentSchemaMigration{}).Where("version = ?", migration.version).Count(&applied).Error; err != nil { return err } if applied > 0 { continue } if err := database.Transaction(func(tx *gorm.DB) error { details, err := migration.run(tx) if err != nil { return err } encoded, err := json.Marshal(details) if err != nil { return err } return tx.Create(&SenlinAgentSchemaMigration{ Version: migration.version, Name: migration.name, Details: string(encoded), AppliedAt: time.Now().UTC(), }).Error }); err != nil { return fmt.Errorf("schema migration %d (%s): %w", migration.version, migration.name, err) } } return nil } func normalizeLegacyProjectIdentifiers(tx *gorm.DB) (map[string]int, error) { var projects []SenlinAgentProject if err := tx.Select("id", "owner_id", "identifier").Order("owner_id asc, id asc").Find(&projects).Error; err != nil { return nil, err } keepers := make(map[uint]map[string]uint) reserved := make(map[uint]map[string]struct{}) for _, project := range projects { identifier := strings.TrimSpace(project.Identifier) if identifier == "" { continue } if keepers[project.OwnerID] == nil { keepers[project.OwnerID] = make(map[string]uint) reserved[project.OwnerID] = make(map[string]struct{}) } if _, exists := keepers[project.OwnerID][identifier]; !exists { keepers[project.OwnerID][identifier] = project.ID } reserved[project.OwnerID][identifier] = struct{}{} } updated := 0 for _, project := range projects { if reserved[project.OwnerID] == nil { reserved[project.OwnerID] = make(map[string]struct{}) } identifier := strings.TrimSpace(project.Identifier) candidate := identifier if identifier == "" { candidate = uniqueIdentifier(fmt.Sprintf("project-%d", project.ID), reserved[project.OwnerID]) } else if keepers[project.OwnerID][identifier] != project.ID { candidate = uniqueIdentifier(fmt.Sprintf("%s-%d", identifier, project.ID), reserved[project.OwnerID]) } reserved[project.OwnerID][candidate] = struct{}{} if candidate == project.Identifier { continue } if err := tx.Model(&SenlinAgentProject{}).Where("id = ?", project.ID).Update("identifier", candidate).Error; err != nil { return nil, err } updated++ } if err := tx.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS uidx_senlin_agent_projects_owner_identifier ON senlin_agent_projects (owner_id, identifier)`).Error; err != nil { return nil, err } return map[string]int{"audited": len(projects), "updated": updated}, nil } func uniqueIdentifier(base string, reserved map[string]struct{}) string { candidate := base for suffix := 2; ; suffix++ { if _, exists := reserved[candidate]; !exists { return candidate } candidate = fmt.Sprintf("%s-%d", base, suffix) } } func deduplicateLegacyProjectTags(tx *gorm.DB) (map[string]int, error) { var tags []SenlinAgentTag if err := tx.Select("id", "identity", "project_id", "name").Order("project_id asc, name asc, id asc").Find(&tags).Error; err != nil { return nil, err } keepers := make(map[string]SenlinAgentTag) deduplicated := 0 for _, tag := range tags { key := fmt.Sprintf("%d\x00%s", tag.ProjectID, tag.Name) keeper, exists := keepers[key] if !exists { keepers[key] = tag continue } if err := tx.Model(&SenlinAgentTask{}).Where("tag_id = ?", tag.ID).Updates(map[string]any{ "tag_id": keeper.ID, "tag_identity": keeper.Identity, }).Error; err != nil { return nil, err } if err := tx.Where("id = ?", tag.ID).Delete(&SenlinAgentTag{}).Error; err != nil { return nil, err } deduplicated++ } if err := tx.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS uidx_senlin_agent_tags_project_name ON senlin_agent_tags (project_id, name)`).Error; err != nil { return nil, err } return map[string]int{"audited": len(tags), "deduplicated": deduplicated}, nil }