package com.iailab.module.system.service.social; import cn.binarywang.wx.miniapp.api.WxMaService; import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl; import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo; import cn.binarywang.wx.miniapp.config.impl.WxMaRedisBetterConfigImpl; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.ReflectUtil; import com.iailab.framework.common.enums.CommonStatusEnum; import com.iailab.framework.common.pojo.PageResult; import com.iailab.framework.common.util.cache.CacheUtils; import com.iailab.framework.common.util.http.HttpUtils; import com.iailab.framework.common.util.object.BeanUtils; import com.iailab.module.system.controller.admin.socail.vo.client.SocialClientPageReqVO; import com.iailab.module.system.controller.admin.socail.vo.client.SocialClientSaveReqVO; import com.iailab.module.system.dal.dataobject.social.SocialClientDO; import com.iailab.module.system.dal.mysql.social.SocialClientMapper; import com.iailab.module.system.enums.social.SocialTypeEnum; 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 com.xingyuv.jushauth.config.AuthConfig; import com.xingyuv.jushauth.model.AuthCallback; import com.xingyuv.jushauth.model.AuthResponse; import com.xingyuv.jushauth.model.AuthUser; import com.xingyuv.jushauth.request.AuthRequest; import com.xingyuv.jushauth.utils.AuthStateUtils; import com.xingyuv.justauth.AuthRequestFactory; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import me.chanjar.weixin.common.bean.WxJsapiSignature; 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 org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.time.Duration; import java.util.Objects; import static com.iailab.framework.common.exception.util.ServiceExceptionUtil.exception; import static com.iailab.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache; import static com.iailab.framework.common.util.json.JsonUtils.toJsonString; import static com.iailab.module.system.enums.ErrorCodeConstants.*; /** * 社交应用 Service 实现类 * * @author iailab */ @Service @Slf4j public class SocialClientServiceImpl implements SocialClientService { /** * å°ç¨‹åºç‰ˆæœ¬ * * 1. release:æ£å¼ç‰ˆ * 2. trial:体验版 * 3. developer:开å‘版 */ @Value("${yudao.wxa-code.env-version:release}") public String envVersion; @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<String, WxMpService> wxMpServiceCache = buildAsyncReloadingCache( Duration.ofSeconds(10L), new CacheLoader<String, WxMpService>() { @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<String, WxMaService> wxMaServiceCache = buildAsyncReloadingCache( Duration.ofSeconds(10L), new CacheLoader<String, WxMaService>() { @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().getPhoneNoInfo(phoneCode); } catch (WxErrorException e) { log.error("[getPhoneNoInfo][userType({}) phoneCode({}) 获得手机å·å¤±è´¥]", userType, phoneCode, e); throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_PHONE_CODE_ERROR); } } /** * 获得 clientId + clientSecret 对应的 WxMpService 对象 * * @param userType 用户类型 * @return WxMpService 对象 */ @VisibleForTesting WxMaService getWxMaService(Integer userType) { // 第一æ¥ï¼ŒæŸ¥è¯¢ DB çš„é…置项,获得对应的 WxMaService 对象 SocialClientDO client = socialClientMapper.selectBySocialTypeAndUserType( SocialTypeEnum.WECHAT_MINI_APP.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); } 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<SocialClientDO> getSocialClientPage(SocialClientPageReqVO pageReqVO) { return socialClientMapper.selectPage(pageReqVO); } }