Files
smart-go/internal/iam/service/role_service.go
T
2026-04-23 18:58:13 +08:00

224 lines
6.3 KiB
Go

package service
import (
"context"
"errors"
"fmt"
"giter.top/smart/internal/iam/entity"
"giter.top/smart/internal/iam/repository"
"giter.top/smart/pkg/utils/id"
)
// RoleService 角色
type RoleService interface {
Create(ctx context.Context, tenantID string, req *CreateRoleRequest, grantorUserID *string) (*entity.Role, error)
Update(ctx context.Context, tenantID string, rid string, req *UpdateRoleRequest, grantorUserID *string) (*entity.Role, error)
Delete(ctx context.Context, tenantID string, ids []string) error
Get(ctx context.Context, tenantID string, rid string) (*entity.Role, error)
List(ctx context.Context, tenantID string, name, code string, page, pageSize int) (*RoleListResponse, error)
AssignMenus(ctx context.Context, tenantID string, roleID string, menuIDs []string, grantorUserID *string) error
}
type CreateRoleRequest struct {
RoleCode string `json:"role_code" binding:"required,max=64"`
RoleName string `json:"role_name" binding:"required,max=128"`
DataScope int16 `json:"data_scope" binding:"required"`
Description string `json:"description"`
MenuIDs []string `json:"menu_ids"`
}
type UpdateRoleRequest struct {
RoleName *string `json:"role_name"`
DataScope *int16 `json:"data_scope"`
Description *string `json:"description"`
MenuIDs []string `json:"menu_ids"`
}
type RoleListResponse struct {
Items []entity.Role `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
TotalPages int `json:"total_pages"`
}
type roleService struct {
roles repository.RoleRepository
users repository.UserRepository
menus repository.MenuRepository
}
func NewRoleService(roles repository.RoleRepository, users repository.UserRepository, menus repository.MenuRepository) RoleService {
return &roleService{roles: roles, users: users, menus: menus}
}
func (s *roleService) grantorMenuSet(ctx context.Context, grantorUserID string) (map[string]struct{}, error) {
rids, err := s.users.ListRoleIDs(ctx, grantorUserID)
if err != nil {
return nil, err
}
ids, err := s.roles.ListMenuIDsByRoles(ctx, rids)
if err != nil {
return nil, err
}
m := make(map[string]struct{}, len(ids))
for _, mid := range ids {
m[mid] = struct{}{}
}
return m, nil
}
func (s *roleService) assertMenuSubset(ctx context.Context, grantorUserID *string, menuIDs []string) error {
if grantorUserID == nil || *grantorUserID == "" {
return nil
}
allowed, err := s.grantorMenuSet(ctx, *grantorUserID)
if err != nil {
return err
}
for _, mid := range menuIDs {
if _, ok := allowed[mid]; !ok {
return fmt.Errorf("防越权: 不能分配自身未拥有的菜单权限 (menu_id=%s)", mid)
}
}
return nil
}
func (s *roleService) Create(ctx context.Context, tenantID string, req *CreateRoleRequest, grantorUserID *string) (*entity.Role, error) {
ok, err := s.roles.ExistsCode(ctx, tenantID, req.RoleCode, "")
if err != nil {
return nil, err
}
if ok {
return nil, fmt.Errorf("角色编码已存在")
}
if err := s.assertMenuSubset(ctx, grantorUserID, req.MenuIDs); err != nil {
return nil, err
}
r := &entity.Role{
ID: id.New(),
TenantID: tenantID,
RoleCode: req.RoleCode,
RoleName: req.RoleName,
DataScope: req.DataScope,
Description: req.Description,
Status: 1,
}
if err := s.roles.Create(ctx, r); err != nil {
return nil, err
}
if len(req.MenuIDs) > 0 {
if err := s.roles.ReplaceRoleMenus(ctx, r.ID, req.MenuIDs); err != nil {
return nil, err
}
}
return r, nil
}
func (s *roleService) Update(ctx context.Context, tenantID string, rid string, req *UpdateRoleRequest, grantorUserID *string) (*entity.Role, error) {
r, err := s.roles.GetByID(ctx, rid)
if err != nil {
if errors.Is(err, repository.ErrNotFound) {
return nil, fmt.Errorf("角色不存在")
}
return nil, err
}
if r.TenantID != tenantID {
return nil, fmt.Errorf("角色不属于当前租户")
}
if r.IsBuiltin {
// 内置角色仅允许改部分字段(MVP:允许改名称与数据范围与菜单需业务再定)
}
if req.RoleName != nil {
r.RoleName = *req.RoleName
}
if req.DataScope != nil {
r.DataScope = *req.DataScope
}
if req.Description != nil {
r.Description = *req.Description
}
if err := s.roles.Update(ctx, r); err != nil {
return nil, err
}
if req.MenuIDs != nil {
if err := s.assertMenuSubset(ctx, grantorUserID, req.MenuIDs); err != nil {
return nil, err
}
if err := s.roles.ReplaceRoleMenus(ctx, r.ID, req.MenuIDs); err != nil {
return nil, err
}
}
return r, nil
}
func (s *roleService) Delete(ctx context.Context, tenantID string, ids []string) error {
for _, rid := range ids {
r, err := s.roles.GetByID(ctx, rid)
if err != nil {
return err
}
if r.TenantID != tenantID {
return fmt.Errorf("角色 %s 不属于当前租户", rid)
}
if r.IsBuiltin {
return fmt.Errorf("内置角色不可删除")
}
n, err := s.roles.CountUsers(ctx, rid)
if err != nil {
return err
}
if n > 0 {
return fmt.Errorf("角色仍被用户使用")
}
if err := s.roles.Delete(ctx, rid); err != nil {
return err
}
}
return nil
}
func (s *roleService) Get(ctx context.Context, tenantID string, rid string) (*entity.Role, error) {
r, err := s.roles.GetByID(ctx, rid)
if err != nil {
return nil, err
}
if r.TenantID != tenantID {
return nil, fmt.Errorf("角色不属于当前租户")
}
return r, nil
}
func (s *roleService) List(ctx context.Context, tenantID string, name, code string, page, pageSize int) (*RoleListResponse, error) {
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 10
}
rows, total, err := s.roles.List(ctx, tenantID, name, code, page, pageSize)
if err != nil {
return nil, err
}
tp := int(total) / pageSize
if int(total)%pageSize != 0 {
tp++
}
return &RoleListResponse{Items: rows, Total: total, Page: page, PageSize: pageSize, TotalPages: tp}, nil
}
func (s *roleService) AssignMenus(ctx context.Context, tenantID string, roleID string, menuIDs []string, grantorUserID *string) error {
r, err := s.roles.GetByID(ctx, roleID)
if err != nil {
return err
}
if r.TenantID != tenantID {
return fmt.Errorf("角色不属于当前租户")
}
if err := s.assertMenuSubset(ctx, grantorUserID, menuIDs); err != nil {
return err
}
return s.roles.ReplaceRoleMenus(ctx, roleID, menuIDs)
}