refactor platform account and access models
This commit is contained in:
@@ -50,7 +50,7 @@ func InitPlatformAccess(database *gorm.DB) error {
|
||||
if err := database.Where("menu_code = ?", menu.MenuCode).FirstOrCreate(&menu).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
relation := models.PlatformRoleMenuRelation{PlatformRoleID: rootRole.ID, PlatformMenuID: menu.ID}
|
||||
relation := models.PlatformRoleMenu{PlatformRoleID: rootRole.ID, PlatformMenuID: menu.ID}
|
||||
if err := database.Where("platform_role_id = ? AND platform_menu_id = ?", rootRole.ID, menu.ID).FirstOrCreate(&relation).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -60,7 +60,7 @@ func InitPlatformAccess(database *gorm.DB) error {
|
||||
|
||||
// InitPlatformRoot 幂等创建平台总后台 root 账号。
|
||||
func InitPlatformRoot(database *gorm.DB) error {
|
||||
var account models.PlatfromAccount
|
||||
var account models.PlatformAccount
|
||||
err := database.Where("username = ?", PlatformRootUsername).First(&account).Error
|
||||
if err == nil {
|
||||
return nil
|
||||
@@ -74,7 +74,7 @@ func InitPlatformRoot(database *gorm.DB) error {
|
||||
return err
|
||||
}
|
||||
|
||||
account = models.PlatfromAccount{
|
||||
account = models.PlatformAccount{
|
||||
Entity: models.Entity{Identity: models.NewIdentity(), Status: "enabled"},
|
||||
Username: PlatformRootUsername,
|
||||
DisplayName: "平台根管理员",
|
||||
|
||||
@@ -35,7 +35,7 @@ func TestInitPlatformAccessSeedsEveryProtectedFrontendDomain(t *testing.T) {
|
||||
WithArgs(domain, 1).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "status", "version", "parent_id", "menu_code", "name", "icon", "path", "sort_no"}).
|
||||
AddRow(menuID, domain+"-menu", "enabled", 1, uint64(0), domain, domain, "", "/"+domain, index))
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "platform_role_menu_relation" WHERE platform_role_id = $1 AND platform_menu_id = $2 ORDER BY "platform_role_menu_relation"."id" LIMIT $3`)).
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "platform_role_menu" WHERE platform_role_id = $1 AND platform_menu_id = $2 ORDER BY "platform_role_menu"."id" LIMIT $3`)).
|
||||
WithArgs(uint64(1), menuID, 1).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "platform_role_id", "platform_menu_id"}).
|
||||
AddRow(uint64(index+100), uint64(1), menuID))
|
||||
|
||||
41
backend/api/internal/logic/common/platform_access.go
Normal file
41
backend/api/internal/logic/common/platform_access.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"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/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// RequirePlatformRoot restricts sensitive platform configuration operations to the root role.
|
||||
func RequirePlatformRoot(ctx *gin.Context) bool {
|
||||
claims, err := middleware.ParseAuth(ctx)
|
||||
if err == nil && claims.Role == "root" {
|
||||
return true
|
||||
}
|
||||
infra.Response.Error(ctx, errcode.ErrPermissionDenied)
|
||||
return false
|
||||
}
|
||||
|
||||
// LoadPlatformMenus returns the menus granted to one platform role.
|
||||
func LoadPlatformMenus(roleCode string) ([]models.PlatformMenu, error) {
|
||||
var menus []models.PlatformMenu
|
||||
if roleCode == "root" {
|
||||
err := impl.DBService.Order("sort_no asc, id asc").Find(&menus).Error
|
||||
return menus, err
|
||||
}
|
||||
|
||||
var role models.PlatformRole
|
||||
if err := impl.DBService.Where("role_code = ? AND status = ?", roleCode, "enabled").First(&role).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err := impl.DBService.
|
||||
Select("platform_menu.*").
|
||||
Joins("JOIN platform_role_menu ON platform_role_menu.platform_menu_id = platform_menu.id").
|
||||
Where("platform_role_menu.platform_role_id = ? AND platform_menu.status = ?", role.ID, "enabled").
|
||||
Order("sort_no asc, id asc").
|
||||
Find(&menus).Error
|
||||
return menus, err
|
||||
}
|
||||
@@ -133,7 +133,7 @@ func isCreatedResponseField(key string) bool {
|
||||
func ProtectPreciseLocation(ctx *gin.Context, model, value any) any {
|
||||
maskPersonalName := reflect.TypeOf(model) == reflect.TypeOf(&models.UserAccount{}) ||
|
||||
reflect.TypeOf(model) == reflect.TypeOf(&models.StaffAccount{})
|
||||
maskDisplayName := reflect.TypeOf(model) == reflect.TypeOf(&models.PlatfromAccount{})
|
||||
maskDisplayName := reflect.TypeOf(model) == reflect.TypeOf(&models.PlatformAccount{})
|
||||
ProtectPublicFields(value, maskPersonalName, maskDisplayName, HasPreciseLocationScope(ctx))
|
||||
return value
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"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"
|
||||
@@ -38,7 +39,7 @@ func Login(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var account models.PlatfromAccount
|
||||
var account models.PlatformAccount
|
||||
err := impl.DBService.Where("username = ?", strings.TrimSpace(request.Username)).First(&account).Error
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
@@ -85,12 +86,12 @@ func CurrentProfile(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
var account models.PlatfromAccount
|
||||
var account models.PlatformAccount
|
||||
if err := impl.DBService.Where("identity = ?", claims.Identity).First(&account).Error; err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrRecordNotFound)
|
||||
return
|
||||
}
|
||||
menus, err := loadPlatformMenus(account.PlatformRoleCode)
|
||||
menus, err := common.LoadPlatformMenus(account.PlatformRoleCode)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrPermissionDenied)
|
||||
return
|
||||
@@ -133,7 +134,7 @@ func ChangePassword(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
var account models.PlatfromAccount
|
||||
var account models.PlatformAccount
|
||||
if err := impl.DBService.Where("identity = ?", claims.Identity).First(&account).Error; err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrRecordNotFound)
|
||||
return
|
||||
|
||||
@@ -2,6 +2,7 @@ package platform
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/errcode"
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
@@ -10,11 +11,83 @@ import (
|
||||
"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) }
|
||||
|
||||
@@ -23,7 +96,7 @@ func GetPlatformRole(ctx *gin.Context) { common.GetByIdentity[models.PlatformRol
|
||||
|
||||
// CreatePlatformRole 创建非内置平台角色。
|
||||
func CreatePlatformRole(ctx *gin.Context) {
|
||||
if !requirePlatformRoot(ctx) {
|
||||
if !common.RequirePlatformRoot(ctx) {
|
||||
return
|
||||
}
|
||||
var request models.PlatformRole
|
||||
@@ -45,7 +118,7 @@ func CreatePlatformRole(ctx *gin.Context) {
|
||||
|
||||
// UpdatePlatformRole 更新非内置平台角色。
|
||||
func UpdatePlatformRole(ctx *gin.Context) {
|
||||
if !requirePlatformRoot(ctx) {
|
||||
if !common.RequirePlatformRoot(ctx) {
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
@@ -118,7 +191,7 @@ func GetPlatformMenu(ctx *gin.Context) {
|
||||
}
|
||||
|
||||
func CreatePlatformMenu(ctx *gin.Context) {
|
||||
if !requirePlatformRoot(ctx) {
|
||||
if !common.RequirePlatformRoot(ctx) {
|
||||
return
|
||||
}
|
||||
var request platformMenuRequest
|
||||
@@ -140,7 +213,7 @@ func CreatePlatformMenu(ctx *gin.Context) {
|
||||
}
|
||||
|
||||
func UpdatePlatformMenu(ctx *gin.Context) {
|
||||
if !requirePlatformRoot(ctx) {
|
||||
if !common.RequirePlatformRoot(ctx) {
|
||||
return
|
||||
}
|
||||
var request platformMenuRequest
|
||||
@@ -157,14 +230,14 @@ func UpdatePlatformMenu(ctx *gin.Context) {
|
||||
}
|
||||
|
||||
func UpdatePlatformMenuStatus(ctx *gin.Context) {
|
||||
if !requirePlatformRoot(ctx) {
|
||||
if !common.RequirePlatformRoot(ctx) {
|
||||
return
|
||||
}
|
||||
common.UpdateRecordStatus(ctx, &models.PlatformMenu{})
|
||||
}
|
||||
|
||||
func ArchivePlatformMenu(ctx *gin.Context) {
|
||||
if !requirePlatformRoot(ctx) {
|
||||
if !common.RequirePlatformRoot(ctx) {
|
||||
return
|
||||
}
|
||||
common.ArchiveRecord(ctx, &models.PlatformMenu{})
|
||||
@@ -176,7 +249,7 @@ type platformRoleMenusRequest struct {
|
||||
|
||||
// ReplacePlatformRoleMenus replaces every menu assignment for a role atomically.
|
||||
func ReplacePlatformRoleMenus(ctx *gin.Context) {
|
||||
if !requirePlatformRoot(ctx) {
|
||||
if !common.RequirePlatformRoot(ctx) {
|
||||
return
|
||||
}
|
||||
var request platformRoleMenusRequest
|
||||
@@ -201,11 +274,11 @@ func ReplacePlatformRoleMenus(ctx *gin.Context) {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
}
|
||||
if err := transaction.Where("platform_role_id = ?", role.ID).Delete(&models.PlatformRoleMenuRelation{}).Error; err != nil {
|
||||
if err := transaction.Where("platform_role_id = ?", role.ID).Delete(&models.PlatformRoleMenu{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, menu := range menus {
|
||||
relation := models.PlatformRoleMenuRelation{PlatformRoleID: role.ID, PlatformMenuID: menu.ID}
|
||||
relation := models.PlatformRoleMenu{PlatformRoleID: role.ID, PlatformMenuID: menu.ID}
|
||||
if err := transaction.Create(&relation).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -228,7 +301,7 @@ func ReplacePlatformRoleMenus(ctx *gin.Context) {
|
||||
|
||||
// ListPlatformRoleMenuIdentities returns the current assignment for the role editor.
|
||||
func ListPlatformRoleMenuIdentities(ctx *gin.Context) {
|
||||
if !requirePlatformRoot(ctx) {
|
||||
if !common.RequirePlatformRoot(ctx) {
|
||||
return
|
||||
}
|
||||
var role models.PlatformRole
|
||||
@@ -238,8 +311,8 @@ func ListPlatformRoleMenuIdentities(ctx *gin.Context) {
|
||||
}
|
||||
var identities []string
|
||||
if err := impl.DBService.Model(&models.PlatformMenu{}).
|
||||
Joins("JOIN platform_role_menu_relation ON platform_role_menu_relation.platform_menu_id = platform_menu.id").
|
||||
Where("platform_role_menu_relation.platform_role_id = ?", role.ID).
|
||||
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)
|
||||
@@ -250,7 +323,7 @@ func ListPlatformRoleMenuIdentities(ctx *gin.Context) {
|
||||
|
||||
// UpdatePlatformRoleStatus 更新非内置平台角色状态,系统角色始终受保护。
|
||||
func UpdatePlatformRoleStatus(ctx *gin.Context) {
|
||||
if !requirePlatformRoot(ctx) {
|
||||
if !common.RequirePlatformRoot(ctx) {
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
@@ -274,7 +347,7 @@ func UpdatePlatformRoleStatus(ctx *gin.Context) {
|
||||
|
||||
// ArchivePlatformRole 归档非内置平台角色,系统角色始终受保护。
|
||||
func ArchivePlatformRole(ctx *gin.Context) {
|
||||
if !requirePlatformRoot(ctx) {
|
||||
if !common.RequirePlatformRoot(ctx) {
|
||||
return
|
||||
}
|
||||
var role models.PlatformRole
|
||||
@@ -286,7 +359,7 @@ func ArchivePlatformRole(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
common.UpdateAllowedByIdentity(ctx, &models.PlatformRole{}, archiveValues(), []string{"status"})
|
||||
common.UpdateAllowedByIdentity(ctx, &models.PlatformRole{}, gin.H{"status": "archived"}, []string{"status"})
|
||||
}
|
||||
|
||||
// ListPlatformMenu 返回菜单树构建所需的有序菜单列表。
|
||||
@@ -296,7 +369,7 @@ func ListPlatformMenu(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
list, err := loadPlatformMenus(claims.Role)
|
||||
list, err := common.LoadPlatformMenus(claims.Role)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
@@ -304,12 +377,12 @@ func ListPlatformMenu(ctx *gin.Context) {
|
||||
infra.Response.Success(ctx, gin.H{"total": len(list), "list": platformMenuViews(list)})
|
||||
}
|
||||
|
||||
// ListPlatfromAccount 查询平台账号列表,手机号在展示层脱敏。
|
||||
func ListPlatfromAccount(ctx *gin.Context) {
|
||||
// ListPlatformAccount 查询平台账号列表,手机号在展示层脱敏。
|
||||
func ListPlatformAccount(ctx *gin.Context) {
|
||||
page, size := common.PageSize(ctx)
|
||||
var list []models.PlatfromAccount
|
||||
var list []models.PlatformAccount
|
||||
var total int64
|
||||
query := common.ApplyKeywordFilter(ctx, impl.DBService.Model(&models.PlatfromAccount{}), &models.PlatfromAccount{})
|
||||
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
|
||||
@@ -321,13 +394,13 @@ func ListPlatfromAccount(ctx *gin.Context) {
|
||||
views := make([]map[string]any, 0, len(list))
|
||||
for _, item := range list {
|
||||
view := platformAccountView(item)
|
||||
common.ProtectPreciseLocation(ctx, &models.PlatfromAccount{}, view)
|
||||
common.ProtectPreciseLocation(ctx, &models.PlatformAccount{}, view)
|
||||
views = append(views, view)
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"total": total, "list": views})
|
||||
}
|
||||
|
||||
type platfromAccountRequest struct {
|
||||
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"`
|
||||
@@ -336,7 +409,7 @@ type platfromAccountRequest struct {
|
||||
Phone string `json:"phone" binding:"max=32"`
|
||||
}
|
||||
|
||||
func platformAccountView(account models.PlatfromAccount) map[string]any {
|
||||
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,
|
||||
@@ -344,21 +417,21 @@ func platformAccountView(account models.PlatfromAccount) map[string]any {
|
||||
}
|
||||
}
|
||||
|
||||
func GetPlatfromAccount(ctx *gin.Context) {
|
||||
var account models.PlatfromAccount
|
||||
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.PlatfromAccount{}, view))
|
||||
infra.Response.Success(ctx, common.ProtectPreciseLocation(ctx, &models.PlatformAccount{}, view))
|
||||
}
|
||||
|
||||
func CreatePlatfromAccount(ctx *gin.Context) {
|
||||
if !requirePlatformRoot(ctx) {
|
||||
func CreatePlatformAccount(ctx *gin.Context) {
|
||||
if !common.RequirePlatformRoot(ctx) {
|
||||
return
|
||||
}
|
||||
var request platfromAccountRequest
|
||||
var request platformAccountRequest
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
@@ -367,21 +440,21 @@ func CreatePlatfromAccount(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
hash, err := passwordHash(request.Password)
|
||||
hash, err := platformPasswordHash(request.Password)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
account := models.PlatfromAccount{Entity: common.NewEntity("enabled"), Username: request.Username, DisplayName: request.DisplayName, Avatar: request.Avatar, PasswordHash: hash, PlatformRoleCode: request.PlatformRoleCode, Phone: request.Phone}
|
||||
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.PlatfromAccount{}, view))
|
||||
infra.Response.Success(ctx, common.ProtectPreciseLocation(ctx, &models.PlatformAccount{}, view))
|
||||
}
|
||||
|
||||
func UpdatePlatfromAccount(ctx *gin.Context) {
|
||||
func UpdatePlatformAccount(ctx *gin.Context) {
|
||||
var request struct {
|
||||
DisplayName string `json:"display_name" binding:"max=64"`
|
||||
Avatar string `json:"avatar" binding:"max=512"`
|
||||
@@ -394,7 +467,7 @@ func UpdatePlatfromAccount(ctx *gin.Context) {
|
||||
}
|
||||
values := gin.H{"display_name": request.DisplayName, "avatar": request.Avatar, "phone": request.Phone}
|
||||
if request.PlatformRoleCode != nil {
|
||||
if !requirePlatformRoot(ctx) {
|
||||
if !common.RequirePlatformRoot(ctx) {
|
||||
return
|
||||
}
|
||||
if !isAssignablePlatformRole(*request.PlatformRoleCode) {
|
||||
@@ -403,7 +476,28 @@ func UpdatePlatfromAccount(ctx *gin.Context) {
|
||||
}
|
||||
values["platform_role_code"] = *request.PlatformRoleCode
|
||||
}
|
||||
common.UpdateAllowedByIdentity(ctx, &models.PlatfromAccount{}, values, []string{"display_name", "avatar", "platform_role_code", "phone"})
|
||||
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 {
|
||||
@@ -1,112 +0,0 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"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/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const platformMenusContextKey = "platform_authorized_menus"
|
||||
|
||||
func loadPlatformMenus(roleCode string) ([]models.PlatformMenu, error) {
|
||||
var menus []models.PlatformMenu
|
||||
if roleCode == "root" {
|
||||
err := impl.DBService.Order("sort_no asc, id asc").Find(&menus).Error
|
||||
return menus, err
|
||||
}
|
||||
|
||||
var role models.PlatformRole
|
||||
if err := impl.DBService.Where("role_code = ? AND status = ?", roleCode, "enabled").First(&role).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err := impl.DBService.
|
||||
Select("platform_menu.*").
|
||||
Joins("JOIN platform_role_menu_relation ON platform_role_menu_relation.platform_menu_id = platform_menu.id").
|
||||
Where("platform_role_menu_relation.platform_role_id = ? AND platform_menu.status = ?", role.ID, "enabled").
|
||||
Order("sort_no asc, id asc").
|
||||
Find(&menus).Error
|
||||
return menus, err
|
||||
}
|
||||
|
||||
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 "platfrom", "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 := 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()
|
||||
}
|
||||
}
|
||||
|
||||
func requirePlatformRoot(ctx *gin.Context) bool {
|
||||
claims, err := middleware.ParseAuth(ctx)
|
||||
if err == nil && claims.Role == "root" {
|
||||
return true
|
||||
}
|
||||
infra.Response.Error(ctx, errcode.ErrPermissionDenied)
|
||||
return false
|
||||
}
|
||||
@@ -354,7 +354,7 @@ func productOperator(ctx *gin.Context) (string, string) {
|
||||
if err != nil {
|
||||
return "", ""
|
||||
}
|
||||
var account models.PlatfromAccount
|
||||
var account models.PlatformAccount
|
||||
if err := impl.DBService.Select("display_name").Where("identity = ?", claims.Identity).First(&account).Error; err != nil {
|
||||
return claims.Identity, ""
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ func ExpectedResources() []ResourceContract {
|
||||
resourceContract("gasorder", "gasorder_track", ReadOnly, "list"), resourceContract("gasorder", "gasorder_track_point", ReadOnly, "list"), resourceContract("gasorder", "gasorder_confirm", ReadOnly, "list"), resourceContract("gasorder", "gasorder_payment", ReadOnly, "list"),
|
||||
resourceContract("finance", "fin_payment", Writable, "list"), resourceContract("finance", "fin_settlement", Writable, "list"), resourceContract("finance", "fin_reconciliation", Writable, "list"),
|
||||
resourceContract("content", "cms_content", Writable, "list"), resourceContract("customer_service", "cs_ticket", Writable, "list"),
|
||||
resourceContract("platform", "platfrom_account", Writable, "list"), resourceContract("platform", "platform_role", Writable, "list"), resourceContract("platform", "platform_menu", Writable, "tree"),
|
||||
resourceContract("platform", "platform_account", Writable, "list"), resourceContract("platform", "platform_role", Writable, "list"), resourceContract("platform", "platform_menu", Writable, "tree"),
|
||||
resourceContract("wallet", "wallet_basic", ReadOnly, "list"), resourceContract("wallet", "wallet_bank", ReadOnly, "list"), resourceContract("wallet", "wallet_payment", ReadOnly, "list"), resourceContract("wallet", "wallet_record", ReadOnly, "list"), resourceContract("wallet", "wallet_refund", ReadOnly, "list"), resourceContract("wallet", "wallet_apply_cash", ReadOnly, "list"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ func UpdateWalletBasicStatus(ctx *gin.Context) {
|
||||
}
|
||||
|
||||
func RechargeWalletBasic(ctx *gin.Context) {
|
||||
if !requirePlatformRoot(ctx) {
|
||||
if !common.RequirePlatformRoot(ctx) {
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
@@ -259,7 +259,7 @@ func RejectWalletApplyCash(ctx *gin.Context) {
|
||||
}
|
||||
|
||||
func reviewWalletApplyCash(ctx *gin.Context, targetStatus string) {
|
||||
if !requirePlatformRoot(ctx) {
|
||||
if !common.RequirePlatformRoot(ctx) {
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
@@ -311,7 +311,7 @@ func walletOperator(ctx *gin.Context) (string, string) {
|
||||
if err != nil {
|
||||
return "", ""
|
||||
}
|
||||
var account models.PlatfromAccount
|
||||
var account models.PlatformAccount
|
||||
if err := impl.DBService.Select("display_name").Where("identity = ?", claims.Identity).First(&account).Error; err != nil {
|
||||
return claims.Identity, ""
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ package models
|
||||
|
||||
import "git.apinb.com/bsm-sdk/core/database"
|
||||
|
||||
// PlatfromAccount 对应 platfrom_account,表示平台总后台登录账号。
|
||||
type PlatfromAccount struct {
|
||||
// PlatformAccount 对应 platform_account,表示平台总后台登录账号。
|
||||
type PlatformAccount struct {
|
||||
Entity // 公共实体字段
|
||||
Username string `gorm:"column:username;type:varchar(64);uniqueIndex;not null" json:"username"` // 登录用户名
|
||||
DisplayName string `gorm:"column:display_name;type:varchar(64);not null;default:''" json:"display_name"` // 用户展示名称
|
||||
@@ -13,7 +13,7 @@ type PlatfromAccount struct {
|
||||
Phone string `gorm:"column:phone;type:varchar(32);uniqueIndex;not null;default:''" json:"phone"` // 手机号
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&PlatfromAccount{}) }
|
||||
func init() { database.AppendMigrate(&PlatformAccount{}) }
|
||||
|
||||
// TableName 返回与模型、文件名一致的单数数据表名。
|
||||
func (table *PlatfromAccount) TableName() string { return "platfrom_account" }
|
||||
func (table *PlatformAccount) TableName() string { return "platform_account" }
|
||||
23
backend/api/internal/models/platform_models_test.go
Normal file
23
backend/api/internal/models/platform_models_test.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package models
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestPlatformModelTableNames(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
table interface{ TableName() string }
|
||||
want string
|
||||
}{
|
||||
{name: "account", table: &PlatformAccount{}, want: "platform_account"},
|
||||
{name: "menu", table: &PlatformMenu{}, want: "platform_menu"},
|
||||
{name: "role", table: &PlatformRole{}, want: "platform_role"},
|
||||
{name: "role menu", table: &PlatformRoleMenu{}, want: "platform_role_menu"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if got := test.table.TableName(); got != test.want {
|
||||
t.Fatalf("TableName() = %q, want %q", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -6,15 +6,15 @@ import (
|
||||
"git.apinb.com/bsm-sdk/core/database"
|
||||
)
|
||||
|
||||
// PlatformRoleMenuRelation 对应 platform_role_menu_relation,记录角色拥有的菜单权限。
|
||||
type PlatformRoleMenuRelation struct {
|
||||
// PlatformRoleMenu 对应 platform_role_menu,记录角色拥有的菜单权限。
|
||||
type PlatformRoleMenu struct {
|
||||
ID uint64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` // 数据库自增主键
|
||||
PlatformRoleID uint64 `gorm:"column:platform_role_id;not null;uniqueIndex:uk_platform_role_menu" json:"platform_role_id"` // 角色自增主键
|
||||
PlatformMenuID uint64 `gorm:"column:platform_menu_id;not null;uniqueIndex:uk_platform_role_menu" json:"platform_menu_id"` // 菜单自增主键
|
||||
CreatedAt time.Time `gorm:"column:created_at;type:timestamptz;not null" json:"created_at"` // 创建时间
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&PlatformRoleMenuRelation{}) }
|
||||
func init() { database.AppendMigrate(&PlatformRoleMenu{}) }
|
||||
|
||||
// TableName 返回与模型、文件名一致的单数数据表名。
|
||||
func (table *PlatformRoleMenuRelation) TableName() string { return "platform_role_menu_relation" }
|
||||
func (table *PlatformRoleMenu) TableName() string { return "platform_role_menu" }
|
||||
@@ -28,11 +28,11 @@ func GetDashboardOverview() (DashboardOverview, error) {
|
||||
return overview, nil
|
||||
}
|
||||
|
||||
// ListPlatfromAccount 返回平台账号分页列表,敏感字段由接口展示层脱敏。
|
||||
func ListPlatfromAccount(page, size int) ([]PlatfromAccount, int64, error) {
|
||||
var list []PlatfromAccount
|
||||
// ListPlatformAccount 返回平台账号分页列表,敏感字段由接口展示层脱敏。
|
||||
func ListPlatformAccount(page, size int) ([]PlatformAccount, int64, error) {
|
||||
var list []PlatformAccount
|
||||
var total int64
|
||||
databaseQuery := impl.DBService.Model(&PlatfromAccount{})
|
||||
databaseQuery := impl.DBService.Model(&PlatformAccount{})
|
||||
if err := databaseQuery.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
@@ -179,7 +179,13 @@ func registerUserRoute(group *gin.RouterGroup) {
|
||||
}
|
||||
|
||||
func registerPlatformRoute(group *gin.RouterGroup) {
|
||||
registerWritableResource(group, "/platfrom_account", platform.ListPlatfromAccount, platform.CreatePlatfromAccount, platform.GetPlatfromAccount, platform.UpdatePlatfromAccount, &models.PlatfromAccount{})
|
||||
account := group.Group("/platform_account")
|
||||
account.GET("", platform.ListPlatformAccount)
|
||||
account.POST("", platform.CreatePlatformAccount)
|
||||
account.GET("/:identity", platform.GetPlatformAccount)
|
||||
account.PUT("/:identity", platform.UpdatePlatformAccount)
|
||||
account.PATCH("/:identity/status", platform.UpdatePlatformAccountStatus)
|
||||
account.DELETE("/:identity", platform.ArchivePlatformAccount)
|
||||
role := group.Group("/platform_role")
|
||||
role.GET("", platform.ListPlatformRole)
|
||||
role.POST("", platform.CreatePlatformRole)
|
||||
@@ -189,7 +195,6 @@ func registerPlatformRoute(group *gin.RouterGroup) {
|
||||
role.DELETE("/:identity", platform.ArchivePlatformRole)
|
||||
role.GET("/:identity/menu", platform.ListPlatformRoleMenuIdentities)
|
||||
role.PUT("/:identity/menu", platform.ReplacePlatformRoleMenus)
|
||||
role.PUT("/:identity/menus", platform.ReplacePlatformRoleMenus)
|
||||
menu := group.Group("/platform_menu")
|
||||
menu.GET("", platform.ListPlatformMenu)
|
||||
menu.POST("", platform.CreatePlatformMenu)
|
||||
|
||||
@@ -85,7 +85,7 @@ func TestPlatformOrganizationAndAccountRoutesExposeResourceCRUD(t *testing.T) {
|
||||
"/user_account",
|
||||
"/user_address",
|
||||
"/user_service_relation",
|
||||
"/platfrom_account",
|
||||
"/platform_account",
|
||||
"/platform_role",
|
||||
"/platform_menu",
|
||||
} {
|
||||
@@ -95,7 +95,8 @@ func TestPlatformOrganizationAndAccountRoutesExposeResourceCRUD(t *testing.T) {
|
||||
}
|
||||
|
||||
assertRouteMethods(t, routes, "/heqi/platform/v1/platform_role/:identity/menu", http.MethodGet, http.MethodPut)
|
||||
assertRouteMethods(t, routes, "/heqi/platform/v1/platform_role/:identity/menus", http.MethodPut)
|
||||
assertNoRouteMethods(t, routes, "/heqi/platform/v1/platform_role/:identity/menus", http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete)
|
||||
assertNoRouteMethods(t, routes, "/heqi/platform/v1/platfrom_account", http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete)
|
||||
}
|
||||
|
||||
func TestPlatformProductCommerceAndDeliveryRoutesFollowTheirContracts(t *testing.T) {
|
||||
|
||||
@@ -7,7 +7,7 @@ export type PlatformMenu = { identity: string; parent_identity?: string; menu_co
|
||||
|
||||
export const platformApi = {
|
||||
overview: () => request<Record<string, number>>('/dashboard/overview'),
|
||||
listAccount: () => request<{ total: number; list: Record<string, unknown>[] }>('/platfrom_account'),
|
||||
listAccount: () => request<{ total: number; list: Record<string, unknown>[] }>('/platform_account'),
|
||||
listRole: () => resourceApi.list<PlatformRole>('/platform_role'),
|
||||
createRole: (data: Record<string, unknown>) => resourceApi.create<PlatformRole>('/platform_role', data),
|
||||
listMenu: () => request<{ total: number; list: PlatformMenu[] }>('/platform_menu'),
|
||||
|
||||
@@ -309,7 +309,7 @@ export const resources: ResourceUiDefinition[] = [
|
||||
define('fin_reconciliation', '财务对账', 'writable', [f('channel', { required: true }), f('bill_date', { required: true }), f('difference_amount', { required: true })]),
|
||||
define('cms_content', '内容', 'writable', [f('content_type', { required: true }), f('title', { required: true }), f('body', { required: true }), f('version_no'), f('publish_status')]),
|
||||
define('cs_ticket', '客服工单', 'writable', [relation('user_account_identity', '/user_account', true), f('ticket_no', { required: true }), f('category', { required: true }), f('priority', { required: true })]),
|
||||
define('platfrom_account', '平台账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name'), f('avatar'), f('platform_role_code', { required: true }), f('phone')]),
|
||||
define('platform_account', '平台账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name'), f('avatar'), f('platform_role_code', { required: true }), f('phone')]),
|
||||
define('platform_role', '平台角色', 'writable', [f('role_code', { required: true }), f('name', { required: true }), f('data_scope', { required: true })], 'list', [
|
||||
{ name: '分配菜单', resource: '/platform_role/:identity/menu', method: 'PUT', fields: [f('menu_identities', { type: 'identity-list', relation: '/platform_menu' })] },
|
||||
]),
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -109,7 +109,7 @@ const routes: AppRouteRecordRaw[] = [
|
||||
child('customer_service', 'tickets', 'tickets', '客服工单', '/cs_ticket'),
|
||||
]),
|
||||
group('platform', 'platform', '平台管理', 'icon-settings', 120, [
|
||||
child('platform', 'accounts', 'accounts', '平台账户', '/platfrom_account'),
|
||||
child('platform', 'accounts', 'accounts', '平台账户', '/platform_account'),
|
||||
child('platform', 'roles', 'roles', '平台角色', '/platform_role'),
|
||||
child('platform', 'menus', 'menus', '平台菜单', '/platform_menu'),
|
||||
]),
|
||||
|
||||
Reference in New Issue
Block a user