// Package repair 提供必须显式执行且保留审计快照的一次性数据修复。 package repair import ( "encoding/json" "errors" "fmt" "strings" "time" "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common" "git.apinb.com/heqiapp/platforms/backend/api/internal/models" "gorm.io/gorm" ) const ( legacyGasAccountRole = "010" gasAdminRole = "admin" gasAccountRepairAction = "repair_legacy_gas_account_role" ) // GasAccountRepairResult 返回本次修复定位到的账号及审计信息。 type GasAccountRepairResult struct { AccountIdentity string `json:"account_identity"` Username string `json:"username"` OldRoleCode string `json:"old_role_code"` NewRoleCode string `json:"new_role_code"` OldDisplayName string `json:"old_display_name"` NewDisplayName string `json:"new_display_name"` Operator string `json:"operator"` RepairedAt *time.Time `json:"repaired_at,omitempty"` } // InspectLegacyGasAccount 只读校验修复目标和操作者,不修改任何业务或审计数据。 func InspectLegacyGasAccount(database *gorm.DB, operatorUsername string, displayName string) (*GasAccountRepairResult, error) { operatorUsername = strings.TrimSpace(operatorUsername) displayName = strings.TrimSpace(displayName) if database == nil || operatorUsername == "" || displayName == "" { return nil, errors.New("database, operator and display name are required") } account, operator, err := legacyGasAccountRepairTarget(database, operatorUsername) if err != nil { return nil, err } return &GasAccountRepairResult{ AccountIdentity: account.Identity, Username: account.Username, OldRoleCode: account.RoleCode, NewRoleCode: gasAdminRole, OldDisplayName: account.DisplayName, NewDisplayName: displayName, Operator: operator.Username, }, nil } func legacyGasAccountRepairTarget(database *gorm.DB, operatorUsername string) (models.GasAccount, models.PlatformAccount, error) { var accounts []models.GasAccount if err := database.Where("role_code = ?", legacyGasAccountRole).Limit(2).Find(&accounts).Error; err != nil { return models.GasAccount{}, models.PlatformAccount{}, fmt.Errorf("find legacy gas account: %w", err) } if len(accounts) == 0 { return models.GasAccount{}, models.PlatformAccount{}, errors.New("legacy gas account role 010 not found") } if len(accounts) > 1 { return models.GasAccount{}, models.PlatformAccount{}, fmt.Errorf("refuse ambiguous repair: found %d gas accounts with role 010", len(accounts)) } var operator models.PlatformAccount if err := database.Where("username = ? AND status = ?", operatorUsername, common.StatusEnable).First(&operator).Error; err != nil { return models.GasAccount{}, models.PlatformAccount{}, fmt.Errorf("find enabled platform operator %q: %w", operatorUsername, err) } return accounts[0], operator, nil } type gasAccountSnapshot struct { RoleCode string `json:"role_code"` DisplayName string `json:"display_name"` } // RepairLegacyGasAccount 将唯一一条历史 010 气站账号显式修复为管理员并写入审计日志。 func RepairLegacyGasAccount(database *gorm.DB, operatorUsername string, displayName string) (*GasAccountRepairResult, error) { operatorUsername = strings.TrimSpace(operatorUsername) displayName = strings.TrimSpace(displayName) if database == nil || operatorUsername == "" || displayName == "" { return nil, errors.New("database, operator and display name are required") } var result GasAccountRepairResult err := database.Transaction(func(tx *gorm.DB) error { account, operator, err := legacyGasAccountRepairTarget(tx, operatorUsername) if err != nil { return err } before := gasAccountSnapshot{RoleCode: account.RoleCode, DisplayName: account.DisplayName} after := gasAccountSnapshot{RoleCode: gasAdminRole, DisplayName: displayName} beforeJSON, err := json.Marshal(before) if err != nil { return fmt.Errorf("marshal repair before snapshot: %w", err) } afterJSON, err := json.Marshal(after) if err != nil { return fmt.Errorf("marshal repair after snapshot: %w", err) } repairedAt := time.Now() update := tx.Model(&models.GasAccount{}). Where("identity = ? AND role_code = ?", account.Identity, legacyGasAccountRole). Updates(map[string]any{"role_code": gasAdminRole, "display_name": displayName, "updated_at": repairedAt}) if update.Error != nil { return fmt.Errorf("repair legacy gas account: %w", update.Error) } if update.RowsAffected != 1 { return errors.New("legacy gas account changed concurrently; repair aborted") } audit := models.AuditOperationLog{ Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusEnable}, OperatorIdentity: operator.Identity, OperatorName: operator.Username, ResourceType: "gas_account", ResourceIdentity: account.Identity, Action: gasAccountRepairAction, BeforeValue: string(beforeJSON), AfterValue: string(afterJSON), OccurredAt: repairedAt, Remark: "显式修复历史非法气站角色编码 010,并补齐显示名称", } if err := tx.Create(&audit).Error; err != nil { return fmt.Errorf("write gas account repair audit: %w", err) } result = GasAccountRepairResult{ AccountIdentity: account.Identity, Username: account.Username, OldRoleCode: before.RoleCode, NewRoleCode: after.RoleCode, OldDisplayName: before.DisplayName, NewDisplayName: after.DisplayName, Operator: operator.Username, RepairedAt: &repairedAt, } return nil }) if err != nil { return nil, err } return &result, nil }