提交 | 用户 | 时间
|
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 |
} |