提交 | 用户 | 时间
e7c126 1 package com.iailab.module.system.service.social;
H 2
3 import cn.binarywang.wx.miniapp.api.WxMaService;
4 import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl;
5 import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo;
6 import cn.binarywang.wx.miniapp.config.impl.WxMaRedisBetterConfigImpl;
7 import cn.hutool.core.bean.BeanUtil;
8 import cn.hutool.core.lang.Assert;
9 import cn.hutool.core.util.ObjUtil;
10 import cn.hutool.core.util.ReflectUtil;
11 import com.iailab.framework.common.enums.CommonStatusEnum;
12 import com.iailab.framework.common.pojo.PageResult;
13 import com.iailab.framework.common.util.cache.CacheUtils;
14 import com.iailab.framework.common.util.http.HttpUtils;
15 import com.iailab.framework.common.util.object.BeanUtils;
16 import com.iailab.module.system.controller.admin.socail.vo.client.SocialClientPageReqVO;
17 import com.iailab.module.system.controller.admin.socail.vo.client.SocialClientSaveReqVO;
18 import com.iailab.module.system.dal.dataobject.social.SocialClientDO;
19 import com.iailab.module.system.dal.mysql.social.SocialClientMapper;
20 import com.iailab.module.system.enums.social.SocialTypeEnum;
21 import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaProperties;
22 import com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties;
23 import com.google.common.annotations.VisibleForTesting;
24 import com.google.common.cache.CacheLoader;
25 import com.google.common.cache.LoadingCache;
26 import com.xingyuv.jushauth.config.AuthConfig;
27 import com.xingyuv.jushauth.model.AuthCallback;
28 import com.xingyuv.jushauth.model.AuthResponse;
29 import com.xingyuv.jushauth.model.AuthUser;
30 import com.xingyuv.jushauth.request.AuthRequest;
31 import com.xingyuv.jushauth.utils.AuthStateUtils;
32 import com.xingyuv.justauth.AuthRequestFactory;
33 import lombok.SneakyThrows;
34 import lombok.extern.slf4j.Slf4j;
35 import me.chanjar.weixin.common.bean.WxJsapiSignature;
36 import me.chanjar.weixin.common.error.WxErrorException;
37 import me.chanjar.weixin.common.redis.RedisTemplateWxRedisOps;
38 import me.chanjar.weixin.mp.api.WxMpService;
39 import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
40 import me.chanjar.weixin.mp.config.impl.WxMpRedisConfigImpl;
325d2f 41 import org.springframework.beans.factory.annotation.Value;
e7c126 42 import org.springframework.data.redis.core.StringRedisTemplate;
H 43 import org.springframework.stereotype.Service;
44
45 import javax.annotation.Resource;
46 import java.time.Duration;
47 import java.util.Objects;
48
49 import static com.iailab.framework.common.exception.util.ServiceExceptionUtil.exception;
50 import static com.iailab.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache;
51 import static com.iailab.framework.common.util.json.JsonUtils.toJsonString;
52 import static com.iailab.module.system.enums.ErrorCodeConstants.*;
53
54 /**
55  * 社交应用 Service 实现类
56  *
57  * @author iailab
58  */
59 @Service
60 @Slf4j
61 public class SocialClientServiceImpl implements SocialClientService {
62
325d2f 63     /**
H 64      * 小程序版本
65      *
66      * 1. release:正式版
67      * 2. trial:体验版
68      * 3. developer:开发版
69      */
70     @Value("${yudao.wxa-code.env-version:release}")
71     public String envVersion;
72
e7c126 73     @Resource
H 74     private AuthRequestFactory authRequestFactory;
75
76     @Resource
77     private WxMpService wxMpService;
78     @Resource
79     private WxMpProperties wxMpProperties;
80     @Resource
81     private StringRedisTemplate stringRedisTemplate; // WxMpService 需要使用到,所以在 Service 注入了它
82     /**
83      * 缓存 WxMpService 对象
84      *
85      * key:使用微信公众号的 appId + secret 拼接,即 {@link SocialClientDO} 的 clientId 和 clientSecret 属性。
86      * 为什么 key 使用这种格式?因为 {@link SocialClientDO} 在管理后台可以变更,通过这个 key 存储它的单例。
87      *
88      * 为什么要做 WxMpService 缓存?因为 WxMpService 构建成本比较大,所以尽量保证它是单例。
89      */
90     private final LoadingCache<String, WxMpService> wxMpServiceCache = buildAsyncReloadingCache(
91             Duration.ofSeconds(10L),
92             new CacheLoader<String, WxMpService>() {
93
94                 @Override
95                 public WxMpService load(String key) {
96                     String[] keys = key.split(":");
97                     return buildWxMpService(keys[0], keys[1]);
98                 }
99
100             });
101
102     @Resource
103     private WxMaService wxMaService;
104     @Resource
105     private WxMaProperties wxMaProperties;
106     /**
107      * 缓存 WxMaService 对象
108      *
109      * 说明同 {@link #wxMpServiceCache} 变量
110      */
111     private final LoadingCache<String, WxMaService> wxMaServiceCache = buildAsyncReloadingCache(
112             Duration.ofSeconds(10L),
113             new CacheLoader<String, WxMaService>() {
114
115                 @Override
116                 public WxMaService load(String key) {
117                     String[] keys = key.split(":");
118                     return buildWxMaService(keys[0], keys[1]);
119                 }
120
121             });
122
123     @Resource
124     private SocialClientMapper socialClientMapper;
125
126     @Override
127     public String getAuthorizeUrl(Integer socialType, Integer userType, String redirectUri) {
128         // 获得对应的 AuthRequest 实现
129         AuthRequest authRequest = buildAuthRequest(socialType, userType);
130         // 生成跳转地址
131         String authorizeUri = authRequest.authorize(AuthStateUtils.createState());
132         return HttpUtils.replaceUrlQuery(authorizeUri, "redirect_uri", redirectUri);
133     }
134
135     @Override
136     public AuthUser getAuthUser(Integer socialType, Integer userType, String code, String state) {
137         // 构建请求
138         AuthRequest authRequest = buildAuthRequest(socialType, userType);
139         AuthCallback authCallback = AuthCallback.builder().code(code).state(state).build();
140         // 执行请求
141         AuthResponse<?> authResponse = authRequest.login(authCallback);
142         log.info("[getAuthUser][请求社交平台 type({}) request({}) response({})]", socialType,
143                 toJsonString(authCallback), toJsonString(authResponse));
144         if (!authResponse.ok()) {
145             throw exception(SOCIAL_USER_AUTH_FAILURE, authResponse.getMsg());
146         }
147         return (AuthUser) authResponse.getData();
148     }
149
150     /**
151      * 构建 AuthRequest 对象,支持多租户配置
152      *
153      * @param socialType 社交类型
154      * @param userType 用户类型
155      * @return AuthRequest 对象
156      */
157     @VisibleForTesting
158     AuthRequest buildAuthRequest(Integer socialType, Integer userType) {
159         // 1. 先查找默认的配置项,从 application-*.yaml 中读取
160         AuthRequest request = authRequestFactory.get(SocialTypeEnum.valueOfType(socialType).getSource());
161         Assert.notNull(request, String.format("社交平台(%d) 不存在", socialType));
162         // 2. 查询 DB 的配置项,如果存在则进行覆盖
163         SocialClientDO client = socialClientMapper.selectBySocialTypeAndUserType(socialType, userType);
164         if (client != null && Objects.equals(client.getStatus(), CommonStatusEnum.ENABLE.getStatus())) {
165             // 2.1 构造新的 AuthConfig 对象
166             AuthConfig authConfig = (AuthConfig) ReflectUtil.getFieldValue(request, "config");
167             AuthConfig newAuthConfig = ReflectUtil.newInstance(authConfig.getClass());
168             BeanUtil.copyProperties(authConfig, newAuthConfig);
169             // 2.2 修改对应的 clientId + clientSecret 密钥
170             newAuthConfig.setClientId(client.getClientId());
171             newAuthConfig.setClientSecret(client.getClientSecret());
172             if (client.getAgentId() != null) { // 如果有 agentId 则修改 agentId
173                 newAuthConfig.setAgentId(client.getAgentId());
174             }
175             // 2.3 设置会 request 里,进行后续使用
176             ReflectUtil.setFieldValue(request, "config", newAuthConfig);
177         }
178         return request;
179     }
180
181     // =================== 微信公众号独有 ===================
182
183     @Override
184     @SneakyThrows
185     public WxJsapiSignature createWxMpJsapiSignature(Integer userType, String url) {
186         WxMpService service = getWxMpService(userType);
187         return service.createJsapiSignature(url);
188     }
189
190     /**
191      * 获得 clientId + clientSecret 对应的 WxMpService 对象
192      *
193      * @param userType 用户类型
194      * @return WxMpService 对象
195      */
196     @VisibleForTesting
197     WxMpService getWxMpService(Integer userType) {
198         // 第一步,查询 DB 的配置项,获得对应的 WxMpService 对象
199         SocialClientDO client = socialClientMapper.selectBySocialTypeAndUserType(
200                 SocialTypeEnum.WECHAT_MP.getType(), userType);
201         if (client != null && Objects.equals(client.getStatus(), CommonStatusEnum.ENABLE.getStatus())) {
202             return wxMpServiceCache.getUnchecked(client.getClientId() + ":" + client.getClientSecret());
203         }
204         // 第二步,不存在 DB 配置项,则使用 application-*.yaml 对应的 WxMpService 对象
205         return wxMpService;
206     }
207
208     /**
209      * 创建 clientId + clientSecret 对应的 WxMpService 对象
210      *
211      * @param clientId 微信公众号 appId
212      * @param clientSecret 微信公众号 secret
213      * @return WxMpService 对象
214      */
215     public WxMpService buildWxMpService(String clientId, String clientSecret) {
216         // 第一步,创建 WxMpRedisConfigImpl 对象
217         WxMpRedisConfigImpl configStorage = new WxMpRedisConfigImpl(
218                 new RedisTemplateWxRedisOps(stringRedisTemplate),
219                 wxMpProperties.getConfigStorage().getKeyPrefix());
220         configStorage.setAppId(clientId);
221         configStorage.setSecret(clientSecret);
222
223         // 第二步,创建 WxMpService 对象
224         WxMpService service = new WxMpServiceImpl();
225         service.setWxMpConfigStorage(configStorage);
226         return service;
227     }
228
229     // =================== 微信小程序独有 ===================
230
231     @Override
232     public WxMaPhoneNumberInfo getWxMaPhoneNumberInfo(Integer userType, String phoneCode) {
233         WxMaService service = getWxMaService(userType);
234         try {
235             return service.getUserService().getPhoneNoInfo(phoneCode);
236         } catch (WxErrorException e) {
237             log.error("[getPhoneNoInfo][userType({}) phoneCode({}) 获得手机号失败]", userType, phoneCode, e);
238             throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_PHONE_CODE_ERROR);
239         }
240     }
241
242     /**
243      * 获得 clientId + clientSecret 对应的 WxMpService 对象
244      *
245      * @param userType 用户类型
246      * @return WxMpService 对象
247      */
248     @VisibleForTesting
249     WxMaService getWxMaService(Integer userType) {
250         // 第一步,查询 DB 的配置项,获得对应的 WxMaService 对象
251         SocialClientDO client = socialClientMapper.selectBySocialTypeAndUserType(
252                 SocialTypeEnum.WECHAT_MINI_APP.getType(), userType);
253         if (client != null && Objects.equals(client.getStatus(), CommonStatusEnum.ENABLE.getStatus())) {
254             return wxMaServiceCache.getUnchecked(client.getClientId() + ":" + client.getClientSecret());
255         }
256         // 第二步,不存在 DB 配置项,则使用 application-*.yaml 对应的 WxMaService 对象
257         return wxMaService;
258     }
259
260     /**
261      * 创建 clientId + clientSecret 对应的 WxMaService 对象
262      *
263      * @param clientId 微信小程序 appId
264      * @param clientSecret 微信小程序 secret
265      * @return WxMaService 对象
266      */
267     private WxMaService buildWxMaService(String clientId, String clientSecret) {
268         // 第一步,创建 WxMaRedisBetterConfigImpl 对象
269         WxMaRedisBetterConfigImpl configStorage = new WxMaRedisBetterConfigImpl(
270                 new RedisTemplateWxRedisOps(stringRedisTemplate),
271                 wxMaProperties.getConfigStorage().getKeyPrefix());
272         configStorage.setAppid(clientId);
273         configStorage.setSecret(clientSecret);
274
275         // 第二步,创建 WxMpService 对象
276         WxMaService service = new WxMaServiceImpl();
277         service.setWxMaConfig(configStorage);
278         return service;
279     }
280
281     // =================== 客户端管理 ===================
282
283     @Override
284     public Long createSocialClient(SocialClientSaveReqVO createReqVO) {
285         // 校验重复
286         validateSocialClientUnique(null, createReqVO.getUserType(), createReqVO.getSocialType());
287
288         // 插入
289         SocialClientDO client = BeanUtils.toBean(createReqVO, SocialClientDO.class);
290         socialClientMapper.insert(client);
291         return client.getId();
292     }
293
294     @Override
295     public void updateSocialClient(SocialClientSaveReqVO updateReqVO) {
296         // 校验存在
297         validateSocialClientExists(updateReqVO.getId());
298         // 校验重复
299         validateSocialClientUnique(updateReqVO.getId(), updateReqVO.getUserType(), updateReqVO.getSocialType());
300
301         // 更新
302         SocialClientDO updateObj = BeanUtils.toBean(updateReqVO, SocialClientDO.class);
303         socialClientMapper.updateById(updateObj);
304     }
305
306     @Override
307     public void deleteSocialClient(Long id) {
308         // 校验存在
309         validateSocialClientExists(id);
310         // 删除
311         socialClientMapper.deleteById(id);
312     }
313
314     private void validateSocialClientExists(Long id) {
315         if (socialClientMapper.selectById(id) == null) {
316             throw exception(SOCIAL_CLIENT_NOT_EXISTS);
317         }
318     }
319
320     /**
321      * 校验社交应用是否重复,需要保证 userType + socialType 唯一
322      *
323      * 原因是,不同端(userType)选择某个社交登录(socialType)时,需要通过 {@link #buildAuthRequest(Integer, Integer)} 构建对应的请求
324      *
325      * @param id 编号
326      * @param userType 用户类型
327      * @param socialType 社交类型
328      */
329     private void validateSocialClientUnique(Long id, Integer userType, Integer socialType) {
330         SocialClientDO client = socialClientMapper.selectBySocialTypeAndUserType(
331                 socialType, userType);
332         if (client == null) {
333             return;
334         }
335         if (id == null // 新增时,说明重复
336                 || ObjUtil.notEqual(id, client.getId())) { // 更新时,如果 id 不一致,说明重复
337             throw exception(SOCIAL_CLIENT_UNIQUE);
338         }
339     }
340
341     @Override
342     public SocialClientDO getSocialClient(Long id) {
343         return socialClientMapper.selectById(id);
344     }
345
346     @Override
347     public PageResult<SocialClientDO> getSocialClientPage(SocialClientPageReqVO pageReqVO) {
348         return socialClientMapper.selectPage(pageReqVO);
349     }
350
351 }