package member import ( "context" "perms-system-server/internal/consts" "perms-system-server/internal/loaders" authHelper "perms-system-server/internal/logic/auth" "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 RemoveMemberLogic struct { logx.Logger ctx context.Context svcCtx *svc.ServiceContext } func NewRemoveMemberLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RemoveMemberLogic { return &RemoveMemberLogic{ Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx, } } // RemoveMember 移除产品成员。在事务内同时清理该用户在产品下的角色和个性化权限绑定后移除成员记录。不能移除产品的最后一个 ADMIN。 // // 审计 H-R16-1(签发层吊销,与 M-R15-1 / L-R15-3 对齐): // 移除成员是"有效成员 → 非成员"的极端降权,授权语义跳变比 UpdateMember 降级更剧烈——loadPerms // 对非成员直接返回 nil,jwtauthMiddleware 对 ud.MemberType=="" 非超管直接 403。因此走到 tx 体 // 一定构成降权,无需再判断"是否收窄"。事务内在删除成员行之前对 sys_user.tokenVersion 做一次 +1, // 让旧 access token 在下一次 middleware 校验时因 `claims.TokenVersion != ud.TokenVersion` 被 401; // 即使 post-commit 的 UserDetailsLoader.Del 因 Redis 抖动失败,也不会残留 5min TTL 的特权窗口。 func (l *RemoveMemberLogic) RemoveMember(req *types.RemoveMemberReq) error { member, err := l.svcCtx.SysProductMemberModel.FindOne(l.ctx, req.Id) if err != nil { return response.ErrNotFound("成员不存在") } if err := authHelper.CheckManageAccess(l.ctx, l.svcCtx, member.UserId, member.ProductCode); err != nil { return err } if err := l.svcCtx.SysProductMemberModel.TransactCtx(l.ctx, func(ctx context.Context, session sqlx.Session) error { locked, err := l.svcCtx.SysProductMemberModel.FindOneForUpdateTx(ctx, session, req.Id) if err != nil { return response.ErrNotFound("成员不存在") } if locked.MemberType == consts.MemberTypeAdmin && locked.Status == consts.StatusEnabled { // 使用 CountOtherActiveAdminsTx 排除目标自己,返回 0 即目标为最后一个 active admin, // 不再依赖"count<=1 包含自己"的反向推理(见审计 L-5)。 otherAdminCount, err := l.svcCtx.SysProductMemberModel.CountOtherActiveAdminsTx(ctx, session, member.ProductCode, locked.Id) if err != nil { return err } if otherAdminCount == 0 { return response.ErrBadRequest("不能移除该产品的最后一个管理员") } } if err := l.svcCtx.SysUserRoleModel.DeleteByUserIdForProductTx(ctx, session, member.UserId, member.ProductCode); err != nil { return err } if err := l.svcCtx.SysUserPermModel.DeleteByUserIdForProductTx(ctx, session, member.UserId, member.ProductCode); err != nil { return err } // 审计 H-R16-1:放在 DeleteWithTx 之前——任一步失败整体回滚,避免"tokenVersion 已 +1 但 // member 行未删"或"member 行已删但 tokenVersion 未 +1"的脏中间态。 if _, err := l.svcCtx.SysUserModel.IncrementTokenVersionWithTx(ctx, session, locked.UserId); err != nil { return err } return l.svcCtx.SysProductMemberModel.DeleteWithTx(ctx, session, req.Id) }); err != nil { return err } // 审计 L-R13-5 方案 A:移除成员后 UD 里仍有旧 MemberType / Roles / Perms,必须立刻失效; // 断连也不能让"已移除"的会话继续活 5 分钟。 cleanCtx, cancel := loaders.DetachCacheCleanCtx(l.ctx) defer cancel() l.svcCtx.UserDetailsLoader.Del(cleanCtx, member.UserId, member.ProductCode) // 审计 H-R16-1:tokenVersion 已在 tx 内 +1;post-commit 必须把 sysUser 低层缓存 // (cacheSysUserIdPrefix / cacheSysUserUsernamePrefix)一起失效,否则 UD loader 下次 miss 时 // 会从 sysUser 低层缓存读回旧 tokenVersion,把刚递增的值抹回去(与 UpdateMember 同口径)。 // FindOne 本身带 sqlc 缓存,额外成本可控。 if user, err := l.svcCtx.SysUserModel.FindOne(cleanCtx, member.UserId); err == nil && user != nil { l.svcCtx.SysUserModel.InvalidateProfileCache(cleanCtx, user.Id, user.Username) } else if err != nil { logx.WithContext(l.ctx).Errorf("RemoveMember post-commit FindOne(%d) failed for token-version cache invalidation: %v", member.UserId, err) } return nil }