package version import ( "database/sql" "encoding/json" "fmt" "runtime" "strings" "gorm.io/gorm" "gorm.io/gorm/clause" "git.ilapage.cn/ila/yovision/Sense/server/app/sense/credential" deviceModels "git.ilapage.cn/ila/yovision/Sense/server/app/sense/device/models" "git.ilapage.cn/ila/yovision/Sense/server/cmd/migrate/migration" migrationModels "git.ilapage.cn/ila/yovision/Sense/server/cmd/migrate/migration/models" common "git.ilapage.cn/ila/yovision/Sense/server/common/models" ) type deviceCasbinRule struct { ID uint `gorm:"primaryKey;autoIncrement"` Ptype string `gorm:"size:512;uniqueIndex:unique_index"` V0 string `gorm:"size:512;uniqueIndex:unique_index"` V1 string `gorm:"size:512;uniqueIndex:unique_index"` V2 string `gorm:"size:512;uniqueIndex:unique_index"` V3 string `gorm:"size:512;uniqueIndex:unique_index"` V4 string `gorm:"size:512;uniqueIndex:unique_index"` V5 string `gorm:"size:512;uniqueIndex:unique_index"` } func (deviceCasbinRule) TableName() string { return "casbin_rule" } func init() { _, fileName, _, _ := runtime.Caller(0) migration.Migrate.SetVersion(migration.GetFilename(fileName), migrateSenseDeviceLedger) } func migrateSenseDeviceLedger(db *gorm.DB, version string) error { return db.Transaction(func(tx *gorm.DB) error { if err := prepareLegacyDeviceCapabilities(tx); err != nil { return err } if err := tx.AutoMigrate(&deviceModels.Device{}, &credential.DeviceCredential{}, &deviceCasbinRule{}); err != nil { return err } page, err := ensureDeviceMenu(tx, migrationModels.SysMenu{ MenuName: "SenseDeviceManage", Title: "设备管理", Icon: "monitor", Path: "/sense/devices", MenuType: "C", Permission: "sense:device:list", ParentId: 0, Component: "/sense/device/index", Sort: 5, Visible: "0", IsFrame: "1", }) if err != nil { return err } buttons := make([]migrationModels.SysMenu, 0, 4) for index, definition := range []struct{ name, title, action, permission string }{ {"SenseDeviceAdd", "新增设备", "POST", "sense:device:add"}, {"SenseDeviceEdit", "编辑设备", "PUT", "sense:device:edit"}, {"SenseDeviceDisable", "停用设备", "PUT", "sense:device:disable"}, {"SenseDeviceCredential", "更新设备凭据", "PUT", "sense:device:credential"}, } { button, err := ensureDeviceMenu(tx, migrationModels.SysMenu{ MenuName: definition.name, Title: definition.title, MenuType: "F", Action: definition.action, Permission: definition.permission, ParentId: page.MenuId, Paths: fmt.Sprintf("/0/%d", page.MenuId), Sort: index + 1, Visible: "1", IsFrame: "1", }) if err != nil { return err } buttons = append(buttons, button) } if err = attachDeviceRole(tx, "implementation_operator", append([]migrationModels.SysMenu{page}, buttons...)); err != nil { return err } if err = attachDeviceRole(tx, "site_admin", append([]migrationModels.SysMenu{page}, buttons...)); err != nil { return err } if err = attachDeviceRole(tx, "viewer", []migrationModels.SysMenu{page}); err != nil { return err } readPolicies := [][2]string{{"/api/v1/devices", "GET"}, {"/api/v1/devices/:id", "GET"}} writePolicies := [][2]string{{"/api/v1/devices", "POST"}, {"/api/v1/devices/:id", "PUT"}, {"/api/v1/devices/:id/disable", "PUT"}, {"/api/v1/devices/:id/credentials", "PUT"}} for _, role := range []string{"implementation_operator", "site_admin", "viewer"} { policies := append([][2]string{}, readPolicies...) if role != "viewer" { policies = append(policies, writePolicies...) } for _, policy := range policies { rule := deviceCasbinRule{Ptype: "p", V0: role, V1: policy[0], V2: policy[1]} if err = tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&rule).Error; err != nil { return err } } } return tx.Create(&common.Migration{Version: version}).Error }) } var supportedLegacyCapabilities = map[string]struct{}{ "video": {}, "radar": {}, "contact": {}, "button": {}, "wearable": {}, "other": {}, } type legacyDeviceCapabilitiesRow struct { ID string Capabilities sql.NullString } // prepareLegacyDeviceCapabilities upgrades the pre-GoAdmin text column before // GORM sees it. PostgreSQL cannot cast the old empty-string default to jsonb, // and old rows stored a single capability token rather than a JSON array. func prepareLegacyDeviceCapabilities(tx *gorm.DB) error { if tx.Dialector.Name() != "postgres" { return nil } type columnMetadata struct { DataType string } var column columnMetadata result := tx.Raw(`SELECT data_type FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = 'sense_devices' AND column_name = 'capabilities'`).Scan(&column) if result.Error != nil { return fmt.Errorf("inspect sense_devices.capabilities: %w", result.Error) } if result.RowsAffected == 0 || column.DataType == "json" || column.DataType == "jsonb" { return nil } if column.DataType != "text" && column.DataType != "character varying" { return fmt.Errorf("sense_devices.capabilities has unsupported legacy type %q", column.DataType) } if err := tx.Exec(`LOCK TABLE "sense_devices" IN ACCESS EXCLUSIVE MODE`).Error; err != nil { return fmt.Errorf("lock sense_devices for capabilities migration: %w", err) } var rows []legacyDeviceCapabilitiesRow if err := tx.Raw(`SELECT id, capabilities FROM "sense_devices" ORDER BY id`).Scan(&rows).Error; err != nil { return fmt.Errorf("read legacy device capabilities: %w", err) } canonical := make(map[string]string, len(rows)) invalid := 0 for _, row := range rows { value, err := canonicalLegacyCapabilities(row.Capabilities) if err != nil { invalid++ continue } canonical[row.ID] = value } if invalid > 0 { return fmt.Errorf("sense_devices.capabilities contains unsupported legacy data in %d row(s); migration rolled back", invalid) } if err := tx.Exec(`ALTER TABLE "sense_devices" ALTER COLUMN "capabilities" DROP DEFAULT`).Error; err != nil { return fmt.Errorf("drop legacy capabilities default: %w", err) } for _, row := range rows { if err := tx.Exec(`UPDATE "sense_devices" SET "capabilities" = ? WHERE "id" = ?`, canonical[row.ID], row.ID).Error; err != nil { return fmt.Errorf("normalize legacy device capabilities: %w", err) } } if err := tx.Exec(`ALTER TABLE "sense_devices" ALTER COLUMN "capabilities" TYPE jsonb USING "capabilities"::jsonb`).Error; err != nil { return fmt.Errorf("convert capabilities to jsonb: %w", err) } if err := tx.Exec(`ALTER TABLE "sense_devices" ALTER COLUMN "capabilities" SET DEFAULT '[]'::jsonb`).Error; err != nil { return fmt.Errorf("set jsonb capabilities default: %w", err) } return nil } func canonicalLegacyCapabilities(value sql.NullString) (string, error) { if !value.Valid || strings.TrimSpace(value.String) == "" { return "[]", nil } trimmed := strings.TrimSpace(value.String) if _, ok := supportedLegacyCapabilities[trimmed]; ok { encoded, _ := json.Marshal([]string{trimmed}) return string(encoded), nil } if !strings.HasPrefix(trimmed, "[") { return "", fmt.Errorf("legacy capability value is not an array") } var values []string if err := json.Unmarshal([]byte(trimmed), &values); err != nil || values == nil || len(values) > 16 { return "", fmt.Errorf("legacy capability array is invalid") } for index, item := range values { item = strings.TrimSpace(item) if _, ok := supportedLegacyCapabilities[item]; !ok { return "", fmt.Errorf("legacy capability array contains an unsupported value") } values[index] = item } encoded, _ := json.Marshal(values) return string(encoded), nil } func ensureDeviceMenu(tx *gorm.DB, desired migrationModels.SysMenu) (migrationModels.SysMenu, error) { var menu migrationModels.SysMenu err := tx.Where("menu_name = ?", desired.MenuName).First(&menu).Error if err != nil && err != gorm.ErrRecordNotFound { return menu, err } if err == gorm.ErrRecordNotFound { menu = desired if err = tx.Create(&menu).Error; err != nil { return menu, err } } paths := desired.Paths if desired.ParentId == 0 { paths = fmt.Sprintf("/0/%d", menu.MenuId) } err = tx.Model(&menu).Updates(map[string]any{ "title": desired.Title, "icon": desired.Icon, "path": desired.Path, "paths": paths, "menu_type": desired.MenuType, "action": desired.Action, "permission": desired.Permission, "parent_id": desired.ParentId, "component": desired.Component, "sort": desired.Sort, "visible": desired.Visible, "is_frame": desired.IsFrame, }).Error return menu, err } func attachDeviceRole(tx *gorm.DB, roleKey string, menus []migrationModels.SysMenu) error { var role migrationModels.SysRole if err := tx.Where("role_key = ?", roleKey).First(&role).Error; err != nil { return err } return tx.Model(&role).Association("SysMenu").Append(menus) }