2026-05-12 19:34:47 +08:00

502 lines
23 KiB
Go

package repository
import (
"hyapp-admin-server/internal/config"
"hyapp-admin-server/internal/model"
"hyapp-admin-server/internal/security"
"gorm.io/gorm"
)
var defaultPermissions = []model.Permission{
{Name: "总览查看", Code: "overview:view", Kind: "menu"},
{Name: "用户查看", Code: "user:view", Kind: "menu"},
{Name: "用户创建", Code: "user:create", Kind: "button"},
{Name: "用户更新", Code: "user:update", Kind: "button"},
{Name: "用户状态", Code: "user:status", Kind: "button"},
{Name: "用户重置密码", Code: "user:reset-password", Kind: "button"},
{Name: "用户导出", Code: "user:export", Kind: "button"},
{Name: "App 用户查看", Code: "app-user:view", Kind: "menu"},
{Name: "App 用户更新", Code: "app-user:update", Kind: "button"},
{Name: "App 用户状态", Code: "app-user:status", Kind: "button"},
{Name: "App 用户设置密码", Code: "app-user:password", Kind: "button"},
{Name: "房间查看", Code: "room:view", Kind: "menu"},
{Name: "房间更新", Code: "room:update", Kind: "button"},
{Name: "房间删除", Code: "room:delete", Kind: "button"},
{Name: "房间配置查看", Code: "room-config:view", Kind: "menu"},
{Name: "房间配置更新", Code: "room-config:update", Kind: "button"},
{Name: "APP 配置查看", Code: "app-config:view", Kind: "menu"},
{Name: "APP 配置更新", Code: "app-config:update", Kind: "button"},
{Name: "资源查看", Code: "resource:view", Kind: "menu"},
{Name: "资源创建", Code: "resource:create", Kind: "button"},
{Name: "资源更新", Code: "resource:update", Kind: "button"},
{Name: "资源组查看", Code: "resource-group:view", Kind: "menu"},
{Name: "资源组创建", Code: "resource-group:create", Kind: "button"},
{Name: "资源组更新", Code: "resource-group:update", Kind: "button"},
{Name: "资源赠送查看", Code: "resource-grant:view", Kind: "menu"},
{Name: "资源赠送创建", Code: "resource-grant:create", Kind: "button"},
{Name: "礼物查看", Code: "gift:view", Kind: "menu"},
{Name: "礼物创建", Code: "gift:create", Kind: "button"},
{Name: "礼物更新", Code: "gift:update", Kind: "button"},
{Name: "礼物状态", Code: "gift:status", Kind: "button"},
{Name: "国家查看", Code: "country:view", Kind: "menu"},
{Name: "国家创建", Code: "country:create", Kind: "button"},
{Name: "国家更新", Code: "country:update", Kind: "button"},
{Name: "国家状态", Code: "country:status", Kind: "button"},
{Name: "区域查看", Code: "region:view", Kind: "menu"},
{Name: "区域创建", Code: "region:create", Kind: "button"},
{Name: "区域更新", Code: "region:update", Kind: "button"},
{Name: "区域状态", Code: "region:status", Kind: "button"},
{Name: "主播查看", Code: "host:view", Kind: "menu"},
{Name: "Agency 查看", Code: "agency:view", Kind: "menu"},
{Name: "Agency 创建", Code: "agency:create", Kind: "button"},
{Name: "Agency 状态", Code: "agency:status", Kind: "button"},
{Name: "BD 查看", Code: "bd:view", Kind: "menu"},
{Name: "BD 创建", Code: "bd:create", Kind: "button"},
{Name: "BD 更新", Code: "bd:update", Kind: "button"},
{Name: "币商查看", Code: "coin-seller:view", Kind: "menu"},
{Name: "币商创建", Code: "coin-seller:create", Kind: "button"},
{Name: "币商更新", Code: "coin-seller:update", Kind: "button"},
{Name: "币商进货", Code: "coin-seller:stock-credit", Kind: "button"},
{Name: "支付账单查看", Code: "payment-bill:view", Kind: "menu"},
{Name: "游戏查看", Code: "game:view", Kind: "menu"},
{Name: "游戏创建", Code: "game:create", Kind: "button"},
{Name: "游戏更新", Code: "game:update", Kind: "button"},
{Name: "游戏状态", Code: "game:status", Kind: "button"},
{Name: "每日任务查看", Code: "daily-task:view", Kind: "menu"},
{Name: "每日任务创建", Code: "daily-task:create", Kind: "button"},
{Name: "每日任务更新", Code: "daily-task:update", Kind: "button"},
{Name: "每日任务状态", Code: "daily-task:status", Kind: "button"},
{Name: "角色查看", Code: "role:view", Kind: "menu"},
{Name: "角色创建", Code: "role:create", Kind: "button"},
{Name: "角色更新", Code: "role:update", Kind: "button"},
{Name: "角色删除", Code: "role:delete", Kind: "button"},
{Name: "角色授权", Code: "role:permission", Kind: "button"},
{Name: "角色数据权限", Code: "role:data-scope", Kind: "button"},
{Name: "角色管理", Code: "role:manage", Kind: "button", Description: "兼容旧版本的角色管理权限"},
{Name: "权限查看", Code: "permission:view", Kind: "menu"},
{Name: "权限创建", Code: "permission:create", Kind: "button"},
{Name: "权限更新", Code: "permission:update", Kind: "button"},
{Name: "权限删除", Code: "permission:delete", Kind: "button"},
{Name: "权限同步", Code: "permission:sync", Kind: "button"},
{Name: "菜单查看", Code: "menu:view", Kind: "menu"},
{Name: "菜单创建", Code: "menu:create", Kind: "button"},
{Name: "菜单更新", Code: "menu:update", Kind: "button"},
{Name: "菜单删除", Code: "menu:delete", Kind: "button"},
{Name: "菜单排序", Code: "menu:sort", Kind: "button"},
{Name: "菜单显隐", Code: "menu:visible", Kind: "button"},
{Name: "日志查看", Code: "log:view", Kind: "menu"},
{Name: "日志导出", Code: "log:export", Kind: "button"},
{Name: "通知查看", Code: "notification:view", Kind: "menu"},
{Name: "通知已读", Code: "notification:read", Kind: "button"},
{Name: "通知全部已读", Code: "notification:read-all", Kind: "button"},
{Name: "通知删除", Code: "notification:delete", Kind: "button"},
{Name: "任务查看", Code: "job:view", Kind: "menu"},
{Name: "任务取消", Code: "job:cancel", Kind: "button"},
{Name: "导出任务创建", Code: "export:create", Kind: "button"},
{Name: "文件上传", Code: "upload:create", Kind: "button"},
}
var deprecatedPermissionCodes = []string{"service:view", "service:create", "service:update", "service:operate"}
var deprecatedMenuCodes = []string{"services"}
func (s *Store) seedPermissions() error {
return s.syncDefaultPermissions(s.db)
}
func (s *Store) seedMenus() error {
systemID := uint(0)
geoID := uint(0)
hostOrgID := uint(0)
appUsersID := uint(0)
appConfigID := uint(0)
resourceID := uint(0)
roomsID := uint(0)
paymentID := uint(0)
activityID := uint(0)
gameID := uint(0)
menus := []model.Menu{
{Title: "总览", Code: "overview", Path: "/overview", Icon: "dashboard", PermissionCode: "overview:view", Sort: 10, Visible: true},
{Title: "后台设置", Code: "system", Path: "", Icon: "settings", PermissionCode: "", Sort: 30, Visible: true},
{Title: "日志审计", Code: "logs", Path: "", Icon: "history", PermissionCode: "", Sort: 40, Visible: false},
{Title: "通知中心", Code: "notifications", Path: "/notifications", Icon: "notifications", PermissionCode: "notification:view", Sort: 50, Visible: true},
{Title: "用户管理", Code: "app-users", Path: "", Icon: "users", PermissionCode: "", Sort: 60, Visible: true},
{Title: "房间管理", Code: "rooms", Path: "", Icon: "room", PermissionCode: "", Sort: 65, Visible: true},
{Title: "APP配置", Code: "app-config", Path: "", Icon: "settings", PermissionCode: "", Sort: 66, Visible: true},
{Title: "资源管理", Code: "resources", Path: "", Icon: "inventory", PermissionCode: "", Sort: 67, Visible: true},
{Title: "支付管理", Code: "payment", Path: "", Icon: "wallet", PermissionCode: "", Sort: 68, Visible: true},
{Title: "活动管理", Code: "activities", Path: "", Icon: "campaign", PermissionCode: "", Sort: 69, Visible: true},
{Title: "游戏管理", Code: "games", Path: "", Icon: "sports_esports", PermissionCode: "", Sort: 70, Visible: true},
{Title: "地区管理", Code: "geo", Path: "", Icon: "map", PermissionCode: "", Sort: 71, Visible: true},
{Title: "团队管理", Code: "host-org", Path: "", Icon: "network", PermissionCode: "", Sort: 80, Visible: true},
{Title: "任务中心", Code: "jobs", Path: "/jobs", Icon: "task", PermissionCode: "job:view", Sort: 90, Visible: true},
}
for _, menu := range menus {
syncedMenu, err := s.syncDefaultMenu(s.db, menu)
if err != nil {
return err
}
if syncedMenu.Code == "system" {
systemID = syncedMenu.ID
}
if syncedMenu.Code == "geo" {
geoID = syncedMenu.ID
}
if syncedMenu.Code == "host-org" {
hostOrgID = syncedMenu.ID
}
if syncedMenu.Code == "app-users" {
appUsersID = syncedMenu.ID
}
if syncedMenu.Code == "app-config" {
appConfigID = syncedMenu.ID
}
if syncedMenu.Code == "resources" {
resourceID = syncedMenu.ID
}
if syncedMenu.Code == "rooms" {
roomsID = syncedMenu.ID
}
if syncedMenu.Code == "payment" {
paymentID = syncedMenu.ID
}
if syncedMenu.Code == "activities" {
activityID = syncedMenu.ID
}
if syncedMenu.Code == "games" {
gameID = syncedMenu.ID
}
}
children := []model.Menu{
{ParentID: &systemID, Title: "后台用户", Code: "system-users", Path: "/system/users", Icon: "users", PermissionCode: "user:view", Sort: 31, Visible: true},
{ParentID: &systemID, Title: "角色配置", Code: "system-roles", Path: "/system/roles", Icon: "shield", PermissionCode: "role:view", Sort: 32, Visible: true},
{ParentID: &systemID, Title: "菜单权限", Code: "system-menus", Path: "/system/menus", Icon: "menu", PermissionCode: "menu:view", Sort: 33, Visible: true},
{ParentID: &systemID, Title: "登录日志", Code: "logs-login", Path: "/logs/login", Icon: "login", PermissionCode: "log:view", Sort: 34, Visible: true},
{ParentID: &systemID, Title: "操作日志", Code: "logs-operations", Path: "/logs/operations", Icon: "receipt", PermissionCode: "log:view", Sort: 35, Visible: true},
{ParentID: &appUsersID, Title: "用户列表", Code: "app-user-list", Path: "/app/users", Icon: "users", PermissionCode: "app-user:view", Sort: 60, Visible: true},
{ParentID: &appUsersID, Title: "登录日志", Code: "app-user-login-logs", Path: "/app/users/login-logs", Icon: "login", PermissionCode: "app-user:view", Sort: 61, Visible: true},
{ParentID: &roomsID, Title: "房间列表", Code: "room-list", Path: "/rooms", Icon: "room", PermissionCode: "room:view", Sort: 65, Visible: true},
{ParentID: &roomsID, Title: "房间配置", Code: "room-config", Path: "/rooms/config", Icon: "settings", PermissionCode: "room-config:view", Sort: 66, Visible: true},
{ParentID: &appConfigID, Title: "H5配置", Code: "app-config-h5", Path: "/app-config/h5", Icon: "settings", PermissionCode: "app-config:view", Sort: 66, Visible: true},
{ParentID: &appConfigID, Title: "BANNER配置", Code: "app-config-banners", Path: "/app-config/banners", Icon: "image", PermissionCode: "app-config:view", Sort: 67, Visible: true},
{ParentID: &resourceID, Title: "资源列表", Code: "resource-list", Path: "/resources", Icon: "inventory", PermissionCode: "resource:view", Sort: 67, Visible: true},
{ParentID: &resourceID, Title: "资源组列表", Code: "resource-group-list", Path: "/resource-groups", Icon: "category", PermissionCode: "resource-group:view", Sort: 68, Visible: true},
{ParentID: &resourceID, Title: "礼物列表", Code: "gift-list", Path: "/gifts", Icon: "gift", PermissionCode: "gift:view", Sort: 69, Visible: true},
{ParentID: &resourceID, Title: "资源赠送", Code: "resource-grant-list", Path: "/resource-grants", Icon: "send", PermissionCode: "resource-grant:view", Sort: 70, Visible: true},
{ParentID: &paymentID, Title: "账单列表", Code: "payment-bill-list", Path: "/payment/bills", Icon: "receipt", PermissionCode: "payment-bill:view", Sort: 68, Visible: true},
{ParentID: &activityID, Title: "每日任务", Code: "daily-task-list", Path: "/activities/daily-tasks", Icon: "task", PermissionCode: "daily-task:view", Sort: 69, Visible: true},
{ParentID: &gameID, Title: "游戏列表", Code: "game-list", Path: "/games", Icon: "sports_esports", PermissionCode: "game:view", Sort: 70, Visible: true},
{ParentID: &geoID, Title: "国家管理", Code: "host-org-countries", Path: "/host/countries", Icon: "public", PermissionCode: "country:view", Sort: 70, Visible: true},
{ParentID: &geoID, Title: "区域管理", Code: "host-org-regions", Path: "/host/regions", Icon: "map", PermissionCode: "region:view", Sort: 71, Visible: true},
{ParentID: &hostOrgID, Title: "Agency 管理", Code: "host-org-agencies", Path: "/host/agencies", Icon: "apartment", PermissionCode: "agency:view", Sort: 80, Visible: true},
{ParentID: &hostOrgID, Title: "BD Leader 管理", Code: "host-org-bd-leaders", Path: "/host/bd-leaders", Icon: "shield", PermissionCode: "bd:view", Sort: 81, Visible: true},
{ParentID: &hostOrgID, Title: "BD 管理", Code: "host-org-bds", Path: "/host/bds", Icon: "team", PermissionCode: "bd:view", Sort: 82, Visible: true},
{ParentID: &hostOrgID, Title: "主播管理", Code: "host-org-hosts", Path: "/host/hosts", Icon: "users", PermissionCode: "host:view", Sort: 83, Visible: true},
{ParentID: &hostOrgID, Title: "币商管理", Code: "host-org-coin-sellers", Path: "/host/coin-sellers", Icon: "wallet", PermissionCode: "coin-seller:view", Sort: 84, Visible: true},
}
for _, menu := range children {
if _, err := s.syncDefaultMenu(s.db, menu); err != nil {
return err
}
}
return nil
}
func (s *Store) seedRoles() error {
var permissions []model.Permission
if err := s.db.Find(&permissions).Error; err != nil {
return err
}
roles := []model.Role{
{Name: "平台管理员", Code: "platform-admin", Description: "拥有管理后台全部权限"},
{Name: "运维管理员", Code: "ops-admin", Description: "管理用户状态和后台操作"},
{Name: "审计员", Code: "auditor", Description: "查看日志和审计记录"},
{Name: "只读用户", Code: "readonly", Description: "只读查看后台数据"},
}
for _, role := range roles {
if err := s.db.Where("code = ?", role.Code).FirstOrCreate(&role).Error; err != nil {
return err
}
if role.Code == "platform-admin" {
if err := s.db.Model(&role).Association("Permissions").Replace(permissions); err != nil {
return err
}
continue
}
// 预置角色只在没有任何权限时写入默认模板,避免后续人工调整被启动种子覆盖。
if s.db.Model(&role).Association("Permissions").Count() == 0 {
if err := s.replaceRolePermissionsByCode(&role, defaultRolePermissionCodes(role.Code)); err != nil {
return err
}
continue
}
if err := s.appendRolePermissionsByCode(s.db, &role, defaultRolePermissionMigrationCodes(role.Code)); err != nil {
return err
}
}
return nil
}
func (s *Store) seedUsers(cfg config.Config) error {
var count int64
if err := s.db.Model(&model.User{}).Count(&count).Error; err != nil {
return err
}
if count > 0 {
return nil
}
if cfg.Bootstrap.Password == "" {
return nil
}
hash, err := security.HashPassword(cfg.Bootstrap.Password)
if err != nil {
return err
}
var role model.Role
if err := s.db.Where("code = ?", "platform-admin").First(&role).Error; err != nil {
return err
}
user := model.User{
Username: cfg.Bootstrap.Username,
Name: cfg.Bootstrap.Name,
PasswordHash: hash,
Team: cfg.Bootstrap.Team,
Status: model.UserStatusActive,
MFAEnabled: false,
}
if err := s.db.Create(&user).Error; err != nil {
return err
}
return s.db.Model(&user).Association("Roles").Replace([]model.Role{role})
}
func (s *Store) seedNotifications() error {
var count int64
if err := s.db.Model(&model.Notification{}).Count(&count).Error; err != nil {
return err
}
if count > 0 {
return nil
}
items := []model.Notification{
{Title: "权限种子已同步", Content: "默认权限和菜单已写入后台库", Level: "info"},
{Title: "审计日志已启用", Content: "写操作会记录操作日志", Level: "info"},
{Title: "导出任务已启用", Content: "任务中心会处理后台导出任务", Level: "info"},
}
return s.db.Create(&items).Error
}
func (s *Store) pruneDeprecatedDefaults() error {
return s.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Where("code IN ? OR permission_code IN ?", deprecatedMenuCodes, deprecatedPermissionCodes).Delete(&model.Menu{}).Error; err != nil {
return err
}
var permissions []model.Permission
if err := tx.Where("code IN ?", deprecatedPermissionCodes).Find(&permissions).Error; err != nil {
return err
}
if len(permissions) == 0 {
return nil
}
permissionIDs := make([]uint, 0, len(permissions))
for _, permission := range permissions {
permissionIDs = append(permissionIDs, permission.ID)
}
if err := tx.Table("admin_role_permissions").Where("permission_id IN ?", permissionIDs).Delete(nil).Error; err != nil {
return err
}
return tx.Where("id IN ?", permissionIDs).Delete(&model.Permission{}).Error
})
}
func (s *Store) syncDefaultPermissions(tx *gorm.DB) error {
for _, definition := range defaultPermissions {
permission := model.Permission{}
// 权限种子是前后端契约来源;已存在时同步名称、类型和说明,避免按钮文案和授权页长期漂移。
result := tx.Where("code = ?", definition.Code).Limit(1).Find(&permission)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
if err := tx.Create(&definition).Error; err != nil {
return err
}
continue
}
if err := tx.Model(&permission).Updates(map[string]any{
"name": definition.Name,
"kind": definition.Kind,
"description": definition.Description,
}).Error; err != nil {
return err
}
}
return nil
}
func (s *Store) syncDefaultMenu(tx *gorm.DB, definition model.Menu) (model.Menu, error) {
menu := model.Menu{}
// 菜单种子同样是前后端契约来源;已存在时同步标题、路径和权限,避免后台菜单文案长期漂移。
result := tx.Where("code = ?", definition.Code).Limit(1).Find(&menu)
if result.Error != nil {
return model.Menu{}, result.Error
}
if result.RowsAffected == 0 {
if err := tx.Create(&definition).Error; err != nil {
return model.Menu{}, err
}
return definition, nil
}
if err := tx.Model(&menu).Updates(map[string]any{
"parent_id": definition.ParentID,
"title": definition.Title,
"path": definition.Path,
"icon": definition.Icon,
"permission_code": definition.PermissionCode,
"sort": definition.Sort,
"visible": definition.Visible,
}).Error; err != nil {
return model.Menu{}, err
}
return menu, nil
}
func (s *Store) replaceRolePermissionsByCode(role *model.Role, codes []string) error {
var permissions []model.Permission
if len(codes) > 0 {
if err := s.db.Where("code IN ?", codes).Find(&permissions).Error; err != nil {
return err
}
}
return s.db.Model(role).Association("Permissions").Replace(permissions)
}
func (s *Store) appendRolePermissionsByCode(tx *gorm.DB, role *model.Role, codes []string) error {
if len(codes) == 0 {
return nil
}
var current model.Role
if err := tx.Preload("Permissions").First(&current, role.ID).Error; err != nil {
return err
}
existing := make(map[string]struct{}, len(current.Permissions))
for _, permission := range current.Permissions {
existing[permission.Code] = struct{}{}
}
missingCodes := make([]string, 0, len(codes))
for _, code := range codes {
if _, ok := existing[code]; !ok {
missingCodes = append(missingCodes, code)
}
}
if len(missingCodes) == 0 {
return nil
}
var permissions []model.Permission
if err := tx.Where("code IN ?", missingCodes).Find(&permissions).Error; err != nil {
return err
}
if len(permissions) == 0 {
return nil
}
return tx.Model(role).Association("Permissions").Append(permissions)
}
func defaultRolePermissionCodes(code string) []string {
switch code {
case "ops-admin":
return []string{
"overview:view",
"user:view", "user:status",
"app-user:view", "app-user:update", "app-user:status", "app-user:password",
"room:view", "room:update", "room:delete", "room-config:view", "room-config:update",
"app-config:view", "app-config:update",
"resource:view", "resource:create", "resource:update",
"resource-group:view", "resource-group:create", "resource-group:update",
"resource-grant:view", "resource-grant:create",
"gift:view", "gift:create", "gift:update", "gift:status",
"country:view", "country:create", "country:update", "country:status",
"region:view", "region:create", "region:update", "region:status",
"host:view",
"agency:view", "agency:create", "agency:status",
"bd:view", "bd:create", "bd:update",
"coin-seller:view", "coin-seller:create", "coin-seller:update", "coin-seller:stock-credit",
"payment-bill:view",
"game:view", "game:create", "game:update", "game:status",
"daily-task:view", "daily-task:create", "daily-task:update", "daily-task:status",
"log:view",
"notification:view", "notification:read",
"job:view", "job:cancel", "export:create",
"upload:create",
}
case "auditor":
return []string{"overview:view", "log:view", "user:view", "app-user:view", "room:view", "room-config:view", "app-config:view", "resource:view", "resource-group:view", "resource-grant:view", "gift:view", "payment-bill:view", "game:view", "daily-task:view", "role:view", "permission:view", "job:view"}
case "readonly":
return []string{
"overview:view",
"user:view",
"app-user:view",
"room:view",
"room-config:view",
"app-config:view",
"resource:view",
"resource-group:view",
"resource-grant:view",
"gift:view",
"country:view",
"region:view",
"host:view",
"agency:view",
"bd:view",
"coin-seller:view",
"payment-bill:view",
"game:view",
"daily-task:view",
"role:view",
"permission:view",
"menu:view",
"log:view",
"notification:view",
"job:view",
}
default:
return nil
}
}
func defaultRolePermissionMigrationCodes(code string) []string {
switch code {
case "ops-admin":
return []string{
"app-user:update", "app-user:status", "app-user:password",
"room:view", "room:update", "room:delete", "room-config:view", "room-config:update",
"app-config:view", "app-config:update",
"resource:view", "resource:create", "resource:update",
"resource-group:view", "resource-group:create", "resource-group:update",
"resource-grant:view", "resource-grant:create",
"gift:view", "gift:create", "gift:update", "gift:status",
"upload:create",
"country:view", "country:create", "country:update", "country:status",
"region:view", "region:create", "region:update", "region:status",
"coin-seller:view", "coin-seller:create", "coin-seller:update", "coin-seller:stock-credit",
"payment-bill:view",
"game:view", "game:create", "game:update", "game:status",
"daily-task:view", "daily-task:create", "daily-task:update", "daily-task:status",
}
case "auditor", "readonly":
return []string{"room:view", "room-config:view", "app-config:view", "resource:view", "resource-group:view", "resource-grant:view", "gift:view", "coin-seller:view", "payment-bill:view", "game:view", "daily-task:view"}
default:
return nil
}
}