| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229 |
- package user
- import (
- "context"
- "errors"
- "strings"
- "perms-system-server/internal/consts"
- "perms-system-server/internal/loaders"
- 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"
- "github.com/zeromicro/go-zero/core/stores/sqlx"
- )
- 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 {
- // 审计 L-R13-4:与 CreateUser 对齐,显式拒绝 deptId < 0。原先的 `>0 / else` 二分会把
- // 负数一路透传进 UpdateProfile(WithTx),导致 sys_user.deptId 出现 -1 之类的脏值,
- // loadDept FindOne(-1) 会 ErrNotFound → 5xx degrade;也会让 FindIdsByDeptId / 部门树
- // 接口永远检索不到该用户,形成隐形僵尸账号。
- if *req.DeptId < 0 {
- return response.ErrBadRequest("部门ID必须为非负整数")
- }
- if *req.DeptId > 0 {
- newDept, err := l.svcCtx.SysDeptModel.FindOne(l.ctx, *req.DeptId)
- if err != nil {
- return response.ErrBadRequest("部门不存在")
- }
- // 审计 L-N2:与 UpdateDept 禁用语义闭环 —— 已禁用的部门代表"冻结该部门所有活动",
- // 再往该部门调入新成员会破坏不变量(新成员会因 DeptStatus!=Enabled 被撤销 DEV 全权
- // 特权),且无法被 AddMember / CheckAddMemberAccess 的校验感知。此处统一拦截。
- if newDept.Status != consts.StatusEnabled {
- return response.ErrBadRequest("目标部门已停用")
- }
- // 审计 L-R13-3:删除原 `caller.DeptPath != ""` 的冗余条件。
- // 走到这里时 caller 一定满足:非本人(line 42-45 已拦 caller==target 改 deptId);
- // 非超管、非 ADMIN(见本分支前的判定);且 CheckManageAccess → checkDeptHierarchy
- // 已经在 access.go:318-324 对 `caller.DeptId == 0 || caller.DeptPath == ""` fail-close
- // 返回 403——因此执行到本行时 caller.DeptPath 恒非空。冗余条件会误导新维护者以为
- // "某条分支下 caller.DeptPath 可以为空",诱导把 checkDeptHierarchy 的护栏拆掉。
- if !caller.IsSuperAdmin &&
- caller.MemberType != consts.MemberTypeAdmin &&
- !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
- }
- // 审计 M-R11-3:改 deptId 到 `newDeptId>0` 时必须把 UPDATE 收敛进事务,并在同事务内对目标
- // sys_dept[newDeptId] 加 S 锁——这样并发 DeleteDept 持有 sys_dept[X] 的 X 锁,会被 S 锁阻塞,
- // 等本事务提交后 DeleteDept 重读 `sys_user WHERE deptId=X FOR SHARE` 就能看到新行并拒绝删除,
- // 闭合"两侧都读不到对方提交 → 各自提交 → orphan deptId"的 write skew。
- // 其余分支(只改其它字段 / 移出部门 deptId=0)无 write skew 风险,沿用非事务的 UpdateProfile。
- needDeptShareLock := req.DeptId != nil && *req.DeptId > 0 && *req.DeptId != user.DeptId
- if !needDeptShareLock {
- 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-R13-5 方案 A:post-commit 的 UD 失效与请求 ctx 解耦,避免 client 断连 /
- // 请求超时取消后 UD 仍然提供旧 DeptPath / MinPermsLevel / 冻结状态长达 TTL 窗口。
- cleanCtx, cancel := loaders.DetachCacheCleanCtx(l.ctx)
- defer cancel()
- l.svcCtx.UserDetailsLoader.Clean(cleanCtx, req.Id)
- return nil
- }
- targetDeptId := *req.DeptId
- if err := l.svcCtx.SysUserModel.TransactCtx(l.ctx, func(ctx context.Context, session sqlx.Session) error {
- // 事务内 S 锁目标 dept,保证 DeleteDept 的 X 锁被阻塞;顺带在事务内复核 Status。
- // 上面非事务的 FindOne 已经校过一遍,这里是"在锁生效后的一致性视图"下的最终校验。
- lockedDept, err := l.svcCtx.SysDeptModel.FindOneForShareTx(ctx, session, targetDeptId)
- if err != nil {
- if errors.Is(err, sqlx.ErrNotFound) {
- return response.ErrBadRequest("部门不存在")
- }
- return err
- }
- if lockedDept.Status != consts.StatusEnabled {
- return response.ErrBadRequest("目标部门已停用")
- }
- return l.svcCtx.SysUserModel.UpdateProfileWithTx(
- ctx, session, 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-R12-1:UpdateProfileWithTx 不再自己 DelCache(避免 pre-commit 窗口里并发 FindOne
- // 把未提交旧值灌回缓存);这里在 commit 成功后显式失效 sysUser 低层 id/username 键,再叠加
- // UserDetails 聚合缓存的 Clean,整条"两级缓存 → DB 权威"读链回到 cache-miss → loadFromDB。
- // 审计 L-R13-5 方案 A:detached ctx + 3s timeout 让 DeptPath 切换 / 冻结状态这类
- // 授权相关的失效不受 client 断连影响。
- cleanCtx, cancel := loaders.DetachCacheCleanCtx(l.ctx)
- defer cancel()
- l.svcCtx.SysUserModel.InvalidateProfileCache(cleanCtx, req.Id, user.Username)
- l.svcCtx.UserDetailsLoader.Clean(cleanCtx, req.Id)
- return nil
- }
|