yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java
@@ -16,14 +16,19 @@
import cn.iocoder.yudao.module.system.controller.admin.auth.vo.*;
import cn.iocoder.yudao.module.system.convert.auth.AuthConvert;
import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO;
import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
import cn.iocoder.yudao.module.system.dal.mysql.user.AdminUserMapper;
import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum;
import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum;
import cn.iocoder.yudao.module.system.enums.oauth2.OAuth2ClientConstants;
import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum;
import cn.iocoder.yudao.module.system.event.SocialLoginEvent;
import cn.iocoder.yudao.module.system.service.logger.LoginLogService;
import cn.iocoder.yudao.module.system.service.member.MemberService;
import cn.iocoder.yudao.module.system.service.oauth2.OAuth2TokenService;
import cn.iocoder.yudao.module.system.service.permission.PermissionService;
import cn.iocoder.yudao.module.system.service.permission.RoleService;
import cn.iocoder.yudao.module.system.service.social.SocialUserService;
import cn.iocoder.yudao.module.system.service.user.AdminUserService;
import com.anji.captcha.model.common.ResponseModel;
@@ -35,10 +40,14 @@
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP;
@@ -53,8 +62,23 @@
@Slf4j
public class AdminAuthServiceImpl implements AdminAuthService {
    /**
     * 企业微信社交类型,对应 {@link cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum#WECHAT_ENTERPRISE}
     */
    private static final Integer WECHAT_ENTERPRISE_TYPE = 30;
    /**
     * 企业微信扫码自动注册的账号,默认绑定的角色编码。
     *
     * 需要在「系统管理 → 角色管理」里建好该编码的角色并勾选菜单,否则新账号登录后没有任何菜单。
     * 编码而非角色编号:角色编号在本地/测试/生产各库不一致,写死会在换库后静默失效。
     */
    private static final String WXCP_DEFAULT_ROLE_CODE = "wxcp_user";
    @Resource
    private AdminUserService userService;
    @Resource
    private AdminUserMapper userMapper;
    @Resource
    private LoginLogService loginLogService;
    @Resource
@@ -69,6 +93,14 @@
    private CaptchaService captchaService;
    @Resource
    private SmsCodeApi smsCodeApi;
    @Resource
    private PasswordEncoder passwordEncoder;
    @Resource
    private ApplicationEventPublisher eventPublisher;
    @Resource
    private RoleService roleService;
    @Resource
    private PermissionService permissionService;
    /**
     * 验证码的开关,默认为 true
@@ -113,7 +145,15 @@
                    reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState()));
        }
        // 创建 Token 令牌,记录登录日志
        return createTokenAfterLoginSuccess(user.getId(), reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME);
        AuthLoginRespVO result = createTokenAfterLoginSuccess(user.getId(), reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME);
        // 发布社交登录事件(绑定新社交账号时)
        if (reqVO.getSocialType() != null) {
            eventPublisher.publishEvent(new SocialLoginEvent(
                    reqVO.getSocialType(), user.getId(), user.getUsername(), "账号密码绑定"));
        }
        return result;
    }
    @Override
@@ -169,22 +209,107 @@
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public AuthLoginRespVO socialLogin(AuthSocialLoginReqVO reqVO) {
        // 使用 code 授权码,进行登录。然后,获得到绑定的用户编号
        // 1. 使用 code 授权码,获取社交用户信息
        SocialUserRespDTO socialUser = socialUserService.getSocialUserByCode(UserTypeEnum.ADMIN.getValue(), reqVO.getType(),
                reqVO.getCode(), reqVO.getState());
        if (socialUser == null || socialUser.getUserId() == null) {
        if (socialUser == null) {
            throw exception(AUTH_THIRD_LOGIN_NOT_BIND);
        }
        // 获得用户
        AdminUserDO user = userService.getUser(socialUser.getUserId());
        if (user == null) {
            throw exception(USER_NOT_EXISTS);
        // 2. 判断是否已有绑定的系统用户
        Long bindUserId = socialUser.getUserId();
        AdminUserDO user;
        if (bindUserId == null) {
            // 2.1 首次登录:自动注册新用户
            user = autoRegisterUser(socialUser, reqVO.getType());
            // 绑定社交账号
            socialUserService.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(),
                    reqVO.getType(), reqVO.getCode(), reqVO.getState()));
            log.info("[socialLogin][社交登录自动注册 userId={} username={} socialType={}]",
                    user.getId(), user.getUsername(), reqVO.getType());
        } else {
            // 2.2 已有绑定:直接获取用户
            user = userService.getUser(bindUserId);
            if (user == null) {
                throw exception(USER_NOT_EXISTS);
            }
        }
        // 创建 Token 令牌,记录登录日志
        return createTokenAfterLoginSuccess(user.getId(), user.getUsername(), LoginLogTypeEnum.LOGIN_SOCIAL);
        // 3. 创建 Token 令牌,记录登录日志
        AuthLoginRespVO result = createTokenAfterLoginSuccess(user.getId(), user.getUsername(), LoginLogTypeEnum.LOGIN_SOCIAL);
        // 4. 发布社交登录事件(供其他模块监听,如发送企业微信通知)
        eventPublisher.publishEvent(new SocialLoginEvent(
                reqVO.getType(), user.getId(), user.getUsername(), "社交扫码"));
        return result;
    }
    /**
     * 社交登录自动注册用户
     *
     * 当用户首次通过社交账号(如企业微信)扫码且未绑定时,自动创建系统用户
     *
     * @param socialUser 社交用户信息
     * @param socialType 社交类型
     * @return 新创建的用户
     */
    private AdminUserDO autoRegisterUser(SocialUserRespDTO socialUser, Integer socialType) {
        // 1. 自动生成用户名(wx_ + 8位随机十六进制)
        String rawUsername = "wx_" + UUID.randomUUID().toString().replaceAll("-", "").substring(0, 8);
        // 2. 确保用户名唯一(如果冲突则重试)
        String username = rawUsername;
        int retry = 0;
        while (userService.getUserByUsername(username) != null && retry < 5) {
            username = "wx_" + UUID.randomUUID().toString().replaceAll("-", "").substring(0, 8);
            retry++;
        }
        if (retry >= 5) {
            throw exception(USER_USERNAME_EXISTS);
        }
        // 3. 生成随机密码
        String password = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 16);
        // 4. 创建用户
        AdminUserDO user = new AdminUserDO();
        user.setUsername(username);
        user.setNickname(socialUser.getNickname() != null ? socialUser.getNickname() : username);
        user.setPassword(passwordEncoder.encode(password));
        user.setStatus(CommonStatusEnum.ENABLE.getStatus());
        userMapper.insert(user);
        // 5. 绑定默认角色
        //    不绑的话账号能登录,但 get-permission-info 返回空菜单,进去一片空白;
        //    这种"能进但什么都看不到"的状态不会报错,很难定位,所以注册时就绑上
        bindDefaultRole(user);
        return user;
    }
    /**
     * 给自动注册的账号绑定默认角色
     *
     * 角色不存在时只打警告、不抛异常:一个配置遗漏不应该让扫码登录整条链路不可用,
     * 而且警告日志里已经写明了去哪儿补。
     *
     * @param user 自动注册出来的用户
     */
    private void bindDefaultRole(AdminUserDO user) {
        RoleDO role = roleService.getRoleByCode(WXCP_DEFAULT_ROLE_CODE);
        if (role == null) {
            log.warn("[bindDefaultRole][角色编码({}) 不存在,用户({}) 将没有任何菜单权限;" +
                            "请在「系统管理 → 角色管理」新建编码为 {} 的角色并分配菜单]",
                    WXCP_DEFAULT_ROLE_CODE, user.getUsername(), WXCP_DEFAULT_ROLE_CODE);
            return;
        }
        permissionService.assignUserRole(user.getId(), Set.of(role.getId()));
        log.info("[bindDefaultRole][用户({}) 已绑定默认角色({}/{})]",
                user.getUsername(), role.getCode(), role.getName());
    }
    @VisibleForTesting
@@ -303,4 +428,4 @@
        userService.updateUserPassword(userByMobile.getId(), reqVO.getPassword());
    }
}
}