package dept import ( "context" "errors" "fmt" "time" "perms-system-server/internal/consts" authHelper "perms-system-server/internal/logic/auth" 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" "github.com/zeromicro/go-zero/core/stores/sqlx" ) type CreateDeptLogic struct { logx.Logger ctx context.Context svcCtx *svc.ServiceContext } func NewCreateDeptLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateDeptLogic { return &CreateDeptLogic{ Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx, } } // CreateDept 创建部门。在指定父部门下新建子部门,自动继承路径层级。仅超管可调用。 func (l *CreateDeptLogic) CreateDept(req *types.CreateDeptReq) (resp *types.IdResp, err error) { if err := authHelper.RequireSuperAdmin(l.ctx); err != nil { return nil, err } if len(req.Name) > 64 { return nil, response.ErrBadRequest("部门名称长度不能超过64个字符") } if len(req.Remark) > 255 { return nil, response.ErrBadRequest("备注长度不能超过255个字符") } // 审计 L-R18-5:Sort 只在同级部门间相对有效,用不到 int64 的极端值;把合法区间固定为 // [-100000, 100000],与 permsLevel 1-999 的"业务侧人类可读范围"思路一致,避免前端偶发 // 把 math.MaxInt64 之类的值透传到 DB 触发"排序溢出"的 edge case。 if req.Sort < -100000 || req.Sort > 100000 { return nil, response.ErrBadRequest("排序值必须在 -100000 到 100000 之间") } parentPath := "/" if req.ParentId > 0 { parent, err := l.svcCtx.SysDeptModel.FindOne(l.ctx, req.ParentId) if err != nil { return nil, response.ErrNotFound("父部门不存在") } parentPath = parent.Path } now := time.Now().Unix() var deptId int64 deptType := req.DeptType if deptType == "" { deptType = consts.DeptTypeNormal } else if deptType != consts.DeptTypeNormal && deptType != consts.DeptTypeDev { return nil, response.ErrBadRequest("无效的部门类型") } err = l.svcCtx.SysDeptModel.TransactCtx(l.ctx, func(ctx context.Context, session sqlx.Session) error { if req.ParentId > 0 { // 审计 L-R12-2:事务内用 FindOneForShareTx 重取父部门快照——同时拿到 status 与 // path。此前只 SELECT `id` 无法感知"父部门在事务外 FindOne 之后被 UpdateDept 禁用" // 这条交错:读到 id 仍存在就放行,结果子部门会以 Enabled 挂在已 Disabled 的父部门下, // 让运营侧的"禁用整个子树"意图被静默绕过。改用事务内带 S 锁的完整行读取,同时把 // parentPath 覆盖为事务内视图——与事务外 FindOne 理论上一致(UpdateDept 不改 path), // 但如果未来 UpdateDept 扩展支持 path 重写,这里已经内建 snapshot。 parent, err := l.svcCtx.SysDeptModel.FindOneForShareTx(ctx, session, req.ParentId) if err != nil { if errors.Is(err, sqlx.ErrNotFound) { return response.ErrNotFound("父部门已被删除") } return err } if parent.Status != consts.StatusEnabled { return response.ErrBadRequest("父部门已被禁用,无法创建子部门") } parentPath = parent.Path } result, err := l.svcCtx.SysDeptModel.InsertWithTx(ctx, session, &deptModel.SysDept{ ParentId: req.ParentId, Name: req.Name, Path: parentPath, Sort: req.Sort, DeptType: deptType, Remark: req.Remark, Status: consts.StatusEnabled, CreateTime: now, UpdateTime: now, }) if err != nil { return err } deptId, _ = result.LastInsertId() // 审计 L-R17-5:以前用 FindOneWithTx 把刚 INSERT 的整行读回来,仅为了把 path 覆写成 // `parentPath + deptId + "/"` 后再 UpdateWithTx 走整行回写——3 次 DB 往返。现在改调 // UpdatePathWithTx 只改 path/updateTime 两列,减一次 SELECT,sys_dept X 锁持有时长 // 同步缩短,降低并发 DeleteDept / UpdateDept 的尾部延迟。 finalPath := fmt.Sprintf("%s%d/", parentPath, deptId) return l.svcCtx.SysDeptModel.UpdatePathWithTx(ctx, session, deptId, finalPath, now) }) if err != nil { return nil, err } return &types.IdResp{Id: deptId}, nil }