فهرست منبع

重构社交登录的时候,增加独立的社交绑定表

YunaiV 3 سال پیش
والد
کامیت
705a5ff645
12فایلهای تغییر یافته به همراه324 افزوده شده و 368 حذف شده
  1. 12 16
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/social/SocialTypeEnum.java
  2. 1 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialUserApiImpl.java
  3. 45 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/social/SocialUserBindDO.java
  4. 11 12
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/social/SocialUserDO.java
  5. 28 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/social/SocialUserBindMapper.java
  6. 10 6
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/social/SocialUserMapper.java
  7. 0 4
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/RedisKeyConstants.java
  8. 0 39
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/social/SocialAuthUserRedisDAO.java
  9. 1 2
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java
  10. 5 28
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialUserService.java
  11. 60 126
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialUserServiceImpl.java
  12. 151 134
      yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/social/SocialUserServiceTest.java

+ 12 - 16
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/social/SocialTypeEnum.java

@@ -22,44 +22,47 @@ public enum SocialTypeEnum implements IntArrayValuable {
      * Gitee
      * 文档链接:https://gitee.com/api/v5/oauth_doc#/
      */
-    GITEE(10, "GITEE"),
+    GITEE(10, 1, "GITEE"),
     /**
      * 钉钉
      * 文档链接:https://developers.dingtalk.com/document/app/obtain-identity-credentials
      */
-    DINGTALK(20, "DINGTALK"),
+    DINGTALK(20, 2, "DINGTALK"),
 
     /**
      * 企业微信
      * 文档链接:https://xkcoding.com/2019/08/06/use-justauth-integration-wechat-enterprise.html
      */
-    WECHAT_ENTERPRISE(30, "WECHAT_ENTERPRISE"),
+    WECHAT_ENTERPRISE(30, 3, "WECHAT_ENTERPRISE"),
     /**
      * 微信公众平台 - 移动端 H5
      * 文档链接:https://www.cnblogs.com/juewuzhe/p/11905461.html
      */
-    WECHAT_MP(31, "WECHAT_MP"),
+    WECHAT_MP(31, 3, "WECHAT_MP"),
     /**
      * 微信开放平台 - 网站应用 PC 端扫码授权登录
      * 文档链接:https://justauth.wiki/guide/oauth/wechat_open/#_2-申请开发者资质认证
      */
-    WECHAT_OPEN(32, "WECHAT_OPEN"),
+    WECHAT_OPEN(32, 3, "WECHAT_OPEN"),
     /**
      * 微信小程序
      * 文档链接:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html
      */
-    WECHAT_MINI_PROGRAM(33, "WECHAT_MINI_PROGRAM"),
+    WECHAT_MINI_PROGRAM(33, 3, "WECHAT_MINI_PROGRAM"),
     ;
 
     public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(SocialTypeEnum::getType).toArray();
 
-    public static final List<Integer> WECHAT_ALL = ListUtil.toList(WECHAT_ENTERPRISE.type, WECHAT_MP.type, WECHAT_OPEN.type,
-            WECHAT_MINI_PROGRAM.type);
-
     /**
      * 类型
      */
     private final Integer type;
+    /**
+     * 平台
+     *
+     * 例如说,微信平台下,有企业微信、公众平台、开放平台、小程序等
+     */
+    private final Integer platform;
     /**
      * 类型的标识
      */
@@ -74,11 +77,4 @@ public enum SocialTypeEnum implements IntArrayValuable {
         return ArrayUtil.firstMatch(o -> o.getType().equals(type), values());
     }
 
-    public static List<Integer> getRelationTypes(Integer type) {
-        if (WECHAT_ALL.contains(type)) {
-            return WECHAT_ALL;
-        }
-        return ListUtil.toList(type);
-    }
-
 }

+ 1 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialUserApiImpl.java

@@ -38,7 +38,7 @@ public class SocialUserApiImpl implements SocialUserApi {
 
     @Override
     public void checkSocialUser(Integer type, String code, String state) {
-        socialUserService.checkSocialUser(type, code, state);
+        socialUserService.authSocialUser(type, code, state);
     }
 
     @Override

+ 45 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/social/SocialUserBindDO.java

@@ -0,0 +1,45 @@
+package cn.iocoder.yudao.module.system.dal.dataobject.social;
+
+import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+/**
+ * 社交用户的绑定
+ * 即 {@link SocialUserDO} 与 UserDO 的关联表
+ *
+ * @author 芋道源码
+ */
+@TableName(value = "system_social_user_bind", autoResultMap = true)
+@Data
+@EqualsAndHashCode(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class SocialUserBindDO extends BaseDO {
+
+    /**
+     * 关联的用户编号
+     */
+    private Long userId;
+    /**
+     * 用户类型
+     *
+     * 枚举 {@link UserTypeEnum}
+     */
+    private Integer userType;
+
+    /**
+     * 社交平台
+     *
+     * 枚举 {@link SocialTypeEnum#getPlatform()}
+     */
+    private Integer platform;
+    /**
+     * 社交的全局编号
+     */
+    private String unionId;
+
+}

+ 11 - 12
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/social/SocialUserDO.java

@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.system.dal.dataobject.social;
 import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.*;
@@ -26,21 +27,10 @@ public class SocialUserDO extends BaseDO {
      */
     @TableId
     private Long id;
-    /**
-     * 关联的用户编号
-     */
-    private Long userId;
-    /**
-     * 用户类型
-     *
-     * 枚举 {@link UserTypeEnum}
-     */
-    private Integer userType;
-
     /**
      * 社交平台的类型
      *
-     * 枚举 {@link UserTypeEnum}
+     * 枚举 {@link SocialTypeEnum}
      */
     private Integer type;
 
@@ -77,6 +67,15 @@ public class SocialUserDO extends BaseDO {
      */
     private String rawUserInfo;
 
+    /**
+     * 最后一次的认证 code
+     */
+    private String code;
+    /**
+     * 最后一次的认证 state
+     */
+    private String state;
+
 }
 
 

+ 28 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/social/SocialUserBindMapper.java

@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.system.dal.mysql.social;
+
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialUserBindDO;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface SocialUserBindMapper extends BaseMapperX<SocialUserBindDO> {
+
+    default void deleteByUserTypeAndUserIdAndPlatformAndUnionId(Integer userType, Long userId,
+                                                                Integer platform, String unionId) {
+        delete(new LambdaQueryWrapperX<SocialUserBindDO>()
+                .eq(SocialUserBindDO::getUserType, userType)
+                .eq(SocialUserBindDO::getUserId, userId)
+                .eq(SocialUserBindDO::getPlatform, platform)
+                .eq(SocialUserBindDO::getUnionId, unionId));
+    }
+
+    default SocialUserBindDO selectByUserTypeAndPlatformAndUnionId(Integer userType,
+                                                                   Integer platform, String unionId) {
+        return selectOne(new LambdaQueryWrapperX<SocialUserBindDO>()
+                .eq(SocialUserBindDO::getUserType, userType)
+                .eq(SocialUserBindDO::getPlatform, platform)
+                .eq(SocialUserBindDO::getUnionId, unionId));
+    }
+
+}

+ 10 - 6
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/social/SocialUserMapper.java

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.system.dal.mysql.social;
 
 import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialUserDO;
 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import org.apache.ibatis.annotations.Mapper;
 
@@ -11,14 +12,17 @@ import java.util.List;
 @Mapper
 public interface SocialUserMapper extends BaseMapperX<SocialUserDO> {
 
-    default List<SocialUserDO> selectListByTypeAndUnionId(Integer userType, Collection<Integer> types, String unionId) {
-        return selectList(new QueryWrapper<SocialUserDO>().eq("user_type", userType)
-                .in("type", types).eq("union_id", unionId));
+    default SocialUserDO selectByTypeAndCodeAnState(Integer type, String code, String state) {
+        return selectOne(new LambdaQueryWrapper<SocialUserDO>()
+                .eq(SocialUserDO::getType, type)
+                .eq(SocialUserDO::getCode, code)
+                .eq(SocialUserDO::getState, state));
     }
 
-    default List<SocialUserDO> selectListByTypeAndUserId(Integer userType, Collection<Integer> types, Long userId) {
-        return selectList(new QueryWrapper<SocialUserDO>().eq("user_type", userType)
-                .in("type", types).eq("user_id", userId));
+    default SocialUserDO selectByTypeAndOpenid(Integer type, String openid) {
+        return selectOne(new LambdaQueryWrapper<SocialUserDO>()
+                .eq(SocialUserDO::getType, type)
+                .eq(SocialUserDO::getCode, openid));
     }
 
     default List<SocialUserDO> selectListByUserId(Integer userType, Long userId) {

+ 0 - 4
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/RedisKeyConstants.java

@@ -23,10 +23,6 @@ public interface RedisKeyConstants {
             "login_user:%s", // 参数为 sessionId
             STRING, LoginUser.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC);
 
-    RedisKeyDefine SOCIAL_AUTH_USER = new RedisKeyDefine("社交登陆的授权用户",
-            "social_auth_user:%d:%s", // 参数为 type,code
-            STRING, AuthUser.class, Duration.ofDays(1));
-
     RedisKeyDefine SOCIAL_AUTH_STATE = new RedisKeyDefine("社交登陆的 state", // 注意,它是被 JustAuth 的 justauth.type.prefix 使用到
             "social_auth_state:%s", // 参数为 state
             STRING, String.class, Duration.ofHours(24)); // 值为 state

+ 0 - 39
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/social/SocialAuthUserRedisDAO.java

@@ -1,39 +0,0 @@
-package cn.iocoder.yudao.module.system.dal.redis.social;
-
-import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
-import me.zhyd.oauth.model.AuthCallback;
-import me.zhyd.oauth.model.AuthUser;
-import org.springframework.data.redis.core.StringRedisTemplate;
-import org.springframework.stereotype.Repository;
-
-import javax.annotation.Resource;
-
-import static cn.iocoder.yudao.module.system.dal.redis.RedisKeyConstants.SOCIAL_AUTH_USER;
-
-
-/**
- * 社交 {@link me.zhyd.oauth.model.AuthUser} 的 RedisDAO
- *
- * @author 芋道源码
- */
-@Repository
-public class SocialAuthUserRedisDAO {
-
-    @Resource
-    private StringRedisTemplate stringRedisTemplate;
-
-    public AuthUser get(Integer type, AuthCallback authCallback) {
-        String redisKey = formatKey(type, authCallback.getCode());
-        return JsonUtils.parseObject(stringRedisTemplate.opsForValue().get(redisKey), AuthUser.class);
-    }
-
-    public void set(Integer type, AuthCallback authCallback, AuthUser authUser) {
-        String redisKey = formatKey(type, authCallback.getCode());
-        stringRedisTemplate.opsForValue().set(redisKey, JsonUtils.toJsonString(authUser), SOCIAL_AUTH_USER.getTimeout());
-    }
-
-    private static String formatKey(Integer type, String code) {
-        return String.format(SOCIAL_AUTH_USER.getKeyTemplate(), type, code);
-    }
-
-}

+ 1 - 2
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java

@@ -219,8 +219,7 @@ public class AdminAuthServiceImpl implements AdminAuthService {
     @Override
     public String socialLogin2(AuthSocialLogin2ReqVO reqVO, String userIp, String userAgent) {
         // 使用 code 授权码,进行登录
-        AuthUser authUser = socialUserService.getAuthUser(reqVO.getType(), reqVO.getCode(), reqVO.getState());
-        Assert.notNull(authUser, "授权用户不为空");
+        socialUserService.authSocialUser(reqVO.getType(), reqVO.getCode(), reqVO.getState());
 
         // 使用账号密码,进行登录。
         LoginUser loginUser = this.login0(reqVO.getUsername(), reqVO.getPassword());

+ 5 - 28
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialUserService.java

@@ -1,11 +1,9 @@
 package cn.iocoder.yudao.module.system.service.social;
 
-import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.exception.ServiceException;
 import cn.iocoder.yudao.module.system.api.social.dto.SocialUserBindReqDTO;
 import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialUserDO;
 import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
-import me.zhyd.oauth.model.AuthUser;
 
 import javax.validation.Valid;
 import javax.validation.constraints.NotNull;
@@ -28,7 +26,7 @@ public interface SocialUserService {
     String getAuthorizeUrl(Integer type, String redirectUri);
 
     /**
-     * 获得授权的用户
+     * 授权获得对应的社交用户
      * 如果授权失败,则会抛出 {@link ServiceException} 异常
      *
      * @param type 社交平台的类型 {@link SocialTypeEnum}
@@ -37,17 +35,7 @@ public interface SocialUserService {
      * @return 授权用户
      */
     @NotNull
-    AuthUser getAuthUser(Integer type, String code, String state);
-
-    /**
-     * 获得社交用户的 unionId 编号
-     *
-     * @param authUser 社交用户
-     * @return unionId 编号
-     */
-    default String getAuthUserUnionId(AuthUser authUser) {
-        return StrUtil.blankToDefault(authUser.getToken().getUnionId(), authUser.getUuid());
-    }
+    SocialUserDO authSocialUser(Integer type, String code, String state);
 
     /**
      * 获得指定用户的社交用户列表
@@ -71,25 +59,14 @@ public interface SocialUserService {
      * @param userId 用户编号
      * @param userType 全局用户类型
      * @param type 社交平台的类型 {@link SocialTypeEnum}
-     * @param unionId 社交平台的 unionId
-     */
-    void unbindSocialUser(Long userId, Integer userType, Integer type, String unionId);
-
-    /**
-     * 校验社交用户的认证信息是否正确
-     * 如果校验不通过,则抛出 {@link ServiceException} 业务异常
-     *
-     * @param type 社交平台的类型
-     * @param code 授权码
-     * @param state state
+     * @param openid 社交平台的 openid
      */
-    void checkSocialUser(Integer type, String code, String state);
+    void unbindSocialUser(Long userId, Integer userType, Integer type, String openid);
 
     /**
      * 获得社交用户的绑定用户编号
      * 注意,返回的是 MemberUser 或者 AdminUser 的 id 编号!
-     * 该方法会执行和 {@link #checkSocialUser(Integer, String, String)} 一样的逻辑。
-     * 所以在认证信息不正确的情况下,也会抛出 {@link ServiceException} 业务异常
+     * 在认证信息不正确的情况下,也会抛出 {@link ServiceException} 业务异常
      *
      * @param userType 用户类型
      * @param type 社交平台的类型

+ 60 - 126
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialUserServiceImpl.java

@@ -1,14 +1,14 @@
 package cn.iocoder.yudao.module.system.service.social;
 
-import cn.hutool.core.collection.CollUtil;
-import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
 import cn.iocoder.yudao.module.system.api.social.dto.SocialUserBindReqDTO;
+import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialUserBindDO;
 import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialUserDO;
+import cn.iocoder.yudao.module.system.dal.mysql.social.SocialUserBindMapper;
 import cn.iocoder.yudao.module.system.dal.mysql.social.SocialUserMapper;
-import cn.iocoder.yudao.module.system.dal.redis.social.SocialAuthUserRedisDAO;
 import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
-import com.google.common.annotations.VisibleForTesting;
 import com.xkcoding.justauth.AuthRequestFactory;
 import lombok.extern.slf4j.Slf4j;
 import me.zhyd.oauth.model.AuthCallback;
@@ -22,7 +22,6 @@ import org.springframework.validation.annotation.Validated;
 
 import javax.annotation.Resource;
 import java.util.List;
-import java.util.Objects;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
@@ -42,8 +41,7 @@ public class SocialUserServiceImpl implements SocialUserService {
     private AuthRequestFactory authRequestFactory;
 
     @Resource
-    private SocialAuthUserRedisDAO authSocialUserRedisDAO;
-
+    private SocialUserBindMapper socialUserBindMapper;
     @Resource
     private SocialUserMapper socialUserMapper;
 
@@ -57,149 +55,85 @@ public class SocialUserServiceImpl implements SocialUserService {
     }
 
     @Override
-    public AuthUser getAuthUser(Integer type, String code, String state) {
-        AuthCallback authCallback = buildAuthCallback(code, state);
-        // 从缓存中获取
-        AuthUser authUser = authSocialUserRedisDAO.get(type, authCallback);
-        if (authUser != null) {
-            return authUser;
+    public SocialUserDO authSocialUser(Integer type, String code, String state) {
+        // 优先从 DB 中获取,因为 code 有且可以使用一次。
+        // 在社交登录时,当未绑定 User 时,需要绑定登录,此时需要 code 使用两次
+        SocialUserDO socialUser = socialUserMapper.selectByTypeAndCodeAnState(type, code, state);
+        if (socialUser != null) {
+            return socialUser;
         }
 
         // 请求获取
-        authUser = this.getAuthUser0(type, authCallback);
-        // 缓存。原因是 code 有且可以使用一次。在社交登录时,当未绑定 User 时,需要绑定登录,此时需要 code 使用两次
-        authSocialUserRedisDAO.set(type, authCallback, authUser);
-        return authUser;
-    }
-
-    /**
-     * 获得 unionId 对应的某个社交平台的“所有”社交用户
-     * 注意,这里的“所有”,指的是类似【微信】平台,包括了小程序、公众号、PC 网站,他们的 unionId 是一致的
-     *
-     * @param type 社交平台的类型 {@link SocialTypeEnum}
-     * @param unionId 社交平台的 unionId
-     * @param userType 全局用户类型
-     * @return 社交用户列表
-     */
-    private List<SocialUserDO> getAllSocialUserList(Integer type, String unionId, Integer userType) {
-        List<Integer> types = SocialTypeEnum.getRelationTypes(type);
-        return socialUserMapper.selectListByTypeAndUnionId(userType, types, unionId);
-    }
-
-    @Override
-    public List<SocialUserDO> getSocialUserList(Long userId, Integer userType) {
-        return socialUserMapper.selectListByUserId(userType, userId);
-    }
-
-    @Override
-    public void bindSocialUser(SocialUserBindReqDTO reqDTO) {
-        // 使用 code 授权
-        AuthUser authUser = getAuthUser(reqDTO.getType(), reqDTO.getCode(),
-                reqDTO.getState());
+        AuthUser authUser = getAuthUser(type, buildAuthCallback(code, state));
         if (authUser == null) {
             throw exception(SOCIAL_USER_NOT_FOUND);
         }
 
-        // 绑定社交用户(新增)
-        bindSocialUser(reqDTO.getUserId(), reqDTO.getUserType(),
-                reqDTO.getType(), authUser);
-    }
-
-    /**
-     * 绑定社交用户
-     *  @param userId 用户编号
-     * @param userType 用户类型
-     * @param type 社交平台的类型 {@link SocialTypeEnum}
-     * @param authUser 授权用户
-     */
-    @Transactional(rollbackFor = Exception.class)
-    protected void bindSocialUser(Long userId, Integer userType, Integer type, AuthUser authUser) {
-        // 获得 unionId 对应的 SocialUserDO 列表
-        String unionId = getAuthUserUnionId(authUser);
-        List<SocialUserDO> socialUsers = this.getAllSocialUserList(type, unionId, userType);
-
-        // 逻辑一:如果 userId 之前绑定过该 type 的其它账号,需要进行解绑
-        this.unbindOldSocialUser(userId, userType, type, unionId);
-
-        // 逻辑二:如果 socialUsers 指定的 userId 改变,需要进行更新
-        // 例如说,一个微信 unionId 对应了多个社交账号,结果其中有个关联了新的 userId,则其它也要跟着修改
-        // 考虑到 socialUsers 一般比较少,直接 for 循环更新即可
-        socialUsers.forEach(socialUser -> {
-            if (Objects.equals(socialUser.getUserId(), userId)) {
-                return;
-            }
-            socialUserMapper.updateById(new SocialUserDO().setId(socialUser.getId()).setUserId(userId));
-        });
-
-        // 逻辑三:如果 authUser 不存在于 socialUsers 中,则进行新增;否则,进行更新
-        SocialUserDO socialUser = CollUtil.findOneByField(socialUsers, "openid", authUser.getUuid());
-        SocialUserDO saveSocialUser = SocialUserDO.builder() // 新增和更新的通用属性
-                .token(authUser.getToken().getAccessToken()).rawTokenInfo(toJsonString(authUser.getToken()))
-                .nickname(authUser.getNickname()).avatar(authUser.getAvatar()).rawUserInfo(toJsonString(authUser.getRawUserInfo()))
-                .build();
+        // 保存到 DB 中
+        socialUser = socialUserMapper.selectByTypeAndOpenid(type, authUser.getUuid());
         if (socialUser == null) {
-            saveSocialUser.setUserId(userId).setUserType(userType)
-                    .setType(type).setOpenid(authUser.getUuid()).setUnionId(unionId);
-            socialUserMapper.insert(saveSocialUser);
+            socialUser = new SocialUserDO();
+        }
+        socialUser.setOpenid(authUser.getUuid()).setToken(authUser.getToken().getAccessToken()).setRawTokenInfo((toJsonString(authUser.getToken())))
+                .setUnionId(StrUtil.blankToDefault(authUser.getToken().getUnionId(), authUser.getUuid())) // unionId 识别多个用户
+                .setNickname(authUser.getNickname()).setAvatar(authUser.getAvatar()).setRawUserInfo(toJsonString(authUser.getRawUserInfo()))
+                .setCode(code).setState(state); // 需要保存 code + state 字段,保证后续可查询
+        if (socialUser.getId() == null) {
+            socialUserMapper.insert(socialUser);
         } else {
-            saveSocialUser.setId(socialUser.getId());
-            socialUserMapper.updateById(saveSocialUser);
+            socialUserMapper.updateById(socialUser);
         }
+        return socialUser;
     }
 
     @Override
-    public void unbindSocialUser(Long userId, Integer userType, Integer type, String unionId) {
-        // 获得 unionId 对应的所有 SocialUserDO 社交用户
-        List<SocialUserDO> socialUsers = this.getAllSocialUserList(type, unionId, userType);
-        if (CollUtil.isEmpty(socialUsers)) {
-            return;
-        }
-        // 校验,是否解绑的是非自己的
-        socialUsers.forEach(socialUser -> {
-            if (!Objects.equals(socialUser.getUserId(), userId)) {
-                throw exception(SOCIAL_USER_UNBIND_NOT_SELF);
-            }
-        });
-
-        // 解绑
-        socialUserMapper.deleteBatchIds(CollectionUtils.convertSet(socialUsers, SocialUserDO::getId));
+    public List<SocialUserDO> getSocialUserList(Long userId, Integer userType) {
+        return socialUserMapper.selectListByUserId(userType, userId);
     }
 
     @Override
-    public void checkSocialUser(Integer type, String code, String state) {
-        AuthUser authUser = getAuthUser(type, code, state);
-        if (authUser == null) {
-            throw exception(SOCIAL_USER_NOT_FOUND);
-        }
+    @Transactional
+    public void bindSocialUser(SocialUserBindReqDTO reqDTO) {
+        // 获得社交用户
+        SocialUserDO socialUser = authSocialUser(reqDTO.getType(), reqDTO.getCode(), reqDTO.getState());
+        Assert.notNull(socialUser, "社交用户不能为空");
+
+        // 如果 userId 之前绑定过该 type 的其它账号,需要进行解绑
+        socialUserBindMapper.deleteByUserTypeAndUserIdAndPlatformAndUnionId(reqDTO.getUserType(), reqDTO.getUserId(),
+                SocialTypeEnum.valueOfType(socialUser.getType()).getPlatform(), socialUser.getUnionId());
+
+        // 绑定当前登录的社交用户
+        SocialUserBindDO socialUserBind = SocialUserBindDO.builder().userId(reqDTO.getUserId()).userType(reqDTO.getUserType())
+                .unionId(socialUser.getUnionId()).build();
+        socialUserBindMapper.insert(socialUserBind);
     }
 
     @Override
-    public Long getBindUserId(Integer userType, Integer type, String code, String state) {
-        AuthUser authUser = getAuthUser(type, code, state);
-        if (authUser == null) {
+    public void unbindSocialUser(Long userId, Integer userType, Integer type, String openid) {
+        // 获得 openid 对应的 SocialUserDO 社交用户
+        SocialUserDO socialUser = socialUserMapper.selectByTypeAndOpenid(type, openid);
+        if (socialUser == null) {
             throw exception(SOCIAL_USER_NOT_FOUND);
         }
 
-        // 如果未绑定 SocialUserDO 用户,则无法自动登录,进行报错
-        String unionId = getAuthUserUnionId(authUser);
-        List<SocialUserDO> socialUsers = getAllSocialUserList(type, unionId, userType);
-        if (CollUtil.isEmpty(socialUsers)) {
-            throw exception(AUTH_THIRD_LOGIN_NOT_BIND);
-        }
-        return socialUsers.get(0).getUserId();
+        // 获得对应的社交绑定关系
+        socialUserBindMapper.deleteByUserTypeAndUserIdAndPlatformAndUnionId(userType, userId,
+                SocialTypeEnum.valueOfType(socialUser.getType()).getPlatform(), socialUser.getUnionId());
     }
 
-    @VisibleForTesting
-    public void unbindOldSocialUser(Long userId, Integer userType, Integer type, String newUnionId) {
-        List<Integer> types = SocialTypeEnum.getRelationTypes(type);
-        List<SocialUserDO> oldSocialUsers = socialUserMapper.selectListByTypeAndUserId(userType, types, userId);
-        // 如果新老的 unionId 是一致的,说明无需解绑
-        if (CollUtil.isEmpty(oldSocialUsers) || Objects.equals(newUnionId, oldSocialUsers.get(0).getUnionId())) {
-            return;
+    @Override
+    public Long getBindUserId(Integer userType, Integer type, String code, String state) {
+        // 获得社交用户
+        SocialUserDO socialUser = authSocialUser(type, code, state);
+        Assert.notNull(socialUser, "社交用户不能为空");
+
+        // 如果未绑定的社交用户,则无法自动登录,进行报错
+        SocialUserBindDO socialUserBind = socialUserBindMapper.selectByUserTypeAndPlatformAndUnionId(userType,
+                SocialTypeEnum.valueOfType(socialUser.getType()).getPlatform(), socialUser.getUnionId());
+        if (socialUserBind == null) {
+            throw exception(AUTH_THIRD_LOGIN_NOT_BIND);
         }
-
-        // 解绑
-        socialUserMapper.deleteBatchIds(CollectionUtils.convertSet(oldSocialUsers, SocialUserDO::getId));
+        return socialUserBind.getUserId();
     }
 
     /**
@@ -209,7 +143,7 @@ public class SocialUserServiceImpl implements SocialUserService {
      * @param authCallback 授权回调
      * @return 授权的用户
      */
-    private AuthUser getAuthUser0(Integer type, AuthCallback authCallback) {
+    private AuthUser getAuthUser(Integer type, AuthCallback authCallback) {
         AuthRequest authRequest = authRequestFactory.get(SocialTypeEnum.valueOfType(type).getSource());
         AuthResponse<?> authResponse = authRequest.login(authCallback);
         log.info("[getAuthUser0][请求社交平台 type({}) request({}) response({})]", type, toJsonString(authCallback),

+ 151 - 134
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/social/SocialUserServiceTest.java

@@ -1,32 +1,29 @@
 package cn.iocoder.yudao.module.system.service.social;
 
-import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialUserDO;
 import cn.iocoder.yudao.module.system.dal.mysql.social.SocialUserMapper;
-import cn.iocoder.yudao.module.system.dal.redis.social.SocialAuthUserRedisDAO;
-import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
-import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
 import cn.iocoder.yudao.framework.test.core.ut.BaseDbAndRedisUnitTest;
+import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
 import com.xkcoding.justauth.AuthRequestFactory;
-import me.zhyd.oauth.model.AuthUser;
+import me.zhyd.oauth.request.AuthRequest;
+import me.zhyd.oauth.utils.AuthStateUtils;
 import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
 import org.springframework.boot.test.mock.mockito.MockBean;
 import org.springframework.context.annotation.Import;
 
 import javax.annotation.Resource;
-import java.util.List;
 
 import static cn.hutool.core.util.RandomUtil.randomEle;
 import static cn.hutool.core.util.RandomUtil.randomString;
-import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
-import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.*;
 
-@Import({SocialUserServiceImpl.class, SocialAuthUserRedisDAO.class})
+@Import(SocialUserServiceImpl.class)
 public class SocialUserServiceTest extends BaseDbAndRedisUnitTest {
 
     @Resource
-    private SocialUserServiceImpl socialService;
+    private SocialUserServiceImpl socialUserService;
 
     @Resource
     private SocialUserMapper socialUserMapper;
@@ -34,132 +31,152 @@ public class SocialUserServiceTest extends BaseDbAndRedisUnitTest {
     @MockBean
     private AuthRequestFactory authRequestFactory;
 
-    /**
-     * 情况一,创建 SocialUserDO 的情况
-     */
     @Test
-    public void testBindSocialUser_create() {
-        // mock 数据
-        // 准备参数
-        Long userId = randomLongId();
-        Integer type = randomEle(SocialTypeEnum.values()).getType();
-        AuthUser authUser = randomPojo(AuthUser.class);
-        // mock 方法
-
-        // 调用
-        socialService.bindSocialUser(userId, UserTypeEnum.ADMIN.getValue(), type, authUser);
-        // 断言
-        List<SocialUserDO> socialUsers = socialUserMapper.selectList("user_id", userId);
-        assertEquals(1, socialUsers.size());
-        assertBindSocialUser(socialUsers.get(0), authUser, userId, type);
-    }
-
-    /**
-     * 情况二,更新 SocialUserDO 的情况
-     */
-    @Test
-    public void testBindSocialUser_update() {
-        // mock 数据
-        SocialUserDO dbSocialUser = randomPojo(SocialUserDO.class, socialUserDO -> {
-            socialUserDO.setUserType(UserTypeEnum.ADMIN.getValue());
-            socialUserDO.setType(randomEle(SocialTypeEnum.values()).getType());
-        });
-        socialUserMapper.insert(dbSocialUser);
-        // 准备参数
-        Long userId = dbSocialUser.getUserId();
-        Integer type = dbSocialUser.getType();
-        AuthUser authUser = randomPojo(AuthUser.class);
-        // mock 方法
-
-        // 调用
-        socialService.bindSocialUser(userId, UserTypeEnum.ADMIN.getValue(), type, authUser);
-        // 断言
-        List<SocialUserDO> socialUsers = socialUserMapper.selectList("user_id", userId);
-        assertEquals(1, socialUsers.size());
-        assertBindSocialUser(socialUsers.get(0), authUser, userId, type);
+    public void testGetAuthorizeUrl() {
+        try (MockedStatic<AuthStateUtils> authStateUtilsMock = mockStatic(AuthStateUtils.class)) {
+            // 准备参数
+            Integer type = 31;
+            String redirectUri = "sss";
+            // mock 获得对应的 AuthRequest 实现
+            AuthRequest authRequest = mock(AuthRequest.class);
+            when(authRequestFactory.get(eq("WECHAT_MP"))).thenReturn(authRequest);
+            // mock 方法
+            authStateUtilsMock.when(AuthStateUtils::createState).thenReturn("aoteman");
+            when(authRequest.authorize(eq("aoteman"))).thenReturn("https://www.iocoder.cn?redirect_uri=yyy");
+
+            // 调用
+            String url = socialUserService.getAuthorizeUrl(type, redirectUri);
+            // 断言
+            assertEquals("https://www.iocoder.cn/?redirect_uri=sss", url);
+        }
     }
 
-    /**
-     * 情况一和二都存在的,逻辑二的场景
-     */
-    @Test
-    public void testBindSocialUser_userId() {
-        // mock 数据
-        SocialUserDO dbSocialUser = randomPojo(SocialUserDO.class, socialUserDO -> {
-            socialUserDO.setUserType(UserTypeEnum.ADMIN.getValue());
-            socialUserDO.setType(randomEle(SocialTypeEnum.values()).getType());
-        });
-        socialUserMapper.insert(dbSocialUser);
-        // 准备参数
-        Long userId = randomLongId();
-        Integer type = dbSocialUser.getType();
-        AuthUser authUser = randomPojo(AuthUser.class);
-        // mock 方法
-
-        // 调用
-        socialService.bindSocialUser(userId, UserTypeEnum.ADMIN.getValue(), type, authUser);
-        // 断言
-        List<SocialUserDO> socialUsers = socialUserMapper.selectList("user_id", userId);
-        assertEquals(1, socialUsers.size());
-    }
-
-    private void assertBindSocialUser(SocialUserDO socialUser, AuthUser authUser, Long userId,
-                                      Integer type) {
-        assertEquals(authUser.getToken().getAccessToken(), socialUser.getToken());
-        assertEquals(toJsonString(authUser.getToken()), socialUser.getRawTokenInfo());
-        assertEquals(authUser.getNickname(), socialUser.getNickname());
-        assertEquals(authUser.getAvatar(), socialUser.getAvatar());
-        assertEquals(toJsonString(authUser.getRawUserInfo()), socialUser.getRawUserInfo());
-        assertEquals(userId, socialUser.getUserId());
-        assertEquals(UserTypeEnum.ADMIN.getValue(), socialUser.getUserType());
-        assertEquals(type, socialUser.getType());
-        assertEquals(authUser.getUuid(), socialUser.getOpenid());
-        assertEquals(socialService.getAuthUserUnionId(authUser), socialUser.getUnionId());
-    }
-
-    /**
-     * 情况一,如果新老的 unionId 是一致的,无需解绑
-     */
-    @Test
-    public void testUnbindOldSocialUser_no() {
-        // mock 数据
-        SocialUserDO oldSocialUser = randomPojo(SocialUserDO.class, socialUserDO -> {
-            socialUserDO.setUserType(UserTypeEnum.ADMIN.getValue());
-            socialUserDO.setType(randomEle(SocialTypeEnum.values()).getType());
-        });
-        socialUserMapper.insert(oldSocialUser);
-        // 准备参数
-        Long userId = oldSocialUser.getUserId();
-        Integer type = oldSocialUser.getType();
-        String newUnionId = oldSocialUser.getUnionId();
-
-        // 调用
-        socialService.unbindOldSocialUser(userId, UserTypeEnum.ADMIN.getValue(), type, newUnionId);
-        // 断言
-        assertEquals(1L, socialUserMapper.selectCount(null).longValue());
-    }
-
-
-    /**
-     * 情况二,如果新老的 unionId 不一致的,需解绑
-     */
-    @Test
-    public void testUnbindOldSocialUser_yes() {
-        // mock 数据
-        SocialUserDO oldSocialUser = randomPojo(SocialUserDO.class, socialUserDO -> {
-            socialUserDO.setUserType(UserTypeEnum.ADMIN.getValue());
-            socialUserDO.setType(randomEle(SocialTypeEnum.values()).getType());
-        });
-        socialUserMapper.insert(oldSocialUser);
-        // 准备参数
-        Long userId = oldSocialUser.getUserId();
-        Integer type = oldSocialUser.getType();
-        String newUnionId = randomString(10);
-
-        // 调用
-        socialService.unbindOldSocialUser(userId, UserTypeEnum.ADMIN.getValue(), type, newUnionId);
-        // 断言
-        assertEquals(0L, socialUserMapper.selectCount(null).longValue());
-    }
+//    /**
+//     * 情况一,创建 SocialUserDO 的情况
+//     */
+//    @Test
+//    public void testBindSocialUser_create() {
+//        // mock 数据
+//        // 准备参数
+//        Long userId = randomLongId();
+//        Integer type = randomEle(SocialTypeEnum.values()).getType();
+//        AuthUser authUser = randomPojo(AuthUser.class);
+//        // mock 方法
+//
+//        // 调用
+//        socialService.bindSocialUser(userId, UserTypeEnum.ADMIN.getValue(), type, authUser);
+//        // 断言
+//        List<SocialUserDO> socialUsers = socialUserMapper.selectList("user_id", userId);
+//        assertEquals(1, socialUsers.size());
+//        assertBindSocialUser(socialUsers.get(0), authUser, userId, type);
+//    }
+//
+//    /**
+//     * 情况二,更新 SocialUserDO 的情况
+//     */
+//    @Test
+//    public void testBindSocialUser_update() {
+//        // mock 数据
+//        SocialUserDO dbSocialUser = randomPojo(SocialUserDO.class, socialUserDO -> {
+//            socialUserDO.setUserType(UserTypeEnum.ADMIN.getValue());
+//            socialUserDO.setType(randomEle(SocialTypeEnum.values()).getType());
+//        });
+//        socialUserMapper.insert(dbSocialUser);
+//        // 准备参数
+//        Long userId = dbSocialUser.getUserId();
+//        Integer type = dbSocialUser.getType();
+//        AuthUser authUser = randomPojo(AuthUser.class);
+//        // mock 方法
+//
+//        // 调用
+//        socialService.bindSocialUser(userId, UserTypeEnum.ADMIN.getValue(), type, authUser);
+//        // 断言
+//        List<SocialUserDO> socialUsers = socialUserMapper.selectList("user_id", userId);
+//        assertEquals(1, socialUsers.size());
+//        assertBindSocialUser(socialUsers.get(0), authUser, userId, type);
+//    }
+//
+//    /**
+//     * 情况一和二都存在的,逻辑二的场景
+//     */
+//    @Test
+//    public void testBindSocialUser_userId() {
+//        // mock 数据
+//        SocialUserDO dbSocialUser = randomPojo(SocialUserDO.class, socialUserDO -> {
+//            socialUserDO.setUserType(UserTypeEnum.ADMIN.getValue());
+//            socialUserDO.setType(randomEle(SocialTypeEnum.values()).getType());
+//        });
+//        socialUserMapper.insert(dbSocialUser);
+//        // 准备参数
+//        Long userId = randomLongId();
+//        Integer type = dbSocialUser.getType();
+//        AuthUser authUser = randomPojo(AuthUser.class);
+//        // mock 方法
+//
+//        // 调用
+//        socialService.bindSocialUser(userId, UserTypeEnum.ADMIN.getValue(), type, authUser);
+//        // 断言
+//        List<SocialUserDO> socialUsers = socialUserMapper.selectList("user_id", userId);
+//        assertEquals(1, socialUsers.size());
+//    }
+//
+//    private void assertBindSocialUser(SocialUserDO socialUser, AuthUser authUser, Long userId,
+//                                      Integer type) {
+//        assertEquals(authUser.getToken().getAccessToken(), socialUser.getToken());
+//        assertEquals(toJsonString(authUser.getToken()), socialUser.getRawTokenInfo());
+//        assertEquals(authUser.getNickname(), socialUser.getNickname());
+//        assertEquals(authUser.getAvatar(), socialUser.getAvatar());
+//        assertEquals(toJsonString(authUser.getRawUserInfo()), socialUser.getRawUserInfo());
+//        assertEquals(userId, socialUser.getUserId());
+//        assertEquals(UserTypeEnum.ADMIN.getValue(), socialUser.getUserType());
+//        assertEquals(type, socialUser.getType());
+//        assertEquals(authUser.getUuid(), socialUser.getOpenid());
+//        assertEquals(socialService.getAuthUserUnionId(authUser), socialUser.getUnionId());
+//    }
+//
+//    /**
+//     * 情况一,如果新老的 unionId 是一致的,无需解绑
+//     */
+//    @Test
+//    public void testUnbindOldSocialUser_no() {
+//        // mock 数据
+//        SocialUserDO oldSocialUser = randomPojo(SocialUserDO.class, socialUserDO -> {
+//            socialUserDO.setUserType(UserTypeEnum.ADMIN.getValue());
+//            socialUserDO.setType(randomEle(SocialTypeEnum.values()).getType());
+//        });
+//        socialUserMapper.insert(oldSocialUser);
+//        // 准备参数
+//        Long userId = oldSocialUser.getUserId();
+//        Integer type = oldSocialUser.getType();
+//        String newUnionId = oldSocialUser.getUnionId();
+//
+//        // 调用
+//        socialService.unbindOldSocialUser(userId, UserTypeEnum.ADMIN.getValue(), type, newUnionId);
+//        // 断言
+//        assertEquals(1L, socialUserMapper.selectCount(null).longValue());
+//    }
+//
+//
+//    /**
+//     * 情况二,如果新老的 unionId 不一致的,需解绑
+//     */
+//    @Test
+//    public void testUnbindOldSocialUser_yes() {
+//        // mock 数据
+//        SocialUserDO oldSocialUser = randomPojo(SocialUserDO.class, socialUserDO -> {
+//            socialUserDO.setUserType(UserTypeEnum.ADMIN.getValue());
+//            socialUserDO.setType(randomEle(SocialTypeEnum.values()).getType());
+//        });
+//        socialUserMapper.insert(oldSocialUser);
+//        // 准备参数
+//        Long userId = oldSocialUser.getUserId();
+//        Integer type = oldSocialUser.getType();
+//        String newUnionId = randomString(10);
+//
+//        // 调用
+//        socialService.unbindOldSocialUser(userId, UserTypeEnum.ADMIN.getValue(), type, newUnionId);
+//        // 断言
+//        assertEquals(0L, socialUserMapper.selectCount(null).longValue());
+//    }
 
 }