mirror of
https://gitee.com/binary/weixin-java-tools.git
synced 2026-03-10 00:13:40 +08:00
🆕 #1952 增加腾讯企点子模块,用于对接企点开放平台。
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package me.chanjar.weixin.qidian.bean;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 企点接口地址域名部分的自定义设置信息.
|
||||
*
|
||||
* @author alegria
|
||||
* @date 2020-12-24
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class WxQidianHostConfig {
|
||||
public static final String API_DEFAULT_HOST_URL = "https://api.weixin.qq.com";
|
||||
public static final String OPEN_DEFAULT_HOST_URL = "https://open.weixin.qq.com";
|
||||
public static final String QIDIAN_DEFAULT_HOST_URL = "https://api.qidian.qq.com";
|
||||
|
||||
/**
|
||||
* 对应于:https://api.weixin.qq.com
|
||||
*/
|
||||
private String apiHost;
|
||||
|
||||
/**
|
||||
* 对应于:https://open.weixin.qq.com
|
||||
*/
|
||||
private String openHost;
|
||||
/**
|
||||
* 对应于:https://api.qidian.qq.com
|
||||
*/
|
||||
private String qidianHost;
|
||||
|
||||
public static String buildUrl(WxQidianHostConfig hostConfig, String prefix, String path) {
|
||||
if (hostConfig == null) {
|
||||
return prefix + path;
|
||||
}
|
||||
|
||||
if (hostConfig.getApiHost() != null && prefix.equals(API_DEFAULT_HOST_URL)) {
|
||||
return hostConfig.getApiHost() + path;
|
||||
}
|
||||
|
||||
if (hostConfig.getQidianHost() != null && prefix.equals(QIDIAN_DEFAULT_HOST_URL)) {
|
||||
return hostConfig.getQidianHost() + path;
|
||||
}
|
||||
|
||||
if (hostConfig.getOpenHost() != null && prefix.equals(OPEN_DEFAULT_HOST_URL)) {
|
||||
return hostConfig.getOpenHost() + path;
|
||||
}
|
||||
|
||||
return prefix + path;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package me.chanjar.weixin.qidian.bean.call;
|
||||
|
||||
import lombok.Data;
|
||||
import me.chanjar.weixin.common.util.json.WxGsonBuilder;
|
||||
import me.chanjar.weixin.qidian.bean.common.QidianResponse;
|
||||
|
||||
@Data
|
||||
public class GetSwitchBoardListResponse extends QidianResponse {
|
||||
private SwitchBoardList data;
|
||||
|
||||
public static GetSwitchBoardListResponse fromJson(String result) {
|
||||
return WxGsonBuilder.create().fromJson(result, GetSwitchBoardListResponse.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package me.chanjar.weixin.qidian.bean.call;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class SwitchBoard {
|
||||
private String switchboard;
|
||||
private String createTime;
|
||||
private Boolean callinStatus;
|
||||
private Boolean calloutStatus;
|
||||
private String spName;
|
||||
private String cityName;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package me.chanjar.weixin.qidian.bean.call;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class SwitchBoardList {
|
||||
private List<SwitchBoard> records;
|
||||
|
||||
public List<String> switchBoards() {
|
||||
return records.stream().map(SwitchBoard::getSwitchboard).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package me.chanjar.weixin.qidian.bean.common;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
public class QidianResponse {
|
||||
private static Map<Integer, String> errorCodesMap = new HashMap<Integer, String>() {
|
||||
private static final long serialVersionUID = 1125349909878104934L;
|
||||
{
|
||||
put(-1, "系统繁忙");
|
||||
put(0, "请求成功");
|
||||
put(40001, "获取access_token时AppSecret错误,或者access_token无效");
|
||||
put(40002, "不合法的凭证类型");
|
||||
put(40003, "不合法的OpenID");
|
||||
put(40004, "不合法的媒体文件类型");
|
||||
put(40005, "不合法的文件类型");
|
||||
put(40006, "不合法的文件大小");
|
||||
put(40007, "不合法的媒体文件id");
|
||||
put(40008, "不合法的消息类型");
|
||||
put(40009, "不合法的图片文件大小");
|
||||
put(40010, "不合法的语音文件大小");
|
||||
put(40011, "不合法的视频文件大小");
|
||||
put(40012, "不合法的缩略图文件大小");
|
||||
put(40013, "不合法的APPID");
|
||||
put(40014, "不合法的access_token");
|
||||
put(40015, "不合法的菜单类型");
|
||||
put(40016, "不合法的按钮个数");
|
||||
put(40017, "不合法的按钮个数");
|
||||
put(40018, "不合法的按钮名字长度");
|
||||
put(40019, "不合法的按钮KEY长度");
|
||||
put(40020, "不合法的按钮URL长度");
|
||||
put(40021, "不合法的菜单版本号");
|
||||
put(40022, "不合法的子菜单级数");
|
||||
put(40023, "不合法的子菜单按钮个数");
|
||||
put(40024, "不合法的子菜单按钮类型");
|
||||
put(40025, "不合法的子菜单按钮名字长度");
|
||||
put(40026, "不合法的子菜单按钮KEY长度");
|
||||
put(40027, "不合法的子菜单按钮URL长度");
|
||||
put(40028, "不合法的自定义菜单使用用户");
|
||||
put(40029, "不合法的oauth_code");
|
||||
put(40030, "不合法的refresh_token");
|
||||
put(40031, "不合法的openid列表");
|
||||
put(40032, "不合法的openid列表长度");
|
||||
put(40033, "不合法的请求字符,不能包含\\uxxxx格式的字符");
|
||||
put(40035, "不合法的参数");
|
||||
put(40038, "不合法的请求格式");
|
||||
put(40039, "不合法的URL长度");
|
||||
put(40050, "不合法的分组id");
|
||||
put(40051, "分组名字不合法");
|
||||
put(41001, "缺少access_token参数");
|
||||
put(41002, "缺少appid参数");
|
||||
put(41003, "缺少refresh_token参数");
|
||||
put(41004, "缺少secret参数");
|
||||
put(41005, "缺少多媒体文件数据");
|
||||
put(41006, "缺少media_id参数");
|
||||
put(41007, "缺少子菜单数据");
|
||||
put(41008, "缺少oauth code");
|
||||
put(41009, "缺少openid");
|
||||
put(42001, "access_token超时");
|
||||
put(42002, "refresh_token超时");
|
||||
put(42003, "oauth_code超时");
|
||||
put(43001, "需要GET请求");
|
||||
put(43002, "需要POST请求");
|
||||
put(43003, "需要HTTPS请求");
|
||||
put(43004, "需要接收者关注");
|
||||
put(43005, "需要好友关系");
|
||||
put(44001, "多媒体文件为空");
|
||||
put(44002, "POST的数据包为空");
|
||||
put(44003, "图文消息内容为空");
|
||||
put(44004, "文本消息内容为空");
|
||||
put(45001, "多媒体文件大小超过限制");
|
||||
put(45002, "消息内容超过限制");
|
||||
put(45003, "标题字段超过限制");
|
||||
put(45004, "描述字段超过限制");
|
||||
put(45005, "链接字段超过限制");
|
||||
put(45006, "图片链接字段超过限制");
|
||||
put(45007, "语音播放时间超过限制");
|
||||
put(45008, "图文消息超过限制");
|
||||
put(45009, "接口调用超过限制");
|
||||
put(45010, "创建菜单个数超过限制");
|
||||
put(45015, "回复时间超过限制");
|
||||
put(45016, "系统分组,不允许修改");
|
||||
put(45017, "分组名字过长");
|
||||
put(45018, "分组数量超过上限");
|
||||
put(46001, "不存在媒体数据");
|
||||
put(46002, "不存在的菜单版本");
|
||||
put(46003, "不存在的菜单数据");
|
||||
put(46004, "不存在的用户");
|
||||
put(47001, "解析JSON/XML内容错误");
|
||||
put(48001, "api功能未授权");
|
||||
put(50001, "用户未授权该api");
|
||||
}
|
||||
};
|
||||
private Integer code = 0;
|
||||
private String msg;
|
||||
private Integer errcode = 0;
|
||||
private String errmsg = "ok";
|
||||
private String errmsgChinese;
|
||||
|
||||
public String getErrmsgChinese() {
|
||||
if (errcode != null && errmsgChinese == null) {
|
||||
errmsgChinese = errorCodesMap.get(errcode);
|
||||
}
|
||||
return errmsgChinese;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package me.chanjar.weixin.qidian.bean.dial;
|
||||
|
||||
import lombok.Data;
|
||||
import me.chanjar.weixin.common.util.json.WxGsonBuilder;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class IVRDialRequest implements Serializable {
|
||||
private static final long serialVersionUID = -5552935329136465927L;
|
||||
|
||||
private String phone_number;
|
||||
private String ivr_id;
|
||||
private List<String> corp_phone_list;
|
||||
private Integer loc_pref_on = 1;
|
||||
private List<String> backup_corp_phone_list;
|
||||
private Boolean skip_restrict = false;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.toJson();
|
||||
}
|
||||
|
||||
public String toJson() {
|
||||
return WxGsonBuilder.create().toJson(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package me.chanjar.weixin.qidian.bean.dial;
|
||||
|
||||
import lombok.Data;
|
||||
import me.chanjar.weixin.common.util.json.WxGsonBuilder;
|
||||
import me.chanjar.weixin.qidian.bean.common.QidianResponse;
|
||||
import me.chanjar.weixin.qidian.util.json.WxQidianGsonBuilder;
|
||||
|
||||
@Data
|
||||
public class IVRDialResponse extends QidianResponse {
|
||||
private String callid;
|
||||
|
||||
public static IVRDialResponse fromJson(String json) {
|
||||
return WxGsonBuilder.create().fromJson(json, IVRDialResponse.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return WxQidianGsonBuilder.create().toJson(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package me.chanjar.weixin.qidian.bean.dial;
|
||||
|
||||
import lombok.Data;
|
||||
import me.chanjar.weixin.common.util.json.WxGsonBuilder;
|
||||
import me.chanjar.weixin.qidian.bean.common.QidianResponse;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class IVRListResponse extends QidianResponse {
|
||||
private List<Ivr> node;
|
||||
|
||||
public static IVRListResponse fromJson(String json) {
|
||||
return WxGsonBuilder.create().fromJson(json, IVRListResponse.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package me.chanjar.weixin.qidian.bean.dial;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class Ivr {
|
||||
private String ivr_id;
|
||||
private String ivr_name;
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
package me.chanjar.weixin.qidian.config;
|
||||
|
||||
import me.chanjar.weixin.common.bean.WxAccessToken;
|
||||
import me.chanjar.weixin.common.enums.TicketType;
|
||||
import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder;
|
||||
import me.chanjar.weixin.qidian.bean.WxQidianHostConfig;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
|
||||
/**
|
||||
* 微信客户端配置存储.
|
||||
*
|
||||
* @author chanjarster
|
||||
*/
|
||||
public interface WxQidianConfigStorage {
|
||||
/**
|
||||
* Gets access token.
|
||||
*
|
||||
* @return the access token
|
||||
*/
|
||||
String getAccessToken();
|
||||
|
||||
/**
|
||||
* Gets access token lock.
|
||||
*
|
||||
* @return the access token lock
|
||||
*/
|
||||
Lock getAccessTokenLock();
|
||||
|
||||
/**
|
||||
* Is access token expired boolean.
|
||||
*
|
||||
* @return the boolean
|
||||
*/
|
||||
boolean isAccessTokenExpired();
|
||||
|
||||
/**
|
||||
* 强制将access token过期掉.
|
||||
*/
|
||||
void expireAccessToken();
|
||||
|
||||
/**
|
||||
* 应该是线程安全的.
|
||||
*
|
||||
* @param accessToken 要更新的WxAccessToken对象
|
||||
*/
|
||||
void updateAccessToken(WxAccessToken accessToken);
|
||||
|
||||
/**
|
||||
* 应该是线程安全的.
|
||||
*
|
||||
* @param accessToken 新的accessToken值
|
||||
* @param expiresInSeconds 过期时间,以秒为单位
|
||||
*/
|
||||
void updateAccessToken(String accessToken, int expiresInSeconds);
|
||||
|
||||
/**
|
||||
* Gets ticket.
|
||||
*
|
||||
* @param type the type
|
||||
* @return the ticket
|
||||
*/
|
||||
String getTicket(TicketType type);
|
||||
|
||||
/**
|
||||
* Gets ticket lock.
|
||||
*
|
||||
* @param type the type
|
||||
* @return the ticket lock
|
||||
*/
|
||||
Lock getTicketLock(TicketType type);
|
||||
|
||||
/**
|
||||
* Is ticket expired boolean.
|
||||
*
|
||||
* @param type the type
|
||||
* @return the boolean
|
||||
*/
|
||||
boolean isTicketExpired(TicketType type);
|
||||
|
||||
/**
|
||||
* 强制将ticket过期掉.
|
||||
*
|
||||
* @param type the type
|
||||
*/
|
||||
void expireTicket(TicketType type);
|
||||
|
||||
/**
|
||||
* 更新ticket.
|
||||
* 应该是线程安全的
|
||||
*
|
||||
* @param type ticket类型
|
||||
* @param ticket 新的ticket值
|
||||
* @param expiresInSeconds 过期时间,以秒为单位
|
||||
*/
|
||||
void updateTicket(TicketType type, String ticket, int expiresInSeconds);
|
||||
|
||||
/**
|
||||
* Gets app id.
|
||||
*
|
||||
* @return the app id
|
||||
*/
|
||||
String getAppId();
|
||||
|
||||
/**
|
||||
* Gets secret.
|
||||
*
|
||||
* @return the secret
|
||||
*/
|
||||
String getSecret();
|
||||
|
||||
/**
|
||||
* Gets token.
|
||||
*
|
||||
* @return the token
|
||||
*/
|
||||
String getToken();
|
||||
|
||||
/**
|
||||
* Gets aes key.
|
||||
*
|
||||
* @return the aes key
|
||||
*/
|
||||
String getAesKey();
|
||||
|
||||
/**
|
||||
* Gets template id.
|
||||
*
|
||||
* @return the template id
|
||||
*/
|
||||
String getTemplateId();
|
||||
|
||||
/**
|
||||
* Gets expires time.
|
||||
*
|
||||
* @return the expires time
|
||||
*/
|
||||
long getExpiresTime();
|
||||
|
||||
/**
|
||||
* Gets oauth 2 redirect uri.
|
||||
*
|
||||
* @return the oauth 2 redirect uri
|
||||
*/
|
||||
String getOauth2redirectUri();
|
||||
|
||||
/**
|
||||
* Gets http proxy host.
|
||||
*
|
||||
* @return the http proxy host
|
||||
*/
|
||||
String getHttpProxyHost();
|
||||
|
||||
/**
|
||||
* Gets http proxy port.
|
||||
*
|
||||
* @return the http proxy port
|
||||
*/
|
||||
int getHttpProxyPort();
|
||||
|
||||
/**
|
||||
* Gets http proxy username.
|
||||
*
|
||||
* @return the http proxy username
|
||||
*/
|
||||
String getHttpProxyUsername();
|
||||
|
||||
/**
|
||||
* Gets http proxy password.
|
||||
*
|
||||
* @return the http proxy password
|
||||
*/
|
||||
String getHttpProxyPassword();
|
||||
|
||||
/**
|
||||
* Gets tmp dir file.
|
||||
*
|
||||
* @return the tmp dir file
|
||||
*/
|
||||
File getTmpDirFile();
|
||||
|
||||
/**
|
||||
* http client builder.
|
||||
*
|
||||
* @return ApacheHttpClientBuilder apache http client builder
|
||||
*/
|
||||
ApacheHttpClientBuilder getApacheHttpClientBuilder();
|
||||
|
||||
/**
|
||||
* 是否自动刷新token.
|
||||
*
|
||||
* @return the boolean
|
||||
*/
|
||||
boolean autoRefreshToken();
|
||||
|
||||
/**
|
||||
* 得到微信接口地址域名部分的自定义设置信息.
|
||||
*
|
||||
* @return the host config
|
||||
*/
|
||||
WxQidianHostConfig getHostConfig();
|
||||
|
||||
/**
|
||||
* 设置微信接口地址域名部分的自定义设置信息.
|
||||
*
|
||||
* @param hostConfig host config
|
||||
*/
|
||||
void setHostConfig(WxQidianHostConfig hostConfig);
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
package me.chanjar.weixin.qidian.config.impl;
|
||||
|
||||
import lombok.Data;
|
||||
import me.chanjar.weixin.common.bean.WxAccessToken;
|
||||
import me.chanjar.weixin.common.enums.TicketType;
|
||||
import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder;
|
||||
import me.chanjar.weixin.qidian.bean.WxQidianHostConfig;
|
||||
import me.chanjar.weixin.qidian.config.WxQidianConfigStorage;
|
||||
import me.chanjar.weixin.qidian.util.json.WxQidianGsonBuilder;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.Serializable;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
* 基于内存的微信配置provider,在实际生产环境中应该将这些配置持久化.
|
||||
*
|
||||
* @author chanjarster
|
||||
*/
|
||||
@Data
|
||||
public class WxQidianDefaultConfigImpl implements WxQidianConfigStorage, Serializable {
|
||||
private static final long serialVersionUID = -6646519023303395185L;
|
||||
|
||||
protected volatile String appId;
|
||||
protected volatile String secret;
|
||||
protected volatile String token;
|
||||
protected volatile String templateId;
|
||||
protected volatile String accessToken;
|
||||
protected volatile String aesKey;
|
||||
protected volatile long expiresTime;
|
||||
|
||||
protected volatile String oauth2redirectUri;
|
||||
|
||||
protected volatile String httpProxyHost;
|
||||
protected volatile int httpProxyPort;
|
||||
protected volatile String httpProxyUsername;
|
||||
protected volatile String httpProxyPassword;
|
||||
|
||||
protected volatile String jsapiTicket;
|
||||
protected volatile long jsapiTicketExpiresTime;
|
||||
|
||||
protected volatile String sdkTicket;
|
||||
protected volatile long sdkTicketExpiresTime;
|
||||
|
||||
protected volatile String cardApiTicket;
|
||||
protected volatile long cardApiTicketExpiresTime;
|
||||
|
||||
protected volatile Lock accessTokenLock = new ReentrantLock();
|
||||
protected volatile Lock jsapiTicketLock = new ReentrantLock();
|
||||
protected volatile Lock sdkTicketLock = new ReentrantLock();
|
||||
protected volatile Lock cardApiTicketLock = new ReentrantLock();
|
||||
|
||||
protected volatile File tmpDirFile;
|
||||
|
||||
protected volatile ApacheHttpClientBuilder apacheHttpClientBuilder;
|
||||
|
||||
private WxQidianHostConfig hostConfig = null;
|
||||
|
||||
@Override
|
||||
public boolean isAccessTokenExpired() {
|
||||
return System.currentTimeMillis() > this.expiresTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void updateAccessToken(WxAccessToken accessToken) {
|
||||
updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void updateAccessToken(String accessToken, int expiresInSeconds) {
|
||||
this.accessToken = accessToken;
|
||||
this.expiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void expireAccessToken() {
|
||||
this.expiresTime = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTicket(TicketType type) {
|
||||
switch (type) {
|
||||
case SDK:
|
||||
return this.sdkTicket;
|
||||
case JSAPI:
|
||||
return this.jsapiTicket;
|
||||
case WX_CARD:
|
||||
return this.cardApiTicket;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void setTicket(TicketType type, String ticket) {
|
||||
switch (type) {
|
||||
case JSAPI:
|
||||
this.jsapiTicket = ticket;
|
||||
break;
|
||||
case WX_CARD:
|
||||
this.cardApiTicket = ticket;
|
||||
break;
|
||||
case SDK:
|
||||
this.sdkTicket = ticket;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Lock getTicketLock(TicketType type) {
|
||||
switch (type) {
|
||||
case SDK:
|
||||
return this.sdkTicketLock;
|
||||
case JSAPI:
|
||||
return this.jsapiTicketLock;
|
||||
case WX_CARD:
|
||||
return this.cardApiTicketLock;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTicketExpired(TicketType type) {
|
||||
switch (type) {
|
||||
case SDK:
|
||||
return System.currentTimeMillis() > this.sdkTicketExpiresTime;
|
||||
case JSAPI:
|
||||
return System.currentTimeMillis() > this.jsapiTicketExpiresTime;
|
||||
case WX_CARD:
|
||||
return System.currentTimeMillis() > this.cardApiTicketExpiresTime;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void updateTicket(TicketType type, String ticket, int expiresInSeconds) {
|
||||
switch (type) {
|
||||
case JSAPI:
|
||||
this.jsapiTicket = ticket;
|
||||
// 预留200秒的时间
|
||||
this.jsapiTicketExpiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L;
|
||||
break;
|
||||
case WX_CARD:
|
||||
this.cardApiTicket = ticket;
|
||||
// 预留200秒的时间
|
||||
this.cardApiTicketExpiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L;
|
||||
break;
|
||||
case SDK:
|
||||
this.sdkTicket = ticket;
|
||||
// 预留200秒的时间
|
||||
this.sdkTicketExpiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void expireTicket(TicketType type) {
|
||||
switch (type) {
|
||||
case JSAPI:
|
||||
this.jsapiTicketExpiresTime = 0;
|
||||
break;
|
||||
case WX_CARD:
|
||||
this.cardApiTicketExpiresTime = 0;
|
||||
break;
|
||||
case SDK:
|
||||
this.sdkTicketExpiresTime = 0;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return WxQidianGsonBuilder.create().toJson(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean autoRefreshToken() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WxQidianHostConfig getHostConfig() {
|
||||
return this.hostConfig;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHostConfig(WxQidianHostConfig hostConfig) {
|
||||
this.hostConfig = hostConfig;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package me.chanjar.weixin.qidian.config.impl;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import me.chanjar.weixin.common.enums.TicketType;
|
||||
import me.chanjar.weixin.common.redis.WxRedisOps;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 基于Redis的微信配置provider.
|
||||
*
|
||||
* <pre>
|
||||
* 使用说明:本实现仅供参考,并不完整,
|
||||
* 比如为减少项目依赖,未加入redis分布式锁的实现,如有需要请自行实现。
|
||||
* </pre>
|
||||
*
|
||||
* @author nickwong
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
public class WxQidianRedisConfigImpl extends WxQidianDefaultConfigImpl {
|
||||
private static final long serialVersionUID = -988502871997239733L;
|
||||
|
||||
private static final String ACCESS_TOKEN_KEY_TPL = "%s:access_token:%s";
|
||||
private static final String TICKET_KEY_TPL = "%s:ticket:key:%s:%s";
|
||||
private static final String LOCK_KEY_TPL = "%s:lock:%s:";
|
||||
|
||||
private final WxRedisOps redisOps;
|
||||
private final String keyPrefix;
|
||||
|
||||
private String accessTokenKey;
|
||||
private String lockKey;
|
||||
|
||||
public WxQidianRedisConfigImpl(WxRedisOps redisOps, String keyPrefix) {
|
||||
this.redisOps = redisOps;
|
||||
this.keyPrefix = keyPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* 每个公众号生成独有的存储key.
|
||||
*/
|
||||
@Override
|
||||
public void setAppId(String appId) {
|
||||
super.setAppId(appId);
|
||||
this.accessTokenKey = String.format(ACCESS_TOKEN_KEY_TPL, this.keyPrefix, appId);
|
||||
this.lockKey = String.format(LOCK_KEY_TPL, this.keyPrefix, appId);
|
||||
accessTokenLock = this.redisOps.getLock(lockKey.concat("accessTokenLock"));
|
||||
jsapiTicketLock = this.redisOps.getLock(lockKey.concat("jsapiTicketLock"));
|
||||
sdkTicketLock = this.redisOps.getLock(lockKey.concat("sdkTicketLock"));
|
||||
cardApiTicketLock = this.redisOps.getLock(lockKey.concat("cardApiTicketLock"));
|
||||
}
|
||||
|
||||
private String getTicketRedisKey(TicketType type) {
|
||||
return String.format(TICKET_KEY_TPL, this.keyPrefix, appId, type.getCode());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAccessToken() {
|
||||
return redisOps.getValue(this.accessTokenKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAccessTokenExpired() {
|
||||
Long expire = redisOps.getExpire(this.accessTokenKey);
|
||||
return expire == null || expire < 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void updateAccessToken(String accessToken, int expiresInSeconds) {
|
||||
redisOps.setValue(this.accessTokenKey, accessToken, expiresInSeconds - 200, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void expireAccessToken() {
|
||||
redisOps.expire(this.accessTokenKey, 0, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTicket(TicketType type) {
|
||||
return redisOps.getValue(this.getTicketRedisKey(type));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTicketExpired(TicketType type) {
|
||||
return redisOps.getExpire(this.getTicketRedisKey(type)) < 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void updateTicket(TicketType type, String jsapiTicket, int expiresInSeconds) {
|
||||
redisOps.setValue(this.getTicketRedisKey(type), jsapiTicket, expiresInSeconds - 200, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void expireTicket(TicketType type) {
|
||||
redisOps.expire(this.getTicketRedisKey(type), 0, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package me.chanjar.weixin.qidian.config.impl;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NonNull;
|
||||
import me.chanjar.weixin.common.enums.TicketType;
|
||||
import me.chanjar.weixin.common.redis.RedissonWxRedisOps;
|
||||
import me.chanjar.weixin.common.redis.WxRedisOps;
|
||||
import org.redisson.api.RedissonClient;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* @author wuxingye
|
||||
* @date 2020/6/12
|
||||
*/
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
public class WxQidianRedissonConfigImpl extends WxQidianDefaultConfigImpl {
|
||||
|
||||
private static final long serialVersionUID = -5139855123878455556L;
|
||||
private static final String ACCESS_TOKEN_KEY_TPL = "%s:access_token:%s";
|
||||
private static final String TICKET_KEY_TPL = "%s:ticket:key:%s:%s";
|
||||
private static final String LOCK_KEY_TPL = "%s:lock:%s:";
|
||||
private final WxRedisOps redisOps;
|
||||
private final String keyPrefix;
|
||||
private String accessTokenKey;
|
||||
private String lockKey;
|
||||
|
||||
public WxQidianRedissonConfigImpl(@NonNull RedissonClient redissonClient, String keyPrefix) {
|
||||
this(new RedissonWxRedisOps(redissonClient), keyPrefix);
|
||||
}
|
||||
|
||||
public WxQidianRedissonConfigImpl(@NonNull RedissonClient redissonClient) {
|
||||
this(redissonClient, null);
|
||||
}
|
||||
|
||||
private WxQidianRedissonConfigImpl(@NonNull WxRedisOps redisOps, String keyPrefix) {
|
||||
this.redisOps = redisOps;
|
||||
this.keyPrefix = keyPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* 每个公众号生成独有的存储key.
|
||||
*/
|
||||
@Override
|
||||
public void setAppId(String appId) {
|
||||
super.setAppId(appId);
|
||||
this.accessTokenKey = String.format(ACCESS_TOKEN_KEY_TPL, this.keyPrefix, appId);
|
||||
this.lockKey = String.format(LOCK_KEY_TPL, this.keyPrefix, appId);
|
||||
accessTokenLock = this.redisOps.getLock(lockKey.concat("accessTokenLock"));
|
||||
jsapiTicketLock = this.redisOps.getLock(lockKey.concat("jsapiTicketLock"));
|
||||
sdkTicketLock = this.redisOps.getLock(lockKey.concat("sdkTicketLock"));
|
||||
cardApiTicketLock = this.redisOps.getLock(lockKey.concat("cardApiTicketLock"));
|
||||
}
|
||||
|
||||
private String getTicketRedisKey(TicketType type) {
|
||||
return String.format(TICKET_KEY_TPL, this.keyPrefix, appId, type.getCode());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAccessToken() {
|
||||
return redisOps.getValue(this.accessTokenKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAccessTokenExpired() {
|
||||
Long expire = redisOps.getExpire(this.accessTokenKey);
|
||||
return expire == null || expire < 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void updateAccessToken(String accessToken, int expiresInSeconds) {
|
||||
redisOps.setValue(this.accessTokenKey, accessToken, expiresInSeconds - 200, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void expireAccessToken() {
|
||||
redisOps.expire(this.accessTokenKey, 0, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTicket(TicketType type) {
|
||||
return redisOps.getValue(this.getTicketRedisKey(type));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTicketExpired(TicketType type) {
|
||||
return redisOps.getExpire(this.getTicketRedisKey(type)) < 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void updateTicket(TicketType type, String jsapiTicket, int expiresInSeconds) {
|
||||
redisOps.setValue(this.getTicketRedisKey(type), jsapiTicket, expiresInSeconds - 200, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void expireTicket(TicketType type) {
|
||||
redisOps.expire(this.getTicketRedisKey(type), 0, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package me.chanjar.weixin.qidian.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import me.chanjar.weixin.qidian.bean.WxQidianHostConfig;
|
||||
import me.chanjar.weixin.qidian.config.WxQidianConfigStorage;
|
||||
|
||||
import static me.chanjar.weixin.qidian.bean.WxQidianHostConfig.*;
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* 腾讯企点接口api地址
|
||||
* Created by alegria on 2020年12月26日.
|
||||
* </pre>
|
||||
*/
|
||||
public interface WxQidianApiUrl {
|
||||
|
||||
/**
|
||||
* 得到api完整地址.
|
||||
*
|
||||
* @param config 微信公众号配置
|
||||
* @return api地址
|
||||
*/
|
||||
default String getUrl(WxQidianConfigStorage config) {
|
||||
WxQidianHostConfig hostConfig = null;
|
||||
if (config != null) {
|
||||
hostConfig = config.getHostConfig();
|
||||
}
|
||||
return buildUrl(hostConfig, this.getPrefix(), this.getPath());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* the path
|
||||
*
|
||||
* @return path
|
||||
*/
|
||||
String getPath();
|
||||
|
||||
/**
|
||||
* the prefix
|
||||
*
|
||||
* @return prefix
|
||||
*/
|
||||
String getPrefix();
|
||||
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
enum OAuth2 implements WxQidianApiUrl {
|
||||
/**
|
||||
* 用code换取oauth2的access token.
|
||||
*/
|
||||
OAUTH2_ACCESS_TOKEN_URL(API_DEFAULT_HOST_URL,
|
||||
"/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code"),
|
||||
/**
|
||||
* 刷新oauth2的access token.
|
||||
*/
|
||||
OAUTH2_REFRESH_TOKEN_URL(API_DEFAULT_HOST_URL,
|
||||
"/sns/oauth2/refresh_token?appid=%s&grant_type=refresh_token&refresh_token=%s"),
|
||||
/**
|
||||
* 用oauth2获取用户信息.
|
||||
*/
|
||||
OAUTH2_USERINFO_URL(API_DEFAULT_HOST_URL, "/sns/userinfo?access_token=%s&openid=%s&lang=%s"),
|
||||
/**
|
||||
* 验证oauth2的access token是否有效.
|
||||
*/
|
||||
OAUTH2_VALIDATE_TOKEN_URL(API_DEFAULT_HOST_URL, "/sns/auth?access_token=%s&openid=%s"),
|
||||
/**
|
||||
* oauth2授权的url连接.
|
||||
*/
|
||||
CONNECT_OAUTH2_AUTHORIZE_URL(OPEN_DEFAULT_HOST_URL,
|
||||
"/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s&connect_redirect=1#wechat_redirect");
|
||||
|
||||
private final String prefix;
|
||||
private final String path;
|
||||
|
||||
}
|
||||
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
enum Other implements WxQidianApiUrl {
|
||||
/**
|
||||
* 获取access_token.
|
||||
*/
|
||||
GET_ACCESS_TOKEN_URL(QIDIAN_DEFAULT_HOST_URL, "/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s"),
|
||||
/**
|
||||
* 获得各种类型的ticket.
|
||||
*/
|
||||
GET_TICKET_URL(API_DEFAULT_HOST_URL, "/cgi-bin/ticket/getticket?type="),
|
||||
/**
|
||||
* 长链接转短链接接口.
|
||||
*/
|
||||
SHORTURL_API_URL(API_DEFAULT_HOST_URL, "/cgi-bin/shorturl"),
|
||||
/**
|
||||
* 语义查询接口.
|
||||
*/
|
||||
SEMANTIC_SEMPROXY_SEARCH_URL(API_DEFAULT_HOST_URL, "/semantic/semproxy/search"),
|
||||
/**
|
||||
* 获取微信服务器IP地址.
|
||||
*/
|
||||
GET_CALLBACK_IP_URL(API_DEFAULT_HOST_URL, "/cgi-bin/getcallbackip"),
|
||||
/**
|
||||
* 网络检测.
|
||||
*/
|
||||
NETCHECK_URL(API_DEFAULT_HOST_URL, "/cgi-bin/callback/check"),
|
||||
/**
|
||||
* 第三方使用网站应用授权登录的url.
|
||||
*/
|
||||
QRCONNECT_URL(OPEN_DEFAULT_HOST_URL,
|
||||
"/connect/qrconnect?appid=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s#wechat_redirect"),
|
||||
/**
|
||||
* 获取公众号的自动回复规则.
|
||||
*/
|
||||
GET_CURRENT_AUTOREPLY_INFO_URL(API_DEFAULT_HOST_URL, "/cgi-bin/get_current_autoreply_info"),
|
||||
/**
|
||||
* 公众号调用或第三方平台帮公众号调用对公众号的所有api调用(包括第三方帮其调用)次数进行清零.
|
||||
*/
|
||||
CLEAR_QUOTA_URL(API_DEFAULT_HOST_URL, "/cgi-bin/clear_quota");
|
||||
|
||||
private final String prefix;
|
||||
private final String path;
|
||||
|
||||
}
|
||||
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
enum Dial implements WxQidianApiUrl {
|
||||
/**
|
||||
* IVR外呼.
|
||||
*/
|
||||
IVR_DIAL(QIDIAN_DEFAULT_HOST_URL, "/cgi-bin/call/dial/ivrdial"),
|
||||
/**
|
||||
* 拉取IVR列表.
|
||||
*/
|
||||
GET_IVR_LIST(QIDIAN_DEFAULT_HOST_URL, "/cgi-bin/call/dial/getivrlist");
|
||||
|
||||
private final String prefix;
|
||||
private final String path;
|
||||
|
||||
}
|
||||
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
enum CallData implements WxQidianApiUrl {
|
||||
/**
|
||||
* 总机号列表拉取.
|
||||
*/
|
||||
GET_SWITCH_BOARD_LIST(QIDIAN_DEFAULT_HOST_URL, "/cgi-bin/call/callData/getswitchboardlist");
|
||||
|
||||
private final String prefix;
|
||||
private final String path;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package me.chanjar.weixin.qidian.util;
|
||||
|
||||
/**
|
||||
* @author alegria
|
||||
* @date 2020年12月26日
|
||||
*/
|
||||
public class WxQidianConfigStorageHolder {
|
||||
private final static ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<String>() {
|
||||
@Override
|
||||
protected String initialValue() {
|
||||
return "default";
|
||||
}
|
||||
};
|
||||
|
||||
public static String get() {
|
||||
return THREAD_LOCAL.get();
|
||||
}
|
||||
|
||||
public static void set(String label) {
|
||||
THREAD_LOCAL.set(label);
|
||||
}
|
||||
|
||||
/**
|
||||
* 此方法需要用户根据自己程序代码,在适当位置手动触发调用,本SDK里无法判断调用时机
|
||||
*/
|
||||
public static void remove() {
|
||||
THREAD_LOCAL.remove();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package me.chanjar.weixin.qidian.util.json;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
|
||||
/**
|
||||
* @author someone
|
||||
*/
|
||||
public class WxQidianGsonBuilder {
|
||||
|
||||
private static final GsonBuilder INSTANCE = new GsonBuilder();
|
||||
|
||||
static {
|
||||
INSTANCE.disableHtmlEscaping();
|
||||
}
|
||||
|
||||
public static Gson create() {
|
||||
return INSTANCE.create();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user