package platform import ( "errors" "strings" "git.apinb.com/bsm-sdk/core/errcode" "git.apinb.com/bsm-sdk/core/infra" "git.apinb.com/bsm-sdk/core/middleware" "git.apinb.com/heqiapp/platforms/backend/api/internal/impl" "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common" "git.apinb.com/heqiapp/platforms/backend/api/internal/models" "github.com/gin-gonic/gin" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" ) var errSystemPlatformRole = errors.New("system platform roles cannot be modified") const platformMenusContextKey = "platform_authorized_menus" func platformMenuAllowsPath(menus []models.PlatformMenu, requestPath string) bool { marker := "/platform/v1/" index := strings.Index(requestPath, marker) if index < 0 { return false } relative := strings.Trim(requestPath[index+len(marker):], "/") resource := strings.Split(relative, "/")[0] domain := platformRouteDomain(resource) for _, menu := range menus { if menu.MenuCode == domain { return true } menuPath := strings.Trim(menu.Path, "/") if menuPath != "" && strings.Split(menuPath, "/")[0] == domain { return true } } return false } func platformRouteDomain(resource string) string { prefix := strings.Split(resource, "_")[0] switch prefix { case "product": return "device" case "gasorder": return "delivery" case "fin": return "finance" case "cms": return "content" case "cs": return "customer_service" case "platform": return "platform" default: return prefix } } // RequirePlatformMenuAccess enforces role-menu authorization after JWT authentication. func RequirePlatformMenuAccess() gin.HandlerFunc { return func(ctx *gin.Context) { if strings.Contains(ctx.Request.URL.Path, "/platform/v1/auth/") { ctx.Next() return } claims, err := middleware.ParseAuth(ctx) if err != nil { infra.Response.Error(ctx, err) ctx.Abort() return } if claims.Role == "root" { ctx.Next() return } menus, err := common.LoadPlatformMenus(claims.Role) if err != nil || !platformMenuAllowsPath(menus, ctx.Request.URL.Path) { infra.Response.Error(ctx, errcode.ErrPermissionDenied) ctx.Abort() return } ctx.Set(platformMenusContextKey, menus) ctx.Next() } } // ListPlatformRole 查询平台角色分页列表。 func ListPlatformRole(ctx *gin.Context) { common.ListPage[models.PlatformRole](ctx) } // GetPlatformRole 查询一个平台角色。 func GetPlatformRole(ctx *gin.Context) { common.GetByIdentity[models.PlatformRole](ctx) } // CreatePlatformRole 创建非内置平台角色。 func CreatePlatformRole(ctx *gin.Context) { if !common.RequirePlatformRoot(ctx) { return } var request models.PlatformRole if err := ctx.ShouldBindJSON(&request); err != nil || request.RoleCode == "" || request.Name == "" || request.RoleCode == "root" { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } request.Entity = common.NewEntity("enabled") request.IsSystem = false if request.DataScope == "" { request.DataScope = "global" } if err := impl.DBService.Create(&request).Error; err != nil { infra.Response.Error(ctx, err) return } common.RespondCreatedResource(ctx, request) } // UpdatePlatformRole 更新非内置平台角色。 func UpdatePlatformRole(ctx *gin.Context) { if !common.RequirePlatformRoot(ctx) { return } var request struct { Name string `json:"name" binding:"required,max=64"` DataScope string `json:"data_scope" binding:"required,max=32"` } if err := ctx.ShouldBindJSON(&request); err != nil { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } var role models.PlatformRole if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&role).Error; err != nil { common.RespondRecordError(ctx, err) return } if role.IsSystem { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } common.UpdateAllowedByIdentity(ctx, &models.PlatformRole{}, gin.H{"name": request.Name, "data_scope": request.DataScope}, []string{"name", "data_scope"}) } type platformMenuRequest struct { ParentIdentity string `json:"parent_identity"` MenuCode string `json:"menu_code" binding:"required,max=64"` Name string `json:"name" binding:"required,max=64"` Icon string `json:"icon" binding:"max=64"` Path string `json:"path" binding:"max=255"` SortNo int `json:"sort_no"` } type platformMenuView struct { Identity string `json:"identity"` ParentIdentity string `json:"parent_identity,omitempty"` MenuCode string `json:"menu_code"` Name string `json:"name"` Icon string `json:"icon"` Path string `json:"path"` SortNo int `json:"sort_no"` Status string `json:"status"` } func platformMenuViews(list []models.PlatformMenu) []platformMenuView { identities := make(map[uint64]string, len(list)) for _, item := range list { identities[item.ID] = item.Identity } views := make([]platformMenuView, 0, len(list)) for _, item := range list { views = append(views, platformMenuView{Identity: item.Identity, ParentIdentity: identities[item.ParentID], MenuCode: item.MenuCode, Name: item.Name, Icon: item.Icon, Path: item.Path, SortNo: item.SortNo, Status: item.Status}) } return views } func GetPlatformMenu(ctx *gin.Context) { var menu models.PlatformMenu if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&menu).Error; err != nil { common.RespondRecordError(ctx, err) return } list := []models.PlatformMenu{menu} if menu.ParentID != 0 { var parent models.PlatformMenu if err := impl.DBService.Select("id", "identity").First(&parent, menu.ParentID).Error; err == nil { list = append(list, parent) } } views := platformMenuViews(list) infra.Response.Success(ctx, views[0]) } func CreatePlatformMenu(ctx *gin.Context) { if !common.RequirePlatformRoot(ctx) { return } var request platformMenuRequest if err := ctx.ShouldBindJSON(&request); err != nil { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } parentID, err := common.ResolveIdentityID(&models.PlatformMenu{}, request.ParentIdentity, false) if err != nil { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } menu := models.PlatformMenu{Entity: common.NewEntity("enabled"), ParentID: parentID, MenuCode: request.MenuCode, Name: request.Name, Icon: request.Icon, Path: request.Path, SortNo: request.SortNo} if err := impl.DBService.Create(&menu).Error; err != nil { infra.Response.Error(ctx, err) return } common.RespondCreatedResource(ctx, menu) } func UpdatePlatformMenu(ctx *gin.Context) { if !common.RequirePlatformRoot(ctx) { return } var request platformMenuRequest if err := ctx.ShouldBindJSON(&request); err != nil { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } parentID, err := common.ResolveIdentityID(&models.PlatformMenu{}, request.ParentIdentity, false) if err != nil { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } common.UpdateAllowedByIdentity(ctx, &models.PlatformMenu{}, gin.H{"parent_id": parentID, "name": request.Name, "icon": request.Icon, "path": request.Path, "sort_no": request.SortNo}, []string{"parent_id", "name", "icon", "path", "sort_no"}) } func UpdatePlatformMenuStatus(ctx *gin.Context) { if !common.RequirePlatformRoot(ctx) { return } common.UpdateRecordStatus(ctx, &models.PlatformMenu{}) } func ArchivePlatformMenu(ctx *gin.Context) { if !common.RequirePlatformRoot(ctx) { return } common.ArchiveRecord(ctx, &models.PlatformMenu{}) } type platformRoleMenusRequest struct { MenuIdentities []string `json:"menu_identities"` } // ReplacePlatformRoleMenus replaces every menu assignment for a role atomically. func ReplacePlatformRoleMenus(ctx *gin.Context) { if !common.RequirePlatformRoot(ctx) { return } var request platformRoleMenusRequest if err := ctx.ShouldBindJSON(&request); err != nil { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } if err := impl.DBService.Transaction(func(transaction *gorm.DB) error { var role models.PlatformRole if err := transaction.Where("identity = ?", ctx.Param("identity")).First(&role).Error; err != nil { return err } if role.IsSystem { return errSystemPlatformRole } var menus []models.PlatformMenu if len(request.MenuIdentities) > 0 { if err := transaction.Where("identity IN ?", request.MenuIdentities).Find(&menus).Error; err != nil { return err } if len(menus) != len(request.MenuIdentities) { return gorm.ErrRecordNotFound } } if err := transaction.Where("platform_role_id = ?", role.ID).Delete(&models.PlatformRoleMenu{}).Error; err != nil { return err } for _, menu := range menus { relation := models.PlatformRoleMenu{PlatformRoleID: role.ID, PlatformMenuID: menu.ID} if err := transaction.Create(&relation).Error; err != nil { return err } } return nil }); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { infra.Response.Error(ctx, errcode.ErrRecordNotFound) return } if errors.Is(err, errSystemPlatformRole) { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } infra.Response.Error(ctx, err) return } infra.Response.Success(ctx, gin.H{"updated": true}) } // ListPlatformRoleMenuIdentities returns the current assignment for the role editor. func ListPlatformRoleMenuIdentities(ctx *gin.Context) { if !common.RequirePlatformRoot(ctx) { return } var role models.PlatformRole if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&role).Error; err != nil { common.RespondRecordError(ctx, err) return } var identities []string if err := impl.DBService.Model(&models.PlatformMenu{}). Joins("JOIN platform_role_menu ON platform_role_menu.platform_menu_id = platform_menu.id"). Where("platform_role_menu.platform_role_id = ?", role.ID). Order("platform_menu.sort_no asc, platform_menu.id asc"). Pluck("platform_menu.identity", &identities).Error; err != nil { infra.Response.Error(ctx, err) return } infra.Response.Success(ctx, gin.H{"menu_identities": identities}) } // UpdatePlatformRoleStatus 更新非内置平台角色状态,系统角色始终受保护。 func UpdatePlatformRoleStatus(ctx *gin.Context) { if !common.RequirePlatformRoot(ctx) { return } var request struct { Status string `json:"status" binding:"required,max=32"` } if err := ctx.ShouldBindJSON(&request); err != nil { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } var role models.PlatformRole if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&role).Error; err != nil { common.RespondRecordError(ctx, err) return } if role.IsSystem { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } common.UpdateAllowedByIdentity(ctx, &models.PlatformRole{}, gin.H{"status": request.Status}, []string{"status"}) } // ArchivePlatformRole 归档非内置平台角色,系统角色始终受保护。 func ArchivePlatformRole(ctx *gin.Context) { if !common.RequirePlatformRoot(ctx) { return } var role models.PlatformRole if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&role).Error; err != nil { common.RespondRecordError(ctx, err) return } if role.IsSystem { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } common.UpdateAllowedByIdentity(ctx, &models.PlatformRole{}, gin.H{"status": "archived"}, []string{"status"}) } // ListPlatformMenu 返回菜单树构建所需的有序菜单列表。 func ListPlatformMenu(ctx *gin.Context) { claims, err := middleware.ParseAuth(ctx) if err != nil { infra.Response.Error(ctx, err) return } list, err := common.LoadPlatformMenus(claims.Role) if err != nil { infra.Response.Error(ctx, err) return } infra.Response.Success(ctx, gin.H{"total": len(list), "list": platformMenuViews(list)}) } // ListPlatformAccount 查询平台账号列表,手机号在展示层脱敏。 func ListPlatformAccount(ctx *gin.Context) { page, size := common.PageSize(ctx) var list []models.PlatformAccount var total int64 query := common.ApplyKeywordFilter(ctx, impl.DBService.Model(&models.PlatformAccount{}), &models.PlatformAccount{}) if err := query.Count(&total).Error; err != nil { infra.Response.Error(ctx, err) return } if err := query.Order("created_at desc").Offset((page - 1) * size).Limit(size).Find(&list).Error; err != nil { infra.Response.Error(ctx, err) return } views := make([]map[string]any, 0, len(list)) for _, item := range list { view := platformAccountView(item) common.ProtectPreciseLocation(ctx, &models.PlatformAccount{}, view) views = append(views, view) } infra.Response.Success(ctx, gin.H{"total": total, "list": views}) } type platformAccountRequest struct { Username string `json:"username" binding:"required,max=64"` Password string `json:"password" binding:"required,min=8,max=128"` DisplayName string `json:"display_name" binding:"max=64"` Avatar string `json:"avatar" binding:"max=512"` PlatformRoleCode string `json:"platform_role_code" binding:"required,max=64"` Phone string `json:"phone" binding:"max=32"` } func platformAccountView(account models.PlatformAccount) map[string]any { return map[string]any{ "identity": account.Identity, "username": account.Username, "display_name": account.DisplayName, "avatar": account.Avatar, "phone": account.Phone, "platform_role_code": account.PlatformRoleCode, "status": account.Status, } } func GetPlatformAccount(ctx *gin.Context) { var account models.PlatformAccount if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&account).Error; err != nil { common.RespondRecordError(ctx, err) return } view := platformAccountView(account) infra.Response.Success(ctx, common.ProtectPreciseLocation(ctx, &models.PlatformAccount{}, view)) } func CreatePlatformAccount(ctx *gin.Context) { if !common.RequirePlatformRoot(ctx) { return } var request platformAccountRequest if err := ctx.ShouldBindJSON(&request); err != nil { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } if !isAssignablePlatformRole(request.PlatformRoleCode) { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } hash, err := platformPasswordHash(request.Password) if err != nil { infra.Response.Error(ctx, err) return } account := models.PlatformAccount{Entity: common.NewEntity("enabled"), Username: request.Username, DisplayName: request.DisplayName, Avatar: request.Avatar, PasswordHash: hash, PlatformRoleCode: request.PlatformRoleCode, Phone: request.Phone} if err := impl.DBService.Create(&account).Error; err != nil { infra.Response.Error(ctx, err) return } view := platformAccountView(account) infra.Response.Success(ctx, common.ProtectPreciseLocation(ctx, &models.PlatformAccount{}, view)) } func UpdatePlatformAccount(ctx *gin.Context) { var request struct { DisplayName string `json:"display_name" binding:"max=64"` Avatar string `json:"avatar" binding:"max=512"` PlatformRoleCode *string `json:"platform_role_code" binding:"omitempty,max=64"` Phone string `json:"phone" binding:"max=32"` } if err := ctx.ShouldBindJSON(&request); err != nil { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } values := gin.H{"display_name": request.DisplayName, "avatar": request.Avatar, "phone": request.Phone} if request.PlatformRoleCode != nil { if !common.RequirePlatformRoot(ctx) { return } if !isAssignablePlatformRole(*request.PlatformRoleCode) { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } values["platform_role_code"] = *request.PlatformRoleCode } common.UpdateAllowedByIdentity(ctx, &models.PlatformAccount{}, values, []string{"display_name", "avatar", "platform_role_code", "phone"}) } // UpdatePlatformAccountStatus updates a platform account lifecycle state. func UpdatePlatformAccountStatus(ctx *gin.Context) { if !common.RequirePlatformRoot(ctx) { return } common.UpdateRecordStatus(ctx, &models.PlatformAccount{}) } // ArchivePlatformAccount archives a platform account without deleting audit history. func ArchivePlatformAccount(ctx *gin.Context) { if !common.RequirePlatformRoot(ctx) { return } common.ArchiveRecord(ctx, &models.PlatformAccount{}) } func platformPasswordHash(password string) (string, error) { hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) return string(hash), err } func isAssignablePlatformRole(roleCode string) bool { if roleCode == "" || roleCode == "root" { return false } var role models.PlatformRole return impl.DBService.Where("role_code = ? AND status = ? AND is_system = ?", roleCode, "enabled", false).First(&role).Error == nil }