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) }