228 lines
8.1 KiB
Go
228 lines
8.1 KiB
Go
package data
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func migrateAll(db *gorm.DB) error {
|
|
if err := migrateLegacyIgnoreAPITable(db); err != nil {
|
|
return err
|
|
}
|
|
if err := db.AutoMigrate(
|
|
&userPO{}, &authorityPO{}, &menuPO{}, &userAuthorityPO{}, &authorityMenuPO{}, &menuParameterPO{},
|
|
&apiPO{}, &ignoredAPIPO{}, &authorityAPIPO{}, &casbinRulePO{}, &menuButtonPO{}, &authorityButtonPO{},
|
|
&departmentPO{}, &positionPO{}, &userDepartmentPO{}, &userPositionPO{}, &authorityDepartmentPO{},
|
|
&dictionaryPO{}, &dictionaryDetailPO{}, ¶meterPO{}, &apiTokenPO{}, &jwtBlacklistPO{}, &securityConfigPO{},
|
|
&versionPO{}, &exportTemplatePO{}, &exportConditionPO{}, &exportJoinPO{},
|
|
&operationPO{}, &loginLogPO{}, &dataAccessLogPO{}, &errorRecordPO{},
|
|
&taskPO{}, &taskLogPO{}, &mediaPO{}, &categoryPO{}, &uploadSessionPO{}, &uploadChunkPO{},
|
|
&announcementPO{},
|
|
); err != nil {
|
|
return err
|
|
}
|
|
if err := migrateLegacyAuthorityAPIsToCasbinRules(db); err != nil {
|
|
return err
|
|
}
|
|
if err := normalizeErrorRecordStatuses(db); err != nil {
|
|
return err
|
|
}
|
|
if err := reconcileRootAuthorityAPIs(db); err != nil {
|
|
return err
|
|
}
|
|
return reconcileReferenceIndexes(db)
|
|
}
|
|
|
|
// Older builds used a status label outside the administration page's supported
|
|
// state set, so normalize existing rows during migration.
|
|
func normalizeErrorRecordStatuses(db *gorm.DB) error {
|
|
return db.Session(&gorm.Session{NewDB: true}).Model(&errorRecordPO{}).Where("status = ?", "未解决").Update("status", "未处理").Error
|
|
}
|
|
|
|
// migrateLegacyAuthorityAPIsToCasbinRules upgrades the early Kra join-table
|
|
// representation to the independent Casbin policy table. Keep the legacy
|
|
// table in place for backwards compatibility, but make casbin_rule the sole
|
|
// live policy source. Existing policy rows are not duplicated.
|
|
func migrateLegacyAuthorityAPIsToCasbinRules(db *gorm.DB) error {
|
|
clean := db.Session(&gorm.Session{NewDB: true})
|
|
if !clean.Migrator().HasTable(&authorityAPIPO{}) || !clean.Migrator().HasTable(&casbinRulePO{}) {
|
|
return nil
|
|
}
|
|
type legacyPolicy struct {
|
|
AuthorityID uint
|
|
Path string
|
|
Method string
|
|
}
|
|
query := clean.Table("sys_authority_apis sa").
|
|
Select("sa.authority_id, a.path, a.method").
|
|
Joins("JOIN sys_apis a ON a.id = sa.api_id")
|
|
// Early Kra schemas stored sys_apis without soft-delete timestamps. The
|
|
// legacy-policy migration must run before assuming that column exists;
|
|
// otherwise an upgrade from those schemas cannot start on MySQL.
|
|
if clean.Migrator().HasColumn(&apiPO{}, "deleted_at") {
|
|
query = query.Where("a.deleted_at IS NULL")
|
|
}
|
|
var rows []legacyPolicy
|
|
if err := query.Find(&rows).Error; err != nil {
|
|
return err
|
|
}
|
|
for _, row := range rows {
|
|
exists, err := policyExists(clean, row.AuthorityID, row.Path, row.Method)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if exists {
|
|
continue
|
|
}
|
|
if err := clean.Create(&casbinRulePO{Ptype: "p", V0: fmt.Sprint(row.AuthorityID), V1: row.Path, V2: row.Method}).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// migrateLegacyIgnoreAPITable upgrades the early Kra-only composite-key
|
|
// shape (path, method) to the compatible model shape (ID/timestamps/soft
|
|
// delete). AutoMigrate can add columns but cannot replace an existing
|
|
// composite primary key portably, so rebuild the small table once while
|
|
// preserving every existing ignore rule.
|
|
func migrateLegacyIgnoreAPITable(db *gorm.DB) error {
|
|
clean := db.Session(&gorm.Session{NewDB: true})
|
|
if !clean.Migrator().HasTable(&ignoredAPIPO{}) || clean.Migrator().HasColumn(&ignoredAPIPO{}, "id") {
|
|
return nil
|
|
}
|
|
legacyTable := fmt.Sprintf("sys_ignore_apis_legacy_%d", time.Now().UnixNano())
|
|
type legacyIgnoredAPI struct {
|
|
Path string
|
|
Method string
|
|
}
|
|
return clean.Transaction(func(tx *gorm.DB) error {
|
|
if err := tx.Migrator().RenameTable(ignoredAPIPO{}.TableName(), legacyTable); err != nil {
|
|
return fmt.Errorf("rename legacy ignore API table: %w", err)
|
|
}
|
|
if err := tx.AutoMigrate(&ignoredAPIPO{}); err != nil {
|
|
return fmt.Errorf("create compatible ignore API table: %w", err)
|
|
}
|
|
var rows []legacyIgnoredAPI
|
|
if err := tx.Table(legacyTable).Find(&rows).Error; err != nil {
|
|
return fmt.Errorf("read legacy ignore API rows: %w", err)
|
|
}
|
|
if len(rows) > 0 {
|
|
items := make([]ignoredAPIPO, 0, len(rows))
|
|
for _, row := range rows {
|
|
items = append(items, ignoredAPIPO{Path: row.Path, Method: row.Method})
|
|
}
|
|
if err := tx.Create(&items).Error; err != nil {
|
|
return fmt.Errorf("copy legacy ignore API rows: %w", err)
|
|
}
|
|
}
|
|
if err := tx.Migrator().DropTable(legacyTable); err != nil {
|
|
return fmt.Errorf("drop legacy ignore API table: %w", err)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// reconcileRootAuthorityAPIs is a one-time upgrade path from the former Kra
|
|
// implementation where authority 888 bypassed policy storage entirely. The compatible behavior
|
|
// grants its root role through persisted Casbin policies, so when a legacy
|
|
// database has the root role but no stored API links, materialize the same
|
|
// policy set and let normal authorization read it thereafter.
|
|
func reconcileRootAuthorityAPIs(db *gorm.DB) error {
|
|
clean := db.Session(&gorm.Session{NewDB: true})
|
|
var authorityCount int64
|
|
if err := clean.Session(&gorm.Session{NewDB: true}).Model(&authorityPO{}).Where("authority_id = ?", 888).Count(&authorityCount).Error; err != nil || authorityCount == 0 {
|
|
return err
|
|
}
|
|
var policyCount int64
|
|
if err := policyScope(clean).Where("v0 = ?", "888").Count(&policyCount).Error; err != nil || policyCount != 0 {
|
|
return err
|
|
}
|
|
var ignored []ignoredAPIPO
|
|
if err := clean.Session(&gorm.Session{NewDB: true}).Find(&ignored).Error; err != nil {
|
|
return err
|
|
}
|
|
ignoreSet := make(map[string]struct{}, len(ignored))
|
|
for _, item := range ignored {
|
|
ignoreSet[item.Method+"\x00"+item.Path] = struct{}{}
|
|
}
|
|
var apis []apiPO
|
|
if err := clean.Session(&gorm.Session{NewDB: true}).Find(&apis).Error; err != nil {
|
|
return err
|
|
}
|
|
rules := make([]casbinRulePO, 0, len(apis))
|
|
for _, api := range apis {
|
|
if _, ok := ignoreSet[api.Method+"\x00"+api.Path]; ok {
|
|
continue
|
|
}
|
|
rules = append(rules, newPolicyRule(888, api.Path, api.Method))
|
|
}
|
|
if len(rules) == 0 {
|
|
return nil
|
|
}
|
|
return clean.Session(&gorm.Session{NewDB: true}).Create(&rules).Error
|
|
}
|
|
|
|
// reconcileReferenceIndexes removes constraints created by older Kra builds
|
|
// that are not part of the administration data model. Business services own
|
|
// duplicate checks and their user-facing error messages.
|
|
func reconcileReferenceIndexes(db *gorm.DB) error {
|
|
clean := db.Session(&gorm.Session{NewDB: true})
|
|
obsolete := []struct {
|
|
model any
|
|
name string
|
|
}{
|
|
{&apiPO{}, "idx_api_path_method"},
|
|
{&dictionaryPO{}, "idx_sys_dictionaries_type"},
|
|
{¶meterPO{}, "idx_sys_params_key"},
|
|
{&apiTokenPO{}, "idx_sys_api_tokens_token"},
|
|
{&exportTemplatePO{}, "idx_sys_export_templates_template_id"},
|
|
}
|
|
for _, item := range obsolete {
|
|
migrator := clean.Session(&gorm.Session{NewDB: true}).Migrator()
|
|
if migrator.HasIndex(item.model, item.name) {
|
|
if err := migrator.DropIndex(item.model, item.name); err != nil {
|
|
return fmt.Errorf("drop obsolete index %s: %w", item.name, err)
|
|
}
|
|
}
|
|
}
|
|
for _, item := range []struct {
|
|
name string
|
|
field string
|
|
}{{"idx_sys_users_uuid", "UUID"}, {"idx_sys_users_username", "Username"}} {
|
|
unique, err := indexIsUnique(clean.Session(&gorm.Session{NewDB: true}), &userPO{}, item.name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !unique {
|
|
continue
|
|
}
|
|
migrator := clean.Session(&gorm.Session{NewDB: true}).Migrator()
|
|
if err = migrator.DropIndex(&userPO{}, item.name); err != nil {
|
|
return fmt.Errorf("drop legacy unique index %s: %w", item.name, err)
|
|
}
|
|
if err = migrator.CreateIndex(&userPO{}, item.field); err != nil {
|
|
return fmt.Errorf("create reference index %s: %w", item.name, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func indexIsUnique(db *gorm.DB, model any, name string) (bool, error) {
|
|
indexes, err := db.Migrator().GetIndexes(model)
|
|
if err != nil {
|
|
// Some third-party GORM drivers do not implement index inspection.
|
|
// Fresh schemas are already correct; skip only the legacy repair there.
|
|
return false, nil
|
|
}
|
|
for _, index := range indexes {
|
|
if index.Name() == name {
|
|
unique, known := index.Unique()
|
|
return known && unique, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|