package dept import ( "context" "strings" "perms-system-server/internal/middleware" deptModel "perms-system-server/internal/model/dept" "perms-system-server/internal/response" "perms-system-server/internal/svc" "perms-system-server/internal/types" "github.com/zeromicro/go-zero/core/logx" ) type DeptTreeLogic struct { logx.Logger ctx context.Context svcCtx *svc.ServiceContext } func NewDeptTreeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeptTreeLogic { return &DeptTreeLogic{ Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx, } } // DeptTree 部门树。仅超管返回完整组织架构树;其他成员(含产品 ADMIN / DEVELOPER / MEMBER) // 一律只返回以其 DeptPath 为根的子树,未归属部门或 DeptPath 为空者返回空树。 // // 审计 L-R15-2:原先产品 ADMIN 走 fullAccess=true 分支拿全量 sys_dept,但 sys_dept 是 // **全局**命名空间(一份组织架构服务于所有产品)——小产品的 ADMIN 能看到大产品的 DEV // 子树 / 跨 BU 的 HR/财务部门命名。即便不构成权限升级,也是针对性撞库 / 社工的前置侦察 // 输入,与 M-2 "MEMBER 不能看全产品列表防止 admin_ 撞库" 的最小授权精神冲突。 // 因此将 fullAccess 收敛给 SuperAdmin;产品 ADMIN 若需要跨部门拉人(AddMember 场景), // 走独立的 ListAddableDepts 或由 SuperAdmin 审批流程,不借 DeptTree 顺便拿到全组织视图。 func (l *DeptTreeLogic) DeptTree() (resp []*types.DeptItem, err error) { caller := middleware.GetUserDetails(l.ctx) if caller == nil { return nil, response.ErrUnauthorized("未登录") } list, err := l.svcCtx.SysDeptModel.FindAll(l.ctx) if err != nil { return nil, err } fullAccess := caller.IsSuperAdmin if !fullAccess { // 审计 L-R17-3:`caller.DeptPath == ""` 是"DeptTree 返空"的两大分支之一,常见于: // (a) 数据链异常:L-R16-1 / H-R17-1 那类孤儿账号(deptId 指向已删部门、或 deptId=0 // 的非超管遗留数据)——UI 只会看到空列表,与"DB 抖动返空"无法区分; // (b) 合规隔离:产品 ADMIN 在"尚未完成部门归属"的过渡期内。 // 打一条 WARN 让运维侧能追溯"哪些 caller 持续踩到空 DeptPath",便于做数据修复。 if caller.DeptPath == "" { l.Errorw("DeptTree returned empty because caller.DeptPath is empty", logx.Field("audit", "dept_tree_empty_caller_deptpath"), logx.Field("userId", caller.UserId), logx.Field("isSuperAdmin", caller.IsSuperAdmin), logx.Field("memberType", caller.MemberType), logx.Field("deptId", caller.DeptId), ) return make([]*types.DeptItem, 0), nil } filtered := make([]*deptModel.SysDept, 0, len(list)) for _, d := range list { if strings.HasPrefix(d.Path, caller.DeptPath) { filtered = append(filtered, d) } } // 审计 L-R17-3:caller.DeptPath 不为空,但过滤后一个都没命中——说明 caller.DeptPath // 指向的部门在 sys_dept 里已经消失(被 SuperAdmin 删除、或 DeptPath 字段陈旧未同步 // 到新的部门重组路径)。是 H-R17-1 orphan user 的前奏迹象,记 INFO 供运维巡检。 if len(list) > 0 && len(filtered) == 0 { l.Infow("DeptTree filtered to empty despite non-empty caller.DeptPath", logx.Field("audit", "dept_tree_empty_after_filter"), logx.Field("userId", caller.UserId), logx.Field("deptPath", caller.DeptPath), logx.Field("totalDepts", len(list)), ) } list = filtered } items := make([]*types.DeptItem, 0, len(list)) for _, d := range list { items = append(items, &types.DeptItem{ Id: d.Id, ParentId: d.ParentId, Name: d.Name, Path: d.Path, Sort: d.Sort, DeptType: d.DeptType, Remark: d.Remark, Status: d.Status, CreateTime: d.CreateTime, Children: make([]*types.DeptItem, 0), }) } itemMap := make(map[int64]*types.DeptItem) for _, item := range items { itemMap[item.Id] = item } roots := make([]*types.DeptItem, 0) for _, item := range items { // 非特权成员下只保留 caller 子树:原树的上级部门不出现在 items 中,item.ParentId // 找不到父节点时应当把当前节点当成局部根展示,而不是报错丢弃。 if _, hasParent := itemMap[item.ParentId]; item.ParentId == 0 || !hasParent { if fullAccess && item.ParentId != 0 { l.Errorf("DeptTree: dept id=%d has parentId=%d which does not exist, treated as root", item.Id, item.ParentId) } roots = append(roots, item) } else { itemMap[item.ParentId].Children = append(itemMap[item.ParentId].Children, item) } } return roots, nil }