package user import ( "context" "errors" "strings" "perms-system-server/internal/consts" authHelper "perms-system-server/internal/logic/auth" "perms-system-server/internal/middleware" userModel "perms-system-server/internal/model/user" "perms-system-server/internal/response" "perms-system-server/internal/svc" "perms-system-server/internal/types" "perms-system-server/internal/util" "github.com/zeromicro/go-zero/core/logx" ) type UpdateUserLogic struct { logx.Logger ctx context.Context svcCtx *svc.ServiceContext } func NewUpdateUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateUserLogic { return &UpdateUserLogic{ Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx, } } // UpdateUser 更新用户信息。修改用户昵称、邮箱、手机、备注、部门归属等。用户可修改自身非敏感字段,管理者可修改下属用户信息。 func (l *UpdateUserLogic) UpdateUser(req *types.UpdateUserReq) error { caller := middleware.GetUserDetails(l.ctx) if caller == nil { return response.ErrUnauthorized("未登录") } if caller.UserId == req.Id { if req.DeptId != nil || req.Status != 0 { return response.ErrForbidden("不允许修改自己的部门和状态") } } // 前置 FindOne,后续 CheckManageAccess / ValidateStatusChange 都复用此对象,避免一次请求内 // 对 target 做 2~3 次重复查询(见审计 M-5)。 user, err := l.svcCtx.SysUserModel.FindOne(l.ctx, req.Id) if err != nil { return response.ErrNotFound("用户不存在") } if caller.UserId != req.Id { productCode := middleware.GetProductCode(l.ctx) if err := authHelper.CheckManageAccess(l.ctx, l.svcCtx, req.Id, productCode, authHelper.WithPrefetchedTarget(user)); err != nil { return err } } // req.Status != 0 仅会落在 caller.UserId != req.Id 分支(上方 caller==target 的请求已经拦截), // 此处沿用 ValidateStatusChange 的超管保护语义,避免再次 FindOne。 if req.Status != 0 && user.IsSuperAdmin == consts.IsSuperAdminYes { return response.ErrForbidden("不能修改超级管理员的状态") } if caller.UserId != req.Id && user.IsSuperAdmin == consts.IsSuperAdminYes { if req.DeptId != nil { return response.ErrForbidden("不能通过此接口修改其他超级管理员的部门") } } if req.Nickname != nil && len(*req.Nickname) > 64 { return response.ErrBadRequest("昵称长度不能超过64个字符") } if req.Email != nil && len(*req.Email) > 64 { return response.ErrBadRequest("邮箱长度不能超过64个字符") } if req.Phone != nil && len(*req.Phone) > 32 { return response.ErrBadRequest("手机号长度不能超过32个字符") } if req.Remark != nil && len(*req.Remark) > 255 { return response.ErrBadRequest("备注长度不能超过255个字符") } nickname := user.Nickname email := user.Email phone := user.Phone remark := user.Remark deptId := user.DeptId if req.Nickname != nil { nickname = *req.Nickname } if req.Email != nil { if *req.Email != "" && !util.IsValidEmail(*req.Email) { return response.ErrBadRequest("邮箱格式不正确") } email = *req.Email } if req.Phone != nil { if *req.Phone != "" && !util.IsValidPhone(*req.Phone) { return response.ErrBadRequest("手机号格式不正确") } phone = *req.Phone } if req.Remark != nil { remark = *req.Remark } if req.DeptId != nil { if *req.DeptId > 0 { newDept, err := l.svcCtx.SysDeptModel.FindOne(l.ctx, *req.DeptId) if err != nil { return response.ErrBadRequest("部门不存在") } if !caller.IsSuperAdmin && caller.MemberType != consts.MemberTypeAdmin && caller.DeptPath != "" && !strings.HasPrefix(newDept.Path, caller.DeptPath) { return response.ErrForbidden("无权将用户调入非自己管辖的部门") } } else { // deptId=0 意味着"把用户移出部门树";一旦生效目标将失去 DeptPath,此后 MEMBER / DEVELOPER // 级别的调用者都通不过 checkDeptHierarchy 对"目标必须归属部门"的强校验,无法再被管辖。 // 因此仅超管和产品 ADMIN 有权执行该破坏组织结构语义的操作(见审计 H-4)。 if !caller.IsSuperAdmin && caller.MemberType != consts.MemberTypeAdmin { return response.ErrForbidden("仅超级管理员或产品管理员可将用户移出部门") } } deptId = *req.DeptId } statusChanged := false if req.Status != 0 { if req.Status != consts.StatusEnabled && req.Status != consts.StatusDisabled { return response.ErrBadRequest("状态值无效,仅支持 1(启用) 和 2(冻结)") } if user.Status != req.Status { statusChanged = true } } newStatus := user.Status if statusChanged { newStatus = req.Status } if err := l.svcCtx.SysUserModel.UpdateProfile( l.ctx, req.Id, user.Username, nickname, email, phone, remark, deptId, newStatus, statusChanged, user.UpdateTime, ); err != nil { if errors.Is(err, userModel.ErrUpdateConflict) { return response.ErrConflict("数据已被其他操作修改,请刷新后重试") } return err } l.svcCtx.UserDetailsLoader.Clean(l.ctx, req.Id) return nil }