diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpJedisConfigStorage.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpJedisConfigStorage.java index 07f38ebb..019cfc38 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpJedisConfigStorage.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpJedisConfigStorage.java @@ -4,6 +4,7 @@ import me.chanjar.weixin.common.bean.WxAccessToken; import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; import java.io.File; @@ -38,6 +39,15 @@ public class WxCpJedisConfigStorage implements WxCpConfigStorage { this.jedisPool = new JedisPool(host, port); } + + public WxCpJedisConfigStorage(JedisPoolConfig poolConfig, String host, int port) { + this.jedisPool = new JedisPool(poolConfig, host, port); + } + + public WxCpJedisConfigStorage(JedisPoolConfig poolConfig, String host, int port, int timeout, final String password) { + this.jedisPool = new JedisPool(poolConfig, host, port, timeout, password); + } + /** * This method will be destroy jedis pool */ diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/AbstractWxCpServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/AbstractWxCpServiceImpl.java new file mode 100644 index 00000000..27928d8b --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/AbstractWxCpServiceImpl.java @@ -0,0 +1,630 @@ +package me.chanjar.weixin.cp.api.impl; + +import com.google.gson.*; +import com.google.gson.reflect.TypeToken; +import me.chanjar.weixin.common.bean.WxJsapiSignature; +import me.chanjar.weixin.common.bean.menu.WxMenu; +import me.chanjar.weixin.common.bean.result.WxError; +import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; +import me.chanjar.weixin.common.exception.WxErrorException; +import me.chanjar.weixin.common.session.StandardSessionManager; +import me.chanjar.weixin.common.session.WxSession; +import me.chanjar.weixin.common.session.WxSessionManager; +import me.chanjar.weixin.common.util.RandomUtils; +import me.chanjar.weixin.common.util.crypto.SHA1; +import me.chanjar.weixin.common.util.fs.FileUtils; +import me.chanjar.weixin.common.util.http.*; +import me.chanjar.weixin.common.util.json.GsonHelper; +import me.chanjar.weixin.cp.api.WxCpConfigStorage; +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.bean.WxCpDepart; +import me.chanjar.weixin.cp.bean.WxCpMessage; +import me.chanjar.weixin.cp.bean.WxCpTag; +import me.chanjar.weixin.cp.bean.WxCpUser; +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.UUID; + +public abstract class AbstractWxCpServiceImpl implements WxCpService, RequestHttp { + + protected final Logger log = LoggerFactory.getLogger(AbstractWxCpServiceImpl.class); + + /** + * 全局的是否正在刷新access token的锁 + */ + protected final Object globalAccessTokenRefreshLock = new Object(); + + /** + * 全局的是否正在刷新jsapi_ticket的锁 + */ + protected final Object globalJsapiTicketRefreshLock = new Object(); + + protected WxCpConfigStorage configStorage; + + + protected WxSessionManager sessionManager = new StandardSessionManager(); + /** + * 临时文件目录 + */ + protected File tmpDirFile; + private int retrySleepMillis = 1000; + private int maxRetryTimes = 5; + + @Override + public boolean checkSignature(String msgSignature, String timestamp, String nonce, String data) { + try { + return SHA1.gen(this.configStorage.getToken(), timestamp, nonce, data) + .equals(msgSignature); + } catch (Exception e) { + return false; + } + } + + @Override + public void userAuthenticated(String userId) throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/user/authsucc?userid=" + userId; + get(url, null); + } + + @Override + public String getAccessToken() throws WxErrorException { + return getAccessToken(false); + } + + + @Override + public String getJsapiTicket() throws WxErrorException { + return getJsapiTicket(false); + } + + @Override + public String getJsapiTicket(boolean forceRefresh) throws WxErrorException { + if (forceRefresh) { + this.configStorage.expireJsapiTicket(); + } + if (this.configStorage.isJsapiTicketExpired()) { + synchronized (this.globalJsapiTicketRefreshLock) { + if (this.configStorage.isJsapiTicketExpired()) { + String url = "https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket"; + String responseContent = execute(new SimpleGetRequestExecutor(), url, null); + JsonElement tmpJsonElement = new JsonParser().parse(responseContent); + JsonObject tmpJsonObject = tmpJsonElement.getAsJsonObject(); + String jsapiTicket = tmpJsonObject.get("ticket").getAsString(); + int expiresInSeconds = tmpJsonObject.get("expires_in").getAsInt(); + this.configStorage.updateJsapiTicket(jsapiTicket, + expiresInSeconds); + } + } + } + return this.configStorage.getJsapiTicket(); + } + + @Override + public WxJsapiSignature createJsapiSignature(String url) throws WxErrorException { + long timestamp = System.currentTimeMillis() / 1000; + String noncestr = RandomUtils.getRandomStr(); + String jsapiTicket = getJsapiTicket(false); + String signature = SHA1.genWithAmple( + "jsapi_ticket=" + jsapiTicket, + "noncestr=" + noncestr, + "timestamp=" + timestamp, + "url=" + url + ); + WxJsapiSignature jsapiSignature = new WxJsapiSignature(); + jsapiSignature.setTimestamp(timestamp); + jsapiSignature.setNonceStr(noncestr); + jsapiSignature.setUrl(url); + jsapiSignature.setSignature(signature); + + // Fixed bug + jsapiSignature.setAppId(this.configStorage.getCorpId()); + + return jsapiSignature; + } + + @Override + public void messageSend(WxCpMessage message) throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/message/send"; + post(url, message.toJson()); + } + + @Override + public void menuCreate(WxMenu menu) throws WxErrorException { + menuCreate(this.configStorage.getAgentId(), menu); + } + + @Override + public void menuCreate(Integer agentId, WxMenu menu) throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/menu/create?agentid=" + + this.configStorage.getAgentId(); + post(url, menu.toJson()); + } + + @Override + public void menuDelete() throws WxErrorException { + menuDelete(this.configStorage.getAgentId()); + } + + @Override + public void menuDelete(Integer agentId) throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/menu/delete?agentid=" + agentId; + get(url, null); + } + + @Override + public WxMenu menuGet() throws WxErrorException { + return menuGet(this.configStorage.getAgentId()); + } + + @Override + public WxMenu menuGet(Integer agentId) throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/menu/get?agentid=" + agentId; + try { + String resultContent = get(url, null); + return WxMenu.fromJson(resultContent); + } catch (WxErrorException e) { + // 46003 不存在的菜单数据 + if (e.getError().getErrorCode() == 46003) { + return null; + } + throw e; + } + } + + @Override + public WxMediaUploadResult mediaUpload(String mediaType, String fileType, InputStream inputStream) + throws WxErrorException, IOException { + return mediaUpload(mediaType, FileUtils.createTmpFile(inputStream, UUID.randomUUID().toString(), fileType)); + } + + @Override + public WxMediaUploadResult mediaUpload(String mediaType, File file) throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/media/upload?type=" + mediaType; + return execute(new MediaUploadRequestExecutor(), url, file); + } + + @Override + public File mediaDownload(String mediaId) throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/media/get"; + return execute( + new MediaDownloadRequestExecutor( + this.configStorage.getTmpDirFile()), + url, "media_id=" + mediaId); + } + + + @Override + public Integer departCreate(WxCpDepart depart) throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/department/create"; + String responseContent = execute( + new SimplePostRequestExecutor(), + url, + depart.toJson()); + JsonElement tmpJsonElement = new JsonParser().parse(responseContent); + return GsonHelper.getAsInteger(tmpJsonElement.getAsJsonObject().get("id")); + } + + @Override + public void departUpdate(WxCpDepart group) throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/department/update"; + post(url, group.toJson()); + } + + @Override + public void departDelete(Integer departId) throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/department/delete?id=" + departId; + get(url, null); + } + + @Override + public List departGet() throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/department/list"; + String responseContent = get(url, null); + /* + * 操蛋的微信API,创建时返回的是 { group : { id : ..., name : ...} } + * 查询时返回的是 { groups : [ { id : ..., name : ..., count : ... }, ... ] } + */ + JsonElement tmpJsonElement = new JsonParser().parse(responseContent); + return WxCpGsonBuilder.INSTANCE.create() + .fromJson( + tmpJsonElement.getAsJsonObject().get("department"), + new TypeToken>() { + }.getType() + ); + } + + @Override + public void userCreate(WxCpUser user) throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/user/create"; + post(url, user.toJson()); + } + + @Override + public void userUpdate(WxCpUser user) throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/user/update"; + post(url, user.toJson()); + } + + @Override + public void userDelete(String userid) throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/user/delete?userid=" + userid; + get(url, null); + } + + @Override + public void userDelete(String[] userids) throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/user/batchdelete"; + JsonObject jsonObject = new JsonObject(); + JsonArray jsonArray = new JsonArray(); + for (String userid : userids) { + jsonArray.add(new JsonPrimitive(userid)); + } + jsonObject.add("useridlist", jsonArray); + post(url, jsonObject.toString()); + } + + @Override + public WxCpUser userGet(String userid) throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/user/get?userid=" + userid; + String responseContent = get(url, null); + return WxCpUser.fromJson(responseContent); + } + + @Override + public List userList(Integer departId, Boolean fetchChild, Integer status) throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/user/list?department_id=" + departId; + String params = ""; + if (fetchChild != null) { + params += "&fetch_child=" + (fetchChild ? "1" : "0"); + } + if (status != null) { + params += "&status=" + status; + } else { + params += "&status=0"; + } + + String responseContent = get(url, params); + JsonElement tmpJsonElement = new JsonParser().parse(responseContent); + return WxCpGsonBuilder.INSTANCE.create() + .fromJson( + tmpJsonElement.getAsJsonObject().get("userlist"), + new TypeToken>() { + }.getType() + ); + } + + @Override + public List departGetUsers(Integer departId, Boolean fetchChild, Integer status) throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/user/simplelist?department_id=" + departId; + String params = ""; + if (fetchChild != null) { + params += "&fetch_child=" + (fetchChild ? "1" : "0"); + } + if (status != null) { + params += "&status=" + status; + } else { + params += "&status=0"; + } + + String responseContent = get(url, params); + JsonElement tmpJsonElement = new JsonParser().parse(responseContent); + return WxCpGsonBuilder.INSTANCE.create() + .fromJson( + tmpJsonElement.getAsJsonObject().get("userlist"), + new TypeToken>() { + }.getType() + ); + } + + @Override + public String tagCreate(String tagName) throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/tag/create"; + JsonObject o = new JsonObject(); + o.addProperty("tagname", tagName); + String responseContent = post(url, o.toString()); + JsonElement tmpJsonElement = new JsonParser().parse(responseContent); + return tmpJsonElement.getAsJsonObject().get("tagid").getAsString(); + } + + @Override + public void tagUpdate(String tagId, String tagName) throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/tag/update"; + JsonObject o = new JsonObject(); + o.addProperty("tagid", tagId); + o.addProperty("tagname", tagName); + post(url, o.toString()); + } + + @Override + public void tagDelete(String tagId) throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/tag/delete?tagid=" + tagId; + get(url, null); + } + + @Override + public List tagGet() throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/tag/list"; + String responseContent = get(url, null); + JsonElement tmpJsonElement = new JsonParser().parse(responseContent); + return WxCpGsonBuilder.INSTANCE.create() + .fromJson( + tmpJsonElement.getAsJsonObject().get("taglist"), + new TypeToken>() { + }.getType() + ); + } + + @Override + public List tagGetUsers(String tagId) throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/tag/get?tagid=" + tagId; + String responseContent = get(url, null); + JsonElement tmpJsonElement = new JsonParser().parse(responseContent); + return WxCpGsonBuilder.INSTANCE.create() + .fromJson( + tmpJsonElement.getAsJsonObject().get("userlist"), + new TypeToken>() { + }.getType() + ); + } + + @Override + public void tagAddUsers(String tagId, List userIds, List partyIds) throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/tag/addtagusers"; + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("tagid", tagId); + if (userIds != null) { + JsonArray jsonArray = new JsonArray(); + for (String userId : userIds) { + jsonArray.add(new JsonPrimitive(userId)); + } + jsonObject.add("userlist", jsonArray); + } + if (partyIds != null) { + JsonArray jsonArray = new JsonArray(); + for (String userId : partyIds) { + jsonArray.add(new JsonPrimitive(userId)); + } + jsonObject.add("partylist", jsonArray); + } + post(url, jsonObject.toString()); + } + + @Override + public void tagRemoveUsers(String tagId, List userIds) throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/tag/deltagusers"; + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("tagid", tagId); + JsonArray jsonArray = new JsonArray(); + for (String userId : userIds) { + jsonArray.add(new JsonPrimitive(userId)); + } + jsonObject.add("userlist", jsonArray); + post(url, jsonObject.toString()); + } + + @Override + public String oauth2buildAuthorizationUrl(String state) { + return this.oauth2buildAuthorizationUrl( + this.configStorage.getOauth2redirectUri(), + state + ); + } + + @Override + public String oauth2buildAuthorizationUrl(String redirectUri, String state) { + String url = "https://open.weixin.qq.com/connect/oauth2/authorize?"; + url += "appid=" + this.configStorage.getCorpId(); + url += "&redirect_uri=" + URIUtil.encodeURIComponent(redirectUri); + url += "&response_type=code"; + url += "&scope=snsapi_base"; + if (state != null) { + url += "&state=" + state; + } + url += "#wechat_redirect"; + return url; + } + + @Override + public String[] oauth2getUserInfo(String code) throws WxErrorException { + return oauth2getUserInfo(this.configStorage.getAgentId(), code); + } + + @Override + public String[] oauth2getUserInfo(Integer agentId, String code) throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?" + + "code=" + code + + "&agentid=" + agentId; + String responseText = get(url, null); + JsonElement je = new JsonParser().parse(responseText); + JsonObject jo = je.getAsJsonObject(); + return new String[]{GsonHelper.getString(jo, "UserId"), GsonHelper.getString(jo, "DeviceId"), GsonHelper.getString(jo, "OpenId")}; + } + + @Override + public int invite(String userId, String inviteTips) throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/invite/send"; + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("userid", userId); + if (StringUtils.isNotEmpty(inviteTips)) { + jsonObject.addProperty("invite_tips", inviteTips); + } + String responseContent = post(url, jsonObject.toString()); + JsonElement tmpJsonElement = new JsonParser().parse(responseContent); + return tmpJsonElement.getAsJsonObject().get("type").getAsInt(); + } + + @Override + public String[] getCallbackIp() throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/getcallbackip"; + String responseContent = get(url, null); + JsonElement tmpJsonElement = new JsonParser().parse(responseContent); + JsonArray jsonArray = tmpJsonElement.getAsJsonObject().get("ip_list").getAsJsonArray(); + String[] ips = new String[jsonArray.size()]; + for (int i = 0; i < jsonArray.size(); i++) { + ips[i] = jsonArray.get(i).getAsString(); + } + return ips; + } + + @Override + public String get(String url, String queryParam) throws WxErrorException { + return execute(new SimpleGetRequestExecutor(), url, queryParam); + } + + @Override + public String post(String url, String postData) throws WxErrorException { + return execute(new SimplePostRequestExecutor(), url, postData); + } + + /** + * 向微信端发送请求,在这里执行的策略是当发生access_token过期时才去刷新,然后重新执行请求,而不是全局定时请求 + */ + @Override + public T execute(RequestExecutor 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) { + this.log.warn("重试达到最大次数【{}】", this.maxRetryTimes); + //最后一次重试失败后,直接抛出异常,不再等待 + throw new RuntimeException("微信服务端异常,超出重试次数"); + } + + WxError error = e.getError(); + /* + * -1 系统繁忙, 1000ms后重试 + */ + if (error.getErrorCode() == -1) { + int sleepMillis = this.retrySleepMillis * (1 << retryTimes); + try { + this.log.debug("微信系统繁忙,{} ms 后重试(第{}次)", sleepMillis, retryTimes + 1); + Thread.sleep(sleepMillis); + } catch (InterruptedException e1) { + throw new RuntimeException(e1); + } + } else { + throw e; + } + } + } while (retryTimes++ < this.maxRetryTimes); + + this.log.warn("重试达到最大次数【{}】", this.maxRetryTimes); + throw new RuntimeException("微信服务端异常,超出重试次数"); + } + + protected synchronized T executeInternal(RequestExecutor executor, String uri, E data) throws WxErrorException { + 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(this, uriWithAccessToken, data); + this.log.debug("\n[URL]: {}\n[PARAMS]: {}\n[RESPONSE]: {}", uriWithAccessToken, data, result); + return result; + } catch (WxErrorException e) { + WxError error = e.getError(); + /* + * 发生以下情况时尝试刷新access_token + * 40001 获取access_token时AppSecret错误,或者access_token无效 + * 42001 access_token超时 + * 40014 不合法的access_token,请开发者认真比对access_token的有效性(如是否过期),或查看是否正在为恰当的公众号调用接口 + */ + if (error.getErrorCode() == 42001 || error.getErrorCode() == 40001 || error.getErrorCode() == 40014) { + // 强制设置wxCpConfigStorage它的access token过期了,这样在下一次请求里就会刷新access token + this.configStorage.expireAccessToken(); + return execute(executor, uri, data); + } + + if (error.getErrorCode() != 0) { + this.log.error("\n[URL]: {}\n[PARAMS]: {}\n[RESPONSE]: {}", uriWithAccessToken, data, error); + throw new WxErrorException(error); + } + return null; + } catch (IOException e) { + this.log.error("\n[URL]: {}\n[PARAMS]: {}\n[EXCEPTION]: {}", uriWithAccessToken, data, e.getMessage()); + throw new RuntimeException(e); + } + } + + @Override + public void setWxCpConfigStorage(WxCpConfigStorage wxConfigProvider) { + this.configStorage = wxConfigProvider; + this.initHttp(); + } + + @Override + public void setRetrySleepMillis(int retrySleepMillis) { + this.retrySleepMillis = retrySleepMillis; + } + + + @Override + public void setMaxRetryTimes(int maxRetryTimes) { + this.maxRetryTimes = maxRetryTimes; + } + + @Override + public WxSession getSession(String id) { + if (this.sessionManager == null) { + return null; + } + return this.sessionManager.getSession(id); + } + + @Override + public WxSession getSession(String id, boolean create) { + if (this.sessionManager == null) { + return null; + } + return this.sessionManager.getSession(id, create); + } + + + @Override + public void setSessionManager(WxSessionManager sessionManager) { + this.sessionManager = sessionManager; + } + + @Override + public String replaceParty(String mediaId) throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/batch/replaceparty"; + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("media_id", mediaId); + return post(url, jsonObject.toString()); + } + + @Override + public String replaceUser(String mediaId) throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/batch/replaceuser"; + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("media_id", mediaId); + return post(url, jsonObject.toString()); + } + + @Override + public String getTaskResult(String joinId) throws WxErrorException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/batch/getresult?jobid=" + joinId; + return get(url, null); + } + + public File getTmpDirFile() { + return this.tmpDirFile; + } + + public void setTmpDirFile(File tmpDirFile) { + this.tmpDirFile = tmpDirFile; + } + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/jodd/WxCpServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/jodd/WxCpServiceImpl.java index d569f262..2096ffd1 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/jodd/WxCpServiceImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/jodd/WxCpServiceImpl.java @@ -5,9 +5,9 @@ import me.chanjar.weixin.common.bean.WxAccessToken; import me.chanjar.weixin.common.bean.result.WxError; import me.chanjar.weixin.common.exception.WxErrorException; import me.chanjar.weixin.cp.api.WxCpConfigStorage; -import me.chanjar.weixin.cp.api.impl.AbstractWxCpService; +import me.chanjar.weixin.cp.api.impl.AbstractWxCpServiceImpl; -public class WxCpServiceImpl extends AbstractWxCpService { +public class WxCpServiceImpl extends AbstractWxCpServiceImpl { protected HttpConnectionProvider httpClient; protected ProxyInfo httpProxy; diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/okhttp/WxCpServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/okhttp/WxCpServiceImpl.java index 1a9c2ac8..c33015d7 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/okhttp/WxCpServiceImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/okhttp/WxCpServiceImpl.java @@ -2,16 +2,15 @@ package me.chanjar.weixin.cp.api.impl.okhttp; import java.io.IOException; -import jodd.http.*; import me.chanjar.weixin.common.bean.WxAccessToken; import me.chanjar.weixin.common.bean.result.WxError; import me.chanjar.weixin.common.exception.WxErrorException; import me.chanjar.weixin.common.util.http.okhttp.OkhttpProxyInfo; import me.chanjar.weixin.cp.api.WxCpConfigStorage; -import me.chanjar.weixin.cp.api.impl.AbstractWxCpService; +import me.chanjar.weixin.cp.api.impl.AbstractWxCpServiceImpl; import okhttp3.*; -public class WxCpServiceImpl extends AbstractWxCpService { +public class WxCpServiceImpl extends AbstractWxCpServiceImpl { protected ConnectionPool httpClient; protected OkhttpProxyInfo httpProxy; diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpKefuService.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpKefuService.java index f57c9630..3e74d041 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpKefuService.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpKefuService.java @@ -9,14 +9,27 @@ import java.io.File; import java.util.Date; /** + *
  * 客服接口 ,
- * 命名采用kefu拼音的原因是:
- * 其英文CustomerService如果再加上Service后缀显得有点啰嗦,
- * 如果不加又显得表意不完整
- *
+ * 注意:命名采用kefu拼音的原因是:其英文CustomerService如果再加上Service后缀显得有点啰嗦,如果不加又显得表意不完整。
+ * 
* @author Binary Wang */ public interface WxMpKefuService { + String MESSAGE_CUSTOM_SEND = "https://api.weixin.qq.com/cgi-bin/message/custom/send"; + String GET_KF_LIST = "https://api.weixin.qq.com/cgi-bin/customservice/getkflist"; + String GET_ONLINE_KF_LIST = "https://api.weixin.qq.com/cgi-bin/customservice/getonlinekflist"; + String KFACCOUNT_ADD = "https://api.weixin.qq.com/customservice/kfaccount/add"; + String KFACCOUNT_UPDATE = "https://api.weixin.qq.com/customservice/kfaccount/update"; + String KFACCOUNT_INVITE_WORKER = "https://api.weixin.qq.com/customservice/kfaccount/inviteworker"; + String KFACCOUNT_UPLOAD_HEAD_IMG = "https://api.weixin.qq.com/customservice/kfaccount/uploadheadimg?kf_account=%s"; + String KFACCOUNT_DEL = "https://api.weixin.qq.com/customservice/kfaccount/del?kf_account=%s"; + String KFSESSION_CREATE = "https://api.weixin.qq.com/customservice/kfsession/create"; + String KFSESSION_CLOSE = "https://api.weixin.qq.com/customservice/kfsession/close"; + String KFSESSION_GET_SESSION = "https://api.weixin.qq.com/customservice/kfsession/getsession?openid=%s"; + String KFSESSION_GET_SESSION_LIST = "https://api.weixin.qq.com/customservice/kfsession/getsessionlist?kf_account=%s"; + String KFSESSION_GET_WAIT_CASE = "https://api.weixin.qq.com/customservice/kfsession/getwaitcase"; + String MSGRECORD_GET_MSG_LIST = "https://api.weixin.qq.com/customservice/msgrecord/getmsglist"; /** *
@@ -82,7 +95,7 @@ public interface WxMpKefuService {
    * 
*/ boolean kfAccountUploadHeadImg(String kfAccount, File imgFile) - throws WxErrorException; + throws WxErrorException; /** *
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpService.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpService.java
index 20cb3064..8f03350b 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpService.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpService.java
@@ -11,6 +11,70 @@ import me.chanjar.weixin.mp.bean.result.*;
  * 微信API的Service
  */
 public interface WxMpService {
+  /**
+   * 获取access_token
+   */
+  String GET_ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s";
+  /**
+   * 获得jsapi_ticket
+   */
+  String GET_JSAPI_TICKET_URL = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=jsapi";
+  /**
+   * 上传群发用的图文消息
+   */
+  String MEDIA_UPLOAD_NEWS_URL = "https://api.weixin.qq.com/cgi-bin/media/uploadnews";
+  /**
+   * 上传群发用的视频
+   */
+  String MEDIA_UPLOAD_VIDEO_URL = "https://api.weixin.qq.com/cgi-bin/media/uploadvideo";
+  /**
+   * 分组群发消息
+   */
+  String MESSAGE_MASS_SENDALL_URL = "https://api.weixin.qq.com/cgi-bin/message/mass/sendall";
+  /**
+   * 按openId列表群发消息
+   */
+  String MESSAGE_MASS_SEND_URL = "https://api.weixin.qq.com/cgi-bin/message/mass/send";
+  /**
+   * 群发消息预览接口
+   */
+  String MESSAGE_MASS_PREVIEW_URL = "https://api.weixin.qq.com/cgi-bin/message/mass/preview";
+  /**
+   * 长链接转短链接接口
+   */
+  String SHORTURL_API_URL = "https://api.weixin.qq.com/cgi-bin/shorturl";
+  /**
+   * 语义查询接口
+   */
+  String SEMANTIC_SEMPROXY_SEARCH_URL = "https://api.weixin.qq.com/semantic/semproxy/search";
+  /**
+   * 用code换取oauth2的access token
+   */
+  String OAUTH2_ACCESS_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code";
+  /**
+   * 刷新oauth2的access token
+   */
+  String OAUTH2_REFRESH_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=%s&grant_type=refresh_token&refresh_token=%s";
+  /**
+   * 用oauth2获取用户信息
+   */
+  String OAUTH2_USERINFO_URL = "https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s&lang=%s";
+  /**
+   * 验证oauth2的access token是否有效
+   */
+  String OAUTH2_VALIDATE_TOKEN_URL = "https://api.weixin.qq.com/sns/auth?access_token=%s&openid=%s";
+  /**
+   * 获取微信服务器IP地址
+   */
+  String GET_CALLBACK_IP_URL = "https://api.weixin.qq.com/cgi-bin/getcallbackip";
+  /**
+   * 第三方使用网站应用授权登录的url
+   */
+  String QRCONNECT_URL = "https://open.weixin.qq.com/connect/qrconnect?appid=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s#wechat_redirect";
+  /**
+   * oauth2授权的url连接
+   */
+  String CONNECT_OAUTH2_AUTHORIZE_URL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s#wechat_redirect";
 
   /**
    * 
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/AbstractWxMpServiceImpl.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/AbstractWxMpServiceImpl.java
new file mode 100644
index 00000000..00880ffd
--- /dev/null
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/AbstractWxMpServiceImpl.java
@@ -0,0 +1,396 @@
+package me.chanjar.weixin.mp.api.impl;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import me.chanjar.weixin.common.bean.WxJsapiSignature;
+import me.chanjar.weixin.common.bean.result.WxError;
+import me.chanjar.weixin.common.exception.WxErrorException;
+import me.chanjar.weixin.common.session.StandardSessionManager;
+import me.chanjar.weixin.common.session.WxSessionManager;
+import me.chanjar.weixin.common.util.RandomUtils;
+import me.chanjar.weixin.common.util.crypto.SHA1;
+import me.chanjar.weixin.common.util.http.*;
+import me.chanjar.weixin.mp.api.*;
+import me.chanjar.weixin.mp.bean.*;
+import me.chanjar.weixin.mp.bean.result.*;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.concurrent.locks.Lock;
+
+public abstract class AbstractWxMpServiceImpl implements WxMpService, RequestHttp {
+
+  private static final JsonParser JSON_PARSER = new JsonParser();
+
+  protected final Logger log = LoggerFactory.getLogger(this.getClass());
+  protected WxSessionManager sessionManager = new StandardSessionManager();
+  private WxMpConfigStorage wxMpConfigStorage;
+  private WxMpKefuService kefuService = new WxMpKefuServiceImpl(this);
+  private WxMpMaterialService materialService = new WxMpMaterialServiceImpl(this);
+  private WxMpMenuService menuService = new WxMpMenuServiceImpl(this);
+  private WxMpUserService userService = new WxMpUserServiceImpl(this);
+  private WxMpUserTagService tagService = new WxMpUserTagServiceImpl(this);
+  private WxMpQrcodeService qrCodeService = new WxMpQrcodeServiceImpl(this);
+  private WxMpCardService cardService = new WxMpCardServiceImpl(this);
+  private WxMpStoreService storeService = new WxMpStoreServiceImpl(this);
+  private WxMpDataCubeService dataCubeService = new WxMpDataCubeServiceImpl(this);
+  private WxMpUserBlacklistService blackListService = new WxMpUserBlacklistServiceImpl(this);
+  private WxMpTemplateMsgService templateMsgService = new WxMpTemplateMsgServiceImpl(this);
+  private WxMpDeviceService deviceService = new WxMpDeviceServiceImpl(this);
+
+  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) {
+      return false;
+    }
+  }
+
+  @Override
+  public String getJsapiTicket() throws WxErrorException {
+    return getJsapiTicket(false);
+  }
+
+  @Override
+  public String getJsapiTicket(boolean forceRefresh) throws WxErrorException {
+    Lock lock = this.getWxMpConfigStorage().getJsapiTicketLock();
+    try {
+      lock.lock();
+
+      if (forceRefresh) {
+        this.getWxMpConfigStorage().expireJsapiTicket();
+      }
+
+      if (this.getWxMpConfigStorage().isJsapiTicketExpired()) {
+        String responseContent = execute(new SimpleGetRequestExecutor(), WxMpService.GET_JSAPI_TICKET_URL, null);
+        JsonElement tmpJsonElement = JSON_PARSER.parse(responseContent);
+        JsonObject tmpJsonObject = tmpJsonElement.getAsJsonObject();
+        String jsapiTicket = tmpJsonObject.get("ticket").getAsString();
+        int expiresInSeconds = tmpJsonObject.get("expires_in").getAsInt();
+        this.getWxMpConfigStorage().updateJsapiTicket(jsapiTicket, expiresInSeconds);
+      }
+    } finally {
+      lock.unlock();
+    }
+    return this.getWxMpConfigStorage().getJsapiTicket();
+  }
+
+  @Override
+  public WxJsapiSignature createJsapiSignature(String url) throws WxErrorException {
+    long timestamp = System.currentTimeMillis() / 1000;
+    String noncestr = RandomUtils.getRandomStr();
+    String jsapiTicket = getJsapiTicket(false);
+    String signature = SHA1.genWithAmple("jsapi_ticket=" + jsapiTicket,
+      "noncestr=" + noncestr, "timestamp=" + timestamp, "url=" + url);
+    WxJsapiSignature jsapiSignature = new WxJsapiSignature();
+    jsapiSignature.setAppId(this.getWxMpConfigStorage().getAppId());
+    jsapiSignature.setTimestamp(timestamp);
+    jsapiSignature.setNonceStr(noncestr);
+    jsapiSignature.setUrl(url);
+    jsapiSignature.setSignature(signature);
+    return jsapiSignature;
+  }
+
+  @Override
+  public String getAccessToken() throws WxErrorException {
+    return getAccessToken(false);
+  }
+
+  @Override
+  public WxMpMassUploadResult massNewsUpload(WxMpMassNews news) throws WxErrorException {
+    String responseContent = this.post(WxMpService.MEDIA_UPLOAD_NEWS_URL, news.toJson());
+    return WxMpMassUploadResult.fromJson(responseContent);
+  }
+
+  @Override
+  public WxMpMassUploadResult massVideoUpload(WxMpMassVideo video) throws WxErrorException {
+    String responseContent = this.post(WxMpService.MEDIA_UPLOAD_VIDEO_URL, video.toJson());
+    return WxMpMassUploadResult.fromJson(responseContent);
+  }
+
+  @Override
+  public WxMpMassSendResult massGroupMessageSend(WxMpMassTagMessage message) throws WxErrorException {
+    String responseContent = this.post(WxMpService.MESSAGE_MASS_SENDALL_URL, message.toJson());
+    return WxMpMassSendResult.fromJson(responseContent);
+  }
+
+  @Override
+  public WxMpMassSendResult massOpenIdsMessageSend(WxMpMassOpenIdsMessage message) throws WxErrorException {
+    String responseContent = this.post(WxMpService.MESSAGE_MASS_SEND_URL, message.toJson());
+    return WxMpMassSendResult.fromJson(responseContent);
+  }
+
+  @Override
+  public WxMpMassSendResult massMessagePreview(WxMpMassPreviewMessage wxMpMassPreviewMessage) throws Exception {
+    String responseContent = this.post(WxMpService.MESSAGE_MASS_PREVIEW_URL, wxMpMassPreviewMessage.toJson());
+    return WxMpMassSendResult.fromJson(responseContent);
+  }
+
+  @Override
+  public String shortUrl(String long_url) throws WxErrorException {
+    JsonObject o = new JsonObject();
+    o.addProperty("action", "long2short");
+    o.addProperty("long_url", long_url);
+    String responseContent = this.post(WxMpService.SHORTURL_API_URL, o.toString());
+    JsonElement tmpJsonElement = JSON_PARSER.parse(responseContent);
+    return tmpJsonElement.getAsJsonObject().get("short_url").getAsString();
+  }
+
+  @Override
+  public WxMpSemanticQueryResult semanticQuery(WxMpSemanticQuery semanticQuery) throws WxErrorException {
+    String responseContent = this.post(WxMpService.SEMANTIC_SEMPROXY_SEARCH_URL, semanticQuery.toJson());
+    return WxMpSemanticQueryResult.fromJson(responseContent);
+  }
+
+  @Override
+  public String oauth2buildAuthorizationUrl(String redirectURI, String scope, String state) {
+    return String.format(WxMpService.CONNECT_OAUTH2_AUTHORIZE_URL,
+      this.getWxMpConfigStorage().getAppId(), URIUtil.encodeURIComponent(redirectURI), scope, StringUtils.trimToEmpty(state));
+  }
+
+  @Override
+  public String buildQrConnectUrl(String redirectURI, String scope, String state) {
+    return String.format(WxMpService.QRCONNECT_URL,
+      this.getWxMpConfigStorage().getAppId(), URIUtil.encodeURIComponent(redirectURI), scope, StringUtils.trimToEmpty(state));
+  }
+
+  private WxMpOAuth2AccessToken getOAuth2AccessToken(String url) throws WxErrorException {
+    try {
+      RequestExecutor executor = new SimpleGetRequestExecutor();
+      String responseText = executor.execute(this, url, null);
+      return WxMpOAuth2AccessToken.fromJson(responseText);
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @Override
+  public WxMpOAuth2AccessToken oauth2getAccessToken(String code) throws WxErrorException {
+    String url = String.format(WxMpService.OAUTH2_ACCESS_TOKEN_URL, this.getWxMpConfigStorage().getAppId(), this.getWxMpConfigStorage().getSecret(), code);
+    return this.getOAuth2AccessToken(url);
+  }
+
+  @Override
+  public WxMpOAuth2AccessToken oauth2refreshAccessToken(String refreshToken) throws WxErrorException {
+    String url = String.format(WxMpService.OAUTH2_REFRESH_TOKEN_URL, this.getWxMpConfigStorage().getAppId(), refreshToken);
+    return this.getOAuth2AccessToken(url);
+  }
+
+  @Override
+  public WxMpUser oauth2getUserInfo(WxMpOAuth2AccessToken oAuth2AccessToken, String lang) throws WxErrorException {
+    if (lang == null) {
+      lang = "zh_CN";
+    }
+
+    String url = String.format(WxMpService.OAUTH2_USERINFO_URL, oAuth2AccessToken.getAccessToken(), oAuth2AccessToken.getOpenId(), lang);
+
+    try {
+      RequestExecutor executor = new SimpleGetRequestExecutor();
+      String responseText = executor.execute(this, url, null);
+      return WxMpUser.fromJson(responseText);
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @Override
+  public boolean oauth2validateAccessToken(WxMpOAuth2AccessToken oAuth2AccessToken) {
+    String url = String.format(WxMpService.OAUTH2_VALIDATE_TOKEN_URL, oAuth2AccessToken.getAccessToken(), oAuth2AccessToken.getOpenId());
+
+    try {
+      RequestExecutor executor = new SimpleGetRequestExecutor();
+      executor.execute(this, url, null);
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    } catch (WxErrorException e) {
+      return false;
+    }
+    return true;
+  }
+
+  @Override
+  public String[] getCallbackIP() throws WxErrorException {
+    String responseContent = this.get(WxMpService.GET_CALLBACK_IP_URL, null);
+    JsonElement tmpJsonElement = JSON_PARSER.parse(responseContent);
+    JsonArray ipList = tmpJsonElement.getAsJsonObject().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 String get(String url, String queryParam) throws WxErrorException {
+    return execute(new SimpleGetRequestExecutor(), url, queryParam);
+  }
+
+  @Override
+  public String post(String url, String postData) throws WxErrorException {
+    return execute(new SimplePostRequestExecutor(), url, postData);
+  }
+
+  /**
+   * 向微信端发送请求,在这里执行的策略是当发生access_token过期时才去刷新,然后重新执行请求,而不是全局定时请求
+   */
+  public  T execute(RequestExecutor 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) {
+          this.log.warn("重试达到最大次数【{}】", maxRetryTimes);
+          //最后一次重试失败后,直接抛出异常,不再等待
+          throw new RuntimeException("微信服务端异常,超出重试次数");
+        }
+
+        WxError error = e.getError();
+        // -1 系统繁忙, 1000ms后重试
+        if (error.getErrorCode() == -1) {
+          int sleepMillis = this.retrySleepMillis * (1 << retryTimes);
+          try {
+            this.log.warn("微信系统繁忙,{} ms 后重试(第{}次)", sleepMillis, retryTimes + 1);
+            Thread.sleep(sleepMillis);
+          } catch (InterruptedException e1) {
+            throw new RuntimeException(e1);
+          }
+        } else {
+          throw e;
+        }
+      }
+    } while (retryTimes++ < this.maxRetryTimes);
+
+    this.log.warn("重试达到最大次数【{}】", this.maxRetryTimes);
+    throw new RuntimeException("微信服务端异常,超出重试次数");
+  }
+
+  public synchronized  T executeInternal(RequestExecutor executor, String uri, E data) throws WxErrorException {
+    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(this, uriWithAccessToken, data);
+      this.log.debug("\n[URL]:  {}\n[PARAMS]: {}\n[RESPONSE]: {}", uriWithAccessToken, data, result);
+      return result;
+    } catch (WxErrorException e) {
+      WxError error = e.getError();
+      /*
+       * 发生以下情况时尝试刷新access_token
+       * 40001 获取access_token时AppSecret错误,或者access_token无效
+       * 42001 access_token超时
+       * 40014 不合法的access_token,请开发者认真比对access_token的有效性(如是否过期),或查看是否正在为恰当的公众号调用接口
+       */
+      if (error.getErrorCode() == 42001 || error.getErrorCode() == 40001 || error.getErrorCode() == 40014) {
+        // 强制设置wxMpConfigStorage它的access token过期了,这样在下一次请求里就会刷新access token
+        this.getWxMpConfigStorage().expireAccessToken();
+        if (this.getWxMpConfigStorage().autoRefreshToken()) {
+          return this.execute(executor, uri, data);
+        }
+      }
+
+      if (error.getErrorCode() != 0) {
+        this.log.error("\n[URL]:  {}\n[PARAMS]: {}\n[RESPONSE]: {}", uriWithAccessToken, data, error);
+        throw new WxErrorException(error);
+      }
+      return null;
+    } catch (IOException e) {
+      this.log.error("\n[URL]:  {}\n[PARAMS]: {}\n[EXCEPTION]: {}", uriWithAccessToken, data, e.getMessage());
+      throw new RuntimeException(e);
+    }
+  }
+
+  @Override
+  public WxMpConfigStorage getWxMpConfigStorage() {
+    return this.wxMpConfigStorage;
+  }
+
+  @Override
+  public void setWxMpConfigStorage(WxMpConfigStorage wxConfigProvider) {
+    this.wxMpConfigStorage = wxConfigProvider;
+    this.initHttp();
+  }
+
+  @Override
+  public void setRetrySleepMillis(int retrySleepMillis) {
+    this.retrySleepMillis = retrySleepMillis;
+  }
+
+  @Override
+  public void setMaxRetryTimes(int maxRetryTimes) {
+    this.maxRetryTimes = maxRetryTimes;
+  }
+
+  @Override
+  public WxMpKefuService getKefuService() {
+    return this.kefuService;
+  }
+
+  @Override
+  public WxMpMaterialService getMaterialService() {
+    return this.materialService;
+  }
+
+  @Override
+  public WxMpMenuService getMenuService() {
+    return this.menuService;
+  }
+
+  @Override
+  public WxMpUserService getUserService() {
+    return this.userService;
+  }
+
+  @Override
+  public WxMpUserTagService getUserTagService() {
+    return this.tagService;
+  }
+
+  @Override
+  public WxMpQrcodeService getQrcodeService() {
+    return this.qrCodeService;
+  }
+
+  @Override
+  public WxMpCardService getCardService() {
+    return this.cardService;
+  }
+
+  @Override
+  public WxMpDataCubeService getDataCubeService() {
+    return this.dataCubeService;
+  }
+
+  @Override
+  public WxMpUserBlacklistService getBlackListService() {
+    return this.blackListService;
+  }
+
+  @Override
+  public WxMpStoreService getStoreService() {
+    return this.storeService;
+  }
+
+  @Override
+  public WxMpTemplateMsgService getTemplateMsgService() {
+    return this.templateMsgService;
+  }
+
+  @Override
+  public WxMpDeviceService getDeviceService() {
+    return this.deviceService;
+  }
+}
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpKefuServiceImpl.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpKefuServiceImpl.java
index 4667f327..b5a6b395 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpKefuServiceImpl.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpKefuServiceImpl.java
@@ -18,15 +18,10 @@ import java.io.File;
 import java.util.Date;
 
 /**
- *
  * @author Binary Wang
- *
  */
 public class WxMpKefuServiceImpl implements WxMpKefuService {
-  protected final Logger log = LoggerFactory
-      .getLogger(WxMpKefuServiceImpl.class);
-  private static final String API_URL_PREFIX = "https://api.weixin.qq.com/customservice";
-  private static final String API_URL_PREFIX_WITH_CGI_BIN = "https://api.weixin.qq.com/cgi-bin/customservice";
+  protected final Logger log = LoggerFactory.getLogger(this.getClass());
   private WxMpService wxMpService;
 
   public WxMpKefuServiceImpl(WxMpService wxMpService) {
@@ -34,127 +29,103 @@ public class WxMpKefuServiceImpl implements WxMpKefuService {
   }
 
   @Override
-  public boolean sendKefuMessage(WxMpKefuMessage message)
-      throws WxErrorException {
-    String url = "https://api.weixin.qq.com/cgi-bin/message/custom/send";
-    String responseContent = this.wxMpService.post(url, message.toJson());
+  public boolean sendKefuMessage(WxMpKefuMessage message) throws WxErrorException {
+    String responseContent = this.wxMpService.post(MESSAGE_CUSTOM_SEND, message.toJson());
     return responseContent != null;
   }
 
   @Override
   public WxMpKfList kfList() throws WxErrorException {
-    String url = API_URL_PREFIX_WITH_CGI_BIN + "/getkflist";
-    String responseContent = this.wxMpService.get(url, null);
+    String responseContent = this.wxMpService.get(GET_KF_LIST, null);
     return WxMpKfList.fromJson(responseContent);
   }
 
   @Override
   public WxMpKfOnlineList kfOnlineList() throws WxErrorException {
-    String url = API_URL_PREFIX_WITH_CGI_BIN + "/getonlinekflist";
-    String responseContent = this.wxMpService.get(url, null);
+    String responseContent = this.wxMpService.get(GET_ONLINE_KF_LIST, null);
     return WxMpKfOnlineList.fromJson(responseContent);
   }
 
   @Override
-  public boolean kfAccountAdd(WxMpKfAccountRequest request)
-      throws WxErrorException {
-    String url = API_URL_PREFIX + "/kfaccount/add";
-    String responseContent = this.wxMpService.post(url, request.toJson());
+  public boolean kfAccountAdd(WxMpKfAccountRequest request) throws WxErrorException {
+    String responseContent = this.wxMpService.post(KFACCOUNT_ADD, request.toJson());
     return responseContent != null;
   }
 
   @Override
-  public boolean kfAccountUpdate(WxMpKfAccountRequest request)
-      throws WxErrorException {
-    String url = API_URL_PREFIX + "/kfaccount/update";
-    String responseContent = this.wxMpService.post(url, request.toJson());
+  public boolean kfAccountUpdate(WxMpKfAccountRequest request) throws WxErrorException {
+    String responseContent = this.wxMpService.post(KFACCOUNT_UPDATE, request.toJson());
     return responseContent != null;
   }
 
   @Override
   public boolean kfAccountInviteWorker(WxMpKfAccountRequest request) throws WxErrorException {
-    String url = API_URL_PREFIX + "/kfaccount/inviteworker";
-    String responseContent = this.wxMpService.post(url, request.toJson());
+    String responseContent = this.wxMpService.post(KFACCOUNT_INVITE_WORKER, request.toJson());
     return responseContent != null;
   }
 
   @Override
-  public boolean kfAccountUploadHeadImg(String kfAccount, File imgFile)
-      throws WxErrorException {
-    String url = API_URL_PREFIX + "/kfaccount/uploadheadimg?kf_account=" + kfAccount;
+  public boolean kfAccountUploadHeadImg(String kfAccount, File imgFile) throws WxErrorException {
     WxMediaUploadResult responseContent = this.wxMpService
-        .execute(new MediaUploadRequestExecutor(), url, imgFile);
+      .execute(new MediaUploadRequestExecutor(), String.format(KFACCOUNT_UPLOAD_HEAD_IMG, kfAccount), imgFile);
     return responseContent != null;
   }
 
   @Override
   public boolean kfAccountDel(String kfAccount) throws WxErrorException {
-    String url = API_URL_PREFIX + "/kfaccount/del?kf_account=" + kfAccount;
-    String responseContent = this.wxMpService.get(url, null);
+    String responseContent = this.wxMpService.get(String.format(KFACCOUNT_DEL, kfAccount), null);
     return responseContent != null;
   }
 
   @Override
-  public boolean kfSessionCreate(String openid, String kfAccount)
-      throws WxErrorException {
+  public boolean kfSessionCreate(String openid, String kfAccount) throws WxErrorException {
     WxMpKfSessionRequest request = new WxMpKfSessionRequest(kfAccount, openid);
-    String url = API_URL_PREFIX + "/kfsession/create";
-    String responseContent = this.wxMpService.post(url, request.toJson());
+    String responseContent = this.wxMpService.post(KFSESSION_CREATE, request.toJson());
     return responseContent != null;
   }
 
   @Override
-  public boolean kfSessionClose(String openid, String kfAccount)
-      throws WxErrorException {
+  public boolean kfSessionClose(String openid, String kfAccount) throws WxErrorException {
     WxMpKfSessionRequest request = new WxMpKfSessionRequest(kfAccount, openid);
-    String url = API_URL_PREFIX + "/kfsession/close";
-    String responseContent = this.wxMpService.post(url, request.toJson());
+    String responseContent = this.wxMpService.post(KFSESSION_CLOSE, request.toJson());
     return responseContent != null;
   }
 
   @Override
-  public WxMpKfSessionGetResult kfSessionGet(String openid)
-      throws WxErrorException {
-    String url = API_URL_PREFIX + "/kfsession/getsession?openid=" + openid;
-    String responseContent = this.wxMpService.get(url, null);
+  public WxMpKfSessionGetResult kfSessionGet(String openid) throws WxErrorException {
+    String responseContent = this.wxMpService.get(String.format(KFSESSION_GET_SESSION, openid), null);
     return WxMpKfSessionGetResult.fromJson(responseContent);
   }
 
   @Override
-  public WxMpKfSessionList kfSessionList(String kfAccount)
-      throws WxErrorException {
-    String url = API_URL_PREFIX + "/kfsession/getsessionlist?kf_account=" + kfAccount;
-    String responseContent = this.wxMpService.get(url, null);
+  public WxMpKfSessionList kfSessionList(String kfAccount) throws WxErrorException {
+    String responseContent = this.wxMpService.get(String.format(KFSESSION_GET_SESSION_LIST, kfAccount), null);
     return WxMpKfSessionList.fromJson(responseContent);
   }
 
   @Override
-  public WxMpKfSessionWaitCaseList kfSessionGetWaitCase()
-      throws WxErrorException {
-    String url = API_URL_PREFIX + "/kfsession/getwaitcase";
-    String responseContent = this.wxMpService.get(url, null);
+  public WxMpKfSessionWaitCaseList kfSessionGetWaitCase() throws WxErrorException {
+    String responseContent = this.wxMpService.get(KFSESSION_GET_WAIT_CASE, null);
     return WxMpKfSessionWaitCaseList.fromJson(responseContent);
   }
 
   @Override
   public WxMpKfMsgList kfMsgList(Date startTime, Date endTime, Long msgId, Integer number) throws WxErrorException {
-    if(number > 10000){
+    if (number > 10000) {
       throw new WxErrorException(WxError.newBuilder().setErrorMsg("非法参数请求,每次最多查询10000条记录!").build());
     }
 
-    if(startTime.after(endTime)){
+    if (startTime.after(endTime)) {
       throw new WxErrorException(WxError.newBuilder().setErrorMsg("起始时间不能晚于结束时间!").build());
     }
 
-    String url = API_URL_PREFIX + "/msgrecord/getmsglist";
-
     JsonObject param = new JsonObject();
     param.addProperty("starttime", startTime.getTime() / 1000); //starttime	起始时间,unix时间戳
     param.addProperty("endtime", endTime.getTime() / 1000); //endtime	结束时间,unix时间戳,每次查询时段不能超过24小时
     param.addProperty("msgid", msgId); //msgid	消息id顺序从小到大,从1开始
     param.addProperty("number", number); //number	每次获取条数,最多10000条
 
-    String responseContent = this.wxMpService.post(url, param.toString());
+    String responseContent = this.wxMpService.post(MSGRECORD_GET_MSG_LIST, param.toString());
 
     return WxMpKfMsgList.fromJson(responseContent);
   }
@@ -162,16 +133,16 @@ public class WxMpKefuServiceImpl implements WxMpKefuService {
   @Override
   public WxMpKfMsgList kfMsgList(Date startTime, Date endTime) throws WxErrorException {
     int number = 10000;
-    WxMpKfMsgList result =  this.kfMsgList(startTime,endTime, 1L, number);
+    WxMpKfMsgList result = this.kfMsgList(startTime, endTime, 1L, number);
 
-    if(result != null && result.getNumber() == number){
+    if (result != null && result.getNumber() == number) {
       Long msgId = result.getMsgId();
-      WxMpKfMsgList followingResult =  this.kfMsgList(startTime,endTime, msgId, number);
-      while(followingResult != null  && followingResult.getRecords().size() > 0){
+      WxMpKfMsgList followingResult = this.kfMsgList(startTime, endTime, msgId, number);
+      while (followingResult != null && followingResult.getRecords().size() > 0) {
         result.getRecords().addAll(followingResult.getRecords());
         result.setNumber(result.getNumber() + followingResult.getNumber());
         result.setMsgId(followingResult.getMsgId());
-        followingResult = this.kfMsgList(startTime,endTime, followingResult.getMsgId(), number);
+        followingResult = this.kfMsgList(startTime, endTime, followingResult.getMsgId(), number);
       }
     }
 
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/apache/WxMpServiceImpl.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/apache/WxMpServiceImpl.java
index 8c8085da..0eaf200a 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/apache/WxMpServiceImpl.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/apache/WxMpServiceImpl.java
@@ -1,8 +1,24 @@
 package me.chanjar.weixin.mp.api.impl.apache;
 
-import java.io.IOException;
-import java.util.concurrent.locks.Lock;
-
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import me.chanjar.weixin.common.bean.WxAccessToken;
+import me.chanjar.weixin.common.bean.WxJsapiSignature;
+import me.chanjar.weixin.common.bean.result.WxError;
+import me.chanjar.weixin.common.exception.WxErrorException;
+import me.chanjar.weixin.common.session.StandardSessionManager;
+import me.chanjar.weixin.common.session.WxSessionManager;
+import me.chanjar.weixin.common.util.RandomUtils;
+import me.chanjar.weixin.common.util.crypto.SHA1;
+import me.chanjar.weixin.common.util.http.*;
+import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder;
+import me.chanjar.weixin.common.util.http.apache.DefaultApacheHttpClientBuilder;
+import me.chanjar.weixin.mp.api.*;
+import me.chanjar.weixin.mp.api.impl.*;
+import me.chanjar.weixin.mp.bean.*;
+import me.chanjar.weixin.mp.bean.result.*;
 import org.apache.http.HttpHost;
 import org.apache.http.client.config.RequestConfig;
 import org.apache.http.client.methods.CloseableHttpResponse;
@@ -10,18 +26,13 @@ import org.apache.http.client.methods.HttpGet;
 import org.apache.http.impl.client.BasicResponseHandler;
 import org.apache.http.impl.client.CloseableHttpClient;
 
-import me.chanjar.weixin.common.bean.WxAccessToken;
-import me.chanjar.weixin.common.bean.result.WxError;
-import me.chanjar.weixin.common.exception.WxErrorException;
-import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder;
-import me.chanjar.weixin.common.util.http.apache.DefaultApacheHttpClientBuilder;
-import me.chanjar.weixin.mp.api.WxMpConfigStorage;
-import me.chanjar.weixin.mp.api.impl.AbstractWxMpService;
+import java.io.IOException;
+import java.util.concurrent.locks.Lock;
 
 /**
  * apache-http方式实现
  */
-public class WxMpServiceImpl extends AbstractWxMpService {
+public class WxMpServiceImpl extends AbstractWxMpServiceImpl {
   private CloseableHttpClient httpClient;
   private HttpHost httpProxy;
 
@@ -66,9 +77,8 @@ public class WxMpServiceImpl extends AbstractWxMpService