deptTreeLogic.go 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. package dept
  2. import (
  3. "context"
  4. "strings"
  5. "perms-system-server/internal/middleware"
  6. deptModel "perms-system-server/internal/model/dept"
  7. "perms-system-server/internal/response"
  8. "perms-system-server/internal/svc"
  9. "perms-system-server/internal/types"
  10. "github.com/zeromicro/go-zero/core/logx"
  11. )
  12. type DeptTreeLogic struct {
  13. logx.Logger
  14. ctx context.Context
  15. svcCtx *svc.ServiceContext
  16. }
  17. func NewDeptTreeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeptTreeLogic {
  18. return &DeptTreeLogic{
  19. Logger: logx.WithContext(ctx),
  20. ctx: ctx,
  21. svcCtx: svcCtx,
  22. }
  23. }
  24. // DeptTree 部门树。仅超管返回完整组织架构树;其他成员(含产品 ADMIN / DEVELOPER / MEMBER)
  25. // 一律只返回以其 DeptPath 为根的子树,未归属部门或 DeptPath 为空者返回空树。
  26. //
  27. // 审计 L-R15-2:原先产品 ADMIN 走 fullAccess=true 分支拿全量 sys_dept,但 sys_dept 是
  28. // **全局**命名空间(一份组织架构服务于所有产品)——小产品的 ADMIN 能看到大产品的 DEV
  29. // 子树 / 跨 BU 的 HR/财务部门命名。即便不构成权限升级,也是针对性撞库 / 社工的前置侦察
  30. // 输入,与 M-2 "MEMBER 不能看全产品列表防止 admin_<code> 撞库" 的最小授权精神冲突。
  31. // 因此将 fullAccess 收敛给 SuperAdmin;产品 ADMIN 若需要跨部门拉人(AddMember 场景),
  32. // 走独立的 ListAddableDepts 或由 SuperAdmin 审批流程,不借 DeptTree 顺便拿到全组织视图。
  33. func (l *DeptTreeLogic) DeptTree() (resp []*types.DeptItem, err error) {
  34. caller := middleware.GetUserDetails(l.ctx)
  35. if caller == nil {
  36. return nil, response.ErrUnauthorized("未登录")
  37. }
  38. list, err := l.svcCtx.SysDeptModel.FindAll(l.ctx)
  39. if err != nil {
  40. return nil, err
  41. }
  42. fullAccess := caller.IsSuperAdmin
  43. if !fullAccess {
  44. // 审计 L-R17-3:`caller.DeptPath == ""` 是"DeptTree 返空"的两大分支之一,常见于:
  45. // (a) 数据链异常:L-R16-1 / H-R17-1 那类孤儿账号(deptId 指向已删部门、或 deptId=0
  46. // 的非超管遗留数据)——UI 只会看到空列表,与"DB 抖动返空"无法区分;
  47. // (b) 合规隔离:产品 ADMIN 在"尚未完成部门归属"的过渡期内。
  48. // 打一条 WARN 让运维侧能追溯"哪些 caller 持续踩到空 DeptPath",便于做数据修复。
  49. if caller.DeptPath == "" {
  50. l.Errorw("DeptTree returned empty because caller.DeptPath is empty",
  51. logx.Field("audit", "dept_tree_empty_caller_deptpath"),
  52. logx.Field("userId", caller.UserId),
  53. logx.Field("isSuperAdmin", caller.IsSuperAdmin),
  54. logx.Field("memberType", caller.MemberType),
  55. logx.Field("deptId", caller.DeptId),
  56. )
  57. return make([]*types.DeptItem, 0), nil
  58. }
  59. filtered := make([]*deptModel.SysDept, 0, len(list))
  60. for _, d := range list {
  61. if strings.HasPrefix(d.Path, caller.DeptPath) {
  62. filtered = append(filtered, d)
  63. }
  64. }
  65. // 审计 L-R17-3:caller.DeptPath 不为空,但过滤后一个都没命中——说明 caller.DeptPath
  66. // 指向的部门在 sys_dept 里已经消失(被 SuperAdmin 删除、或 DeptPath 字段陈旧未同步
  67. // 到新的部门重组路径)。是 H-R17-1 orphan user 的前奏迹象,记 INFO 供运维巡检。
  68. if len(list) > 0 && len(filtered) == 0 {
  69. l.Infow("DeptTree filtered to empty despite non-empty caller.DeptPath",
  70. logx.Field("audit", "dept_tree_empty_after_filter"),
  71. logx.Field("userId", caller.UserId),
  72. logx.Field("deptPath", caller.DeptPath),
  73. logx.Field("totalDepts", len(list)),
  74. )
  75. }
  76. list = filtered
  77. }
  78. items := make([]*types.DeptItem, 0, len(list))
  79. for _, d := range list {
  80. items = append(items, &types.DeptItem{
  81. Id: d.Id,
  82. ParentId: d.ParentId,
  83. Name: d.Name,
  84. Path: d.Path,
  85. Sort: d.Sort,
  86. DeptType: d.DeptType,
  87. Remark: d.Remark,
  88. Status: d.Status,
  89. CreateTime: d.CreateTime,
  90. Children: make([]*types.DeptItem, 0),
  91. })
  92. }
  93. itemMap := make(map[int64]*types.DeptItem)
  94. for _, item := range items {
  95. itemMap[item.Id] = item
  96. }
  97. roots := make([]*types.DeptItem, 0)
  98. for _, item := range items {
  99. // 非特权成员下只保留 caller 子树:原树的上级部门不出现在 items 中,item.ParentId
  100. // 找不到父节点时应当把当前节点当成局部根展示,而不是报错丢弃。
  101. if _, hasParent := itemMap[item.ParentId]; item.ParentId == 0 || !hasParent {
  102. if fullAccess && item.ParentId != 0 {
  103. l.Errorf("DeptTree: dept id=%d has parentId=%d which does not exist, treated as root", item.Id, item.ParentId)
  104. }
  105. roots = append(roots, item)
  106. } else {
  107. itemMap[item.ParentId].Children = append(itemMap[item.ParentId].Children, item)
  108. }
  109. }
  110. return roots, nil
  111. }