From 1c2006d05c141361dfab4992afdfd8e57d9c9434 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E9=B9=8F?= <812968653@qq.com> Date: Sat, 15 Aug 2026 19:17:52 +0800 Subject: [PATCH] =?UTF-8?q?bug=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/handler/GlobalExceptionHandler.java | 6 +++ .../admin/user/MemberUserController.java | 8 ++++ .../user/vo/MemberUserUpdateStatusReqVO.java | 23 ++++++++++ .../service/auth/MemberAuthServiceImpl.java | 18 +++++++- .../service/user/MemberUserService.java | 8 ++++ .../service/user/MemberUserServiceImpl.java | 43 ++++++++++++++++++- .../system/api/oauth2/OAuth2TokenApi.java | 9 ++++ .../system/api/oauth2/OAuth2TokenApiImpl.java | 5 +++ .../mysql/oauth2/OAuth2AccessTokenMapper.java | 6 +++ .../oauth2/OAuth2RefreshTokenMapper.java | 6 +++ .../service/oauth2/OAuth2TokenService.java | 9 ++++ .../oauth2/OAuth2TokenServiceImpl.java | 19 ++++++++ 12 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/vo/MemberUserUpdateStatusReqVO.java diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java index 6090d1d..508035d 100644 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java @@ -387,6 +387,12 @@ public class GlobalExceptionHandler { return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[支付模块 yudao-module-pay - 表结构未导入][参考 https://cloud.iocoder.cn/pay/build/ 开启]"); } + // 7.1 会员互动消息 + if (message.contains("member_interaction")) { + log.error("[会员互动消息表 member_interaction 未导入][请执行 friend_back/sql/mysql/member/member_interaction.sql]"); + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "会员互动消息表未导入,请执行 sql/mysql/member/member_interaction.sql"); + } // 8. AI 大模型 if (message.contains("ai_")) { log.error("[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]"); diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/MemberUserController.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/MemberUserController.java index 2b6d1e0..75ccf12 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/MemberUserController.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/MemberUserController.java @@ -71,6 +71,14 @@ public class MemberUserController { return success(true); } + @PutMapping("/update-status") + @Operation(summary = "更新会员用户状态(禁用后踢出 App 登录,且无法再登录)") + @PreAuthorize("@ss.hasPermission('member:user:update')") + public CommonResult updateUserStatus(@Valid @RequestBody MemberUserUpdateStatusReqVO reqVO) { + memberUserService.updateUserStatus(reqVO.getId(), reqVO.getStatus()); + return success(true); + } + @GetMapping("/fill-missing-user-codes") @Operation(summary = "为历史会员补齐用户 Code") // @PreAuthorize("@ss.hasPermission('member:user:update')") diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/vo/MemberUserUpdateStatusReqVO.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/vo/MemberUserUpdateStatusReqVO.java new file mode 100644 index 0000000..689c912 --- /dev/null +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/admin/user/vo/MemberUserUpdateStatusReqVO.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.member.controller.admin.user.vo; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 会员用户更新状态 Request VO") +@Data +public class MemberUserUpdateStatusReqVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "用户编号不能为空") + private Long id; + + @Schema(description = "状态,见 CommonStatusEnum 枚举。0 开启,1 禁用", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + @InEnum(value = CommonStatusEnum.class, message = "修改状态必须是 {value}") + private Integer status; + +} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthServiceImpl.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthServiceImpl.java index 5032724..5f03081 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthServiceImpl.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/auth/MemberAuthServiceImpl.java @@ -173,6 +173,11 @@ public class MemberAuthServiceImpl implements MemberAuthService { private AppAuthLoginRespVO createTokenAfterLoginSuccess(MemberUserDO user, String mobile, LoginLogTypeEnum logType, String openid) { + // 统一拦截:已禁用账号不可登录 / 注册后自动登录 + if (CommonStatusEnum.isDisable(user.getStatus())) { + createLoginLog(user.getId(), mobile, logType, LoginResultEnum.USER_DISABLED); + throw exception(AUTH_LOGIN_USER_DISABLED); + } // 插入登陆日志 createLoginLog(user.getId(), mobile, logType, LoginResultEnum.SUCCESS); // 创建 Token 令牌 @@ -339,6 +344,13 @@ public class MemberAuthServiceImpl implements MemberAuthService { public AppAuthLoginRespVO refreshToken(String refreshToken) { OAuth2AccessTokenRespDTO accessTokenDO = oauth2TokenApi.refreshAccessToken(refreshToken, OAuth2ClientConstants.CLIENT_ID_DEFAULT); + MemberUserDO user = userService.getUser(accessTokenDO.getUserId()); + if (user == null || CommonStatusEnum.isDisable(user.getStatus())) { + // 禁用后禁止续期,并清掉该用户全部 token + oauth2TokenApi.removeAccessTokenByUserId(accessTokenDO.getUserId(), getUserType().getValue()); + throw exception(AUTH_LOGIN_USER_DISABLED); + } + MemberUserUpdateReqVO update = new MemberUserUpdateReqVO(); update.setId(accessTokenDO.getUserId()); update.setAppRefreshDate(LocalDateTime.now()); @@ -371,9 +383,13 @@ public class MemberAuthServiceImpl implements MemberAuthService { @Override public AppAuthLoginRespVO.ChatToken reissueChatToken(Long userId) { - if (userService.getUser(userId) == null) { + MemberUserDO user = userService.getUser(userId); + if (user == null) { throw exception(USER_NOT_EXISTS); } + if (CommonStatusEnum.isDisable(user.getStatus())) { + throw exception(AUTH_LOGIN_USER_DISABLED); + } if (!userService.syncChatUser(userId)) { throw exception(CHAT_TOKEN_CREATE_FAILED); } diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserService.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserService.java index cc64a79..2f9e3fc 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserService.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserService.java @@ -147,6 +147,14 @@ public interface MemberUserService { */ void updateUser(@Valid MemberUserUpdateReqVO updateReqVO); + /** + * 【管理员】更新会员用户状态(禁用时踢出全部登录态) + * + * @param id 用户编号 + * @param status 状态,见 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum} + */ + void updateUserStatus(Long id, Integer status); + /** * 【管理员】获得会员用户分页 * diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImpl.java b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImpl.java index e19b154..02b149e 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImpl.java +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImpl.java @@ -30,6 +30,7 @@ import cn.iocoder.yudao.module.member.enums.tag.MemberRecentTagEnum; import cn.iocoder.yudao.module.member.mq.producer.user.MemberUserProducer; import cn.iocoder.yudao.module.member.service.user.BO.MemberExtBO; import cn.iocoder.yudao.module.member.service.user.BO.MemberWechatBO; +import cn.iocoder.yudao.module.system.api.oauth2.OAuth2TokenApi; import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi; import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeUseReqDTO; import cn.iocoder.yudao.module.system.api.social.SocialClientApi; @@ -100,6 +101,8 @@ public class MemberUserServiceImpl implements MemberUserService { private ChatApi chatApi; @Resource private TradeBrokerageUserApi tradeBrokerageUserApi; + @Resource + private OAuth2TokenApi oauth2TokenApi; @Override public MemberUserDO getUserByMobile(String mobile) { @@ -416,7 +419,7 @@ public class MemberUserServiceImpl implements MemberUserService { @Transactional(rollbackFor = Exception.class) public void updateUser(MemberUserUpdateReqVO updateReqVO) { // 校验存在 - validateUserExists(updateReqVO.getId()); + MemberUserDO dbUser = validateUserExists(updateReqVO.getId()); // 校验手机唯一 validateMobileUnique(updateReqVO.getId(), updateReqVO.getMobile()); @@ -446,6 +449,44 @@ public class MemberUserServiceImpl implements MemberUserService { // 更新用户新人tag memberRecentTagService.createOrUpdateNewPersonTag(updateReqVO.getId()); memberUserMapper.updateById(updateObj); + + // 变为禁用:踢出全部 App 登录态 + Integer newStatus = updateObj.getStatus(); + if (newStatus == null && updateReqVO.getStatus() != null) { + newStatus = updateReqVO.getStatus().intValue(); + } + if (CommonStatusEnum.isDisable(newStatus) && !CommonStatusEnum.isDisable(dbUser.getStatus())) { + kickMemberLogin(updateReqVO.getId()); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateUserStatus(Long id, Integer status) { + MemberUserDO dbUser = validateUserExists(id); + if (ObjectUtil.equal(dbUser.getStatus(), status)) { + // 已禁用时再点一次,仍强制踢一次,避免漏踢 + if (CommonStatusEnum.isDisable(status)) { + kickMemberLogin(id); + } + return; + } + memberUserMapper.updateById(new MemberUserDO().setId(id).setStatus(status)); + if (CommonStatusEnum.isDisable(status)) { + kickMemberLogin(id); + } + } + + /** + * 清理会员全部 OAuth2 access + refresh,App 下次请求会 401 并退出登录。 + */ + private void kickMemberLogin(Long userId) { + try { + oauth2TokenApi.removeAccessTokenByUserId(userId, UserTypeEnum.MEMBER.getValue()); + } catch (Exception e) { + log.warn("踢出会员登录态失败, userId={}", userId, e); + throw e; + } } @VisibleForTesting diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/oauth2/OAuth2TokenApi.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/oauth2/OAuth2TokenApi.java index 79393cf..2fec551 100644 --- a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/oauth2/OAuth2TokenApi.java +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/oauth2/OAuth2TokenApi.java @@ -37,6 +37,15 @@ public interface OAuth2TokenApi { */ OAuth2AccessTokenRespDTO removeAccessToken(String accessToken); + /** + * 移除指定用户的全部访问令牌与刷新令牌(踢下线) + * + * @param userId 用户编号 + * @param userType 用户类型 + * @return 删除的访问令牌数量 + */ + int removeAccessTokenByUserId(Long userId, Integer userType); + /** * 刷新访问令牌 * diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/oauth2/OAuth2TokenApiImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/oauth2/OAuth2TokenApiImpl.java index be7eba3..dfcaf5c 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/oauth2/OAuth2TokenApiImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/oauth2/OAuth2TokenApiImpl.java @@ -40,6 +40,11 @@ public class OAuth2TokenApiImpl implements OAuth2TokenApi { return BeanUtils.toBean(accessTokenDO, OAuth2AccessTokenRespDTO.class); } + @Override + public int removeAccessTokenByUserId(Long userId, Integer userType) { + return oauth2TokenService.removeAccessTokenByUserId(userId, userType); + } + @Override public OAuth2AccessTokenRespDTO refreshAccessToken(String refreshToken, String clientId) { OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.refreshAccessToken(refreshToken, clientId); diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/oauth2/OAuth2AccessTokenMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/oauth2/OAuth2AccessTokenMapper.java index 81ca13f..1fa2e0e 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/oauth2/OAuth2AccessTokenMapper.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/oauth2/OAuth2AccessTokenMapper.java @@ -23,6 +23,12 @@ public interface OAuth2AccessTokenMapper extends BaseMapperX selectListByUserIdAndUserType(Long userId, Integer userType) { + return selectList(new LambdaQueryWrapperX() + .eq(OAuth2AccessTokenDO::getUserId, userId) + .eq(OAuth2AccessTokenDO::getUserType, userType)); + } + default PageResult selectPage(OAuth2AccessTokenPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .eqIfPresent(OAuth2AccessTokenDO::getUserId, reqVO.getUserId()) diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/oauth2/OAuth2RefreshTokenMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/oauth2/OAuth2RefreshTokenMapper.java index bf91457..ff9a79c 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/oauth2/OAuth2RefreshTokenMapper.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/oauth2/OAuth2RefreshTokenMapper.java @@ -14,6 +14,12 @@ public interface OAuth2RefreshTokenMapper extends BaseMapperX() + .eq(OAuth2RefreshTokenDO::getUserId, userId) + .eq(OAuth2RefreshTokenDO::getUserType, userType)); + } + @TenantIgnore // 获取 token 的时候,需要忽略租户编号。原因是:一些场景下,可能不会传递 tenant-id 请求头,例如说文件上传、积木报表等等 default OAuth2RefreshTokenDO selectByRefreshToken(String refreshToken) { return selectOne(OAuth2RefreshTokenDO::getRefreshToken, refreshToken); diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenService.java index 977d935..29e975f 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenService.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenService.java @@ -69,6 +69,15 @@ public interface OAuth2TokenService { */ OAuth2AccessTokenDO removeAccessToken(String accessToken); + /** + * 移除指定用户的全部访问令牌与刷新令牌(踢下线) + * + * @param userId 用户编号 + * @param userType 用户类型 + * @return 删除的访问令牌数量 + */ + int removeAccessTokenByUserId(Long userId, Integer userType); + /** * 获得访问令牌分页 * diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImpl.java index 04bc744..98ae92e 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImpl.java @@ -153,6 +153,25 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService { return accessTokenDO; } + @Override + @Transactional(rollbackFor = Exception.class) + public int removeAccessTokenByUserId(Long userId, Integer userType) { + if (userId == null || userType == null) { + return 0; + } + List accessTokenDOs = + oauth2AccessTokenMapper.selectListByUserIdAndUserType(userId, userType); + int removed = 0; + if (CollUtil.isNotEmpty(accessTokenDOs)) { + oauth2AccessTokenMapper.deleteByIds(convertSet(accessTokenDOs, OAuth2AccessTokenDO::getId)); + oauth2AccessTokenRedisDAO.deleteList(convertSet(accessTokenDOs, OAuth2AccessTokenDO::getAccessToken)); + removed = accessTokenDOs.size(); + } + // 清理可能残留的 refresh token(无对应 access 的场景) + oauth2RefreshTokenMapper.deleteByUserIdAndUserType(userId, userType); + return removed; + } + @Override public PageResult getAccessTokenPage(OAuth2AccessTokenPageReqVO reqVO) { return oauth2AccessTokenMapper.selectPage(reqVO);