justAuthRedisCacheTemplate,
+ JustAuthProperties justAuthProperties) {
+ return new RedisStateCache(justAuthRedisCacheTemplate, justAuthProperties.getCache());
+ }
+
+}
diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/justauth/core/AuthRequestFactory.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/justauth/core/AuthRequestFactory.java
new file mode 100644
index 0000000..afd37ed
--- /dev/null
+++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/justauth/core/AuthRequestFactory.java
@@ -0,0 +1,322 @@
+/*
+ * Copyright (c) 2019-2029, xkcoding & Yangkai.Shen & 沈扬凯 (237497819@qq.com & xkcoding.com).
+ *
+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.gnu.org/licenses/lgpl.html
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package cn.iocoder.yudao.module.system.framework.justauth.core;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.EnumUtil;
+import cn.hutool.core.util.ReflectUtil;
+import cn.hutool.core.util.StrUtil;
+import com.xkcoding.http.config.HttpConfig;
+import com.xkcoding.justauth.autoconfigure.ExtendProperties;
+import com.xkcoding.justauth.autoconfigure.JustAuthProperties;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import me.zhyd.oauth.cache.AuthStateCache;
+import me.zhyd.oauth.config.AuthConfig;
+import me.zhyd.oauth.config.AuthDefaultSource;
+import me.zhyd.oauth.config.AuthSource;
+import me.zhyd.oauth.enums.AuthResponseStatus;
+import me.zhyd.oauth.exception.AuthException;
+import me.zhyd.oauth.request.*;
+import org.springframework.util.CollectionUtils;
+
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+// TODO @芋艿:等官方发布 1.4.1!!!
+
+/**
+ *
+ * AuthRequest工厂类
+ *
+ *
+ * @author yangkai.shen
+ * @date Created in 2019-07-22 14:21
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class AuthRequestFactory {
+ private final JustAuthProperties properties;
+ private final AuthStateCache authStateCache;
+
+ /**
+ * 返回当前Oauth列表
+ *
+ * @return Oauth列表
+ */
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ public List oauthList() {
+ // 默认列表
+ List defaultList = new ArrayList<>(properties.getType().keySet());
+ // 扩展列表
+ List extendList = new ArrayList<>();
+ ExtendProperties extend = properties.getExtend();
+ if (null != extend) {
+ Class enumClass = extend.getEnumClass();
+ List names = EnumUtil.getNames(enumClass);
+ // 扩展列表
+ extendList = extend.getConfig()
+ .keySet()
+ .stream()
+ .filter(x -> names.contains(x.toUpperCase()))
+ .map(String::toUpperCase)
+ .collect(Collectors.toList());
+ }
+
+ // 合并
+ return (List) CollUtil.addAll(defaultList, extendList);
+ }
+
+ /**
+ * 返回AuthRequest对象
+ *
+ * @param source {@link AuthSource}
+ * @return {@link AuthRequest}
+ */
+ public AuthRequest get(String source) {
+ if (StrUtil.isBlank(source)) {
+ throw new AuthException(AuthResponseStatus.NO_AUTH_SOURCE);
+ }
+
+ // 获取 JustAuth 中已存在的
+ AuthRequest authRequest = getDefaultRequest(source);
+
+ // 如果获取不到则尝试取自定义的
+ if (authRequest == null) {
+ authRequest = getExtendRequest(properties.getExtend().getEnumClass(), source);
+ }
+
+ if (authRequest == null) {
+ throw new AuthException(AuthResponseStatus.UNSUPPORTED);
+ }
+
+ return authRequest;
+ }
+
+ /**
+ * 获取自定义的 request
+ *
+ * @param clazz 枚举类 {@link AuthSource}
+ * @param source {@link AuthSource}
+ * @return {@link AuthRequest}
+ */
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ private AuthRequest getExtendRequest(Class clazz, String source) {
+ String upperSource = source.toUpperCase();
+ try {
+ EnumUtil.fromString(clazz, upperSource);
+ } catch (IllegalArgumentException e) {
+ // 无自定义匹配
+ return null;
+ }
+
+ Map extendConfig = properties.getExtend().getConfig();
+
+ // key 转大写
+ Map upperConfig = new HashMap<>(6);
+ extendConfig.forEach((k, v) -> upperConfig.put(k.toUpperCase(), v));
+
+ ExtendProperties.ExtendRequestConfig extendRequestConfig = upperConfig.get(upperSource);
+ if (extendRequestConfig != null) {
+
+ // 配置 http config
+ configureHttpConfig(upperSource, extendRequestConfig, properties.getHttpConfig());
+
+ Class extends AuthRequest> requestClass = extendRequestConfig.getRequestClass();
+
+ if (requestClass != null) {
+ // 反射获取 Request 对象,所以必须实现 2 个参数的构造方法
+ return ReflectUtil.newInstance(requestClass, (AuthConfig) extendRequestConfig, authStateCache);
+ }
+ }
+
+ return null;
+ }
+
+
+ /**
+ * 获取默认的 Request
+ *
+ * @param source {@link AuthSource}
+ * @return {@link AuthRequest}
+ */
+ private AuthRequest getDefaultRequest(String source) {
+ AuthDefaultSource authDefaultSource;
+
+ try {
+ authDefaultSource = EnumUtil.fromString(AuthDefaultSource.class, source.toUpperCase());
+ } catch (IllegalArgumentException e) {
+ // 无自定义匹配
+ return null;
+ }
+
+ AuthConfig config = properties.getType().get(authDefaultSource.name());
+ // 找不到对应关系,直接返回空
+ if (config == null) {
+ return null;
+ }
+
+ // 配置 http config
+ configureHttpConfig(authDefaultSource.name(), config, properties.getHttpConfig());
+
+ switch (authDefaultSource) {
+ case GITHUB:
+ return new AuthGithubRequest(config, authStateCache);
+ case WEIBO:
+ return new AuthWeiboRequest(config, authStateCache);
+ case GITEE:
+ return new AuthGiteeRequest(config, authStateCache);
+ case DINGTALK:
+ return new AuthDingTalkRequest(config, authStateCache);
+ case DINGTALK_V2:
+ return new AuthDingTalkV2Request(config, authStateCache);
+ case DINGTALK_ACCOUNT:
+ return new AuthDingTalkAccountRequest(config, authStateCache);
+ case BAIDU:
+ return new AuthBaiduRequest(config, authStateCache);
+ case CSDN:
+ return new AuthCsdnRequest(config, authStateCache);
+ case CODING:
+ return new AuthCodingRequest(config, authStateCache);
+ case OSCHINA:
+ return new AuthOschinaRequest(config, authStateCache);
+ case ALIPAY:
+ return new AuthAlipayRequest(config, authStateCache);
+ case QQ:
+ return new AuthQqRequest(config, authStateCache);
+ case WECHAT_OPEN:
+ return new AuthWeChatOpenRequest(config, authStateCache);
+ case WECHAT_MP:
+ return new AuthWeChatMpRequest(config, authStateCache);
+ case TAOBAO:
+ return new AuthTaobaoRequest(config, authStateCache);
+ case GOOGLE:
+ return new AuthGoogleRequest(config, authStateCache);
+ case FACEBOOK:
+ return new AuthFacebookRequest(config, authStateCache);
+ case DOUYIN:
+ return new AuthDouyinRequest(config, authStateCache);
+ case LINKEDIN:
+ return new AuthLinkedinRequest(config, authStateCache);
+ case MICROSOFT:
+ return new AuthMicrosoftRequest(config, authStateCache);
+ case MICROSOFT_CN:
+ return new AuthMicrosoftCnRequest(config, authStateCache);
+
+ case MI:
+ return new AuthMiRequest(config, authStateCache);
+ case TOUTIAO:
+ return new AuthToutiaoRequest(config, authStateCache);
+ case TEAMBITION:
+ return new AuthTeambitionRequest(config, authStateCache);
+ case RENREN:
+ return new AuthRenrenRequest(config, authStateCache);
+ case PINTEREST:
+ return new AuthPinterestRequest(config, authStateCache);
+ case STACK_OVERFLOW:
+ return new AuthStackOverflowRequest(config, authStateCache);
+ case HUAWEI:
+ return new AuthHuaweiRequest(config, authStateCache);
+ case HUAWEI_V3:
+ return new AuthHuaweiV3Request(config, authStateCache);
+ case WECHAT_ENTERPRISE:
+ return new AuthWeChatEnterpriseQrcodeRequest(config, authStateCache);
+ case WECHAT_ENTERPRISE_V2:
+ return new AuthWeChatEnterpriseQrcodeV2Request(config, authStateCache);
+ case WECHAT_ENTERPRISE_QRCODE_THIRD:
+ return new AuthWeChatEnterpriseThirdQrcodeRequest(config, authStateCache);
+ case WECHAT_ENTERPRISE_WEB:
+ return new AuthWeChatEnterpriseWebRequest(config, authStateCache);
+ case KUJIALE:
+ return new AuthKujialeRequest(config, authStateCache);
+ case GITLAB:
+ return new AuthGitlabRequest(config, authStateCache);
+ case MEITUAN:
+ return new AuthMeituanRequest(config, authStateCache);
+ case ELEME:
+ return new AuthElemeRequest(config, authStateCache);
+ case TWITTER:
+ return new AuthTwitterRequest(config, authStateCache);
+ case FEISHU:
+ return new AuthFeishuRequest(config, authStateCache);
+ case JD:
+ return new AuthJdRequest(config, authStateCache);
+ case ALIYUN:
+ return new AuthAliyunRequest(config, authStateCache);
+ case XMLY:
+ return new AuthXmlyRequest(config, authStateCache);
+ case AMAZON:
+ return new AuthAmazonRequest(config, authStateCache);
+ case SLACK:
+ return new AuthSlackRequest(config, authStateCache);
+ case LINE:
+ return new AuthLineRequest(config, authStateCache);
+ case OKTA:
+ return new AuthOktaRequest(config, authStateCache);
+ case PROGINN:
+ return new AuthProginnRequest(config,authStateCache);
+ case AFDIAN:
+ return new AuthAfDianRequest(config,authStateCache);
+ case APPLE:
+ return new AuthAppleRequest(config,authStateCache);
+ case FIGMA:
+ return new AuthFigmaRequest(config,authStateCache);
+ case WECHAT_MINI_PROGRAM:
+ config.setIgnoreCheckRedirectUri(true);
+ config.setIgnoreCheckState(true);
+ return new AuthWechatMiniProgramRequest(config, authStateCache);
+ case QQ_MINI_PROGRAM:
+ config.setIgnoreCheckRedirectUri(true);
+ config.setIgnoreCheckState(true);
+ return new AuthQQMiniProgramRequest(config, authStateCache);
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * 配置 http 相关的配置
+ *
+ * @param authSource {@link AuthSource}
+ * @param authConfig {@link AuthConfig}
+ */
+ private void configureHttpConfig(String authSource, AuthConfig authConfig, JustAuthProperties.JustAuthHttpConfig httpConfig) {
+ if (null == httpConfig) {
+ return;
+ }
+ Map proxyConfigMap = httpConfig.getProxy();
+ if (CollectionUtils.isEmpty(proxyConfigMap)) {
+ return;
+ }
+ JustAuthProperties.JustAuthProxyConfig proxyConfig = proxyConfigMap.get(authSource);
+
+ if (null == proxyConfig) {
+ return;
+ }
+
+ authConfig.setHttpConfig(HttpConfig.builder()
+ .timeout(httpConfig.getTimeout())
+ .proxy(new Proxy(Proxy.Type.valueOf(proxyConfig.getType()), new InetSocketAddress(proxyConfig.getHostname(), proxyConfig.getPort())))
+ .build());
+ }
+}
\ No newline at end of file
diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/justauth/package-info.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/justauth/package-info.java
new file mode 100644
index 0000000..e9af3ab
--- /dev/null
+++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/justauth/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * justauth 三方登录的拓展
+ *
+ * @author 芋道源码
+ */
+package cn.iocoder.yudao.module.system.framework.justauth;
\ No newline at end of file
diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthService.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthService.java
index 8c30958..5916c78 100644
--- a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthService.java
+++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthService.java
@@ -56,6 +56,14 @@ public interface AdminAuthService {
/**
+ * 社交快捷登录,使用 code 授权码
+ *
+ * @param reqVO 登录信息
+ * @return 登录结果
+ */
+ AuthLoginRespVO socialLogin(@Valid AuthSocialLoginReqVO reqVO);
+
+ /**
* 刷新访问令牌
*
* @param refreshToken 刷新令牌
diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java
index 0159f9b..b1aac03 100644
--- a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java
+++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java
@@ -9,6 +9,7 @@ import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
import cn.iocoder.yudao.module.system.api.logger.dto.LoginLogCreateReqDTO;
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.dto.SocialUserRespDTO;
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;
@@ -20,6 +21,7 @@ import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum;
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.social.SocialUserService;
import cn.iocoder.yudao.module.system.service.user.AdminUserService;
import com.anji.captcha.model.common.ResponseModel;
import com.anji.captcha.model.vo.CaptchaVO;
@@ -63,6 +65,8 @@ public class AdminAuthServiceImpl implements AdminAuthService {
private CaptchaService captchaService;
@Resource
private SmsCodeApi smsCodeApi;
+ @Resource
+ private SocialUserService socialUserService;
/**
* 验证码的开关,默认为 true
@@ -138,6 +142,25 @@ public class AdminAuthServiceImpl implements AdminAuthService {
return createTokenAfterLoginSuccess(user.getId(), reqVO.getMobile(), LoginLogTypeEnum.LOGIN_MOBILE);
}
+ @Override
+ public AuthLoginRespVO socialLogin(AuthSocialLoginReqVO reqVO) {
+ // 使用 code 授权码,进行登录。然后,获得到绑定的用户编号
+ SocialUserRespDTO socialUser = socialUserService.getSocialUserByCode(UserTypeEnum.ADMIN.getValue(), reqVO.getType(),
+ reqVO.getCode(), reqVO.getState());
+ if (socialUser == null || socialUser.getUserId() == null) {
+ throw exception(AUTH_THIRD_LOGIN_NOT_BIND);
+ }
+
+ // 获得用户
+ AdminUserDO user = userService.getUser(socialUser.getUserId());
+ if (user == null) {
+ throw exception(USER_NOT_EXISTS);
+ }
+
+ // 创建 Token 令牌,记录登录日志
+ return createTokenAfterLoginSuccess(user.getId(), user.getUsername(), LoginLogTypeEnum.LOGIN_SOCIAL);
+ }
+
private void createLoginLog(Long userId, String username,
LoginLogTypeEnum logTypeEnum, LoginResultEnum loginResult) {
// 插入登录日志
diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientService.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientService.java
new file mode 100644
index 0000000..ee08ccf
--- /dev/null
+++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientService.java
@@ -0,0 +1,160 @@
+package cn.iocoder.yudao.module.system.service.social;
+
+import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.system.api.social.dto.SocialWxQrcodeReqDTO;
+import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaOrderNotifyConfirmReceiveReqDTO;
+import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaOrderUploadShippingInfoReqDTO;
+import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaSubscribeMessageSendReqDTO;
+import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientPageReqVO;
+import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientSaveReqVO;
+import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialClientDO;
+import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
+import jakarta.validation.Valid;
+import me.chanjar.weixin.common.bean.WxJsapiSignature;
+import me.chanjar.weixin.common.bean.subscribemsg.TemplateInfo;
+import me.zhyd.oauth.model.AuthUser;
+
+import java.util.List;
+
+/**
+ * 社交应用 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface SocialClientService {
+
+ /**
+ * 获得社交平台的授权 URL
+ *
+ * @param socialType 社交平台的类型 {@link SocialTypeEnum}
+ * @param userType 用户类型
+ * @param redirectUri 重定向 URL
+ * @return 社交平台的授权 URL
+ */
+ String getAuthorizeUrl(Integer socialType, Integer userType, String redirectUri);
+
+ /**
+ * 请求社交平台,获得授权的用户
+ *
+ * @param socialType 社交平台的类型
+ * @param userType 用户类型
+ * @param code 授权码
+ * @param state 授权 state
+ * @return 授权的用户
+ */
+ AuthUser getAuthUser(Integer socialType, Integer userType, String code, String state);
+
+ // =================== 微信公众号独有 ===================
+
+ /**
+ * 创建微信公众号的 JS SDK 初始化所需的签名
+ *
+ * @param userType 用户类型
+ * @param url 访问的 URL 地址
+ * @return 签名
+ */
+ WxJsapiSignature createWxMpJsapiSignature(Integer userType, String url);
+
+ // =================== 微信小程序独有 ===================
+
+ /**
+ * 获得微信小程序的手机信息
+ *
+ * @param userType 用户类型
+ * @param phoneCode 手机授权码
+ * @return 手机信息
+ */
+ WxMaPhoneNumberInfo getWxMaPhoneNumberInfo(Integer userType, String phoneCode);
+
+ /**
+ * 获得小程序二维码
+ *
+ * @param reqVO 请求信息
+ * @return 小程序二维码
+ */
+ byte[] getWxaQrcode(SocialWxQrcodeReqDTO reqVO);
+
+ /**
+ * 获得微信小程订阅模板
+ *
+ * 缓存的目的:考虑到微信小程序订阅消息选择好模版后几乎不会变动,缓存增加查询效率
+ *
+ * @param userType 用户类型
+ * @return 微信小程订阅模板
+ */
+ List getSubscribeTemplateList(Integer userType);
+
+ /**
+ * 发送微信小程序订阅消息
+ *
+ * @param reqDTO 请求
+ * @param templateId 模版编号
+ * @param openId 会员 openId
+ */
+ void sendSubscribeMessage(SocialWxaSubscribeMessageSendReqDTO reqDTO, String templateId, String openId);
+
+ /**
+ * 上传订单发货到微信小程序
+ *
+ * @param userType 用户类型
+ * @param reqDTO 请求
+ */
+ void uploadWxaOrderShippingInfo(Integer userType, SocialWxaOrderUploadShippingInfoReqDTO reqDTO);
+
+ /**
+ * 通知订单收货到微信小程序
+ *
+ * @param userType 用户类型
+ * @param reqDTO 请求
+ */
+ void notifyWxaOrderConfirmReceive(Integer userType, SocialWxaOrderNotifyConfirmReceiveReqDTO reqDTO);
+
+ // =================== 客户端管理 ===================
+
+ /**
+ * 创建社交客户端
+ *
+ * @param createReqVO 创建信息
+ * @return 编号
+ */
+ Long createSocialClient(@Valid SocialClientSaveReqVO createReqVO);
+
+ /**
+ * 更新社交客户端
+ *
+ * @param updateReqVO 更新信息
+ */
+ void updateSocialClient(@Valid SocialClientSaveReqVO updateReqVO);
+
+ /**
+ * 删除社交客户端
+ *
+ * @param id 编号
+ */
+ void deleteSocialClient(Long id);
+
+ /**
+ * 批量删除社交客户端
+ *
+ * @param ids 编号数组
+ */
+ void deleteSocialClientList(List ids);
+
+ /**
+ * 获得社交客户端
+ *
+ * @param id 编号
+ * @return 社交客户端
+ */
+ SocialClientDO getSocialClient(Long id);
+
+ /**
+ * 获得社交客户端分页
+ *
+ * @param pageReqVO 分页查询
+ * @return 社交客户端分页
+ */
+ PageResult getSocialClientPage(SocialClientPageReqVO pageReqVO);
+
+}
diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImpl.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImpl.java
new file mode 100644
index 0000000..759c701
--- /dev/null
+++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImpl.java
@@ -0,0 +1,510 @@
+package cn.iocoder.yudao.module.system.service.social;
+
+import cn.binarywang.wx.miniapp.api.WxMaService;
+import cn.binarywang.wx.miniapp.api.WxMaSubscribeService;
+import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl;
+import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo;
+import cn.binarywang.wx.miniapp.bean.WxMaSubscribeMessage;
+import cn.binarywang.wx.miniapp.bean.shop.request.shipping.*;
+import cn.binarywang.wx.miniapp.bean.shop.response.WxMaOrderShippingInfoBaseResponse;
+import cn.binarywang.wx.miniapp.config.impl.WxMaRedisBetterConfigImpl;
+import cn.binarywang.wx.miniapp.constant.WxMaConstants;
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.DesensitizedUtil;
+import cn.hutool.core.util.ObjUtil;
+import cn.hutool.core.util.ReflectUtil;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.cache.CacheUtils;
+import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.system.api.social.dto.SocialWxQrcodeReqDTO;
+import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaOrderNotifyConfirmReceiveReqDTO;
+import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaOrderUploadShippingInfoReqDTO;
+import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaSubscribeMessageSendReqDTO;
+import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientPageReqVO;
+import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientSaveReqVO;
+import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialClientDO;
+import cn.iocoder.yudao.module.system.dal.mysql.social.SocialClientMapper;
+import cn.iocoder.yudao.module.system.dal.redis.RedisKeyConstants;
+import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
+import cn.iocoder.yudao.module.system.framework.justauth.core.AuthRequestFactory;
+import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaProperties;
+import com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import jakarta.annotation.Resource;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import me.chanjar.weixin.common.bean.WxJsapiSignature;
+import me.chanjar.weixin.common.bean.subscribemsg.TemplateInfo;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.redis.RedisTemplateWxRedisOps;
+import me.chanjar.weixin.mp.api.WxMpService;
+import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
+import me.chanjar.weixin.mp.config.impl.WxMpRedisConfigImpl;
+import me.zhyd.oauth.config.AuthConfig;
+import me.zhyd.oauth.model.AuthCallback;
+import me.zhyd.oauth.model.AuthResponse;
+import me.zhyd.oauth.model.AuthUser;
+import me.zhyd.oauth.request.AuthRequest;
+import me.zhyd.oauth.utils.AuthStateUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Service;
+
+import java.time.Duration;
+import java.time.ZonedDateTime;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
+import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.UTC_MS_WITH_XXX_OFFSET_FORMATTER;
+import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.toEpochSecond;
+import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
+import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
+import static java.util.Collections.singletonList;
+
+/**
+ * 社交应用 Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Slf4j
+public class SocialClientServiceImpl implements SocialClientService {
+
+ /**
+ * 小程序码要打开的小程序版本
+ *
+ * 1. release:正式版
+ * 2. trial:体验版
+ * 3. developer:开发版
+ */
+ @Value("${yudao.wxa-code.env-version:release}")
+ public String envVersion;
+ /**
+ * 订阅消息跳转小程序类型
+ *
+ * 1. developer:开发版
+ * 2. trial:体验版
+ * 3. formal:正式版
+ */
+ @Value("${yudao.wxa-subscribe-message.miniprogram-state:formal}")
+ public String miniprogramState;
+
+ @Resource
+ private AuthRequestFactory authRequestFactory;
+
+ @Resource
+ private WxMpService wxMpService;
+ @Resource
+ private WxMpProperties wxMpProperties;
+ @Resource
+ private StringRedisTemplate stringRedisTemplate; // WxMpService 需要使用到,所以在 Service 注入了它
+ /**
+ * 缓存 WxMpService 对象
+ *
+ * key:使用微信公众号的 appId + secret 拼接,即 {@link SocialClientDO} 的 clientId 和 clientSecret 属性。
+ * 为什么 key 使用这种格式?因为 {@link SocialClientDO} 在管理后台可以变更,通过这个 key 存储它的单例。
+ *
+ * 为什么要做 WxMpService 缓存?因为 WxMpService 构建成本比较大,所以尽量保证它是单例。
+ */
+ private final LoadingCache wxMpServiceCache = CacheUtils.buildAsyncReloadingCache(
+ Duration.ofSeconds(10L),
+ new CacheLoader() {
+
+ @Override
+ public WxMpService load(String key) {
+ String[] keys = key.split(":");
+ return buildWxMpService(keys[0], keys[1]);
+ }
+
+ });
+
+ @Resource
+ private WxMaService wxMaService;
+ @Resource
+ private WxMaProperties wxMaProperties;
+ /**
+ * 缓存 WxMaService 对象
+ *
+ * 说明同 {@link #wxMpServiceCache} 变量
+ */
+ private final LoadingCache wxMaServiceCache = CacheUtils.buildAsyncReloadingCache(
+ Duration.ofSeconds(10L),
+ new CacheLoader() {
+
+ @Override
+ public WxMaService load(String key) {
+ String[] keys = key.split(":");
+ return buildWxMaService(keys[0], keys[1]);
+ }
+
+ });
+
+ @Resource
+ private SocialClientMapper socialClientMapper;
+
+ @Override
+ public String getAuthorizeUrl(Integer socialType, Integer userType, String redirectUri) {
+ // 获得对应的 AuthRequest 实现
+ AuthRequest authRequest = buildAuthRequest(socialType, userType);
+ // 生成跳转地址
+ String authorizeUri = authRequest.authorize(AuthStateUtils.createState());
+ return HttpUtils.replaceUrlQuery(authorizeUri, "redirect_uri", redirectUri);
+ }
+
+ @Override
+ public AuthUser getAuthUser(Integer socialType, Integer userType, String code, String state) {
+ // 构建请求
+ AuthRequest authRequest = buildAuthRequest(socialType, userType);
+ AuthCallback authCallback = AuthCallback.builder().code(code).state(state).build();
+ // 执行请求
+ AuthResponse> authResponse = authRequest.login(authCallback);
+ log.info("[getAuthUser][请求社交平台 type({}) request({}) response({})]", socialType,
+ toJsonString(authCallback), toJsonString(authResponse));
+ if (!authResponse.ok()) {
+ throw exception(SOCIAL_USER_AUTH_FAILURE, authResponse.getMsg());
+ }
+ return (AuthUser) authResponse.getData();
+ }
+
+ /**
+ * 构建 AuthRequest 对象,支持多租户配置
+ *
+ * @param socialType 社交类型
+ * @param userType 用户类型
+ * @return AuthRequest 对象
+ */
+ @VisibleForTesting
+ AuthRequest buildAuthRequest(Integer socialType, Integer userType) {
+ // 1. 先查找默认的配置项,从 application-*.yaml 中读取
+ AuthRequest request = authRequestFactory.get(SocialTypeEnum.valueOfType(socialType).getSource());
+ Assert.notNull(request, String.format("社交平台(%d) 不存在", socialType));
+ // 2. 查询 DB 的配置项,如果存在则进行覆盖
+ SocialClientDO client = socialClientMapper.selectBySocialTypeAndUserType(socialType, userType);
+ if (client != null && Objects.equals(client.getStatus(), CommonStatusEnum.ENABLE.getStatus())) {
+ // 2.1 构造新的 AuthConfig 对象
+ AuthConfig authConfig = (AuthConfig) ReflectUtil.getFieldValue(request, "config");
+ AuthConfig newAuthConfig = ReflectUtil.newInstance(authConfig.getClass());
+ BeanUtil.copyProperties(authConfig, newAuthConfig);
+ // 2.2 修改对应的 clientId + clientSecret 密钥
+ newAuthConfig.setClientId(client.getClientId());
+ newAuthConfig.setClientSecret(client.getClientSecret());
+ if (client.getAgentId() != null) { // 如果有 agentId 则修改 agentId
+ newAuthConfig.setAgentId(client.getAgentId());
+ }
+ // 2.3 设置会 request 里,进行后续使用
+ ReflectUtil.setFieldValue(request, "config", newAuthConfig);
+ }
+ return request;
+ }
+
+ // =================== 微信公众号独有 ===================
+
+ @Override
+ @SneakyThrows
+ public WxJsapiSignature createWxMpJsapiSignature(Integer userType, String url) {
+ WxMpService service = getWxMpService(userType);
+ return service.createJsapiSignature(url);
+ }
+
+ /**
+ * 获得 clientId + clientSecret 对应的 WxMpService 对象
+ *
+ * @param userType 用户类型
+ * @return WxMpService 对象
+ */
+ @VisibleForTesting
+ WxMpService getWxMpService(Integer userType) {
+ // 第一步,查询 DB 的配置项,获得对应的 WxMpService 对象
+ SocialClientDO client = socialClientMapper.selectBySocialTypeAndUserType(
+ SocialTypeEnum.WECHAT_MP.getType(), userType);
+ if (client != null && Objects.equals(client.getStatus(), CommonStatusEnum.ENABLE.getStatus())) {
+ return wxMpServiceCache.getUnchecked(client.getClientId() + ":" + client.getClientSecret());
+ }
+ // 第二步,不存在 DB 配置项,则使用 application-*.yaml 对应的 WxMpService 对象
+ return wxMpService;
+ }
+
+ /**
+ * 创建 clientId + clientSecret 对应的 WxMpService 对象
+ *
+ * @param clientId 微信公众号 appId
+ * @param clientSecret 微信公众号 secret
+ * @return WxMpService 对象
+ */
+ public WxMpService buildWxMpService(String clientId, String clientSecret) {
+ // 第一步,创建 WxMpRedisConfigImpl 对象
+ WxMpRedisConfigImpl configStorage = new WxMpRedisConfigImpl(
+ new RedisTemplateWxRedisOps(stringRedisTemplate),
+ wxMpProperties.getConfigStorage().getKeyPrefix());
+ configStorage.setAppId(clientId);
+ configStorage.setSecret(clientSecret);
+
+ // 第二步,创建 WxMpService 对象
+ WxMpService service = new WxMpServiceImpl();
+ service.setWxMpConfigStorage(configStorage);
+ return service;
+ }
+
+ // =================== 微信小程序独有 ===================
+
+ @Override
+ public WxMaPhoneNumberInfo getWxMaPhoneNumberInfo(Integer userType, String phoneCode) {
+ WxMaService service = getWxMaService(userType);
+ try {
+ return service.getUserService().getPhoneNumber(phoneCode);
+ } catch (WxErrorException e) {
+ log.error("[getPhoneNumber][userType({}) phoneCode({}) 获得手机号失败]", userType, phoneCode, e);
+ throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_PHONE_CODE_ERROR);
+ }
+ }
+
+ @Override
+ public byte[] getWxaQrcode(SocialWxQrcodeReqDTO reqVO) {
+ WxMaService service = getWxMaService(UserTypeEnum.MEMBER.getValue());
+ try {
+ return service.getQrcodeService().createWxaCodeUnlimitBytes(
+ ObjUtil.defaultIfEmpty(reqVO.getScene(), SocialWxQrcodeReqDTO.SCENE),
+ reqVO.getPath(),
+ ObjUtil.defaultIfNull(reqVO.getCheckPath(), SocialWxQrcodeReqDTO.CHECK_PATH),
+ envVersion,
+ ObjUtil.defaultIfNull(reqVO.getWidth(), SocialWxQrcodeReqDTO.WIDTH),
+ ObjUtil.defaultIfNull(reqVO.getAutoColor(), SocialWxQrcodeReqDTO.AUTO_COLOR),
+ null,
+ ObjUtil.defaultIfNull(reqVO.getHyaline(), SocialWxQrcodeReqDTO.HYALINE));
+ } catch (WxErrorException e) {
+ log.error("[getWxQrcode][reqVO({}) 获得小程序码失败]", reqVO, e);
+ throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_QRCODE_ERROR);
+ }
+ }
+
+ @Override
+ @Cacheable(cacheNames = RedisKeyConstants.WXA_SUBSCRIBE_TEMPLATE, key = "#userType",
+ unless = "#result == null")
+ public List getSubscribeTemplateList(Integer userType) {
+ WxMaService service = getWxMaService(userType);
+ try {
+ WxMaSubscribeService subscribeService = service.getSubscribeService();
+ return subscribeService.getTemplateList();
+ } catch (WxErrorException e) {
+ log.error("[getSubscribeTemplate][userType({}) 获得小程序订阅消息模版]", userType, e);
+ throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_SUBSCRIBE_TEMPLATE_ERROR);
+ }
+ }
+
+ @Override
+ public void sendSubscribeMessage(SocialWxaSubscribeMessageSendReqDTO reqDTO, String templateId, String openId) {
+ WxMaService service = getWxMaService(reqDTO.getUserType());
+ try {
+ WxMaSubscribeService subscribeService = service.getSubscribeService();
+ subscribeService.sendSubscribeMsg(buildMessageSendReqDTO(reqDTO, templateId, openId));
+ } catch (WxErrorException e) {
+ log.error("[sendSubscribeMessage][reqVO({}) templateId({}) openId({}) 发送小程序订阅消息失败]", reqDTO, templateId, openId, e);
+ throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_SUBSCRIBE_MESSAGE_ERROR);
+ }
+ }
+
+ /**
+ * 构建发送消息请求参数
+ *
+ * @param reqDTO 请求
+ * @param templateId 模版编号
+ * @param openId 会员 openId
+ * @return 微信小程序订阅消息请求参数
+ */
+ private WxMaSubscribeMessage buildMessageSendReqDTO(SocialWxaSubscribeMessageSendReqDTO reqDTO,
+ String templateId, String openId) {
+ // 设置订阅消息基本参数
+ WxMaSubscribeMessage subscribeMessage = new WxMaSubscribeMessage().setLang(WxMaConstants.MiniProgramLang.ZH_CN)
+ .setMiniprogramState(miniprogramState).setTemplateId(templateId).setToUser(openId).setPage(reqDTO.getPage());
+ // 设置具体消息参数
+ Map messages = reqDTO.getMessages();
+ if (CollUtil.isNotEmpty(messages)) {
+ reqDTO.getMessages().keySet().forEach(key -> findAndThen(messages, key, value ->
+ subscribeMessage.addData(new WxMaSubscribeMessage.MsgData(key, value))));
+ }
+ return subscribeMessage;
+ }
+
+ @Override
+ public void uploadWxaOrderShippingInfo(Integer userType, SocialWxaOrderUploadShippingInfoReqDTO reqDTO) {
+ WxMaService service = getWxMaService(userType);
+ List shippingList;
+ if (Objects.equals(reqDTO.getLogisticsType(), SocialWxaOrderUploadShippingInfoReqDTO.LOGISTICS_TYPE_EXPRESS)) {
+ shippingList = singletonList(ShippingListBean.builder()
+ .trackingNo(reqDTO.getLogisticsNo())
+ .expressCompany(reqDTO.getExpressCompany())
+ .itemDesc(reqDTO.getItemDesc())
+ .contact(ContactBean.builder().receiverContact(DesensitizedUtil.mobilePhone(reqDTO.getReceiverContact())).build())
+ .build());
+ } else {
+ shippingList = singletonList(ShippingListBean.builder().itemDesc(reqDTO.getItemDesc()).build());
+ }
+ WxMaOrderShippingInfoUploadRequest request = WxMaOrderShippingInfoUploadRequest.builder()
+ .orderKey(OrderKeyBean.builder()
+ .orderNumberType(2) // 使用原支付交易对应的微信订单号,即渠道单号
+ .transactionId(reqDTO.getTransactionId())
+ .build())
+ .logisticsType(reqDTO.getLogisticsType()) // 配送方式
+ .deliveryMode(1) // 统一发货
+ .shippingList(shippingList)
+ .payer(PayerBean.builder().openid(reqDTO.getOpenid()).build())
+ .uploadTime(ZonedDateTime.now().format(UTC_MS_WITH_XXX_OFFSET_FORMATTER))
+ .build();
+ try {
+ WxMaOrderShippingInfoBaseResponse response = service.getWxMaOrderShippingService().upload(request);
+ if (response.getErrCode() != 0) {
+ log.error("[uploadWxaOrderShippingInfo][上传微信小程序发货信息失败:request({}) response({})]", request, response);
+ throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_ORDER_UPLOAD_SHIPPING_INFO_ERROR, response.getErrMsg());
+ }
+ log.info("[uploadWxaOrderShippingInfo][上传微信小程序发货信息成功:request({}) response({})]", request, response);
+ } catch (WxErrorException ex) {
+ log.error("[uploadWxaOrderShippingInfo][上传微信小程序发货信息失败:request({})]", request, ex);
+ throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_ORDER_UPLOAD_SHIPPING_INFO_ERROR, ex.getError().getErrorMsg());
+ }
+ }
+
+ @Override
+ public void notifyWxaOrderConfirmReceive(Integer userType, SocialWxaOrderNotifyConfirmReceiveReqDTO reqDTO) {
+ WxMaService service = getWxMaService(userType);
+ WxMaOrderShippingInfoNotifyConfirmRequest request = WxMaOrderShippingInfoNotifyConfirmRequest.builder()
+ .transactionId(reqDTO.getTransactionId())
+ .receivedTime(toEpochSecond(reqDTO.getReceivedTime()))
+ .build();
+ try {
+ WxMaOrderShippingInfoBaseResponse response = service.getWxMaOrderShippingService().notifyConfirmReceive(request);
+ if (response.getErrCode() != 0) {
+ log.error("[notifyWxaOrderConfirmReceive][确认收货提醒到微信小程序失败:request({}) response({})]", request, response);
+ throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_ORDER_NOTIFY_CONFIRM_RECEIVE_ERROR, response.getErrMsg());
+ }
+ log.info("[notifyWxaOrderConfirmReceive][确认收货提醒到微信小程序成功:request({}) response({})]", request, response);
+ } catch (WxErrorException ex) {
+ log.error("[notifyWxaOrderConfirmReceive][确认收货提醒到微信小程序失败:request({})]", request, ex);
+ throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_ORDER_NOTIFY_CONFIRM_RECEIVE_ERROR, ex.getError().getErrorMsg());
+ }
+ }
+
+ /**
+ * 获得 clientId + clientSecret 对应的 WxMpService 对象
+ *
+ * @param userType 用户类型
+ * @return WxMpService 对象
+ */
+ @VisibleForTesting
+ WxMaService getWxMaService(Integer userType) {
+ // 第一步,查询 DB 的配置项,获得对应的 WxMaService 对象
+ SocialClientDO client = socialClientMapper.selectBySocialTypeAndUserType(
+ SocialTypeEnum.WECHAT_MINI_PROGRAM.getType(), userType);
+ if (client != null && Objects.equals(client.getStatus(), CommonStatusEnum.ENABLE.getStatus())) {
+ return wxMaServiceCache.getUnchecked(client.getClientId() + ":" + client.getClientSecret());
+ }
+ // 第二步,不存在 DB 配置项,则使用 application-*.yaml 对应的 WxMaService 对象
+ return wxMaService;
+ }
+
+ /**
+ * 创建 clientId + clientSecret 对应的 WxMaService 对象
+ *
+ * @param clientId 微信小程序 appId
+ * @param clientSecret 微信小程序 secret
+ * @return WxMaService 对象
+ */
+ private WxMaService buildWxMaService(String clientId, String clientSecret) {
+ // 第一步,创建 WxMaRedisBetterConfigImpl 对象
+ WxMaRedisBetterConfigImpl configStorage = new WxMaRedisBetterConfigImpl(
+ new RedisTemplateWxRedisOps(stringRedisTemplate),
+ wxMaProperties.getConfigStorage().getKeyPrefix());
+ configStorage.setAppid(clientId);
+ configStorage.setSecret(clientSecret);
+
+ // 第二步,创建 WxMpService 对象
+ WxMaService service = new WxMaServiceImpl();
+ service.setWxMaConfig(configStorage);
+ return service;
+ }
+
+ // =================== 客户端管理 ===================
+
+ @Override
+ public Long createSocialClient(SocialClientSaveReqVO createReqVO) {
+ // 校验重复
+ validateSocialClientUnique(null, createReqVO.getUserType(), createReqVO.getSocialType());
+
+ // 插入
+ SocialClientDO client = BeanUtils.toBean(createReqVO, SocialClientDO.class);
+ socialClientMapper.insert(client);
+ return client.getId();
+ }
+
+ @Override
+ public void updateSocialClient(SocialClientSaveReqVO updateReqVO) {
+ // 校验存在
+ validateSocialClientExists(updateReqVO.getId());
+ // 校验重复
+ validateSocialClientUnique(updateReqVO.getId(), updateReqVO.getUserType(), updateReqVO.getSocialType());
+
+ // 更新
+ SocialClientDO updateObj = BeanUtils.toBean(updateReqVO, SocialClientDO.class);
+ socialClientMapper.updateById(updateObj);
+ }
+
+ @Override
+ public void deleteSocialClient(Long id) {
+ // 校验存在
+ validateSocialClientExists(id);
+ // 删除
+ socialClientMapper.deleteById(id);
+ }
+
+ @Override
+ public void deleteSocialClientList(List ids) {
+ socialClientMapper.deleteByIds(ids);
+ }
+
+ private void validateSocialClientExists(Long id) {
+ if (socialClientMapper.selectById(id) == null) {
+ throw exception(SOCIAL_CLIENT_NOT_EXISTS);
+ }
+ }
+
+ /**
+ * 校验社交应用是否重复,需要保证 userType + socialType 唯一
+ * 原因是,不同端(userType)选择某个社交登录(socialType)时,需要通过 {@link #buildAuthRequest(Integer, Integer)} 构建对应的请求
+ *
+ * @param id 编号
+ * @param userType 用户类型
+ * @param socialType 社交类型
+ */
+ private void validateSocialClientUnique(Long id, Integer userType, Integer socialType) {
+ SocialClientDO client = socialClientMapper.selectBySocialTypeAndUserType(
+ socialType, userType);
+ if (client == null) {
+ return;
+ }
+ if (id == null // 新增时,说明重复
+ || ObjUtil.notEqual(id, client.getId())) { // 更新时,如果 id 不一致,说明重复
+ throw exception(SOCIAL_CLIENT_UNIQUE);
+ }
+ }
+
+ @Override
+ public SocialClientDO getSocialClient(Long id) {
+ return socialClientMapper.selectById(id);
+ }
+
+ @Override
+ public PageResult getSocialClientPage(SocialClientPageReqVO pageReqVO) {
+ return socialClientMapper.selectPage(pageReqVO);
+ }
+
+}
diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialUserService.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialUserService.java
new file mode 100644
index 0000000..743e580
--- /dev/null
+++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialUserService.java
@@ -0,0 +1,89 @@
+package cn.iocoder.yudao.module.system.service.social;
+
+import cn.iocoder.yudao.framework.common.exception.ServiceException;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.system.api.social.dto.SocialUserBindReqDTO;
+import cn.iocoder.yudao.module.system.api.social.dto.SocialUserRespDTO;
+import cn.iocoder.yudao.module.system.controller.admin.socail.vo.user.SocialUserPageReqVO;
+import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialUserDO;
+import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
+import jakarta.validation.Valid;
+
+import java.util.List;
+
+/**
+ * 社交用户 Service 接口,例如说社交平台的授权登录
+ *
+ * @author 芋道源码
+ */
+public interface SocialUserService {
+
+ /**
+ * 获得指定用户的社交用户列表
+ *
+ * @param userId 用户编号
+ * @param userType 用户类型
+ * @return 社交用户列表
+ */
+ List getSocialUserList(Long userId, Integer userType);
+
+ /**
+ * 绑定社交用户
+ *
+ * @param reqDTO 绑定信息
+ * @return 社交用户 openid
+ */
+ String bindSocialUser(@Valid SocialUserBindReqDTO reqDTO);
+
+ /**
+ * 取消绑定社交用户
+ *
+ * @param userId 用户编号
+ * @param userType 全局用户类型
+ * @param socialType 社交平台的类型 {@link SocialTypeEnum}
+ * @param openid 社交平台的 openid
+ */
+ void unbindSocialUser(Long userId, Integer userType, Integer socialType, String openid);
+
+ /**
+ * 获得社交用户,基于 userId
+ *
+ * @param userType 用户类型
+ * @param userId 用户编号
+ * @param socialType 社交平台的类型
+ * @return 社交用户
+ */
+ SocialUserRespDTO getSocialUserByUserId(Integer userType, Long userId, Integer socialType);
+
+ /**
+ * 获得社交用户
+ *
+ * 在认证信息不正确的情况下,也会抛出 {@link ServiceException} 业务异常
+ *
+ * @param userType 用户类型
+ * @param socialType 社交平台的类型
+ * @param code 授权码
+ * @param state state
+ * @return 社交用户
+ */
+ SocialUserRespDTO getSocialUserByCode(Integer userType, Integer socialType, String code, String state);
+
+ // ==================== 社交用户 CRUD ====================
+
+ /**
+ * 获得社交用户
+ *
+ * @param id 编号
+ * @return 社交用户
+ */
+ SocialUserDO getSocialUser(Long id);
+
+ /**
+ * 获得社交用户分页
+ *
+ * @param pageReqVO 分页查询
+ * @return 社交用户分页
+ */
+ PageResult getSocialUserPage(SocialUserPageReqVO pageReqVO);
+
+}
diff --git a/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialUserServiceImpl.java b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialUserServiceImpl.java
new file mode 100644
index 0000000..476f31a
--- /dev/null
+++ b/cc-admin-master/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialUserServiceImpl.java
@@ -0,0 +1,173 @@
+package cn.iocoder.yudao.module.system.service.social;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.lang.Assert;
+import cn.iocoder.yudao.framework.common.exception.ServiceException;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.system.api.social.dto.SocialUserBindReqDTO;
+import cn.iocoder.yudao.module.system.api.social.dto.SocialUserRespDTO;
+import cn.iocoder.yudao.module.system.controller.admin.socail.vo.user.SocialUserPageReqVO;
+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.enums.social.SocialTypeEnum;
+import jakarta.annotation.Resource;
+import jakarta.validation.constraints.NotNull;
+import lombok.extern.slf4j.Slf4j;
+import me.zhyd.oauth.model.AuthUser;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.validation.annotation.Validated;
+
+import java.util.Collections;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
+import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.SOCIAL_USER_NOT_FOUND;
+
+/**
+ * 社交用户 Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Validated
+@Slf4j
+public class SocialUserServiceImpl implements SocialUserService {
+
+ @Resource
+ private SocialUserBindMapper socialUserBindMapper;
+ @Resource
+ private SocialUserMapper socialUserMapper;
+
+ @Resource
+ private SocialClientService socialClientService;
+
+ @Override
+ public List getSocialUserList(Long userId, Integer userType) {
+ // 获得绑定
+ List socialUserBinds = socialUserBindMapper.selectListByUserIdAndUserType(userId, userType);
+ if (CollUtil.isEmpty(socialUserBinds)) {
+ return Collections.emptyList();
+ }
+ // 获得社交用户
+ return socialUserMapper.selectByIds(convertSet(socialUserBinds, SocialUserBindDO::getSocialUserId));
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public String bindSocialUser(SocialUserBindReqDTO reqDTO) {
+ // 获得社交用户
+ SocialUserDO socialUser = authSocialUser(reqDTO.getSocialType(), reqDTO.getUserType(),
+ reqDTO.getCode(), reqDTO.getState());
+ Assert.notNull(socialUser, "社交用户不能为空");
+
+ // 社交用户可能之前绑定过别的用户,需要进行解绑
+ socialUserBindMapper.deleteByUserTypeAndSocialUserId(reqDTO.getUserType(), socialUser.getId());
+
+ // 用户可能之前已经绑定过该社交类型,需要进行解绑
+ socialUserBindMapper.deleteByUserTypeAndUserIdAndSocialType(reqDTO.getUserType(), reqDTO.getUserId(),
+ socialUser.getType());
+
+ // 绑定当前登录的社交用户
+ SocialUserBindDO socialUserBind = SocialUserBindDO.builder()
+ .userId(reqDTO.getUserId()).userType(reqDTO.getUserType())
+ .socialUserId(socialUser.getId()).socialType(socialUser.getType()).build();
+ socialUserBindMapper.insert(socialUserBind);
+ return socialUser.getOpenid();
+ }
+
+ @Override
+ public void unbindSocialUser(Long userId, Integer userType, Integer socialType, String openid) {
+ // 获得 openid 对应的 SocialUserDO 社交用户
+ SocialUserDO socialUser = socialUserMapper.selectByTypeAndOpenid(socialType, openid);
+ if (socialUser == null) {
+ throw exception(SOCIAL_USER_NOT_FOUND);
+ }
+
+ // 获得对应的社交绑定关系
+ socialUserBindMapper.deleteByUserTypeAndUserIdAndSocialType(userType, userId, socialUser.getType());
+ }
+
+ @Override
+ public SocialUserRespDTO getSocialUserByUserId(Integer userType, Long userId, Integer socialType) {
+ // 获得绑定用户
+ SocialUserBindDO socialUserBind = socialUserBindMapper.selectByUserIdAndUserTypeAndSocialType(userId, userType, socialType);
+ if (socialUserBind == null) {
+ return null;
+ }
+ // 获得社交用户
+ SocialUserDO socialUser = socialUserMapper.selectById(socialUserBind.getSocialUserId());
+ Assert.notNull(socialUser, "社交用户不能为空");
+ return new SocialUserRespDTO(socialUser.getOpenid(), socialUser.getNickname(), socialUser.getAvatar(),
+ socialUserBind.getUserId());
+ }
+
+ @Override
+ public SocialUserRespDTO getSocialUserByCode(Integer userType, Integer socialType, String code, String state) {
+ // 获得社交用户
+ SocialUserDO socialUser = authSocialUser(socialType, userType, code, state);
+ Assert.notNull(socialUser, "社交用户不能为空");
+
+ // 获得绑定用户
+ SocialUserBindDO socialUserBind = socialUserBindMapper.selectByUserTypeAndSocialUserId(userType,
+ socialUser.getId());
+ return new SocialUserRespDTO(socialUser.getOpenid(), socialUser.getNickname(), socialUser.getAvatar(),
+ socialUserBind != null ? socialUserBind.getUserId() : null);
+ }
+
+ /**
+ * 授权获得对应的社交用户
+ * 如果授权失败,则会抛出 {@link ServiceException} 异常
+ *
+ * @param socialType 社交平台的类型 {@link SocialTypeEnum}
+ * @param userType 用户类型
+ * @param code 授权码
+ * @param state state
+ * @return 授权用户
+ */
+ @NotNull
+ public SocialUserDO authSocialUser(Integer socialType, Integer userType, String code, String state) {
+ // 优先从 DB 中获取,因为 code 有且可以使用一次。
+ // 在社交登录时,当未绑定 User 时,需要绑定登录,此时需要 code 使用两次
+ SocialUserDO socialUser = socialUserMapper.selectByTypeAndCodeAnState(socialType, code, state);
+ if (socialUser != null) {
+ return socialUser;
+ }
+
+ // 请求获取
+ AuthUser authUser = socialClientService.getAuthUser(socialType, userType, code, state);
+ Assert.notNull(authUser, "三方用户不能为空");
+
+ // 保存到 DB 中
+ socialUser = socialUserMapper.selectByTypeAndOpenid(socialType, authUser.getUuid());
+ if (socialUser == null) {
+ socialUser = new SocialUserDO();
+ }
+ socialUser.setType(socialType).setCode(code).setState(state) // 需要保存 code + state 字段,保证后续可查询
+ .setOpenid(authUser.getUuid()).setToken(authUser.getToken().getAccessToken()).setRawTokenInfo((toJsonString(authUser.getToken())))
+ .setNickname(authUser.getNickname()).setAvatar(authUser.getAvatar()).setRawUserInfo(toJsonString(authUser.getRawUserInfo()));
+ if (socialUser.getId() == null) {
+ socialUserMapper.insert(socialUser);
+ } else {
+ socialUserMapper.updateById(socialUser);
+ }
+ return socialUser;
+ }
+
+ // ==================== 社交用户 CRUD ====================
+
+ @Override
+ public SocialUserDO getSocialUser(Long id) {
+ return socialUserMapper.selectById(id);
+ }
+
+ @Override
+ public PageResult getSocialUserPage(SocialUserPageReqVO pageReqVO) {
+ return socialUserMapper.selectPage(pageReqVO);
+ }
+
+}
diff --git a/cc-admin-master/yudao-server/src/main/resources/application-local.yaml b/cc-admin-master/yudao-server/src/main/resources/application-local.yaml
index 180dd96..bebd598 100644
--- a/cc-admin-master/yudao-server/src/main/resources/application-local.yaml
+++ b/cc-admin-master/yudao-server/src/main/resources/application-local.yaml
@@ -48,9 +48,9 @@ spring:
primary: master
datasource:
master:
- url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例
+ url: jdbc:mysql://192.168.0.180:3306/lock-dev?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例
username: root
- password: roomasd111
+ password: Gsking164411
driver-class-name: com.mysql.cj.jdbc.Driver # MySQL Connector/J 8.X 连接的示例
# tdengine:
# url: jdbc:TAOS-RS://192.168.0.180:6041/test
@@ -180,6 +180,12 @@ wx:
app-id: wxf56b1542b9e85f8a # 测试号(Kongdy 提供的)
secret: 496379dcef1ba869e9234de8d598cfd3
# 存储配置,解决 AccessToken 的跨节点的共享
+ cp:
+ # 你的企业ID
+ corpId: ww6e1eee0a8ae45397
+ agentId: 1000002
+ corpSecret: ITbfuoZkmUifGoDL5ZB8SyuMzVM8VXZNkfZJzYn5sGo
+
config-storage:
type: RedisTemplate # 采用 RedisTemplate 操作 Redis,会自动从 Spring 中获取
key-prefix: wx # Redis Key 的前缀