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个字符") } 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() d, err := l.svcCtx.SysDeptModel.FindOneWithTx(ctx, session, deptId) if err != nil { return err } d.Path = fmt.Sprintf("%s%d/", parentPath, deptId) return l.svcCtx.SysDeptModel.UpdateWithTx(ctx, session, d) }) if err != nil { return nil, err } return &types.IdResp{Id: deptId}, nil }