🆕 #1952 增加腾讯企点子模块,用于对接企点开放平台。

This commit is contained in:
fanxiayang12
2020-12-30 09:17:35 +08:00
committed by GitHub
parent a8232f6c91
commit e7f2bd62f8
54 changed files with 3928 additions and 12 deletions

View File

@@ -0,0 +1,13 @@
package me.chanjar.weixin.qidian.api;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.qidian.bean.call.GetSwitchBoardListResponse;
/**
* 通话数据相关操作接口.
*
* @author alegria
*/
public interface WxQidianCallDataService {
public GetSwitchBoardListResponse getSwitchBoardList() throws WxErrorException;
}

View File

@@ -0,0 +1,18 @@
package me.chanjar.weixin.qidian.api;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.qidian.bean.dial.IVRDialRequest;
import me.chanjar.weixin.qidian.bean.dial.IVRDialResponse;
import me.chanjar.weixin.qidian.bean.dial.IVRListResponse;
/**
* 基础话务相关操作接口.
*
* @author alegria
*/
public interface WxQidianDialService {
IVRDialResponse ivrDial(IVRDialRequest ivrDial) throws WxErrorException;
IVRListResponse getIVRList() throws WxErrorException;
}

View File

@@ -0,0 +1,348 @@
package me.chanjar.weixin.qidian.api;
import java.util.Map;
import com.google.gson.JsonObject;
import me.chanjar.weixin.common.bean.WxJsapiSignature;
import me.chanjar.weixin.common.bean.WxNetCheckResult;
import me.chanjar.weixin.common.enums.TicketType;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.common.service.WxService;
import me.chanjar.weixin.common.util.http.MediaUploadRequestExecutor;
import me.chanjar.weixin.common.util.http.RequestExecutor;
import me.chanjar.weixin.common.util.http.RequestHttp;
import me.chanjar.weixin.qidian.config.WxQidianConfigStorage;
import me.chanjar.weixin.qidian.enums.WxQidianApiUrl;
/**
* 腾讯企点API的Service.
*
* @author alegria
*/
public interface WxQidianService extends WxService {
/**
* <pre>
* 验证消息的确来自微信服务器.
* 详情请见: http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421135319&token=&lang=zh_CN
* </pre>
*
* @param timestamp 时间戳
* @param nonce 随机串
* @param signature 签名
* @return 是否验证通过 boolean
*/
boolean checkSignature(String timestamp, String nonce, String signature);
/**
* 获取access_token, 不强制刷新access_token.
*
* @return token access token
* @throws WxErrorException .
* @see #getAccessToken(boolean) #getAccessToken(boolean)
*/
String getAccessToken() throws WxErrorException;
/**
* <pre>
* 获取access_token本方法线程安全.
* 且在多线程同时刷新时只刷新一次避免超出2000次/日的调用次数上限
*
* 另本service的所有方法都会在access_token过期时调用此方法
*
* 程序员在非必要情况下尽量不要主动调用此方法
*
* 详情请见: http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140183&token=&lang=zh_CN
* </pre>
*
* @param forceRefresh 是否强制刷新
* @return token access token
* @throws WxErrorException .
*/
String getAccessToken(boolean forceRefresh) throws WxErrorException;
/**
* 获得ticket,不强制刷新ticket.
*
* @param type ticket 类型
* @return ticket ticket
* @throws WxErrorException .
* @see #getTicket(TicketType, boolean) #getTicket(TicketType, boolean)
*/
String getTicket(TicketType type) throws WxErrorException;
/**
* <pre>
* 获得ticket.
* 获得时会检查 Token是否过期如果过期了那么就刷新一下否则就什么都不干
* </pre>
*
* @param type ticket类型
* @param forceRefresh 强制刷新
* @return ticket ticket
* @throws WxErrorException .
*/
String getTicket(TicketType type, boolean forceRefresh) throws WxErrorException;
/**
* 获得jsapi_ticket,不强制刷新jsapi_ticket.
*
* @return jsapi ticket
* @throws WxErrorException .
* @see #getJsapiTicket(boolean) #getJsapiTicket(boolean)
*/
String getJsapiTicket() throws WxErrorException;
/**
* <pre>
* 获得jsapi_ticket.
* 获得时会检查jsapiToken是否过期如果过期了那么就刷新一下否则就什么都不干
*
* 详情请见http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141115&token=&lang=zh_CN
* </pre>
*
* @param forceRefresh 强制刷新
* @return jsapi ticket
* @throws WxErrorException .
*/
String getJsapiTicket(boolean forceRefresh) throws WxErrorException;
/**
* <pre>
* 创建调用jsapi时所需要的签名.
*
* 详情请见http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141115&token=&lang=zh_CN
* </pre>
*
* @param url 地址
* @return 生成的签名对象 wx jsapi signature
* @throws WxErrorException .
*/
WxJsapiSignature createJsapiSignature(String url) throws WxErrorException;
/**
* <pre>
* 长链接转短链接接口.
* 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=长链接转短链接接口
* </pre>
*
* @param longUrl 长url
* @return 生成的短地址 string
* @throws WxErrorException .
*/
String shortUrl(String longUrl) throws WxErrorException;
/**
* <pre>
* 构造第三方使用网站应用授权登录的url.
* 详情请见: <a href=
"https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419316505&token=&lang=zh_CN">网站应用微信登录开发指南</a>
* URL格式为https://open.weixin.qq.com/connect/qrconnect?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
* </pre>
*
* @param redirectUri 用户授权完成后的重定向链接无需urlencode, 方法内会进行encode
* @param scope 应用授权作用域,拥有多个作用域用逗号(,分隔网页应用目前仅填写snsapi_login即可
* @param state 非必填用于保持请求和回调的状态授权请求后原样带回给第三方。该参数可用于防止csrf攻击跨站请求伪造攻击建议第三方带上该参数可设置为简单的随机数加session进行校验
* @return url string
*/
String buildQrConnectUrl(String redirectUri, String scope, String state);
/**
* <pre>
* 获取微信服务器IP地址
* http://mp.weixin.qq.com/wiki/0/2ad4b6bfd29f30f71d39616c2a0fcedc.html
* </pre>
*
* @return 微信服务器ip地址数组 string [ ]
* @throws WxErrorException .
*/
String[] getCallbackIP() throws WxErrorException;
/**
* <pre>
* 网络检测
* https://mp.weixin.qq.com/wiki?t=resource/res_main&id=21541575776DtsuT
* 为了帮助开发者排查回调连接失败的问题提供这个网络检测的API。它可以对开发者URL做域名解析然后对所有IP进行一次ping操作得到丢包率和耗时。
* </pre>
*
* @param action 执行的检测动作
* @param operator 指定平台从某个运营商进行检测
* @return 检测结果 wx net check result
* @throws WxErrorException .
*/
WxNetCheckResult netCheck(String action, String operator) throws WxErrorException;
/**
* <pre>
* 公众号调用或第三方平台帮公众号调用对公众号的所有api调用包括第三方帮其调用次数进行清零
* HTTP调用https://api.weixin.qq.com/cgi-bin/clear_quota?access_token=ACCESS_TOKEN
* 接口文档地址https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1433744592
*
* </pre>
*
* @param appid 公众号的APPID
* @throws WxErrorException the wx error exception
*/
void clearQuota(String appid) throws WxErrorException;
/**
* <pre>
* Service没有实现某个API的时候可以用这个
* 比{@link #get}和{@link #post}方法更灵活可以自己构造RequestExecutor用来处理不同的参数和不同的返回类型。
* 可以参考,{@link MediaUploadRequestExecutor}的实现方法
* </pre>
*
* @param <T> the type parameter
* @param <E> the type parameter
* @param executor 执行器
* @param url 接口地址
* @param data 参数数据
* @return 结果 t
* @throws WxErrorException 异常
*/
<T, E> T execute(RequestExecutor<T, E> executor, String url, E data) throws WxErrorException;
/**
* 当本Service没有实现某个API的时候可以用这个针对所有微信API中的GET请求.
*
* @param url 请求接口地址
* @param queryParam 参数
* @return 接口响应字符串 string
* @throws WxErrorException 异常
*/
String get(WxQidianApiUrl url, String queryParam) throws WxErrorException;
/**
* 当本Service没有实现某个API的时候可以用这个针对所有微信API中的POST请求.
*
* @param url 请求接口地址
* @param postData 请求参数json值
* @return 接口响应字符串 string
* @throws WxErrorException 异常
*/
String post(WxQidianApiUrl url, String postData) throws WxErrorException;
/**
* 当本Service没有实现某个API的时候可以用这个针对所有微信API中的POST请求.
*
* @param url 请求接口地址
* @param jsonObject 请求参数json对象
* @return 接口响应字符串 string
* @throws WxErrorException 异常
*/
String post(WxQidianApiUrl url, JsonObject jsonObject) throws WxErrorException;
/**
* <pre>
* Service没有实现某个API的时候可以用这个
* 比{@link #get}和{@link #post}方法更灵活可以自己构造RequestExecutor用来处理不同的参数和不同的返回类型。
* 可以参考,{@link MediaUploadRequestExecutor}的实现方法
* </pre>
*
* @param <T> the type parameter
* @param <E> the type parameter
* @param executor 执行器
* @param url 接口地址
* @param data 参数数据
* @return 结果 t
* @throws WxErrorException 异常
*/
<T, E> T execute(RequestExecutor<T, E> executor, WxQidianApiUrl url, E data) throws WxErrorException;
/**
* 设置当微信系统响应系统繁忙时,要等待多少 retrySleepMillis(ms) * 2^(重试次数 - 1) 再发起重试.
*
* @param retrySleepMillis 默认1000ms
*/
void setRetrySleepMillis(int retrySleepMillis);
/**
* <pre>
* 设置当微信系统响应系统繁忙时,最大重试次数.
* 默认5次
* </pre>
*
* @param maxRetryTimes 最大重试次数
*/
void setMaxRetryTimes(int maxRetryTimes);
/**
* 获取WxMpConfigStorage 对象.
*
* @return WxMpConfigStorage wx mp config storage
*/
WxQidianConfigStorage getWxMpConfigStorage();
/**
* 设置 {@link WxQidianConfigStorage} 的实现. 兼容老版本
*
* @param wxConfigProvider .
*/
void setWxMpConfigStorage(WxQidianConfigStorage wxConfigProvider);
/**
* Map里 加入新的 {@link WxQidianConfigStorage},适用于动态添加新的微信公众号配置.
*
* @param mpId 公众号id
* @param configStorage 新的微信配置
*/
void addConfigStorage(String mpId, WxQidianConfigStorage configStorage);
/**
* 从 Map中 移除 {@link String mpId} 所对应的
* {@link WxQidianConfigStorage},适用于动态移除微信公众号配置.
*
* @param mpId 对应公众号的标识
*/
void removeConfigStorage(String mpId);
/**
* 注入多个 {@link WxQidianConfigStorage} 的实现. 并为每个 {@link WxQidianConfigStorage}
* 赋予不同的 {@link String mpId} 值 随机采用一个{@link String mpId}进行Http初始化操作
*
* @param configStorages WxMpConfigStorage map
*/
void setMultiConfigStorages(Map<String, WxQidianConfigStorage> configStorages);
/**
* 注入多个 {@link WxQidianConfigStorage} 的实现. 并为每个 {@link WxQidianConfigStorage}
* 赋予不同的 {@link String label} 值
*
* @param configStorages WxMpConfigStorage map
* @param defaultMpId 设置一个{@link WxQidianConfigStorage} 所对应的{@link String
* mpId}进行Http初始化
*/
void setMultiConfigStorages(Map<String, WxQidianConfigStorage> configStorages, String defaultMpId);
/**
* 进行相应的公众号切换.
*
* @param mpId 公众号标识
* @return 切换是否成功 boolean
*/
boolean switchover(String mpId);
/**
* 进行相应的公众号切换.
*
* @param mpId 公众号标识
* @return 切换成功 ,则返回当前对象,方便链式调用,否则抛出异常
*/
WxQidianService switchoverTo(String mpId);
/**
* 初始化http请求对象.
*/
void initHttp();
/**
* 获取RequestHttp对象.
*
* @return RequestHttp对象 request http
*/
RequestHttp getRequestHttp();
WxQidianDialService getDialService();
WxQidianCallDataService getCallDataService();
}

View File

@@ -0,0 +1,420 @@
package me.chanjar.weixin.qidian.api.impl;
import static me.chanjar.weixin.qidian.enums.WxQidianApiUrl.Other.CLEAR_QUOTA_URL;
import static me.chanjar.weixin.qidian.enums.WxQidianApiUrl.Other.GET_CALLBACK_IP_URL;
import static me.chanjar.weixin.qidian.enums.WxQidianApiUrl.Other.GET_CURRENT_AUTOREPLY_INFO_URL;
import static me.chanjar.weixin.qidian.enums.WxQidianApiUrl.Other.GET_TICKET_URL;
import static me.chanjar.weixin.qidian.enums.WxQidianApiUrl.Other.NETCHECK_URL;
import static me.chanjar.weixin.qidian.enums.WxQidianApiUrl.Other.QRCONNECT_URL;
import static me.chanjar.weixin.qidian.enums.WxQidianApiUrl.Other.SHORTURL_API_URL;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import org.apache.commons.lang3.StringUtils;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.api.WxConsts;
import me.chanjar.weixin.common.bean.ToJson;
import me.chanjar.weixin.common.bean.WxAccessToken;
import me.chanjar.weixin.common.bean.WxJsapiSignature;
import me.chanjar.weixin.common.bean.WxNetCheckResult;
import me.chanjar.weixin.common.enums.TicketType;
import me.chanjar.weixin.common.enums.WxType;
import me.chanjar.weixin.common.error.WxError;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.common.error.WxRuntimeException;
import me.chanjar.weixin.common.util.DataUtils;
import me.chanjar.weixin.common.util.RandomUtils;
import me.chanjar.weixin.common.util.crypto.SHA1;
import me.chanjar.weixin.common.util.http.RequestExecutor;
import me.chanjar.weixin.common.util.http.RequestHttp;
import me.chanjar.weixin.common.util.http.SimpleGetRequestExecutor;
import me.chanjar.weixin.common.util.http.SimplePostRequestExecutor;
import me.chanjar.weixin.common.util.http.URIUtil;
import me.chanjar.weixin.common.util.json.GsonParser;
import me.chanjar.weixin.common.util.json.WxGsonBuilder;
import me.chanjar.weixin.qidian.api.WxQidianCallDataService;
import me.chanjar.weixin.qidian.api.WxQidianDialService;
import me.chanjar.weixin.qidian.api.WxQidianService;
import me.chanjar.weixin.qidian.config.WxQidianConfigStorage;
import me.chanjar.weixin.qidian.enums.WxQidianApiUrl;
import me.chanjar.weixin.qidian.util.WxQidianConfigStorageHolder;
/**
* 基础实现类.
*
* @author someone
*/
@Slf4j
public abstract class BaseWxQidianServiceImpl<H, P> implements WxQidianService, RequestHttp<H, P> {
@Getter
private WxQidianDialService dialService = new WxQidianDialServiceImpl(this);
@Getter
private WxQidianCallDataService callDataService = new WxQidianCallDataServiceImpl(this);
private Map<String, WxQidianConfigStorage> configStorageMap;
private int retrySleepMillis = 1000;
private int maxRetryTimes = 5;
@Override
public boolean checkSignature(String timestamp, String nonce, String signature) {
try {
return SHA1.gen(this.getWxMpConfigStorage().getToken(), timestamp, nonce).equals(signature);
} catch (Exception e) {
log.error("Checking signature failed, and the reason is :" + e.getMessage());
return false;
}
}
@Override
public String getTicket(TicketType type) throws WxErrorException {
return this.getTicket(type, false);
}
@Override
public String getTicket(TicketType type, boolean forceRefresh) throws WxErrorException {
if (forceRefresh) {
this.getWxMpConfigStorage().expireTicket(type);
}
if (this.getWxMpConfigStorage().isTicketExpired(type)) {
Lock lock = this.getWxMpConfigStorage().getTicketLock(type);
lock.lock();
try {
if (this.getWxMpConfigStorage().isTicketExpired(type)) {
String responseContent = execute(SimpleGetRequestExecutor.create(this),
GET_TICKET_URL.getUrl(this.getWxMpConfigStorage()) + type.getCode(), null);
JsonObject tmpJsonObject = GsonParser.parse(responseContent);
String jsapiTicket = tmpJsonObject.get("ticket").getAsString();
int expiresInSeconds = tmpJsonObject.get("expires_in").getAsInt();
this.getWxMpConfigStorage().updateTicket(type, jsapiTicket, expiresInSeconds);
}
} finally {
lock.unlock();
}
}
return this.getWxMpConfigStorage().getTicket(type);
}
@Override
public String getJsapiTicket() throws WxErrorException {
return this.getJsapiTicket(false);
}
@Override
public String getJsapiTicket(boolean forceRefresh) throws WxErrorException {
return this.getTicket(TicketType.JSAPI, forceRefresh);
}
@Override
public WxJsapiSignature createJsapiSignature(String url) throws WxErrorException {
long timestamp = System.currentTimeMillis() / 1000;
String randomStr = RandomUtils.getRandomStr();
String jsapiTicket = getJsapiTicket(false);
String signature = SHA1.genWithAmple("jsapi_ticket=" + jsapiTicket, "noncestr=" + randomStr,
"timestamp=" + timestamp, "url=" + url);
WxJsapiSignature jsapiSignature = new WxJsapiSignature();
jsapiSignature.setAppId(this.getWxMpConfigStorage().getAppId());
jsapiSignature.setTimestamp(timestamp);
jsapiSignature.setNonceStr(randomStr);
jsapiSignature.setUrl(url);
jsapiSignature.setSignature(signature);
return jsapiSignature;
}
@Override
public String getAccessToken() throws WxErrorException {
return getAccessToken(false);
}
@Override
public String shortUrl(String longUrl) throws WxErrorException {
if (longUrl.contains("&access_token=")) {
throw new WxErrorException("要转换的网址中存在非法字符{&access_token=" + "会导致微信接口报错属于微信bug请调整地址否则不建议使用此方法");
}
JsonObject o = new JsonObject();
o.addProperty("action", "long2short");
o.addProperty("long_url", longUrl);
String responseContent = this.post(SHORTURL_API_URL, o.toString());
return GsonParser.parse(responseContent).get("short_url").getAsString();
}
@Override
public String buildQrConnectUrl(String redirectUri, String scope, String state) {
return String.format(QRCONNECT_URL.getUrl(this.getWxMpConfigStorage()), this.getWxMpConfigStorage().getAppId(),
URIUtil.encodeURIComponent(redirectUri), scope, StringUtils.trimToEmpty(state));
}
@Override
public String[] getCallbackIP() throws WxErrorException {
String responseContent = this.get(GET_CALLBACK_IP_URL, null);
JsonObject tmpJsonObject = GsonParser.parse(responseContent);
JsonArray ipList = tmpJsonObject.get("ip_list").getAsJsonArray();
String[] ipArray = new String[ipList.size()];
for (int i = 0; i < ipList.size(); i++) {
ipArray[i] = ipList.get(i).getAsString();
}
return ipArray;
}
@Override
public WxNetCheckResult netCheck(String action, String operator) throws WxErrorException {
JsonObject o = new JsonObject();
o.addProperty("action", action);
o.addProperty("check_operator", operator);
String responseContent = this.post(NETCHECK_URL, o.toString());
return WxNetCheckResult.fromJson(responseContent);
}
@Override
public void clearQuota(String appid) throws WxErrorException {
JsonObject o = new JsonObject();
o.addProperty("appid", appid);
this.post(CLEAR_QUOTA_URL, o.toString());
}
@Override
public String get(String url, String queryParam) throws WxErrorException {
return execute(SimpleGetRequestExecutor.create(this), url, queryParam);
}
@Override
public String get(WxQidianApiUrl url, String queryParam) throws WxErrorException {
return this.get(url.getUrl(this.getWxMpConfigStorage()), queryParam);
}
@Override
public String post(String url, String postData) throws WxErrorException {
return execute(SimplePostRequestExecutor.create(this), url, postData);
}
@Override
public String post(WxQidianApiUrl url, String postData) throws WxErrorException {
return this.post(url.getUrl(this.getWxMpConfigStorage()), postData);
}
@Override
public String post(WxQidianApiUrl url, JsonObject jsonObject) throws WxErrorException {
return this.post(url.getUrl(this.getWxMpConfigStorage()), jsonObject.toString());
}
@Override
public String post(String url, ToJson obj) throws WxErrorException {
return this.post(url, obj.toJson());
}
@Override
public String post(String url, JsonObject jsonObject) throws WxErrorException {
return this.post(url, jsonObject.toString());
}
@Override
public String post(String url, Object obj) throws WxErrorException {
return this.execute(SimplePostRequestExecutor.create(this), url, WxGsonBuilder.create().toJson(obj));
}
@Override
public <T, E> T execute(RequestExecutor<T, E> executor, WxQidianApiUrl url, E data) throws WxErrorException {
return this.execute(executor, url.getUrl(this.getWxMpConfigStorage()), data);
}
/**
* 向微信端发送请求在这里执行的策略是当发生access_token过期时才去刷新然后重新执行请求而不是全局定时请求.
*/
@Override
public <T, E> T execute(RequestExecutor<T, E> executor, String uri, E data) throws WxErrorException {
int retryTimes = 0;
do {
try {
return this.executeInternal(executor, uri, data);
} catch (WxErrorException e) {
if (retryTimes + 1 > this.maxRetryTimes) {
log.warn("重试达到最大次数【{}】", maxRetryTimes);
// 最后一次重试失败后,直接抛出异常,不再等待
throw new WxRuntimeException("微信服务端异常,超出重试次数");
}
WxError error = e.getError();
// -1 系统繁忙, 1000ms后重试
if (error.getErrorCode() == -1) {
int sleepMillis = this.retrySleepMillis * (1 << retryTimes);
try {
log.warn("微信系统繁忙,{} ms 后重试(第{}次)", sleepMillis, retryTimes + 1);
Thread.sleep(sleepMillis);
} catch (InterruptedException e1) {
throw new WxRuntimeException(e1);
}
} else {
throw e;
}
}
} while (retryTimes++ < this.maxRetryTimes);
log.warn("重试达到最大次数【{}】", this.maxRetryTimes);
throw new WxRuntimeException("微信服务端异常,超出重试次数");
}
protected <T, E> T executeInternal(RequestExecutor<T, E> executor, String uri, E data) throws WxErrorException {
E dataForLog = DataUtils.handleDataWithSecret(data);
if (uri.contains("access_token=")) {
throw new IllegalArgumentException("uri参数中不允许有access_token: " + uri);
}
String accessToken = getAccessToken(false);
String uriWithAccessToken = uri + (uri.contains("?") ? "&" : "?") + "access_token=" + accessToken;
try {
T result = executor.execute(uriWithAccessToken, data, WxType.MP);
log.debug("\n【请求地址】: {}\n【请求参数】{}\n【响应数据】{}", uriWithAccessToken, dataForLog, result);
return result;
} catch (WxErrorException e) {
WxError error = e.getError();
if (WxConsts.ACCESS_TOKEN_ERROR_CODES.contains(error.getErrorCode())) {
// 强制设置wxMpConfigStorage它的access token过期了这样在下一次请求里就会刷新access token
Lock lock = this.getWxMpConfigStorage().getAccessTokenLock();
lock.lock();
try {
if (StringUtils.equals(this.getWxMpConfigStorage().getAccessToken(), accessToken)) {
this.getWxMpConfigStorage().expireAccessToken();
}
} catch (Exception ex) {
this.getWxMpConfigStorage().expireAccessToken();
} finally {
lock.unlock();
}
if (this.getWxMpConfigStorage().autoRefreshToken()) {
log.warn("即将重新获取新的access_token错误代码{},错误信息:{}", error.getErrorCode(), error.getErrorMsg());
return this.execute(executor, uri, data);
}
}
if (error.getErrorCode() != 0) {
log.error("\n【请求地址】: {}\n【请求参数】{}\n【错误信息】{}", uriWithAccessToken, dataForLog, error);
throw new WxErrorException(error, e);
}
return null;
} catch (IOException e) {
log.error("\n【请求地址】: {}\n【请求参数】{}\n【异常信息】{}", uriWithAccessToken, dataForLog, e.getMessage());
throw new WxErrorException(e);
}
}
@Override
public WxQidianConfigStorage getWxMpConfigStorage() {
if (this.configStorageMap.size() == 1) {
// 只有一个公众号,直接返回其配置即可
return this.configStorageMap.values().iterator().next();
}
return this.configStorageMap.get(WxQidianConfigStorageHolder.get());
}
protected String extractAccessToken(String resultContent) throws WxErrorException {
WxQidianConfigStorage config = this.getWxMpConfigStorage();
WxError error = WxError.fromJson(resultContent, WxType.MP);
if (error.getErrorCode() != 0) {
throw new WxErrorException(error);
}
WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
config.updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());
return config.getAccessToken();
}
@Override
public void setWxMpConfigStorage(WxQidianConfigStorage wxConfigProvider) {
final String defaultMpId = wxConfigProvider.getAppId();
this.setMultiConfigStorages(ImmutableMap.of(defaultMpId, wxConfigProvider), defaultMpId);
}
@Override
public void setMultiConfigStorages(Map<String, WxQidianConfigStorage> configStorages) {
this.setMultiConfigStorages(configStorages, configStorages.keySet().iterator().next());
}
@Override
public void setMultiConfigStorages(Map<String, WxQidianConfigStorage> configStorages, String defaultMpId) {
this.configStorageMap = Maps.newHashMap(configStorages);
WxQidianConfigStorageHolder.set(defaultMpId);
this.initHttp();
}
@Override
public void addConfigStorage(String mpId, WxQidianConfigStorage configStorages) {
synchronized (this) {
if (this.configStorageMap == null) {
this.setWxMpConfigStorage(configStorages);
} else {
this.configStorageMap.put(mpId, configStorages);
}
}
}
@Override
public void removeConfigStorage(String mpId) {
synchronized (this) {
if (this.configStorageMap.size() == 1) {
this.configStorageMap.remove(mpId);
log.warn("已删除最后一个公众号配置:{}须立即使用setWxMpConfigStorage或setMultiConfigStorages添加配置", mpId);
return;
}
if (WxQidianConfigStorageHolder.get().equals(mpId)) {
this.configStorageMap.remove(mpId);
final String defaultMpId = this.configStorageMap.keySet().iterator().next();
WxQidianConfigStorageHolder.set(defaultMpId);
log.warn("已删除默认公众号配置,公众号【{}】被设为默认配置", defaultMpId);
return;
}
this.configStorageMap.remove(mpId);
}
}
@Override
public WxQidianService switchoverTo(String mpId) {
if (this.configStorageMap.containsKey(mpId)) {
WxQidianConfigStorageHolder.set(mpId);
return this;
}
throw new WxRuntimeException(String.format("无法找到对应【%s】的公众号配置信息请核实", mpId));
}
@Override
public boolean switchover(String mpId) {
if (this.configStorageMap.containsKey(mpId)) {
WxQidianConfigStorageHolder.set(mpId);
return true;
}
log.error("无法找到对应【{}】的公众号配置信息,请核实!", mpId);
return false;
}
@Override
public void setRetrySleepMillis(int retrySleepMillis) {
this.retrySleepMillis = retrySleepMillis;
}
@Override
public void setMaxRetryTimes(int maxRetryTimes) {
this.maxRetryTimes = maxRetryTimes;
}
@Override
public RequestHttp getRequestHttp() {
return this;
}
}

View File

@@ -0,0 +1,23 @@
package me.chanjar.weixin.qidian.api.impl;
import static me.chanjar.weixin.qidian.enums.WxQidianApiUrl.CallData.GET_SWITCH_BOARD_LIST;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.qidian.api.WxQidianCallDataService;
import me.chanjar.weixin.qidian.api.WxQidianService;
import me.chanjar.weixin.qidian.bean.call.GetSwitchBoardListResponse;
@Slf4j
@RequiredArgsConstructor
public class WxQidianCallDataServiceImpl implements WxQidianCallDataService {
private final WxQidianService wxQidianService;
@Override
public GetSwitchBoardListResponse getSwitchBoardList() throws WxErrorException {
String result = this.wxQidianService.get(GET_SWITCH_BOARD_LIST, null);
return GetSwitchBoardListResponse.fromJson(result);
}
}

View File

@@ -0,0 +1,43 @@
package me.chanjar.weixin.qidian.api.impl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.qidian.api.WxQidianDialService;
import me.chanjar.weixin.qidian.api.WxQidianService;
import me.chanjar.weixin.qidian.bean.dial.IVRDialRequest;
import me.chanjar.weixin.qidian.bean.dial.IVRDialResponse;
import me.chanjar.weixin.qidian.bean.dial.IVRListResponse;
import static me.chanjar.weixin.qidian.enums.WxQidianApiUrl.Dial.GET_IVR_LIST;
import static me.chanjar.weixin.qidian.enums.WxQidianApiUrl.Dial.IVR_DIAL;
/**
* Created by Binary Wang on 2016/7/21.
*
* @author Binary Wang
*/
@Slf4j
@RequiredArgsConstructor
public class WxQidianDialServiceImpl implements WxQidianDialService {
private final WxQidianService wxQidianService;
@Override
public IVRDialResponse ivrDial(IVRDialRequest ivrDial) throws WxErrorException {
String json = ivrDial.toJson();
log.debug("IVR外呼{}", json);
String result = this.wxQidianService.post(IVR_DIAL, json);
log.debug("创建菜单:{},结果:{}", json, result);
return IVRDialResponse.fromJson(result);
}
@Override
public IVRListResponse getIVRList() throws WxErrorException {
String result = this.wxQidianService.get(GET_IVR_LIST, null);
return IVRListResponse.fromJson(result);
}
}

View File

@@ -0,0 +1,106 @@
package me.chanjar.weixin.qidian.api.impl;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.common.error.WxRuntimeException;
import me.chanjar.weixin.common.util.http.HttpType;
import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder;
import me.chanjar.weixin.common.util.http.apache.DefaultApacheHttpClientBuilder;
import me.chanjar.weixin.qidian.config.WxQidianConfigStorage;
import org.apache.http.HttpHost;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.CloseableHttpClient;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import static me.chanjar.weixin.qidian.enums.WxQidianApiUrl.Other.GET_ACCESS_TOKEN_URL;
/**
* apache http client方式实现.
*
* @author someone
*/
public class WxQidianServiceHttpClientImpl extends BaseWxQidianServiceImpl<CloseableHttpClient, HttpHost> {
private CloseableHttpClient httpClient;
private HttpHost httpProxy;
@Override
public CloseableHttpClient getRequestHttpClient() {
return httpClient;
}
@Override
public HttpHost getRequestHttpProxy() {
return httpProxy;
}
@Override
public HttpType getRequestType() {
return HttpType.APACHE_HTTP;
}
@Override
public void initHttp() {
WxQidianConfigStorage configStorage = this.getWxMpConfigStorage();
ApacheHttpClientBuilder apacheHttpClientBuilder = configStorage.getApacheHttpClientBuilder();
if (null == apacheHttpClientBuilder) {
apacheHttpClientBuilder = DefaultApacheHttpClientBuilder.get();
}
apacheHttpClientBuilder.httpProxyHost(configStorage.getHttpProxyHost())
.httpProxyPort(configStorage.getHttpProxyPort()).httpProxyUsername(configStorage.getHttpProxyUsername())
.httpProxyPassword(configStorage.getHttpProxyPassword());
if (configStorage.getHttpProxyHost() != null && configStorage.getHttpProxyPort() > 0) {
this.httpProxy = new HttpHost(configStorage.getHttpProxyHost(), configStorage.getHttpProxyPort());
}
this.httpClient = apacheHttpClientBuilder.build();
}
@Override
public String getAccessToken(boolean forceRefresh) throws WxErrorException {
final WxQidianConfigStorage config = this.getWxMpConfigStorage();
if (!config.isAccessTokenExpired() && !forceRefresh) {
return config.getAccessToken();
}
Lock lock = config.getAccessTokenLock();
boolean locked = false;
try {
do {
locked = lock.tryLock(100, TimeUnit.MILLISECONDS);
if (!forceRefresh && !config.isAccessTokenExpired()) {
return config.getAccessToken();
}
} while (!locked);
String url = String.format(GET_ACCESS_TOKEN_URL.getUrl(config), config.getAppId(), config.getSecret());
try {
HttpGet httpGet = new HttpGet(url);
if (this.getRequestHttpProxy() != null) {
RequestConfig requestConfig = RequestConfig.custom().setProxy(this.getRequestHttpProxy()).build();
httpGet.setConfig(requestConfig);
}
try (CloseableHttpResponse response = getRequestHttpClient().execute(httpGet)) {
return this.extractAccessToken(new BasicResponseHandler().handleResponse(response));
} finally {
httpGet.releaseConnection();
}
} catch (IOException e) {
throw new WxRuntimeException(e);
}
} catch (InterruptedException e) {
throw new WxRuntimeException(e);
} finally {
if (locked) {
lock.unlock();
}
}
}
}

View File

@@ -0,0 +1,12 @@
package me.chanjar.weixin.qidian.api.impl;
/**
* <pre>
* 默认接口实现类使用apache httpclient实现
* Created by Binary Wang on 2017-5-27.
* </pre>
*
* @author <a href="https://github.com/binarywang">Binary Wang</a>
*/
public class WxQidianServiceImpl extends WxQidianServiceHttpClientImpl {
}

View File

@@ -0,0 +1,90 @@
package me.chanjar.weixin.qidian.api.impl;
import jodd.http.HttpConnectionProvider;
import jodd.http.HttpRequest;
import jodd.http.ProxyInfo;
import jodd.http.net.SocketHttpConnectionProvider;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.common.error.WxRuntimeException;
import me.chanjar.weixin.common.util.http.HttpType;
import me.chanjar.weixin.qidian.config.WxQidianConfigStorage;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import static me.chanjar.weixin.qidian.enums.WxQidianApiUrl.Other.GET_ACCESS_TOKEN_URL;
/**
* jodd-http方式实现.
*
* @author someone
*/
public class WxQidianServiceJoddHttpImpl extends BaseWxQidianServiceImpl<HttpConnectionProvider, ProxyInfo> {
private HttpConnectionProvider httpClient;
private ProxyInfo httpProxy;
@Override
public HttpConnectionProvider getRequestHttpClient() {
return httpClient;
}
@Override
public ProxyInfo getRequestHttpProxy() {
return httpProxy;
}
@Override
public HttpType getRequestType() {
return HttpType.JODD_HTTP;
}
@Override
public void initHttp() {
WxQidianConfigStorage configStorage = this.getWxMpConfigStorage();
if (configStorage.getHttpProxyHost() != null && configStorage.getHttpProxyPort() > 0) {
httpProxy = new ProxyInfo(ProxyInfo.ProxyType.HTTP, configStorage.getHttpProxyHost(),
configStorage.getHttpProxyPort(), configStorage.getHttpProxyUsername(), configStorage.getHttpProxyPassword());
}
httpClient = new SocketHttpConnectionProvider();
}
@Override
public String getAccessToken(boolean forceRefresh) throws WxErrorException {
final WxQidianConfigStorage config = this.getWxMpConfigStorage();
if (!config.isAccessTokenExpired() && !forceRefresh) {
return config.getAccessToken();
}
Lock lock = config.getAccessTokenLock();
boolean locked = false;
try {
do {
locked = lock.tryLock(100, TimeUnit.MILLISECONDS);
if (!forceRefresh && !config.isAccessTokenExpired()) {
return config.getAccessToken();
}
} while (!locked);
String url = String.format(GET_ACCESS_TOKEN_URL.getUrl(config), config.getAppId(), config.getSecret());
HttpRequest request = HttpRequest.get(url);
if (this.getRequestHttpProxy() != null) {
SocketHttpConnectionProvider provider = new SocketHttpConnectionProvider();
provider.useProxy(getRequestHttpProxy());
request.withConnectionProvider(provider);
}
return this.extractAccessToken(request.send().bodyText());
} catch (InterruptedException e) {
throw new WxRuntimeException(e);
} finally {
if (locked) {
lock.unlock();
}
}
}
}

View File

@@ -0,0 +1,98 @@
package me.chanjar.weixin.qidian.api.impl;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.common.error.WxRuntimeException;
import me.chanjar.weixin.common.util.http.HttpType;
import me.chanjar.weixin.common.util.http.okhttp.OkHttpProxyInfo;
import me.chanjar.weixin.qidian.config.WxQidianConfigStorage;
import okhttp3.*;
import java.io.IOException;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import static me.chanjar.weixin.qidian.enums.WxQidianApiUrl.Other.GET_ACCESS_TOKEN_URL;
/**
* okhttp实现.
*
* @author someone
*/
public class WxQidianServiceOkHttpImpl extends BaseWxQidianServiceImpl<OkHttpClient, OkHttpProxyInfo> {
private OkHttpClient httpClient;
private OkHttpProxyInfo httpProxy;
@Override
public OkHttpClient getRequestHttpClient() {
return httpClient;
}
@Override
public OkHttpProxyInfo getRequestHttpProxy() {
return httpProxy;
}
@Override
public HttpType getRequestType() {
return HttpType.OK_HTTP;
}
@Override
public String getAccessToken(boolean forceRefresh) throws WxErrorException {
final WxQidianConfigStorage config = this.getWxMpConfigStorage();
if (!config.isAccessTokenExpired() && !forceRefresh) {
return config.getAccessToken();
}
Lock lock = config.getAccessTokenLock();
boolean locked = false;
try {
do {
locked = lock.tryLock(100, TimeUnit.MILLISECONDS);
if (!forceRefresh && !config.isAccessTokenExpired()) {
return config.getAccessToken();
}
} while (!locked);
String url = String.format(GET_ACCESS_TOKEN_URL.getUrl(config), config.getAppId(), config.getSecret());
Request request = new Request.Builder().url(url).get().build();
Response response = getRequestHttpClient().newCall(request).execute();
return this.extractAccessToken(Objects.requireNonNull(response.body()).string());
} catch (IOException e) {
throw new WxRuntimeException(e);
} catch (InterruptedException e) {
throw new WxRuntimeException(e);
} finally {
if (locked) {
lock.unlock();
}
}
}
@Override
public void initHttp() {
WxQidianConfigStorage wxMpConfigStorage = getWxMpConfigStorage();
// 设置代理
if (wxMpConfigStorage.getHttpProxyHost() != null && wxMpConfigStorage.getHttpProxyPort() > 0) {
httpProxy = OkHttpProxyInfo.httpProxy(wxMpConfigStorage.getHttpProxyHost(), wxMpConfigStorage.getHttpProxyPort(),
wxMpConfigStorage.getHttpProxyUsername(), wxMpConfigStorage.getHttpProxyPassword());
}
OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder();
if (httpProxy != null) {
clientBuilder.proxy(getRequestHttpProxy().getProxy());
// 设置授权
clientBuilder.authenticator(new Authenticator() {
@Override
public Request authenticate(Route route, Response response) throws IOException {
String credential = Credentials.basic(httpProxy.getProxyUsername(), httpProxy.getProxyPassword());
return response.request().newBuilder().header("Authorization", credential).build();
}
});
}
httpClient = clientBuilder.build();
}
}