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