feat: 优化web
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
package service
|
||||
|
||||
// DefaultTenantAdminRoleCode 新租户初始化时的单位管理员角色编码
|
||||
const DefaultTenantAdminRoleCode = "tenant_admin"
|
||||
@@ -0,0 +1,326 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"giter.top/smart/internal/iam/entity"
|
||||
"giter.top/smart/internal/iam/repository"
|
||||
"giter.top/smart/pkg/utils/id"
|
||||
)
|
||||
|
||||
// DeptService 部门
|
||||
type DeptService interface {
|
||||
Tree(ctx context.Context, tenantID string, keyword string, leaderID *string) ([]DeptNode, error)
|
||||
Create(ctx context.Context, tenantID string, req *CreateDeptRequest) (*entity.Dept, error)
|
||||
Update(ctx context.Context, tenantID string, id string, req *UpdateDeptRequest) (*entity.Dept, error)
|
||||
Delete(ctx context.Context, tenantID string, ids []string) error
|
||||
Get(ctx context.Context, tenantID string, id string) (*entity.Dept, error)
|
||||
}
|
||||
|
||||
type CreateDeptRequest struct {
|
||||
ParentID string `json:"parent_id"`
|
||||
DeptName string `json:"dept_name" binding:"required,max=128"`
|
||||
LeaderID *string `json:"leader_id"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
}
|
||||
|
||||
type UpdateDeptRequest struct {
|
||||
ParentID *string `json:"parent_id"`
|
||||
DeptName *string `json:"dept_name" binding:"omitempty,max=128"`
|
||||
LeaderID *string `json:"leader_id"`
|
||||
SortOrder *int `json:"sort_order"`
|
||||
}
|
||||
|
||||
// DeptNode 树节点
|
||||
type DeptNode struct {
|
||||
entity.Dept
|
||||
Children []DeptNode `json:"children,omitempty"`
|
||||
}
|
||||
|
||||
type deptService struct {
|
||||
depts repository.DeptRepository
|
||||
users repository.UserRepository
|
||||
}
|
||||
|
||||
func NewDeptService(depts repository.DeptRepository, users repository.UserRepository) DeptService {
|
||||
return &deptService{depts: depts, users: users}
|
||||
}
|
||||
|
||||
func isDeptRoot(d *entity.Dept) bool {
|
||||
return d.ParentID == "" || d.ParentID == "0"
|
||||
}
|
||||
|
||||
func (s *deptService) Tree(ctx context.Context, tenantID string, keyword string, leaderID *string) ([]DeptNode, error) {
|
||||
rows, err := s.depts.ListByTenant(ctx, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
filtered := rows
|
||||
if keyword != "" || leaderID != nil {
|
||||
filtered = make([]entity.Dept, 0)
|
||||
for _, d := range rows {
|
||||
if keyword != "" && !strings.Contains(d.DeptName, keyword) {
|
||||
continue
|
||||
}
|
||||
if leaderID != nil && (d.LeaderID == nil || *d.LeaderID != *leaderID) {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, d)
|
||||
}
|
||||
if keyword != "" || leaderID != nil {
|
||||
filtered = s.includeAncestors(rows, filtered)
|
||||
}
|
||||
}
|
||||
return buildDeptTree(filtered, ""), nil
|
||||
}
|
||||
|
||||
func (s *deptService) includeAncestors(all []entity.Dept, matched []entity.Dept) []entity.Dept {
|
||||
idSet := map[string]struct{}{}
|
||||
byID := map[string]entity.Dept{}
|
||||
for _, d := range all {
|
||||
byID[d.ID] = d
|
||||
}
|
||||
for _, d := range matched {
|
||||
cur := d
|
||||
for {
|
||||
idSet[cur.ID] = struct{}{}
|
||||
if isDeptRoot(&cur) {
|
||||
break
|
||||
}
|
||||
p, ok := byID[cur.ParentID]
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
cur = p
|
||||
}
|
||||
}
|
||||
out := make([]entity.Dept, 0, len(idSet))
|
||||
for _, d := range all {
|
||||
if _, ok := idSet[d.ID]; ok {
|
||||
out = append(out, d)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func buildDeptTree(rows []entity.Dept, parentID string) []DeptNode {
|
||||
children := map[string][]entity.Dept{}
|
||||
for _, d := range rows {
|
||||
pid := d.ParentID
|
||||
if d.ParentID == "0" {
|
||||
pid = ""
|
||||
}
|
||||
children[pid] = append(children[pid], d)
|
||||
}
|
||||
var walk func(pid string) []DeptNode
|
||||
walk = func(pid string) []DeptNode {
|
||||
list := children[pid]
|
||||
out := make([]DeptNode, 0, len(list))
|
||||
for _, d := range list {
|
||||
out = append(out, DeptNode{Dept: d, Children: walk(d.ID)})
|
||||
}
|
||||
return out
|
||||
}
|
||||
return walk(parentID)
|
||||
}
|
||||
|
||||
func (s *deptService) Create(ctx context.Context, tenantID string, req *CreateDeptRequest) (*entity.Dept, error) {
|
||||
parentKey := req.ParentID
|
||||
if parentKey == "0" {
|
||||
parentKey = ""
|
||||
}
|
||||
ok, err := s.depts.ExistsSiblingName(ctx, tenantID, parentKey, req.DeptName, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ok {
|
||||
return nil, fmt.Errorf("同级部门名称已存在")
|
||||
}
|
||||
d := &entity.Dept{
|
||||
ID: id.New(),
|
||||
TenantID: tenantID,
|
||||
ParentID: parentKey,
|
||||
DeptName: req.DeptName,
|
||||
LeaderID: req.LeaderID,
|
||||
SortOrder: req.SortOrder,
|
||||
Status: 1,
|
||||
}
|
||||
if err := s.depts.Create(ctx, d); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
path := fmt.Sprintf("/%s/", d.ID)
|
||||
if parentKey != "" {
|
||||
p, err := s.depts.GetByID(ctx, parentKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if p.TenantID != tenantID {
|
||||
return nil, fmt.Errorf("父部门不属于当前租户")
|
||||
}
|
||||
base := p.DeptPath
|
||||
if base == "" {
|
||||
base = fmt.Sprintf("/%s/", p.ID)
|
||||
}
|
||||
path = base + fmt.Sprintf("%s/", d.ID)
|
||||
}
|
||||
if err := s.depts.UpdatePath(ctx, d.ID, path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d.DeptPath = path
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (s *deptService) Update(ctx context.Context, tenantID string, id string, req *UpdateDeptRequest) (*entity.Dept, error) {
|
||||
d, err := s.depts.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrNotFound) {
|
||||
return nil, fmt.Errorf("部门不存在")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if d.TenantID != tenantID {
|
||||
return nil, fmt.Errorf("部门不属于当前租户")
|
||||
}
|
||||
if isDeptRoot(d) {
|
||||
if req.ParentID != nil && *req.ParentID != "" && *req.ParentID != "0" {
|
||||
return nil, fmt.Errorf("根部门禁止移动")
|
||||
}
|
||||
if req.DeptName != nil && *req.DeptName != "" && *req.DeptName != d.DeptName {
|
||||
return nil, fmt.Errorf("根部门禁止重命名")
|
||||
}
|
||||
}
|
||||
curParent := d.ParentID
|
||||
if curParent == "0" {
|
||||
curParent = ""
|
||||
}
|
||||
var newParentForName string
|
||||
if req.ParentID != nil {
|
||||
np := *req.ParentID
|
||||
if np == "0" {
|
||||
np = ""
|
||||
}
|
||||
newParentForName = np
|
||||
} else {
|
||||
newParentForName = curParent
|
||||
}
|
||||
if req.DeptName != nil && *req.DeptName != "" {
|
||||
ok, err := s.depts.ExistsSiblingName(ctx, tenantID, newParentForName, *req.DeptName, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ok {
|
||||
return nil, fmt.Errorf("同级部门名称已存在")
|
||||
}
|
||||
d.DeptName = *req.DeptName
|
||||
}
|
||||
if req.ParentID != nil {
|
||||
npID := *req.ParentID
|
||||
if npID == "0" {
|
||||
npID = ""
|
||||
}
|
||||
if npID != curParent {
|
||||
if npID == id {
|
||||
return nil, fmt.Errorf("不能将部门移动到自身之下")
|
||||
}
|
||||
if npID != "" {
|
||||
if s.isDescendant(ctx, id, npID) {
|
||||
return nil, fmt.Errorf("禁止移动至子部门(防环)")
|
||||
}
|
||||
np, err := s.depts.GetByID(ctx, npID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("父部门无效")
|
||||
}
|
||||
if np.TenantID != tenantID {
|
||||
return nil, fmt.Errorf("父部门不属于当前租户")
|
||||
}
|
||||
d.ParentID = npID
|
||||
base := np.DeptPath
|
||||
if base == "" {
|
||||
base = fmt.Sprintf("/%s/", np.ID)
|
||||
}
|
||||
d.DeptPath = base + fmt.Sprintf("%s/", d.ID)
|
||||
} else {
|
||||
d.ParentID = ""
|
||||
d.DeptPath = fmt.Sprintf("/%s/", d.ID)
|
||||
}
|
||||
_ = s.depts.UpdatePath(ctx, d.ID, d.DeptPath)
|
||||
}
|
||||
}
|
||||
if req.LeaderID != nil {
|
||||
d.LeaderID = req.LeaderID
|
||||
}
|
||||
if req.SortOrder != nil {
|
||||
d.SortOrder = *req.SortOrder
|
||||
}
|
||||
if err := s.depts.Update(ctx, d); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (s *deptService) isDescendant(ctx context.Context, rootID, nodeID string) bool {
|
||||
if nodeID == rootID {
|
||||
return true
|
||||
}
|
||||
cur, err := s.depts.GetByID(ctx, nodeID)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < 64 && cur.ParentID != "" && cur.ParentID != "0"; i++ {
|
||||
if cur.ParentID == rootID {
|
||||
return true
|
||||
}
|
||||
cur, err = s.depts.GetByID(ctx, cur.ParentID)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *deptService) Delete(ctx context.Context, tenantID string, ids []string) error {
|
||||
for _, did := range ids {
|
||||
d, err := s.depts.GetByID(ctx, did)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.TenantID != tenantID {
|
||||
return fmt.Errorf("部门 %s 不属于当前租户", did)
|
||||
}
|
||||
if isDeptRoot(d) {
|
||||
return fmt.Errorf("根部门禁止删除")
|
||||
}
|
||||
n, err := s.depts.CountChildren(ctx, did)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n > 0 {
|
||||
return fmt.Errorf("部门 %s 存在子部门", did)
|
||||
}
|
||||
uc, err := s.users.CountByDept(ctx, did)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if uc > 0 {
|
||||
return fmt.Errorf("部门 %s 仍存在用户", did)
|
||||
}
|
||||
if err := s.depts.Delete(ctx, did); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *deptService) Get(ctx context.Context, tenantID string, id string) (*entity.Dept, error) {
|
||||
d, err := s.depts.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if d.TenantID != tenantID {
|
||||
return nil, fmt.Errorf("部门不属于当前租户")
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"giter.top/smart/internal/iam/entity"
|
||||
"giter.top/smart/internal/iam/repository"
|
||||
"giter.top/smart/pkg/utils/id"
|
||||
)
|
||||
|
||||
// MenuService 菜单(全局资源)
|
||||
type MenuService interface {
|
||||
Create(ctx context.Context, req *CreateMenuRequest, isPlatform bool) (*entity.Menu, error)
|
||||
Update(ctx context.Context, mid string, req *UpdateMenuRequest, isPlatform bool) (*entity.Menu, error)
|
||||
Delete(ctx context.Context, ids []string, isPlatform bool) error
|
||||
Get(ctx context.Context, mid string) (*entity.Menu, error)
|
||||
Tree(ctx context.Context, menuType *int16) ([]MenuNode, error)
|
||||
NavForUser(ctx context.Context, userID string) ([]MenuNode, error)
|
||||
PermsForUser(ctx context.Context, userID string) ([]string, error)
|
||||
}
|
||||
|
||||
type CreateMenuRequest struct {
|
||||
ParentID string `json:"parent_id"`
|
||||
MenuName string `json:"menu_name" binding:"required,max=128"`
|
||||
MenuType int16 `json:"menu_type" binding:"required"`
|
||||
Perms string `json:"perms"`
|
||||
Path string `json:"path"`
|
||||
Component string `json:"component"`
|
||||
Icon string `json:"icon"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
IsVisible bool `json:"is_visible"`
|
||||
IsBuiltin bool `json:"is_builtin"`
|
||||
ExternalLink string `json:"external_link"`
|
||||
}
|
||||
|
||||
type UpdateMenuRequest struct {
|
||||
ParentID *string `json:"parent_id"`
|
||||
MenuName *string `json:"menu_name"`
|
||||
SortOrder *int `json:"sort_order"`
|
||||
IsVisible *bool `json:"is_visible"`
|
||||
Path *string `json:"path"`
|
||||
Component *string `json:"component"`
|
||||
Icon *string `json:"icon"`
|
||||
ExternalLink *string `json:"external_link"`
|
||||
Status *int16 `json:"status"`
|
||||
}
|
||||
|
||||
// MenuNode 菜单树节点
|
||||
type MenuNode struct {
|
||||
entity.Menu
|
||||
Children []MenuNode `json:"children,omitempty"`
|
||||
}
|
||||
|
||||
type menuService struct {
|
||||
menus repository.MenuRepository
|
||||
roles repository.RoleRepository
|
||||
users repository.UserRepository
|
||||
}
|
||||
|
||||
func NewMenuService(menus repository.MenuRepository, roles repository.RoleRepository, users repository.UserRepository) MenuService {
|
||||
return &menuService{menus: menus, roles: roles, users: users}
|
||||
}
|
||||
|
||||
func normalizeMenuParent(pid string) string {
|
||||
if pid == "0" {
|
||||
return ""
|
||||
}
|
||||
return pid
|
||||
}
|
||||
|
||||
func (s *menuService) Create(ctx context.Context, req *CreateMenuRequest, isPlatform bool) (*entity.Menu, error) {
|
||||
if !isPlatform {
|
||||
return nil, repository.ErrForbidden
|
||||
}
|
||||
if req.Perms != "" {
|
||||
ok, err := s.menus.ExistsPerms(ctx, req.Perms, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ok {
|
||||
return nil, fmt.Errorf("权限标识已存在")
|
||||
}
|
||||
}
|
||||
m := &entity.Menu{
|
||||
ID: id.New(),
|
||||
ParentID: normalizeMenuParent(req.ParentID),
|
||||
MenuName: req.MenuName,
|
||||
MenuType: req.MenuType,
|
||||
Perms: req.Perms,
|
||||
Path: req.Path,
|
||||
Component: req.Component,
|
||||
Icon: req.Icon,
|
||||
SortOrder: req.SortOrder,
|
||||
IsVisible: req.IsVisible,
|
||||
IsBuiltin: req.IsBuiltin,
|
||||
ExternalLink: req.ExternalLink,
|
||||
Status: 1,
|
||||
}
|
||||
if err := s.menus.Create(ctx, m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (s *menuService) Update(ctx context.Context, mid string, req *UpdateMenuRequest, isPlatform bool) (*entity.Menu, error) {
|
||||
if !isPlatform {
|
||||
return nil, repository.ErrForbidden
|
||||
}
|
||||
m, err := s.menus.GetByID(ctx, mid)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrNotFound) {
|
||||
return nil, fmt.Errorf("菜单不存在")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if m.IsBuiltin {
|
||||
return nil, fmt.Errorf("系统内置菜单禁止修改")
|
||||
}
|
||||
if req.MenuName != nil {
|
||||
m.MenuName = *req.MenuName
|
||||
}
|
||||
if req.SortOrder != nil {
|
||||
m.SortOrder = *req.SortOrder
|
||||
}
|
||||
if req.IsVisible != nil {
|
||||
m.IsVisible = *req.IsVisible
|
||||
}
|
||||
if req.Path != nil {
|
||||
m.Path = *req.Path
|
||||
}
|
||||
if req.Component != nil {
|
||||
m.Component = *req.Component
|
||||
}
|
||||
if req.Icon != nil {
|
||||
m.Icon = *req.Icon
|
||||
}
|
||||
if req.ExternalLink != nil {
|
||||
m.ExternalLink = *req.ExternalLink
|
||||
}
|
||||
if req.Status != nil {
|
||||
m.Status = *req.Status
|
||||
}
|
||||
if err := s.menus.Update(ctx, m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (s *menuService) Delete(ctx context.Context, ids []string, isPlatform bool) error {
|
||||
if !isPlatform {
|
||||
return repository.ErrForbidden
|
||||
}
|
||||
for _, mid := range ids {
|
||||
m, err := s.menus.GetByID(ctx, mid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if m.IsBuiltin {
|
||||
return fmt.Errorf("系统内置菜单禁止删除")
|
||||
}
|
||||
n, err := s.menus.CountChildren(ctx, mid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n > 0 {
|
||||
return fmt.Errorf("存在子菜单,无法删除")
|
||||
}
|
||||
rn, err := s.menus.CountRoleRefs(ctx, mid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rn > 0 {
|
||||
return fmt.Errorf("菜单仍被角色引用")
|
||||
}
|
||||
if err := s.menus.Delete(ctx, mid); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *menuService) Get(ctx context.Context, mid string) (*entity.Menu, error) {
|
||||
return s.menus.GetByID(ctx, mid)
|
||||
}
|
||||
|
||||
func (s *menuService) Tree(ctx context.Context, menuType *int16) ([]MenuNode, error) {
|
||||
rows, err := s.menus.ListByType(ctx, menuType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buildMenuTreeRows(rows), nil
|
||||
}
|
||||
|
||||
func buildMenuTreeRows(rows []entity.Menu) []MenuNode {
|
||||
byParent := map[string][]entity.Menu{}
|
||||
for _, m := range rows {
|
||||
pid := normalizeMenuParent(m.ParentID)
|
||||
byParent[pid] = append(byParent[pid], m)
|
||||
}
|
||||
for k := range byParent {
|
||||
sort.Slice(byParent[k], func(i, j int) bool {
|
||||
if byParent[k][i].SortOrder != byParent[k][j].SortOrder {
|
||||
return byParent[k][i].SortOrder < byParent[k][j].SortOrder
|
||||
}
|
||||
return byParent[k][i].ID < byParent[k][j].ID
|
||||
})
|
||||
}
|
||||
var walk func(pid string) []MenuNode
|
||||
walk = func(pid string) []MenuNode {
|
||||
list := byParent[pid]
|
||||
out := make([]MenuNode, 0, len(list))
|
||||
for _, m := range list {
|
||||
out = append(out, MenuNode{Menu: m, Children: walk(m.ID)})
|
||||
}
|
||||
return out
|
||||
}
|
||||
return walk("")
|
||||
}
|
||||
|
||||
func (s *menuService) NavForUser(ctx context.Context, userID string) ([]MenuNode, error) {
|
||||
rids, err := s.users.ListRoleIDs(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
menuIDs, err := s.roles.ListMenuIDsByRoles(ctx, rids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allowed := map[string]struct{}{}
|
||||
for _, mid := range menuIDs {
|
||||
allowed[mid] = struct{}{}
|
||||
}
|
||||
pub, err := s.menus.ListByPerms(ctx, entity.PublicOverviewPerms)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, m := range pub {
|
||||
allowed[m.ID] = struct{}{}
|
||||
}
|
||||
all, err := s.menus.ListAll(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
byID := map[string]entity.Menu{}
|
||||
for _, m := range all {
|
||||
byID[m.ID] = m
|
||||
}
|
||||
for _, m := range all {
|
||||
if _, ok := allowed[m.ID]; !ok {
|
||||
continue
|
||||
}
|
||||
cur := m
|
||||
for {
|
||||
pid := normalizeMenuParent(cur.ParentID)
|
||||
if pid == "" {
|
||||
break
|
||||
}
|
||||
p, ok := byID[pid]
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
allowed[p.ID] = struct{}{}
|
||||
cur = p
|
||||
}
|
||||
}
|
||||
filtered := make([]entity.Menu, 0)
|
||||
for _, m := range all {
|
||||
if _, ok := allowed[m.ID]; ok && m.Status == 1 && m.IsVisible {
|
||||
filtered = append(filtered, m)
|
||||
}
|
||||
}
|
||||
tree := buildMenuTreeRows(filtered)
|
||||
return pruneEmptyDirs(tree), nil
|
||||
}
|
||||
|
||||
func pruneEmptyDirs(nodes []MenuNode) []MenuNode {
|
||||
out := make([]MenuNode, 0, len(nodes))
|
||||
for _, n := range nodes {
|
||||
ch := pruneEmptyDirs(n.Children)
|
||||
if n.MenuType == 1 && len(ch) == 0 {
|
||||
continue
|
||||
}
|
||||
n.Children = ch
|
||||
out = append(out, n)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *menuService) PermsForUser(ctx context.Context, userID string) ([]string, error) {
|
||||
rids, err := s.users.ListRoleIDs(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mids, err := s.roles.ListMenuIDsByRoles(ctx, rids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
all, err := s.menus.ListAll(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
idset := map[string]struct{}{}
|
||||
for _, mid := range mids {
|
||||
idset[mid] = struct{}{}
|
||||
}
|
||||
var perms []string
|
||||
for _, m := range all {
|
||||
if _, ok := idset[m.ID]; !ok {
|
||||
continue
|
||||
}
|
||||
if m.Perms != "" {
|
||||
perms = append(perms, m.Perms)
|
||||
}
|
||||
}
|
||||
return perms, nil
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package service
|
||||
|
||||
import "giter.top/smart/internal/iam/entity"
|
||||
|
||||
// MergeDataScope 多角色数据范围并集:取最大(PRD:全部 > 本部门及子部门 > 本部门 > 仅本人)
|
||||
func MergeDataScope(scopes []int16) int16 {
|
||||
var m int16
|
||||
for _, s := range scopes {
|
||||
if s > m {
|
||||
m = s
|
||||
}
|
||||
}
|
||||
if m == 0 {
|
||||
return entity.DataScopeSelf
|
||||
}
|
||||
return m
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"giter.top/smart/internal/iam/entity"
|
||||
"giter.top/smart/internal/iam/repository"
|
||||
"giter.top/smart/pkg/utils/id"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// TenantService 租户
|
||||
type TenantService interface {
|
||||
Create(ctx context.Context, req *CreateTenantRequest) (*entity.Tenant, error)
|
||||
Update(ctx context.Context, id string, req *UpdateTenantRequest) (*entity.Tenant, error)
|
||||
Delete(ctx context.Context, ids []string) error
|
||||
Get(ctx context.Context, id string) (*entity.Tenant, error)
|
||||
List(ctx context.Context, name, code string, status *int16, page, pageSize int) (*TenantListResponse, error)
|
||||
}
|
||||
|
||||
type CreateTenantRequest struct {
|
||||
TenantCode string `json:"tenant_code" binding:"required,max=64"`
|
||||
TenantName string `json:"tenant_name" binding:"required,max=128"`
|
||||
AdminUserName string `json:"admin_user_name" binding:"required,max=64"`
|
||||
AdminPassword string `json:"admin_password" binding:"required,min=6,max=64"`
|
||||
AdminRealName string `json:"admin_real_name" binding:"max=64"`
|
||||
}
|
||||
|
||||
type UpdateTenantRequest struct {
|
||||
TenantName *string `json:"tenant_name"`
|
||||
TenantCode *string `json:"tenant_code" binding:"omitempty,max=64"`
|
||||
Status *int16 `json:"status"`
|
||||
ExpireTime *string `json:"expire_time"` // RFC3339
|
||||
}
|
||||
|
||||
type TenantListItem struct {
|
||||
entity.Tenant
|
||||
UserCount int64 `json:"user_count"`
|
||||
DeptCount int64 `json:"dept_count"`
|
||||
}
|
||||
|
||||
type TenantListResponse struct {
|
||||
Items []TenantListItem `json:"items"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
}
|
||||
|
||||
type tenantService struct {
|
||||
db *gorm.DB
|
||||
tenants repository.TenantRepository
|
||||
depts repository.DeptRepository
|
||||
users repository.UserRepository
|
||||
roles repository.RoleRepository
|
||||
menus repository.MenuRepository
|
||||
}
|
||||
|
||||
func NewTenantService(
|
||||
db *gorm.DB,
|
||||
tenants repository.TenantRepository,
|
||||
depts repository.DeptRepository,
|
||||
users repository.UserRepository,
|
||||
roles repository.RoleRepository,
|
||||
menus repository.MenuRepository,
|
||||
) TenantService {
|
||||
return &tenantService{db: db, tenants: tenants, depts: depts, users: users, roles: roles, menus: menus}
|
||||
}
|
||||
|
||||
func (s *tenantService) Create(ctx context.Context, req *CreateTenantRequest) (*entity.Tenant, error) {
|
||||
ok, err := s.tenants.ExistsCode(ctx, req.TenantCode, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ok {
|
||||
return nil, fmt.Errorf("租户编码已存在")
|
||||
}
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(req.AdminPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out *entity.Tenant
|
||||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
t := &entity.Tenant{
|
||||
ID: id.New(),
|
||||
TenantCode: req.TenantCode,
|
||||
TenantName: req.TenantName,
|
||||
Status: 1,
|
||||
}
|
||||
if err := tx.Create(t).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var ucount int64
|
||||
if err := tx.Model(&entity.User{}).Where("tenant_id = ? AND user_name = ?", t.ID, req.AdminUserName).Count(&ucount).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if ucount > 0 {
|
||||
return fmt.Errorf("管理员账号已存在")
|
||||
}
|
||||
root := &entity.Dept{
|
||||
ID: id.New(),
|
||||
TenantID: t.ID,
|
||||
ParentID: "",
|
||||
DeptName: req.TenantName,
|
||||
SortOrder: 0,
|
||||
Status: 1,
|
||||
}
|
||||
if err := tx.Create(root).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
path := fmt.Sprintf("/%s/", root.ID)
|
||||
if err := tx.Model(&entity.Dept{}).Where("id = ?", root.ID).Update("dept_path", path).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
admin := &entity.User{
|
||||
ID: id.New(),
|
||||
TenantID: t.ID,
|
||||
DeptID: &root.ID,
|
||||
UserName: req.AdminUserName,
|
||||
RealName: req.AdminRealName,
|
||||
PasswordHash: string(hash),
|
||||
Status: 1,
|
||||
}
|
||||
if err := tx.Create(admin).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Create(&entity.UserDept{ID: id.New(), UserID: admin.ID, DeptID: root.ID, IsPrimary: true}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
role := &entity.Role{
|
||||
ID: id.New(),
|
||||
TenantID: t.ID,
|
||||
RoleCode: DefaultTenantAdminRoleCode,
|
||||
RoleName: "超级管理员",
|
||||
DataScope: entity.DataScopeAll,
|
||||
Description: "租户初始化角色",
|
||||
IsBuiltin: true,
|
||||
Status: 1,
|
||||
}
|
||||
if err := tx.Create(role).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var allMenus []entity.Menu
|
||||
if err := tx.Find(&allMenus).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, m := range allMenus {
|
||||
if err := tx.Create(&entity.RoleMenu{ID: id.New(), RoleID: role.ID, MenuID: m.ID}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := tx.Create(&entity.UserRole{ID: id.New(), UserID: admin.ID, RoleID: role.ID}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
aid := admin.ID
|
||||
if err := tx.Model(t).Update("admin_user_id", aid).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
t.AdminUserID = &aid
|
||||
out = t
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *tenantService) Update(ctx context.Context, id string, req *UpdateTenantRequest) (*entity.Tenant, error) {
|
||||
t, err := s.tenants.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrNotFound) {
|
||||
return nil, fmt.Errorf("租户不存在")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if req.TenantName != nil && *req.TenantName != "" {
|
||||
t.TenantName = *req.TenantName
|
||||
if root, err := s.depts.FindRoot(ctx, t.ID); err == nil {
|
||||
root.DeptName = *req.TenantName
|
||||
_ = s.depts.Update(ctx, root)
|
||||
}
|
||||
}
|
||||
if req.TenantCode != nil && *req.TenantCode != "" {
|
||||
ok, err := s.tenants.ExistsCode(ctx, *req.TenantCode, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ok {
|
||||
return nil, fmt.Errorf("租户编码已存在")
|
||||
}
|
||||
t.TenantCode = *req.TenantCode
|
||||
}
|
||||
if req.Status != nil {
|
||||
t.Status = *req.Status
|
||||
}
|
||||
if req.ExpireTime != nil && *req.ExpireTime != "" {
|
||||
et, err := time.Parse(time.RFC3339, *req.ExpireTime)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("到期时间格式无效: %w", err)
|
||||
}
|
||||
t.ExpireTime = &et
|
||||
if et.Before(time.Now()) {
|
||||
t.Status = 0
|
||||
}
|
||||
}
|
||||
if err := s.tenants.Update(ctx, t); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (s *tenantService) Delete(ctx context.Context, ids []string) error {
|
||||
for _, tid := range ids {
|
||||
n, err := s.tenants.CountUsers(ctx, tid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n > 0 {
|
||||
return fmt.Errorf("租户 %s 仍存在用户,无法删除", tid)
|
||||
}
|
||||
}
|
||||
for _, tid := range ids {
|
||||
if err := s.db.WithContext(ctx).Delete(&entity.Tenant{}, "id = ?", tid).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *tenantService) Get(ctx context.Context, id string) (*entity.Tenant, error) {
|
||||
return s.tenants.GetByID(ctx, id)
|
||||
}
|
||||
|
||||
func (s *tenantService) List(ctx context.Context, name, code string, status *int16, page, pageSize int) (*TenantListResponse, error) {
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = 10
|
||||
}
|
||||
rows, total, err := s.tenants.List(ctx, name, code, status, page, pageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]TenantListItem, 0, len(rows))
|
||||
for _, t := range rows {
|
||||
uc, _ := s.tenants.CountUsers(ctx, t.ID)
|
||||
dc, _ := s.tenants.CountDepts(ctx, t.ID)
|
||||
items = append(items, TenantListItem{Tenant: t, UserCount: uc, DeptCount: dc})
|
||||
}
|
||||
tp := int(total) / pageSize
|
||||
if int(total)%pageSize != 0 {
|
||||
tp++
|
||||
}
|
||||
return &TenantListResponse{Items: items, Total: total, Page: page, PageSize: pageSize, TotalPages: tp}, nil
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"giter.top/smart/internal/iam/entity"
|
||||
"giter.top/smart/internal/iam/repository"
|
||||
"giter.top/smart/pkg/utils/id"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// UserService 用户
|
||||
type UserService interface {
|
||||
Create(ctx context.Context, tenantID string, req *CreateUserRequest) (*entity.User, error)
|
||||
Update(ctx context.Context, tenantID string, uid string, req *UpdateUserRequest) (*entity.User, error)
|
||||
Delete(ctx context.Context, tenantID string, ids []string) error
|
||||
Get(ctx context.Context, tenantID string, uid string) (*entity.User, error)
|
||||
List(ctx context.Context, tenantID string, q *UserListQuery) (*UserListResponse, error)
|
||||
DataScopeForUser(ctx context.Context, userID string) (int16, error)
|
||||
}
|
||||
|
||||
type CreateUserRequest struct {
|
||||
UserName string `json:"user_name" binding:"required,max=64"`
|
||||
Password string `json:"password" binding:"required,min=6,max=64"`
|
||||
RealName string `json:"real_name" binding:"max=64"`
|
||||
Phone string `json:"phone"`
|
||||
Email string `json:"email"`
|
||||
DeptID *string `json:"dept_id"`
|
||||
DeptIDs []string `json:"dept_ids"`
|
||||
RoleIDs []string `json:"role_ids"`
|
||||
}
|
||||
|
||||
type UpdateUserRequest struct {
|
||||
RealName *string `json:"real_name"`
|
||||
Phone *string `json:"phone"`
|
||||
Email *string `json:"email"`
|
||||
DeptID *string `json:"dept_id"`
|
||||
DeptIDs []string `json:"dept_ids"`
|
||||
RoleIDs []string `json:"role_ids"`
|
||||
Status *int16 `json:"status"`
|
||||
Password *string `json:"password"`
|
||||
}
|
||||
|
||||
type UserListQuery struct {
|
||||
DeptID *string
|
||||
RoleID *string
|
||||
Keyword string
|
||||
Status *int16
|
||||
Page int
|
||||
PageSize int
|
||||
}
|
||||
|
||||
type UserListResponse struct {
|
||||
Items []entity.User `json:"items"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
}
|
||||
|
||||
type userService struct {
|
||||
users repository.UserRepository
|
||||
roles repository.RoleRepository
|
||||
}
|
||||
|
||||
func NewUserService(users repository.UserRepository, roles repository.RoleRepository) UserService {
|
||||
return &userService{users: users, roles: roles}
|
||||
}
|
||||
|
||||
func (s *userService) Create(ctx context.Context, tenantID string, req *CreateUserRequest) (*entity.User, error) {
|
||||
ok, err := s.users.ExistsUserName(ctx, tenantID, req.UserName, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ok {
|
||||
return nil, fmt.Errorf("账号已存在")
|
||||
}
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u := &entity.User{
|
||||
ID: id.New(),
|
||||
TenantID: tenantID,
|
||||
UserName: req.UserName,
|
||||
RealName: req.RealName,
|
||||
Phone: req.Phone,
|
||||
Email: req.Email,
|
||||
PasswordHash: string(hash),
|
||||
Status: 1,
|
||||
}
|
||||
if req.DeptID != nil {
|
||||
u.DeptID = req.DeptID
|
||||
}
|
||||
if err := s.users.Create(ctx, u); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
depts := req.DeptIDs
|
||||
primary := ""
|
||||
if req.DeptID != nil {
|
||||
primary = *req.DeptID
|
||||
}
|
||||
if len(depts) == 0 && primary != "" {
|
||||
depts = []string{primary}
|
||||
}
|
||||
if len(depts) > 0 {
|
||||
if primary == "" {
|
||||
primary = depts[0]
|
||||
}
|
||||
u.DeptID = &primary
|
||||
_ = s.users.Update(ctx, u)
|
||||
if err := s.users.ReplaceUserDepts(ctx, u.ID, primary, depts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if len(req.RoleIDs) > 0 {
|
||||
if err := s.users.ReplaceUserRoles(ctx, u.ID, req.RoleIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (s *userService) Update(ctx context.Context, tenantID string, uid string, req *UpdateUserRequest) (*entity.User, error) {
|
||||
u, err := s.users.GetByID(ctx, uid)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrNotFound) {
|
||||
return nil, fmt.Errorf("用户不存在")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if u.TenantID != tenantID {
|
||||
return nil, fmt.Errorf("用户不属于当前租户")
|
||||
}
|
||||
if req.RealName != nil {
|
||||
u.RealName = *req.RealName
|
||||
}
|
||||
if req.Phone != nil {
|
||||
u.Phone = *req.Phone
|
||||
}
|
||||
if req.Email != nil {
|
||||
u.Email = *req.Email
|
||||
}
|
||||
if req.Status != nil {
|
||||
u.Status = *req.Status
|
||||
}
|
||||
if req.Password != nil && *req.Password != "" {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(*req.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u.PasswordHash = string(hash)
|
||||
}
|
||||
if err := s.users.Update(ctx, u); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.DeptIDs != nil || req.DeptID != nil {
|
||||
depts := req.DeptIDs
|
||||
primary := ""
|
||||
if req.DeptID != nil {
|
||||
primary = *req.DeptID
|
||||
u.DeptID = req.DeptID
|
||||
_ = s.users.Update(ctx, u)
|
||||
}
|
||||
if len(depts) == 0 && primary != "" {
|
||||
depts = []string{primary}
|
||||
}
|
||||
if primary == "" && len(depts) > 0 {
|
||||
primary = depts[0]
|
||||
}
|
||||
if err := s.users.ReplaceUserDepts(ctx, u.ID, primary, depts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if req.RoleIDs != nil {
|
||||
if err := s.users.ReplaceUserRoles(ctx, u.ID, req.RoleIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (s *userService) Delete(ctx context.Context, tenantID string, ids []string) error {
|
||||
for _, uid := range ids {
|
||||
u, err := s.users.GetByID(ctx, uid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if u.TenantID != tenantID {
|
||||
return fmt.Errorf("用户 %s 不属于当前租户", uid)
|
||||
}
|
||||
if err := s.users.Delete(ctx, uid); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *userService) Get(ctx context.Context, tenantID string, uid string) (*entity.User, error) {
|
||||
u, err := s.users.GetByID(ctx, uid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if u.TenantID != tenantID {
|
||||
return nil, fmt.Errorf("用户不属于当前租户")
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (s *userService) List(ctx context.Context, tenantID string, q *UserListQuery) (*UserListResponse, error) {
|
||||
if q.Page <= 0 {
|
||||
q.Page = 1
|
||||
}
|
||||
if q.PageSize <= 0 {
|
||||
q.PageSize = 10
|
||||
}
|
||||
rows, total, err := s.users.List(ctx, tenantID, q.DeptID, q.RoleID, q.Keyword, q.Status, q.Page, q.PageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tp := int(total) / q.PageSize
|
||||
if int(total)%q.PageSize != 0 {
|
||||
tp++
|
||||
}
|
||||
return &UserListResponse{Items: rows, Total: total, Page: q.Page, PageSize: q.PageSize, TotalPages: tp}, nil
|
||||
}
|
||||
|
||||
func (s *userService) DataScopeForUser(ctx context.Context, userID string) (int16, error) {
|
||||
roles, err := s.roles.ListRolesByUser(ctx, userID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
scopes := make([]int16, 0, len(roles))
|
||||
for _, r := range roles {
|
||||
scopes = append(scopes, r.DataScope)
|
||||
}
|
||||
return MergeDataScope(scopes), nil
|
||||
}
|
||||
Reference in New Issue
Block a user