diff --git a/pom.xml b/pom.xml index 61a04739..7f6b66a6 100644 --- a/pom.xml +++ b/pom.xml @@ -44,6 +44,7 @@ 4.5 1.7.10 1.1.2 + 3.6.7 @@ -68,6 +69,11 @@ httpmime ${httpclient.version} + + org.jodd + jodd-http + ${jodd-http.version} + com.google.code.gson gson diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/api/WxConsts.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/api/WxConsts.java index 0455e6c2..85efcd3f 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/api/WxConsts.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/api/WxConsts.java @@ -47,7 +47,7 @@ public class WxConsts { /////////////////////// public static final String MASS_ST_SUCCESS = "send success"; public static final String MASS_ST_FAIL = "send fail"; - public static final String MASS_ST_涉嫌广告 = "err(10001)"; + public static final String MASS_ST_涉嫌广告 = "err(10001)"; public static final String MASS_ST_涉嫌政治 = "err(20001)"; public static final String MASS_ST_涉嫌社会 = "err(20004)"; public static final String MASS_ST_涉嫌色情 = "err(20002)"; @@ -93,6 +93,15 @@ public class WxConsts { public static final String EVT_LOCATION_SELECT = "location_select"; public static final String EVT_TEMPLATESENDJOBFINISH = "TEMPLATESENDJOBFINISH"; public static final String EVT_ENTER_AGENT = "enter_agent"; + public static final String EVT_CARD_PASS_CHECK = "card_pass_check"; + public static final String EVT_CARD_NOT_PASS_CHECK = "card_not_pass_check"; + public static final String EVT_USER_GET_CARD = "user_get_card"; + public static final String EVT_USER_DEL_CARD = "user_del_card"; + public static final String EVT_USER_CONSUME_CARD = "user_consume_card"; + public static final String EVT_USER_PAY_FROM_PAY_CELL = "user_pay_from_pay_cell"; + public static final String EVT_USER_VIEW_CARD = "user_view_card"; + public static final String EVT_USER_ENTER_SESSION_FROM_CARD = "user_enter_session_from_card"; + public static final String EVT_CARD_SKU_REMIND = "card_sku_remind"; // 库存报警 /////////////////////// // 上传多媒体文件的类型 diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/WxCardApiSignature.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/WxCardApiSignature.java new file mode 100644 index 00000000..5d4d46a9 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/WxCardApiSignature.java @@ -0,0 +1,102 @@ +package me.chanjar.weixin.common.bean; + +import java.io.Serializable; + +/** + * 卡券Api签名 + * + * @author YuJian + * @version 15/11/8 + */ +public class WxCardApiSignature implements Serializable { + + private String appId; + + private String cardId; + + private String cardType; + + private String locationId; + + private String code; + + private String openId; + + private Long timestamp; + + private String nonceStr; + + private String signature; + + public String getAppId() { + return appId; + } + + public void setAppId(String appId) { + this.appId = appId; + } + + public String getCardId() { + return cardId; + } + + public void setCardId(String cardId) { + this.cardId = cardId; + } + + public String getCardType() { + return cardType; + } + + public void setCardType(String cardType) { + this.cardType = cardType; + } + + public String getLocationId() { + return locationId; + } + + public void setLocationId(String locationId) { + this.locationId = locationId; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getOpenId() { + return openId; + } + + public void setOpenId(String openId) { + this.openId = openId; + } + + public Long getTimestamp() { + return timestamp; + } + + public void setTimestamp(Long timestamp) { + this.timestamp = timestamp; + } + + public String getNonceStr() { + return nonceStr; + } + + public void setNonceStr(String nonceStr) { + this.nonceStr = nonceStr; + } + + public String getSignature() { + return signature; + } + + public void setSignature(String signature) { + this.signature = signature; + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/WxMenu.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/WxMenu.java index e20be680..ccc7bc49 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/WxMenu.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/WxMenu.java @@ -1,15 +1,14 @@ package me.chanjar.weixin.common.bean; +import me.chanjar.weixin.common.util.json.WxGsonBuilder; +import org.apache.commons.codec.Charsets; + import java.io.InputStream; import java.io.InputStreamReader; import java.io.Serializable; import java.util.ArrayList; import java.util.List; -import org.apache.commons.codec.Charsets; - -import me.chanjar.weixin.common.util.json.WxGsonBuilder; - /** * 企业号菜单 * @author Daniel Qian @@ -136,6 +135,7 @@ public class WxMenu implements Serializable { private String province; private String city; private String clientPlatformType; + private String language; public String getGroupId() { return groupId; @@ -184,8 +184,16 @@ public class WxMenu implements Serializable { public void setClientPlatformType(String clientPlatformType) { this.clientPlatformType = clientPlatformType; } - - @Override + + public String getLanguage() { + return language; + } + + public void setLanguage(String language) { + this.language = language; + } + + @Override public String toString() { return "matchrule:{" + "group_id='" + groupId + '\'' + @@ -194,6 +202,7 @@ public class WxMenu implements Serializable { ", province" + province + '\'' + ", city" + city + '\'' + ", client_platform_type" + clientPlatformType + '\'' + + ", language" + language + '\'' + "}"; } } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/ApacheHttpClientBuilder.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/ApacheHttpClientBuilder.java new file mode 100644 index 00000000..1bd6a98c --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/ApacheHttpClientBuilder.java @@ -0,0 +1,51 @@ +package me.chanjar.weixin.common.util.http; + +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.impl.client.CloseableHttpClient; + +/** + * httpclient build interface + */ +public interface ApacheHttpClientBuilder { + + /** + * 构建httpclient实例 + * @return new instance of CloseableHttpClient + */ + CloseableHttpClient build(); + + /** + * 代理服务器地址 + * @param httpProxyHost + * @return + */ + ApacheHttpClientBuilder httpProxyHost(String httpProxyHost); + + /** + * 代理服务器端口 + * @param httpProxyPort + * @return + */ + ApacheHttpClientBuilder httpProxyPort(int httpProxyPort); + + /** + * 代理服务器用户名 + * @param httpProxyUsername + * @return + */ + ApacheHttpClientBuilder httpProxyUsername(String httpProxyUsername); + + /** + * 代理服务器密码 + * @param httpProxyPassword + * @return + */ + ApacheHttpClientBuilder httpProxyPassword(String httpProxyPassword); + + /** + * ssl连接socket工厂 + * @param sslConnectionSocketFactory + * @return + */ + ApacheHttpClientBuilder sslConnectionSocketFactory(SSLConnectionSocketFactory sslConnectionSocketFactory); +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/DefaultApacheHttpHttpClientBuilder.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/DefaultApacheHttpHttpClientBuilder.java new file mode 100644 index 00000000..aa20fa0b --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/DefaultApacheHttpHttpClientBuilder.java @@ -0,0 +1,199 @@ +package me.chanjar.weixin.common.util.http; + +import me.chanjar.weixin.common.util.StringUtils; +import org.apache.http.annotation.NotThreadSafe; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.client.HttpRequestRetryHandler; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.config.Registry; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.config.SocketConfig; +import org.apache.http.conn.HttpClientConnectionManager; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.conn.socket.PlainConnectionSocketFactory; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.protocol.HttpContext; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +/** + * httpclient 连接管理器 + */ +@NotThreadSafe +public class DefaultApacheHttpHttpClientBuilder implements ApacheHttpClientBuilder { + private int connectionRequestTimeout = 3000; + private int connectionTimeout = 5000; + private int soTimeout = 5000; + private int idleConnTimeout = 60000; + private int checkWaitTime = 5000; + private int maxConnPerHost = 10; + private int maxTotalConn = 50; + private String userAgent; + private HttpRequestRetryHandler httpRequestRetryHandler = new HttpRequestRetryHandler() { + @Override + public boolean retryRequest(IOException exception, int executionCount, HttpContext context) { + return false; + } + }; + private SSLConnectionSocketFactory sslConnectionSocketFactory = SSLConnectionSocketFactory.getSocketFactory(); + private PlainConnectionSocketFactory plainConnectionSocketFactory = PlainConnectionSocketFactory.getSocketFactory(); + + private String httpProxyHost; + private int httpProxyPort; + private String httpProxyUsername; + private String httpProxyPassword; + + /** + * 连接管理器 + */ + private PoolingHttpClientConnectionManager connectionManager; + /** + * 闲置连接监控线程 + */ + private IdleConnectionMonitorThread idleConnectionMonitorThread; + + /** + * httpClientBuilder + */ + private HttpClientBuilder httpClientBuilder; + + private boolean prepared = false; + + private DefaultApacheHttpHttpClientBuilder() { + } + + public static DefaultApacheHttpHttpClientBuilder get() { + return new DefaultApacheHttpHttpClientBuilder(); + } + + public ApacheHttpClientBuilder httpProxyHost(String httpProxyHost) { + this.httpProxyHost = httpProxyHost; + return this; + } + + public ApacheHttpClientBuilder httpProxyPort(int httpProxyPort) { + this.httpProxyPort = httpProxyPort; + return this; + } + + public ApacheHttpClientBuilder httpProxyUsername(String httpProxyUsername) { + this.httpProxyUsername = httpProxyUsername; + return this; + } + + public ApacheHttpClientBuilder httpProxyPassword(String httpProxyPassword) { + this.httpProxyPassword = httpProxyPassword; + return this; + } + + public ApacheHttpClientBuilder sslConnectionSocketFactory(SSLConnectionSocketFactory sslConnectionSocketFactory){ + this.sslConnectionSocketFactory = sslConnectionSocketFactory; + return this; + } + + public IdleConnectionMonitorThread getIdleConnectionMonitorThread() { + return idleConnectionMonitorThread; + } + + private void prepare(){ + Registry registry = RegistryBuilder.create() + .register("http", plainConnectionSocketFactory) + .register("https", sslConnectionSocketFactory) + .build(); + connectionManager = new PoolingHttpClientConnectionManager(registry); + connectionManager.setMaxTotal(maxTotalConn); + connectionManager.setDefaultMaxPerRoute(maxConnPerHost); + connectionManager.setDefaultSocketConfig( + SocketConfig.copy(SocketConfig.DEFAULT) + .setSoTimeout(soTimeout) + .build() + ); + + idleConnectionMonitorThread = new IdleConnectionMonitorThread(connectionManager, idleConnTimeout, checkWaitTime); + idleConnectionMonitorThread.setDaemon(true); + idleConnectionMonitorThread.start(); + + httpClientBuilder = HttpClients.custom() + .setConnectionManager(connectionManager) + .setDefaultRequestConfig( + RequestConfig.custom() + .setSocketTimeout(soTimeout) + .setConnectTimeout(connectionTimeout) + .setConnectionRequestTimeout(connectionRequestTimeout) + .build() + ) + .setRetryHandler(httpRequestRetryHandler); + + if (StringUtils.isNotBlank(httpProxyHost) && StringUtils.isNotBlank(httpProxyUsername)) { + // 使用代理服务器 需要用户认证的代理服务器 + CredentialsProvider credsProvider = new BasicCredentialsProvider(); + credsProvider.setCredentials( + new AuthScope(httpProxyHost, httpProxyPort), + new UsernamePasswordCredentials(httpProxyUsername, httpProxyPassword)); + httpClientBuilder.setDefaultCredentialsProvider(credsProvider); + } + + if (StringUtils.isNotBlank(userAgent)) { + httpClientBuilder.setUserAgent(userAgent); + } + + } + + public CloseableHttpClient build() { + if(!prepared){ + prepare(); + prepared = true; + } + + return httpClientBuilder.build(); + } + + public static class IdleConnectionMonitorThread extends Thread { + private final HttpClientConnectionManager connMgr; + private final int idleConnTimeout; + private final int checkWaitTime; + private volatile boolean shutdown; + + public IdleConnectionMonitorThread(HttpClientConnectionManager connMgr, int idleConnTimeout, int checkWaitTime) { + super("IdleConnectionMonitorThread"); + this.connMgr = connMgr; + this.idleConnTimeout = idleConnTimeout; + this.checkWaitTime = checkWaitTime; + } + + @Override + public void run() { + try { + while (!shutdown) { + synchronized (this) { + wait(checkWaitTime); + connMgr.closeExpiredConnections(); + connMgr.closeIdleConnections(idleConnTimeout, TimeUnit.MILLISECONDS); + } + } + } catch (InterruptedException ignore) { + } + } + + public void trigger() { + synchronized (this) { + notifyAll(); + } + } + + public void shutdown() { + shutdown = true; + synchronized (this) { + notifyAll(); + } + } + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/JoddGetRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/JoddGetRequestExecutor.java new file mode 100644 index 00000000..10011304 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/JoddGetRequestExecutor.java @@ -0,0 +1,56 @@ +package me.chanjar.weixin.common.util.http; + +import jodd.http.HttpRequest; +import jodd.http.HttpResponse; +import jodd.http.ProxyInfo; +import jodd.http.net.SocketHttpConnectionProvider; +import me.chanjar.weixin.common.bean.result.WxError; +import me.chanjar.weixin.common.exception.WxErrorException; +import org.apache.http.HttpHost; +import org.apache.http.impl.client.CloseableHttpClient; + +import java.io.IOException; + +/** + * 简单的GET请求执行器,请求的参数是String, 返回的结果也是String + * + * @author Daniel Qian + */ +public class JoddGetRequestExecutor implements RequestExecutor { + + @Override + public String execute(CloseableHttpClient httpclient, HttpHost httpProxy, String uri, + String queryParam) throws WxErrorException, IOException { + if (queryParam != null) { + if (uri.indexOf('?') == -1) { + uri += '?'; + } + uri += uri.endsWith("?") ? queryParam : '&' + queryParam; + } + + SocketHttpConnectionProvider provider = new SocketHttpConnectionProvider(); + + if (httpProxy != null) { + ProxyInfo proxyInfoObj = new ProxyInfo( + ProxyInfo.ProxyType.HTTP, + httpProxy.getHostName(), + httpProxy.getPort(), "", ""); + provider.useProxy(proxyInfoObj); + } + + HttpRequest request = HttpRequest.get(uri); + request.method("GET"); + request.charset("UTF-8"); + + HttpResponse response = request.open(provider).send(); + response.charset("UTF-8"); + String result = response.bodyText(); + + WxError error = WxError.fromJson(result); + if (error.getErrorCode() != 0) { + throw new WxErrorException(error); + } + return result; + } + +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/JoddPostRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/JoddPostRequestExecutor.java new file mode 100644 index 00000000..d054835b --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/JoddPostRequestExecutor.java @@ -0,0 +1,50 @@ +package me.chanjar.weixin.common.util.http; + +import jodd.http.HttpRequest; +import jodd.http.HttpResponse; +import jodd.http.ProxyInfo; +import jodd.http.net.SocketHttpConnectionProvider; +import me.chanjar.weixin.common.bean.result.WxError; +import me.chanjar.weixin.common.exception.WxErrorException; +import org.apache.http.HttpHost; +import org.apache.http.impl.client.CloseableHttpClient; + +import java.io.IOException; + +/** + * 简单的POST请求执行器,请求的参数是String, 返回的结果也是String + * + * @author Edison Guo + */ +public class JoddPostRequestExecutor implements RequestExecutor { + + @Override + public String execute(CloseableHttpClient httpclient, HttpHost httpProxy, String uri, + String postEntity) throws WxErrorException, IOException { + SocketHttpConnectionProvider provider = new SocketHttpConnectionProvider(); + + if (httpProxy != null) { + ProxyInfo proxyInfoObj = new ProxyInfo( + ProxyInfo.ProxyType.HTTP, + httpProxy.getAddress().getHostAddress(), + httpProxy.getPort(), "", ""); + provider.useProxy(proxyInfoObj); + } + + HttpRequest request = HttpRequest.get(uri); + request.method("POST"); + request.charset("UTF-8"); + request.bodyText(postEntity); + + HttpResponse response = request.open(provider).send(); + response.charset("UTF-8"); + String result = response.bodyText(); + + WxError error = WxError.fromJson(result); + if (error.getErrorCode() != 0) { + throw new WxErrorException(error); + } + return result; + } + +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/MediaDownloadRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/MediaDownloadRequestExecutor.java index f9e3a25f..c0bd21ce 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/MediaDownloadRequestExecutor.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/MediaDownloadRequestExecutor.java @@ -74,6 +74,8 @@ public class MediaDownloadRequestExecutor implements RequestExecutor throw new WxErrorException(error); } return responseContent; + }finally { + httpGet.releaseConnection(); } } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/SimplePostRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/SimplePostRequestExecutor.java index 1ab1ad7e..1613c96c 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/SimplePostRequestExecutor.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/SimplePostRequestExecutor.java @@ -1,24 +1,17 @@ package me.chanjar.weixin.common.util.http; -import java.io.IOException; - import me.chanjar.weixin.common.bean.result.WxError; import me.chanjar.weixin.common.exception.WxErrorException; import org.apache.http.Consts; import org.apache.http.HttpHost; -import org.apache.http.auth.AuthScope; -import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.ClientProtocolException; -import org.apache.http.client.CredentialsProvider; 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.client.methods.HttpPost; import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClients; -import org.apache.http.util.EntityUtils; + +import java.io.IOException; /** * 简单的POST请求执行器,请求的参数是String, 返回的结果也是String @@ -47,6 +40,8 @@ public class SimplePostRequestExecutor implements RequestExecutor, JsonDeserializ json.add("button", buttonArray); if (menu.getMatchRule() != null) { - Gson gson = new Gson(); - json.add("matchrule", gson.toJsonTree(menu.getMatchRule())); + json.add("matchrule", convertToJson(menu.getMatchRule())); } return json; @@ -63,6 +60,18 @@ public class WxMenuGsonAdapter implements JsonSerializer, JsonDeserializ return buttonJson; } + protected JsonObject convertToJson(WxMenu.WxMenuRule menuRule){ + JsonObject matchRule = new JsonObject(); + matchRule.addProperty("group_id",menuRule.getGroupId()); + matchRule.addProperty("sex",menuRule.getSex()); + matchRule.addProperty("country",menuRule.getCountry()); + matchRule.addProperty("province",menuRule.getProvince()); + matchRule.addProperty("city",menuRule.getCity()); + matchRule.addProperty("client_platform_type",menuRule.getClientPlatformType()); + matchRule.addProperty("language",menuRule.getLanguage()); + return matchRule; + } + public WxMenu deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { /* * 操蛋的微信 diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/bean/WxMenuTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/bean/WxMenuTest.java index 015889d7..79d28e47 100644 --- a/weixin-java-common/src/test/java/me/chanjar/weixin/common/bean/WxMenuTest.java +++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/bean/WxMenuTest.java @@ -55,6 +55,29 @@ public class WxMenuTest { Assert.assertEquals(menu.toJson(), json); } + + @Test(dataProvider = "wxAddConditionalMenu") + public void testAddConditionalToJson(String json) { + WxMenu menu = new WxMenu(); + WxMenuButton button1 = new WxMenuButton(); + button1.setType("click"); + button1.setName("今日歌曲"); + button1.setKey("V1001_TODAY_MUSIC"); + + menu.getButtons().add(button1); + + WxMenu.WxMenuRule wxMenuRule = new WxMenu.WxMenuRule(); + wxMenuRule.setGroupId("2"); + wxMenuRule.setSex("1"); + wxMenuRule.setCountry("中国"); + wxMenuRule.setProvince("广东"); + wxMenuRule.setCity("广州"); + wxMenuRule.setClientPlatformType("2"); + wxMenuRule.setLanguage("zh_CN"); + menu.setMatchRule(wxMenuRule); + + Assert.assertEquals(menu.toJson(), json); + } @DataProvider public Object[][] wxReturnMenu() { @@ -106,5 +129,31 @@ public class WxMenuTest { new Object[] { json } }; } + + @DataProvider(name = "wxAddConditionalMenu") + public Object[][] addConditionalMenuJson(){ + String json = + "{" + +"\"button\":[" + +"{" + +"\"type\":\"click\"," + +"\"name\":\"今日歌曲\"," + +"\"key\":\"V1001_TODAY_MUSIC\"" + +"}" + +"]," + +"\"matchrule\":{" + +"\"group_id\":\"2\"," + +"\"sex\":\"1\"," + +"\"country\":\"中国\"," + +"\"province\":\"广东\"," + +"\"city\":\"广州\"," + +"\"client_platform_type\":\"2\"," + +"\"language\":\"zh_CN\"" + +"}" + +"}"; + return new Object[][]{ + new Object[]{json} + }; + } } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpConfigStorage.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpConfigStorage.java index f5fca81a..38e6135a 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpConfigStorage.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpConfigStorage.java @@ -1,8 +1,9 @@ package me.chanjar.weixin.cp.api; -import java.io.File; - import me.chanjar.weixin.common.bean.WxAccessToken; +import me.chanjar.weixin.common.util.http.ApacheHttpClientBuilder; + +import java.io.File; /** * 微信客户端配置存储 @@ -63,4 +64,9 @@ public interface WxCpConfigStorage { public File getTmpDirFile(); + /** + * http client builder + * @return ApacheHttpClientBuilder + */ + public ApacheHttpClientBuilder getApacheHttpClientBuilder(); } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpInMemoryConfigStorage.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpInMemoryConfigStorage.java index 638deb6b..5e0ba777 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpInMemoryConfigStorage.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpInMemoryConfigStorage.java @@ -1,8 +1,9 @@ package me.chanjar.weixin.cp.api; -import java.io.File; - import me.chanjar.weixin.common.bean.WxAccessToken; +import me.chanjar.weixin.common.util.http.ApacheHttpClientBuilder; + +import java.io.File; /** * 基于内存的微信配置provider,在实际生产环境中应该将这些配置持久化 @@ -32,6 +33,8 @@ public class WxCpInMemoryConfigStorage implements WxCpConfigStorage { protected volatile File tmpDirFile; + private volatile ApacheHttpClientBuilder apacheHttpClientBuilder; + public String getAccessToken() { return this.accessToken; } @@ -205,4 +208,12 @@ public class WxCpInMemoryConfigStorage implements WxCpConfigStorage { this.tmpDirFile = tmpDirFile; } + @Override + public ApacheHttpClientBuilder getApacheHttpClientBuilder() { + return this.apacheHttpClientBuilder; + } + + public void setApacheHttpClientBuilder(ApacheHttpClientBuilder apacheHttpClientBuilder) { + this.apacheHttpClientBuilder = apacheHttpClientBuilder; + } } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpServiceImpl.java index 3b9e8fe7..46e56929 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpServiceImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpServiceImpl.java @@ -1,14 +1,12 @@ package me.chanjar.weixin.cp.api; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.StringReader; -import java.math.BigDecimal; -import java.security.NoSuchAlgorithmException; -import java.util.List; -import java.util.UUID; - +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import com.google.gson.internal.Streams; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; import me.chanjar.weixin.common.bean.WxAccessToken; import me.chanjar.weixin.common.bean.WxJsapiSignature; import me.chanjar.weixin.common.bean.WxMenu; @@ -22,41 +20,30 @@ import me.chanjar.weixin.common.util.RandomUtils; import me.chanjar.weixin.common.util.StringUtils; import me.chanjar.weixin.common.util.crypto.SHA1; import me.chanjar.weixin.common.util.fs.FileUtils; -import me.chanjar.weixin.common.util.http.MediaDownloadRequestExecutor; -import me.chanjar.weixin.common.util.http.MediaUploadRequestExecutor; -import me.chanjar.weixin.common.util.http.RequestExecutor; -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.http.*; import me.chanjar.weixin.common.util.json.GsonHelper; 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.http.HttpHost; -import org.apache.http.auth.AuthScope; -import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.ClientProtocolException; -import org.apache.http.client.CredentialsProvider; 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.BasicCredentialsProvider; import org.apache.http.impl.client.BasicResponseHandler; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClients; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonPrimitive; -import com.google.gson.internal.Streams; -import com.google.gson.reflect.TypeToken; -import com.google.gson.stream.JsonReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringReader; +import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.UUID; public class WxCpServiceImpl implements WxCpService { @@ -126,6 +113,8 @@ public class WxCpServiceImpl implements WxCpService { String resultContent = null; try (CloseableHttpResponse response = httpclient.execute(httpGet)) { resultContent = new BasicResponseHandler().handleResponse(response); + }finally { + httpGet.releaseConnection(); } WxError error = WxError.fromJson(resultContent); if (error.getErrorCode() != 0) { @@ -256,9 +245,9 @@ public class WxCpServiceImpl implements WxCpService { 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()); + new SimplePostRequestExecutor(), + url, + depart.toJson()); JsonElement tmpJsonElement = Streams.parse(new JsonReader(new StringReader(responseContent))); return GsonHelper.getAsInteger(tmpJsonElement.getAsJsonObject().get("id")); } @@ -594,32 +583,16 @@ public class WxCpServiceImpl implements WxCpService { public void setWxCpConfigStorage(WxCpConfigStorage wxConfigProvider) { this.wxCpConfigStorage = wxConfigProvider; - - String http_proxy_host = wxCpConfigStorage.getHttp_proxy_host(); - int http_proxy_port = wxCpConfigStorage.getHttp_proxy_port(); - String http_proxy_username = wxCpConfigStorage.getHttp_proxy_username(); - String http_proxy_password = wxCpConfigStorage.getHttp_proxy_password(); - - if(StringUtils.isNotBlank(http_proxy_host)) { - // 使用代理服务器 - if(StringUtils.isNotBlank(http_proxy_username)) { - // 需要用户认证的代理服务器 - CredentialsProvider credsProvider = new BasicCredentialsProvider(); - credsProvider.setCredentials( - new AuthScope(http_proxy_host, http_proxy_port), - new UsernamePasswordCredentials(http_proxy_username, http_proxy_password)); - httpClient = HttpClients - .custom() - .setDefaultCredentialsProvider(credsProvider) - .build(); - } else { - // 无需用户认证的代理服务器 - httpClient = HttpClients.createDefault(); - } - httpProxy = new HttpHost(http_proxy_host, http_proxy_port); - } else { - httpClient = HttpClients.createDefault(); + ApacheHttpClientBuilder apacheHttpClientBuilder = wxCpConfigStorage.getApacheHttpClientBuilder(); + if (null == apacheHttpClientBuilder) { + apacheHttpClientBuilder = DefaultApacheHttpHttpClientBuilder.get(); } + apacheHttpClientBuilder.httpProxyHost(wxCpConfigStorage.getHttp_proxy_host()) + .httpProxyPort(wxCpConfigStorage.getHttp_proxy_port()) + .httpProxyUsername(wxCpConfigStorage.getHttp_proxy_username()) + .httpProxyPassword(wxCpConfigStorage.getHttp_proxy_password()); + + httpClient = apacheHttpClientBuilder.build(); } @Override @@ -685,12 +658,5 @@ public class WxCpServiceImpl implements WxCpService { this.tmpDirFile = tmpDirFile; } - public static void main(String[] args) { - Float a = 3.1f; - System.out.println(3.1d); - System.out.println(new BigDecimal(3.1d)); - System.out.println(new BigDecimal(a)); - System.out.println(a.toString()); - System.out.println(a.doubleValue()); - } + } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/util/xml/XStreamTransformer.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/util/xml/XStreamTransformer.java index c6c5a5f1..12dd8992 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/util/xml/XStreamTransformer.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/util/xml/XStreamTransformer.java @@ -31,6 +31,15 @@ public class XStreamTransformer { return object; } + /** + * 注册扩展消息的解析器 + * @param clz 类型 + * @param xStream xml解析器 + */ + public static void register(Class clz,XStream xStream){ + CLASS_2_XSTREAM_INSTANCE.put(clz,xStream); + } + /** * pojo -> xml * diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpConfigStorage.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpConfigStorage.java index 09d90d64..739e16b7 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpConfigStorage.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpConfigStorage.java @@ -1,10 +1,10 @@ package me.chanjar.weixin.mp.api; -import java.io.File; +import me.chanjar.weixin.common.bean.WxAccessToken; +import me.chanjar.weixin.common.util.http.ApacheHttpClientBuilder; import javax.net.ssl.SSLContext; - -import me.chanjar.weixin.common.bean.WxAccessToken; +import java.io.File; /** * 微信客户端配置存储 @@ -50,6 +50,21 @@ public interface WxMpConfigStorage { */ public void updateJsapiTicket(String jsapiTicket, int expiresInSeconds); + public String getCardApiTicket(); + + public boolean isCardApiTicketExpired(); + + /** + * 强制将卡券api ticket过期掉 + */ + public void expireCardApiTicket(); + + /** + * 应该是线程安全的 + * @param cardApiTicket + */ + public void updateCardApiTicket(String cardApiTicket, int expiresInSeconds); + public String getAppId(); public String getSecret(); @@ -77,4 +92,10 @@ public interface WxMpConfigStorage { public File getTmpDirFile(); public SSLContext getSSLContext(); + + /** + * http client builder + * @return ApacheHttpClientBuilder + */ + public ApacheHttpClientBuilder getApacheHttpClientBuilder(); } diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpInMemoryConfigStorage.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpInMemoryConfigStorage.java index e0c7aaa3..79064d6f 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpInMemoryConfigStorage.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpInMemoryConfigStorage.java @@ -1,10 +1,10 @@ package me.chanjar.weixin.mp.api; -import java.io.File; +import me.chanjar.weixin.common.bean.WxAccessToken; +import me.chanjar.weixin.common.util.http.ApacheHttpClientBuilder; import javax.net.ssl.SSLContext; - -import me.chanjar.weixin.common.bean.WxAccessToken; +import java.io.File; /** * 基于内存的微信配置provider,在实际生产环境中应该将这些配置持久化 @@ -32,13 +32,18 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage { protected volatile String jsapiTicket; protected volatile long jsapiTicketExpiresTime; + protected volatile String cardApiTicket; + protected volatile long cardApiTicketExpiresTime; + /** * 临时文件目录 */ protected volatile File tmpDirFile; protected volatile SSLContext sslContext; - + + protected volatile ApacheHttpClientBuilder apacheHttpClientBuilder; + public String getAccessToken() { return this.accessToken; } @@ -90,6 +95,27 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage { this.jsapiTicketExpiresTime = 0; } + /** + * 卡券api_ticket + */ + public String getCardApiTicket() { + return cardApiTicket; + } + + public boolean isCardApiTicketExpired() { + return System.currentTimeMillis() > this.cardApiTicketExpiresTime; + } + + public synchronized void updateCardApiTicket(String cardApiTicket, int expiresInSeconds) { + this.cardApiTicket = cardApiTicket; + // 预留200秒的时间 + this.cardApiTicketExpiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000l; + } + + public void expireCardApiTicket() { + this.cardApiTicketExpiresTime = 0; + } + public String getAppId() { return this.appId; } @@ -192,6 +218,8 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage { ", http_proxy_password='" + http_proxy_password + '\'' + ", jsapiTicket='" + jsapiTicket + '\'' + ", jsapiTicketExpiresTime='" + jsapiTicketExpiresTime + '\'' + + ", cardApiTicket='" + cardApiTicket + '\'' + + ", cardApiTicketExpiresTime='" + cardApiTicketExpiresTime + '\'' + ", tmpDirFile='" + tmpDirFile + '\'' + '}'; } @@ -232,4 +260,12 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage { sslContext = context; } + @Override + public ApacheHttpClientBuilder getApacheHttpClientBuilder() { + return this.apacheHttpClientBuilder; + } + + public void setApacheHttpClientBuilder(ApacheHttpClientBuilder apacheHttpClientBuilder) { + this.apacheHttpClientBuilder = apacheHttpClientBuilder; + } } 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 e6ce30cf..66a98e6e 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 @@ -1,5 +1,6 @@ package me.chanjar.weixin.mp.api; +import me.chanjar.weixin.common.bean.WxCardApiSignature; import me.chanjar.weixin.common.bean.WxMenu; import me.chanjar.weixin.common.bean.WxJsapiSignature; import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; @@ -725,8 +726,9 @@ public interface WxMpService { * @param parameters * the required or optional parameters * @return + * @throws WxErrorException */ - Map getJSSDKPayInfo(Map parameters); + Map getJSSDKPayInfo(Map parameters) throws WxErrorException; /** * 该接口调用“统一下单”接口,并拼装JSSDK发起支付请求需要的参数 @@ -739,10 +741,11 @@ public interface WxMpService { * @param ip 发起支付的客户端IP * @param notifyUrl 通知地址 * @return + * @throws WxErrorException * @deprecated Use me.chanjar.weixin.mp.api.WxMpService.getJSSDKPayInfo(Map) instead */ @Deprecated - Map getJSSDKPayInfo(String openId, String outTradeNo, double amt, String body, String tradeType, String ip, String notifyUrl); + Map getJSSDKPayInfo(String openId, String outTradeNo, double amt, String body, String tradeType, String ip, String notifyUrl) throws WxErrorException; /** * 该接口提供所有微信支付订单的查询,当支付通知处理异常戒丢失的情冴,商户可以通过该接口查询订单支付状态。 @@ -760,6 +763,20 @@ public interface WxMpService { */ WxMpPayCallback getJSSDKCallbackData(String xmlData); + /** + * 微信支付-申请退款 + * 详见 https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_4 + * @param parameters 需要传入的退款参数的Map。以下几项为参数的必须项:
+ *
  • transaction_id + *
  • out_trade_no (仅在上述transaction_id为空时是必须项) + *
  • out_refund_no + *
  • total_fee + *
  • refund_fee + * @return 退款操作结果 + * @throws WxErrorException + */ + public WxMpPayRefundResult refundPay(Map parameters) throws WxErrorException; + /** *
          * 计算Map键值对是否和签名相符,
    @@ -770,12 +787,131 @@ public interface WxMpService {
          * @return
          */
         public boolean checkJSSDKCallbackDataSignature(Map kvm, String signature);
    -    
    +
        /**
         * 发送微信红包给个人用户
    +    *
    +    * 需要传入的必填参数如下:
    +    * mch_billno//商户订单号
    +    * send_name//商户名称
    +    * re_openid//用户openid
    +    * total_amount//红包总额
    +    * total_num//红包发放总人数
    +    * wishing//红包祝福语
    +    * client_ip//服务器Ip地址
    +    * act_name//活动名称
    +    * remark //备注
    +    * 文档详见:https://pay.weixin.qq.com/wiki/doc/api/tools/cash_coupon.php?chapter=13_5
    +    *
    +    * 使用现金红包功能需要在xml配置文件中额外设置:
    +    * 微信商户平台ID
    +    * 商户平台设置的API密钥
    +    *
         * @param parameters
         * @return
         * @throws WxErrorException
         */
         public WxRedpackResult sendRedpack(Map parameters) throws WxErrorException;
    +
    +  /**
    +   * 获得卡券api_ticket,不强制刷新卡券api_ticket
    +   * @see #getCardApiTicket(boolean)
    +   * @return 卡券api_ticket
    +   * @throws WxErrorException
    +   */
    +  public String getCardApiTicket() throws WxErrorException;
    +
    +  /**
    +   * 
    +   * 获得卡券api_ticket
    +   * 获得时会检查卡券apiToken是否过期,如果过期了,那么就刷新一下,否则就什么都不干
    +   *
    +   * 详情请见:http://mp.weixin.qq.com/wiki/7/aaa137b55fb2e0456bf8dd9148dd613f.html#.E9.99.84.E5.BD.954-.E5.8D.A1.E5.88.B8.E6.89.A9.E5.B1.95.E5.AD.97.E6.AE.B5.E5.8F.8A.E7.AD.BE.E5.90.8D.E7.94.9F.E6.88.90.E7.AE.97.E6.B3.95
    +   * 
    + * @param forceRefresh 强制刷新 + * @return 卡券api_ticket + * @throws WxErrorException + */ + public String getCardApiTicket(boolean forceRefresh) throws WxErrorException; + + /** + *
    +   * 创建调用卡券api时所需要的签名
    +   *
    +   * 详情请见:http://mp.weixin.qq.com/wiki/7/aaa137b55fb2e0456bf8dd9148dd613f.html#.E9.99.84.E5.BD
    +   * .954-.E5.8D.A1.E5.88.B8.E6.89.A9.E5.B1.95.E5.AD.97.E6.AE.B5.E5.8F.8A.E7.AD.BE.E5.90.8D.E7.94
    +   * .9F.E6.88.90.E7.AE.97.E6.B3.95
    +   * 
    + * + * @param optionalSignParam 参与签名的参数数组。 + * 可以为下列字段:app_id, card_id, card_type, code, openid, location_id + *
    注意:当做wx.chooseCard调用时,必须传入app_id参与签名,否则会造成签名失败导致拉取卡券列表为空 + * @return 卡券Api签名对象 + */ + public WxCardApiSignature createCardApiSignature(String... optionalSignParam) throws + WxErrorException; + + /** + * 卡券Code解码 + * @param encryptCode 加密Code,通过JSSDK的chooseCard接口获得 + * @return 解密后的Code + * @throws WxErrorException + */ + public String decryptCardCode(String encryptCode) throws WxErrorException; + + /** + * 卡券Code查询 + * @param cardId 卡券ID代表一类卡券 + * @param code 单张卡券的唯一标准 + * @param checkConsume 是否校验code核销状态,填入true和false时的code异常状态返回数据不同 + * @return WxMpCardResult对象 + * @throws WxErrorException + */ + public WxMpCardResult queryCardCode(String cardId, String code, boolean checkConsume) + throws WxErrorException; + + /** + * 卡券Code核销。核销失败会抛出异常 + * + * @param code 单张卡券的唯一标准 + * @return 调用返回的JSON字符串。 + *
    可用 com.google.gson.JsonParser#parse 等方法直接取JSON串中的errcode等信息。 + * @throws WxErrorException + */ + public String consumeCardCode(String code) throws WxErrorException; + + /** + * 卡券Code核销。核销失败会抛出异常 + * + * @param code 单张卡券的唯一标准 + * @param cardId 当自定义Code卡券时需要传入card_id + * @return 调用返回的JSON字符串。 + *
    可用 com.google.gson.JsonParser#parse 等方法直接取JSON串中的errcode等信息。 + * @throws WxErrorException + */ + public String consumeCardCode(String code, String cardId) throws WxErrorException; + + /** + * 卡券Mark接口。 + * 开发者在帮助消费者核销卡券之前,必须帮助先将此code(卡券串码)与一个openid绑定(即mark住), + * 才能进一步调用核销接口,否则报错。 + * @param code 卡券的code码 + * @param cardId 卡券的ID + * @param openId 用券用户的openid + * @param isMark 是否要mark(占用)这个code,填写true或者false,表示占用或解除占用 + * @throws WxErrorException + */ + public void markCardCode(String code, String cardId, String openId, boolean isMark) throws + WxErrorException; + + /** + * 查看卡券详情接口 + * 详见 https://mp.weixin.qq.com/wiki/14/8dd77aeaee85f922db5f8aa6386d385e.html#.E6.9F.A5.E7.9C.8B.E5.8D.A1.E5.88.B8.E8.AF.A6.E6.83.85 + * @param cardId 卡券的ID + * @return 返回的卡券详情JSON字符串 + *
    [注] 由于返回的JSON格式过于复杂,难以定义其对应格式的Bean并且难以维护,因此只返回String格式的JSON串。 + *
    可由 com.google.gson.JsonParser#parse 等方法直接取JSON串中的某个字段。 + * @throws WxErrorException + */ + public String getCardDetail(String cardId) throws WxErrorException; } diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpServiceImpl.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpServiceImpl.java index 4223caef..7c4f290f 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpServiceImpl.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpServiceImpl.java @@ -1,21 +1,13 @@ package me.chanjar.weixin.mp.api; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.StringReader; -import java.security.NoSuchAlgorithmException; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.SortedMap; -import java.util.TreeMap; -import java.util.UUID; - +import com.google.gson.*; +import com.google.gson.internal.Streams; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.thoughtworks.xstream.XStream; import me.chanjar.weixin.common.api.WxConsts; import me.chanjar.weixin.common.bean.WxAccessToken; +import me.chanjar.weixin.common.bean.WxCardApiSignature; import me.chanjar.weixin.common.bean.WxJsapiSignature; import me.chanjar.weixin.common.bean.WxMenu; import me.chanjar.weixin.common.bean.result.WxError; @@ -24,85 +16,39 @@ 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.StringUtils; import me.chanjar.weixin.common.util.crypto.SHA1; import me.chanjar.weixin.common.util.crypto.WxCryptUtil; import me.chanjar.weixin.common.util.fs.FileUtils; -import me.chanjar.weixin.common.util.http.MediaDownloadRequestExecutor; -import me.chanjar.weixin.common.util.http.MediaUploadRequestExecutor; -import me.chanjar.weixin.common.util.http.RequestExecutor; -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.http.Utf8ResponseHandler; +import me.chanjar.weixin.common.util.http.*; import me.chanjar.weixin.common.util.json.GsonHelper; import me.chanjar.weixin.common.util.json.WxGsonBuilder; import me.chanjar.weixin.common.util.xml.XStreamInitializer; -import me.chanjar.weixin.mp.bean.WxMpCustomMessage; -import me.chanjar.weixin.mp.bean.WxMpGroup; -import me.chanjar.weixin.mp.bean.WxMpMassGroupMessage; -import me.chanjar.weixin.mp.bean.WxMpMassNews; -import me.chanjar.weixin.mp.bean.WxMpMassOpenIdsMessage; -import me.chanjar.weixin.mp.bean.WxMpMassVideo; -import me.chanjar.weixin.mp.bean.WxMpMaterial; -import me.chanjar.weixin.mp.bean.WxMpMaterialArticleUpdate; -import me.chanjar.weixin.mp.bean.WxMpMaterialNews; -import me.chanjar.weixin.mp.bean.WxMpSemanticQuery; -import me.chanjar.weixin.mp.bean.WxMpTemplateMessage; -import me.chanjar.weixin.mp.bean.result.WxMpMassSendResult; -import me.chanjar.weixin.mp.bean.result.WxMpMassUploadResult; -import me.chanjar.weixin.mp.bean.result.WxMpMaterialCountResult; -import me.chanjar.weixin.mp.bean.result.WxMpMaterialFileBatchGetResult; -import me.chanjar.weixin.mp.bean.result.WxMpMaterialNewsBatchGetResult; -import me.chanjar.weixin.mp.bean.result.WxMpMaterialUploadResult; -import me.chanjar.weixin.mp.bean.result.WxMpMaterialVideoInfoResult; -import me.chanjar.weixin.mp.bean.result.WxMpOAuth2AccessToken; -import me.chanjar.weixin.mp.bean.result.WxMpPayCallback; -import me.chanjar.weixin.mp.bean.result.WxMpPayResult; -import me.chanjar.weixin.mp.bean.result.WxMpPrepayIdResult; -import me.chanjar.weixin.mp.bean.result.WxMpQrCodeTicket; -import me.chanjar.weixin.mp.bean.result.WxMpSemanticQueryResult; -import me.chanjar.weixin.mp.bean.result.WxMpUser; -import me.chanjar.weixin.mp.bean.result.WxMpUserCumulate; -import me.chanjar.weixin.mp.bean.result.WxMpUserList; -import me.chanjar.weixin.mp.bean.result.WxMpUserSummary; -import me.chanjar.weixin.mp.bean.result.WxRedpackResult; -import me.chanjar.weixin.mp.util.http.MaterialDeleteRequestExecutor; -import me.chanjar.weixin.mp.util.http.MaterialNewsInfoRequestExecutor; -import me.chanjar.weixin.mp.util.http.MaterialUploadRequestExecutor; -import me.chanjar.weixin.mp.util.http.MaterialVideoInfoRequestExecutor; -import me.chanjar.weixin.mp.util.http.MaterialVoiceAndImageDownloadRequestExecutor; -import me.chanjar.weixin.mp.util.http.QrCodeRequestExecutor; +import me.chanjar.weixin.mp.bean.*; +import me.chanjar.weixin.mp.bean.result.*; +import me.chanjar.weixin.mp.util.http.*; import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder; - import org.apache.http.Consts; import org.apache.http.HttpHost; -import org.apache.http.auth.AuthScope; -import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.ClientProtocolException; -import org.apache.http.client.CredentialsProvider; 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.client.methods.HttpPost; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.client.BasicResponseHandler; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; -import org.apache.http.impl.client.HttpClients; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.helpers.MessageFormatter; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.internal.Streams; -import com.google.gson.reflect.TypeToken; -import com.google.gson.stream.JsonReader; -import com.thoughtworks.xstream.XStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringReader; +import java.security.NoSuchAlgorithmException; +import java.util.*; +import java.util.Map.Entry; public class WxMpServiceImpl implements WxMpService { @@ -118,6 +64,11 @@ public class WxMpServiceImpl implements WxMpService { */ protected final Object globalJsapiTicketRefreshLock = new Object(); + /** + * 全局的是否正在刷新卡券api_ticket的锁 + */ + protected final Object globalCardApiTicketRefreshLock = new Object(); + protected WxMpConfigStorage wxMpConfigStorage; protected CloseableHttpClient httpClient; @@ -158,14 +109,17 @@ public class WxMpServiceImpl implements WxMpService { RequestConfig config = RequestConfig.custom().setProxy(httpProxy).build(); httpGet.setConfig(config); } - CloseableHttpResponse response = getHttpclient().execute(httpGet); - String resultContent = new BasicResponseHandler().handleResponse(response); - WxError error = WxError.fromJson(resultContent); - if (error.getErrorCode() != 0) { - throw new WxErrorException(error); + try (CloseableHttpResponse response = getHttpclient().execute(httpGet)) { + String resultContent = new BasicResponseHandler().handleResponse(response); + WxError error = WxError.fromJson(resultContent); + if (error.getErrorCode() != 0) { + throw new WxErrorException(error); + } + WxAccessToken accessToken = WxAccessToken.fromJson(resultContent); + wxMpConfigStorage.updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn()); + }finally { + httpGet.releaseConnection(); } - WxAccessToken accessToken = WxAccessToken.fromJson(resultContent); - wxMpConfigStorage.updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn()); } catch (ClientProtocolException e) { throw new RuntimeException(e); } catch (IOException e) { @@ -766,36 +720,25 @@ public class WxMpServiceImpl implements WxMpService { public void setWxMpConfigStorage(WxMpConfigStorage wxConfigProvider) { this.wxMpConfigStorage = wxConfigProvider; - String http_proxy_host = wxMpConfigStorage.getHttp_proxy_host(); - int http_proxy_port = wxMpConfigStorage.getHttp_proxy_port(); - String http_proxy_username = wxMpConfigStorage.getHttp_proxy_username(); - String http_proxy_password = wxMpConfigStorage.getHttp_proxy_password(); - - final HttpClientBuilder builder = HttpClients.custom(); - if (StringUtils.isNotBlank(http_proxy_host)) { - // 使用代理服务器 - if (StringUtils.isNotBlank(http_proxy_username)) { - // 需要用户认证的代理服务器 - CredentialsProvider credsProvider = new BasicCredentialsProvider(); - credsProvider.setCredentials( - new AuthScope(http_proxy_host, http_proxy_port), - new UsernamePasswordCredentials(http_proxy_username, http_proxy_password)); - builder - .setDefaultCredentialsProvider(credsProvider); - } else { - // 无需用户认证的代理服务器 - } - httpProxy = new HttpHost(http_proxy_host, http_proxy_port); + ApacheHttpClientBuilder apacheHttpClientBuilder = wxMpConfigStorage.getApacheHttpClientBuilder(); + if (null == apacheHttpClientBuilder) { + apacheHttpClientBuilder = DefaultApacheHttpHttpClientBuilder.get(); } + apacheHttpClientBuilder.httpProxyHost(wxMpConfigStorage.getHttp_proxy_host()) + .httpProxyPort(wxMpConfigStorage.getHttp_proxy_port()) + .httpProxyUsername(wxMpConfigStorage.getHttp_proxy_username()) + .httpProxyPassword(wxMpConfigStorage.getHttp_proxy_password()); + if (wxConfigProvider.getSSLContext() != null){ SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory( wxConfigProvider.getSSLContext(), new String[] { "TLSv1" }, null, SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER); - builder.setSSLSocketFactory(sslsf); + apacheHttpClientBuilder.sslConnectionSocketFactory(sslsf); } - httpClient = builder.build(); + + httpClient = apacheHttpClientBuilder.build(); } @Override @@ -851,8 +794,7 @@ public class WxMpServiceImpl implements WxMpService { StringEntity entity = new StringEntity(request.toString(), Consts.UTF_8); httpPost.setEntity(entity); - try { - CloseableHttpResponse response = getHttpclient().execute(httpPost); + try(CloseableHttpResponse response = getHttpclient().execute(httpPost)) { String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response); XStream xstream = XStreamInitializer.getInstance(); xstream.alias("xml", WxMpPrepayIdResult.class); @@ -860,6 +802,8 @@ public class WxMpServiceImpl implements WxMpService { return wxMpPrepayIdResult; } catch (IOException e) { throw new RuntimeException("Failed to get prepay id due to IO exception.", e); + }finally { + httpPost.releaseConnection(); } } @@ -878,13 +822,14 @@ public class WxMpServiceImpl implements WxMpService { } @Override - public Map getJSSDKPayInfo(String openId, String outTradeNo, double amt, String body, String tradeType, String ip, String callbackUrl) { + public Map getJSSDKPayInfo(String openId, String outTradeNo, double amt, String body, String tradeType, String ip, String callbackUrl) + throws WxErrorException { Map packageParams = new HashMap(); packageParams.put("appid", wxMpConfigStorage.getAppId()); packageParams.put("mch_id", wxMpConfigStorage.getPartnerId()); packageParams.put("body", body); packageParams.put("out_trade_no", outTradeNo); - packageParams.put("total_fee", (int) (amt * 100) + ""); + packageParams.put("total_fee", String.format("%.0f", amt * 100)); packageParams.put("spbill_create_ip", ip); packageParams.put("notify_url", callbackUrl); packageParams.put("trade_type", tradeType); @@ -894,8 +839,21 @@ public class WxMpServiceImpl implements WxMpService { } @Override - public Map getJSSDKPayInfo(Map parameters) { + public Map getJSSDKPayInfo(Map parameters) throws WxErrorException { WxMpPrepayIdResult wxMpPrepayIdResult = getPrepayId(parameters); + + if (!"SUCCESS".equalsIgnoreCase(wxMpPrepayIdResult.getReturn_code()) + ||!"SUCCESS".equalsIgnoreCase(wxMpPrepayIdResult.getResult_code())) { + WxError error = new WxError(); + error.setErrorCode(-1); + error.setErrorMsg("return_code:" + wxMpPrepayIdResult.getReturn_code() + + ";return_msg:" + wxMpPrepayIdResult.getReturn_msg() + + ";result_code:" + wxMpPrepayIdResult.getResult_code() + + ";err_code" + wxMpPrepayIdResult.getErr_code() + + ";err_code_des" + wxMpPrepayIdResult.getErr_code_des()); + throw new WxErrorException(error); + } + String prepayId = wxMpPrepayIdResult.getPrepay_id(); if (prepayId == null || prepayId.equals("")) { throw new RuntimeException(String.format("Failed to get prepay id due to error code '%s'(%s).", wxMpPrepayIdResult.getErr_code(), wxMpPrepayIdResult.getErr_code_des())); @@ -908,6 +866,7 @@ public class WxMpServiceImpl implements WxMpService { payInfo.put("nonceStr", System.currentTimeMillis() + ""); payInfo.put("package", "prepay_id=" + prepayId); payInfo.put("signType", "MD5"); + payInfo.put("code_url",wxMpPrepayIdResult.getCode_url()); String finalSign = WxCryptUtil.createSign(payInfo, wxMpConfigStorage.getPartnerKey()); payInfo.put("paySign", finalSign); @@ -944,8 +903,7 @@ public class WxMpServiceImpl implements WxMpService { StringEntity entity = new StringEntity(request.toString(), Consts.UTF_8); httpPost.setEntity(entity); - try { - CloseableHttpResponse response = httpClient.execute(httpPost); + try(CloseableHttpResponse response = httpClient.execute(httpPost)) { String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response); XStream xstream = XStreamInitializer.getInstance(); xstream.alias("xml", WxMpPayResult.class); @@ -969,6 +927,61 @@ public class WxMpServiceImpl implements WxMpService { return new WxMpPayCallback(); } + @Override + public WxMpPayRefundResult refundPay(Map parameters) throws WxErrorException { + SortedMap refundParams = new TreeMap(parameters); + refundParams.put("appid", wxMpConfigStorage.getAppId()); + refundParams.put("mch_id", wxMpConfigStorage.getPartnerId()); + refundParams.put("nonce_str", System.currentTimeMillis() + ""); + refundParams.put("op_user_id", wxMpConfigStorage.getPartnerId()); + String sign = WxCryptUtil.createSign(refundParams, wxMpConfigStorage.getPartnerKey()); + refundParams.put("sign", sign); + + StringBuilder request = new StringBuilder(""); + for (Entry para : refundParams.entrySet()) { + request.append(String.format("<%s>%s", para.getKey(), para.getValue(), para.getKey())); + } + request.append(""); + + HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/secapi/pay/refund"); + if (httpProxy != null) { + RequestConfig config = RequestConfig.custom().setProxy(httpProxy).build(); + httpPost.setConfig(config); + } + + StringEntity entity = new StringEntity(request.toString(), Consts.UTF_8); + httpPost.setEntity(entity); + try( + CloseableHttpResponse response = getHttpclient().execute(httpPost)) { + String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response); + XStream xstream = XStreamInitializer.getInstance(); + xstream.processAnnotations(WxMpPayRefundResult.class); + WxMpPayRefundResult wxMpPayRefundResult = (WxMpPayRefundResult) xstream.fromXML(responseContent); + + if (!"SUCCESS".equalsIgnoreCase(wxMpPayRefundResult.getResultCode()) + ||!"SUCCESS".equalsIgnoreCase(wxMpPayRefundResult.getReturnCode())) { + WxError error = new WxError(); + error.setErrorCode(-1); + error.setErrorMsg("return_code:" + wxMpPayRefundResult.getReturnCode() + + ";return_msg:" + wxMpPayRefundResult.getReturnMsg() + + ";result_code:" + wxMpPayRefundResult.getResultCode() + + ";err_code" + wxMpPayRefundResult.getErrCode() + + ";err_code_des" + wxMpPayRefundResult.getErrCodeDes()); + throw new WxErrorException(error); + } + + return wxMpPayRefundResult; + } catch (IOException e) { + log.error(MessageFormatter.format("The exception was happened when sending refund '{}'.", request.toString()).getMessage(), e); + WxError error = new WxError(); + error.setErrorCode(-1); + error.setErrorMsg("incorrect response."); + throw new WxErrorException(error); + }finally { + httpPost.releaseConnection(); + } + } + @Override public boolean checkJSSDKCallbackDataSignature(Map kvm, String signature) { return signature.equals(WxCryptUtil.createSign(kvm, wxMpConfigStorage.getPartnerKey())); @@ -1000,8 +1013,7 @@ public class WxMpServiceImpl implements WxMpService { StringEntity entity = new StringEntity(request.toString(), Consts.UTF_8); httpPost.setEntity(entity); - try { - CloseableHttpResponse response = getHttpclient().execute(httpPost); + try(CloseableHttpResponse response = getHttpclient().execute(httpPost)) { String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response); XStream xstream = XStreamInitializer.getInstance(); xstream.processAnnotations(WxRedpackResult.class); @@ -1012,6 +1024,221 @@ public class WxMpServiceImpl implements WxMpService { WxError error = new WxError(); error.setErrorCode(-1); throw new WxErrorException(error); + }finally { + httpPost.releaseConnection(); + } + } + + /** + * 获得卡券api_ticket,不强制刷新卡券api_ticket + * + * @return 卡券api_ticket + * @throws WxErrorException + * @see #getCardApiTicket(boolean) + */ + @Override + public String getCardApiTicket() throws WxErrorException { + return getCardApiTicket(false); + } + + /** + *
    +   * 获得卡券api_ticket
    +   * 获得时会检查卡券apiToken是否过期,如果过期了,那么就刷新一下,否则就什么都不干
    +   *
    +   * 详情请见:http://mp.weixin.qq.com/wiki/7/aaa137b55fb2e0456bf8dd9148dd613f.html#.E9.99.84.E5.BD
    +   * .954-.E5.8D.A1.E5.88.B8.E6.89.A9.E5.B1.95.E5.AD.97.E6.AE.B5.E5.8F.8A.E7.AD.BE.E5.90.8D.E7.94
    +   * .9F.E6.88.90.E7.AE.97.E6.B3.95
    +   * 
    + * + * @param forceRefresh 强制刷新 + * @return 卡券api_ticket + * @throws WxErrorException + */ + @Override + public String getCardApiTicket(boolean forceRefresh) throws WxErrorException { + if (forceRefresh) { + wxMpConfigStorage.expireCardApiTicket(); + } + if (wxMpConfigStorage.isCardApiTicketExpired()) { + synchronized (globalCardApiTicketRefreshLock) { + if (wxMpConfigStorage.isCardApiTicketExpired()) { + String url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=wx_card"; + String responseContent = execute(new SimpleGetRequestExecutor(), url, null); + JsonElement tmpJsonElement = Streams.parse(new JsonReader(new StringReader(responseContent))); + JsonObject tmpJsonObject = tmpJsonElement.getAsJsonObject(); + String cardApiTicket = tmpJsonObject.get("ticket").getAsString(); + int expiresInSeconds = tmpJsonObject.get("expires_in").getAsInt(); + wxMpConfigStorage.updateCardApiTicket(cardApiTicket, expiresInSeconds); + } + } + } + return wxMpConfigStorage.getCardApiTicket(); + } + + /** + *
    +   * 创建调用卡券api时所需要的签名
    +   *
    +   * 详情请见:http://mp.weixin.qq.com/wiki/7/aaa137b55fb2e0456bf8dd9148dd613f.html#.E9.99.84.E5.BD
    +   * .954-.E5.8D.A1.E5.88.B8.E6.89.A9.E5.B1.95.E5.AD.97.E6.AE.B5.E5.8F.8A.E7.AD.BE.E5.90.8D.E7.94
    +   * .9F.E6.88.90.E7.AE.97.E6.B3.95
    +   * 
    + * + * @param optionalSignParam 参与签名的参数数组。 + * 可以为下列字段:app_id, card_id, card_type, code, openid, location_id + *
    注意:当做wx.chooseCard调用时,必须传入app_id参与签名,否则会造成签名失败导致拉取卡券列表为空 + * @return 卡券Api签名对象 + */ + @Override + public WxCardApiSignature createCardApiSignature(String... optionalSignParam) throws + WxErrorException { + long timestamp = System.currentTimeMillis() / 1000; + String nonceStr = RandomUtils.getRandomStr(); + String cardApiTicket = getCardApiTicket(false); + + String[] signParam = Arrays.copyOf(optionalSignParam, optionalSignParam.length + 3); + signParam[optionalSignParam.length] = String.valueOf(timestamp); + signParam[optionalSignParam.length + 1] = nonceStr; + signParam[optionalSignParam.length + 2] = cardApiTicket; + try { + String signature = SHA1.gen(signParam); + WxCardApiSignature cardApiSignature = new WxCardApiSignature(); + cardApiSignature.setTimestamp(timestamp); + cardApiSignature.setNonceStr(nonceStr); + cardApiSignature.setSignature(signature); + return cardApiSignature; + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + /** + * 卡券Code解码 + * + * @param encryptCode 加密Code,通过JSSDK的chooseCard接口获得 + * @return 解密后的Code + * @throws WxErrorException + */ + @Override + public String decryptCardCode(String encryptCode) throws WxErrorException { + String url = "https://api.weixin.qq.com/card/code/decrypt"; + JsonObject param = new JsonObject(); + param.addProperty("encrypt_code", encryptCode); + String responseContent = post(url, param.toString()); + JsonElement tmpJsonElement = Streams.parse(new JsonReader(new StringReader(responseContent))); + JsonObject tmpJsonObject = tmpJsonElement.getAsJsonObject(); + JsonPrimitive jsonPrimitive = tmpJsonObject.getAsJsonPrimitive("code"); + return jsonPrimitive.getAsString(); + } + + /** + * 卡券Code查询 + * + * @param cardId 卡券ID代表一类卡券 + * @param code 单张卡券的唯一标准 + * @param checkConsume 是否校验code核销状态,填入true和false时的code异常状态返回数据不同 + * @return WxMpCardResult对象 + * @throws WxErrorException + */ + @Override + public WxMpCardResult queryCardCode(String cardId, String code, boolean checkConsume) throws WxErrorException { + String url = "https://api.weixin.qq.com/card/code/get"; + JsonObject param = new JsonObject(); + param.addProperty("card_id", cardId); + param.addProperty("code", code); + param.addProperty("check_consume", checkConsume); + String responseContent = post(url, param.toString()); + JsonElement tmpJsonElement = Streams.parse(new JsonReader(new StringReader(responseContent))); + return WxMpGsonBuilder.INSTANCE.create().fromJson(tmpJsonElement, + new TypeToken() { + }.getType()); + } + + /** + * 卡券Code核销。核销失败会抛出异常 + * + * @param code 单张卡券的唯一标准 + * @return 调用返回的JSON字符串。 + *
    可用 com.google.gson.JsonParser#parse 等方法直接取JSON串中的errcode等信息。 + * @throws WxErrorException + */ + @Override + public String consumeCardCode(String code) throws WxErrorException { + return consumeCardCode(code, null); + } + + /** + * 卡券Code核销。核销失败会抛出异常 + * + * @param code 单张卡券的唯一标准 + * @param cardId 当自定义Code卡券时需要传入card_id + * @return 调用返回的JSON字符串。 + *
    可用 com.google.gson.JsonParser#parse 等方法直接取JSON串中的errcode等信息。 + * @throws WxErrorException + */ + @Override + public String consumeCardCode(String code, String cardId) throws WxErrorException { + String url = "https://api.weixin.qq.com/card/code/consume"; + JsonObject param = new JsonObject(); + param.addProperty("code", code); + + if (cardId != null && !"".equals(cardId)) { + param.addProperty("card_id", cardId); + } + + String responseContent = post(url, param.toString()); + return responseContent; + } + + /** + * 卡券Mark接口。 + * 开发者在帮助消费者核销卡券之前,必须帮助先将此code(卡券串码)与一个openid绑定(即mark住), + * 才能进一步调用核销接口,否则报错。 + * + * @param code 卡券的code码 + * @param cardId 卡券的ID + * @param openId 用券用户的openid + * @param isMark 是否要mark(占用)这个code,填写true或者false,表示占用或解除占用 + * @throws WxErrorException + */ + @Override + public void markCardCode(String code, String cardId, String openId, boolean isMark) throws + WxErrorException { + String url = "https://api.weixin.qq.com/card/code/mark"; + JsonObject param = new JsonObject(); + param.addProperty("code", code); + param.addProperty("card_id", cardId); + param.addProperty("openid", openId); + param.addProperty("is_mark", isMark); + String responseContent = post(url, param.toString()); + JsonElement tmpJsonElement = Streams.parse(new JsonReader(new StringReader(responseContent))); + WxMpCardResult cardResult = WxMpGsonBuilder.INSTANCE.create().fromJson(tmpJsonElement, + new TypeToken() { }.getType()); + if (!cardResult.getErrorCode().equals("0")) { + log.warn("朋友的券mark失败:{}", cardResult.getErrorMsg()); + } + } + + @Override + public String getCardDetail(String cardId) throws WxErrorException { + String url = "https://api.weixin.qq.com/card/get"; + JsonObject param = new JsonObject(); + param.addProperty("card_id", cardId); + String responseContent = post(url, param.toString()); + + // 判断返回值 + JsonObject json = (new JsonParser()).parse(responseContent).getAsJsonObject(); + String errcode = json.get("errcode").getAsString(); + if (!"0".equals(errcode)) { + String errmsg = json.get("errmsg").getAsString(); + WxError error = new WxError(); + error.setErrorCode(Integer.valueOf(errcode)); + error.setErrorMsg(errmsg); + throw new WxErrorException(error); } + + return responseContent; } + } diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpCard.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpCard.java new file mode 100644 index 00000000..d530fbe8 --- /dev/null +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpCard.java @@ -0,0 +1,72 @@ +package me.chanjar.weixin.mp.bean; + + +/** + * 微信卡券 + * + * @author YuJian + * @version 15/11/11 + */ +public class WxMpCard { + + private String cardId; + + private Long beginTime; + + private Long endTime; + + private String userCardStatus; + + private Boolean canConsume; + + public String getCardId() { + return cardId; + } + + public void setCardId(String cardId) { + this.cardId = cardId; + } + + public Long getBeginTime() { + return beginTime; + } + + public void setBeginTime(Long beginTime) { + this.beginTime = beginTime; + } + + public Long getEndTime() { + return endTime; + } + + public void setEndTime(Long endTime) { + this.endTime = endTime; + } + + public String getUserCardStatus() { + return userCardStatus; + } + + public void setUserCardStatus(String userCardStatus) { + this.userCardStatus = userCardStatus; + } + + public Boolean getCanConsume() { + return canConsume; + } + + public void setCanConsume(Boolean canConsume) { + this.canConsume = canConsume; + } + + @Override + public String toString() { + return "WxMpCard{" + + "cardId='" + cardId + '\'' + + ", beginTime=" + beginTime + + ", endTime=" + endTime + + ", userCardStatus='" + userCardStatus + '\'' + + ", canConsume=" + canConsume + + '}'; + } +} diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpMaterialNews.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpMaterialNews.java index f1de7d9e..75ee479f 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpMaterialNews.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpMaterialNews.java @@ -46,6 +46,10 @@ public class WxMpMaterialNews implements Serializable { * (必填) 图文消息缩略图的media_id,可以在基础支持-上传多媒体文件接口中获得 */ private String thumbMediaId; + /** + * 图文消息的封面url + */ + private String thumbUrl; /** * 图文消息的作者 */ @@ -141,9 +145,17 @@ public class WxMpMaterialNews implements Serializable { this.url = url; } + public String getThumbUrl() { + return thumbUrl; + } + + public void setThumbUrl(String thumbUrl) { + this.thumbUrl = thumbUrl; + } + @Override public String toString() { - return "WxMpMassNewsArticle [" + "thumbMediaId=" + thumbMediaId + ", author=" + author + ", title=" + title + + return "WxMpMassNewsArticle [" + "thumbMediaId=" + thumbMediaId + "thumbUrl=" + thumbUrl + ", author=" + author + ", title=" + title + ", contentSourceUrl=" + contentSourceUrl + ", content=" + content + ", digest=" + digest + ", showCoverPic=" + showCoverPic +", url=" + url + "]"; } diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpXmlMessage.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpXmlMessage.java index 8796b110..9f42ffc7 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpXmlMessage.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpXmlMessage.java @@ -150,6 +150,31 @@ public class WxMpXmlMessage implements Serializable { @XStreamAlias("ErrorCount") private Integer errorCount; + /////////////////////////////////////// + // 卡券相关事件推送 + /////////////////////////////////////// + @XStreamAlias("CardId") + @XStreamConverter(value=XStreamCDataConverter.class) + private String cardId; + + @XStreamAlias("FriendUserName") + @XStreamConverter(value=XStreamCDataConverter.class) + private String friendUserName; + + @XStreamAlias("IsGiveByFriend") + private Integer isGiveByFriend; // 是否为转赠,1代表是,0代表否 + + @XStreamAlias("UserCardCode") + @XStreamConverter(value=XStreamCDataConverter.class) + private String userCardCode; + + @XStreamAlias("OldUserCardCode") + @XStreamConverter(value=XStreamCDataConverter.class) + private String oldUserCardCode; + + @XStreamAlias("OuterId") + private Integer outerId; + @XStreamAlias("ScanCodeInfo") private ScanCodeInfo scanCodeInfo = new ScanCodeInfo(); @@ -456,6 +481,54 @@ public class WxMpXmlMessage implements Serializable { this.errorCount = errorCount; } + public String getCardId() { + return cardId; + } + + public void setCardId(String cardId) { + this.cardId = cardId; + } + + public String getFriendUserName() { + return friendUserName; + } + + public void setFriendUserName(String friendUserName) { + this.friendUserName = friendUserName; + } + + public Integer getIsGiveByFriend() { + return isGiveByFriend; + } + + public void setIsGiveByFriend(Integer isGiveByFriend) { + this.isGiveByFriend = isGiveByFriend; + } + + public String getUserCardCode() { + return userCardCode; + } + + public void setUserCardCode(String userCardCode) { + this.userCardCode = userCardCode; + } + + public String getOldUserCardCode() { + return oldUserCardCode; + } + + public void setOldUserCardCode(String oldUserCardCode) { + this.oldUserCardCode = oldUserCardCode; + } + + public Integer getOuterId() { + return outerId; + } + + public void setOuterId(Integer outerId) { + this.outerId = outerId; + } + public WxMpXmlMessage.ScanCodeInfo getScanCodeInfo() { return scanCodeInfo; } @@ -652,6 +725,11 @@ public class WxMpXmlMessage implements Serializable { ", filterCount=" + filterCount + ", sentCount=" + sentCount + ", errorCount=" + errorCount + + ", cardId='" + cardId + '\'' + + ", isGiveByFriend=" + isGiveByFriend + + ", userCardCode='" + userCardCode + '\'' + + ", oldUserCardCode='" + oldUserCardCode + '\'' + + ", outerId=" + outerId + ", scanCodeInfo=" + scanCodeInfo + ", sendPicsInfo=" + sendPicsInfo + ", sendLocationInfo=" + sendLocationInfo + diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpXmlOutTransferCustomerServiceMessage.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpXmlOutTransferCustomerServiceMessage.java index 5783d2b4..67c3d28e 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpXmlOutTransferCustomerServiceMessage.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpXmlOutTransferCustomerServiceMessage.java @@ -4,23 +4,22 @@ import com.thoughtworks.xstream.annotations.XStreamAlias; import com.thoughtworks.xstream.annotations.XStreamConverter; import me.chanjar.weixin.common.api.WxConsts; import me.chanjar.weixin.common.util.xml.XStreamCDataConverter; -import me.chanjar.weixin.common.util.xml.XStreamMediaIdConverter; @XStreamAlias("xml") public class WxMpXmlOutTransferCustomerServiceMessage extends WxMpXmlOutMessage { @XStreamAlias("TransInfo") - protected final TransInfo transInfo = new TransInfo(); + protected TransInfo transInfo; public WxMpXmlOutTransferCustomerServiceMessage() { this.msgType = WxConsts.CUSTOM_MSG_TRANSFER_CUSTOMER_SERVICE; } - public String getKfAccount() { - return transInfo.getKfAccount(); + public TransInfo getTransInfo() { + return transInfo; } - public void setKfAccount(String kfAccount) { - transInfo.setKfAccount(kfAccount); + public void setTransInfo(TransInfo transInfo) { + this.transInfo = transInfo; } @XStreamAlias("TransInfo") diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/outxmlbuilder/TransferCustomerServiceBuilder.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/outxmlbuilder/TransferCustomerServiceBuilder.java index 01acf8d7..d004eb56 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/outxmlbuilder/TransferCustomerServiceBuilder.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/outxmlbuilder/TransferCustomerServiceBuilder.java @@ -1,5 +1,6 @@ package me.chanjar.weixin.mp.bean.outxmlbuilder; +import me.chanjar.weixin.common.util.StringUtils; import me.chanjar.weixin.mp.bean.WxMpXmlOutTransferCustomerServiceMessage; /** @@ -22,7 +23,11 @@ public final class TransferCustomerServiceBuilder extends BaseBuilder params = new HashMap<>(); params.put("media_id", materialId); httpPost.setEntity(new StringEntity(WxGsonBuilder.create().toJson(params))); - CloseableHttpResponse response = httpclient.execute(httpPost); - String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response); - WxError error = WxError.fromJson(responseContent); - if (error.getErrorCode() != 0) { - throw new WxErrorException(error); - } else { - return true; + try(CloseableHttpResponse response = httpclient.execute(httpPost)){ + String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response); + WxError error = WxError.fromJson(responseContent); + if (error.getErrorCode() != 0) { + throw new WxErrorException(error); + } else { + return true; + } + }finally { + httpPost.releaseConnection(); } } diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/http/MaterialNewsInfoRequestExecutor.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/http/MaterialNewsInfoRequestExecutor.java index e23ecc50..fb77b54d 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/http/MaterialNewsInfoRequestExecutor.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/http/MaterialNewsInfoRequestExecutor.java @@ -35,14 +35,18 @@ public class MaterialNewsInfoRequestExecutor implements RequestExecutor params = new HashMap<>(); params.put("media_id", materialId); httpPost.setEntity(new StringEntity(WxGsonBuilder.create().toJson(params))); - CloseableHttpResponse response = httpclient.execute(httpPost); - String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response); - WxError error = WxError.fromJson(responseContent); - if (error.getErrorCode() != 0) { - throw new WxErrorException(error); - } else { - return WxMpGsonBuilder.create().fromJson(responseContent, WxMpMaterialNews.class); + try(CloseableHttpResponse response = httpclient.execute(httpPost)){ + String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response); + WxError error = WxError.fromJson(responseContent); + if (error.getErrorCode() != 0) { + throw new WxErrorException(error); + } else { + return WxMpGsonBuilder.create().fromJson(responseContent, WxMpMaterialNews.class); + } + }finally { + httpPost.releaseConnection(); } + } } diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/http/MaterialUploadRequestExecutor.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/http/MaterialUploadRequestExecutor.java index 41ee9883..5e90c07a 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/http/MaterialUploadRequestExecutor.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/http/MaterialUploadRequestExecutor.java @@ -15,7 +15,6 @@ import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.ContentType; import org.apache.http.entity.mime.HttpMultipartMode; import org.apache.http.entity.mime.MultipartEntityBuilder; -import org.apache.http.entity.mime.content.InputStreamBody; import org.apache.http.impl.client.CloseableHttpClient; import java.io.*; @@ -23,6 +22,7 @@ import java.util.Map; public class MaterialUploadRequestExecutor implements RequestExecutor { + @Override public WxMpMaterialUploadResult execute(CloseableHttpClient httpclient, HttpHost httpProxy, String uri, WxMpMaterial material) throws WxErrorException, ClientProtocolException, IOException { HttpPost httpPost = new HttpPost(uri); if (httpProxy != null) { @@ -35,10 +35,9 @@ public class MaterialUploadRequestExecutor implements RequestExecutor form = material.getForm(); if (material.getForm() != null) { @@ -48,13 +47,16 @@ public class MaterialUploadRequestExecutor implements RequestExecutor params = new HashMap<>(); params.put("media_id", materialId); httpPost.setEntity(new StringEntity(WxGsonBuilder.create().toJson(params))); - CloseableHttpResponse response = httpclient.execute(httpPost); - String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response); - WxError error = WxError.fromJson(responseContent); - if (error.getErrorCode() != 0) { - throw new WxErrorException(error); - } else { - return WxMpMaterialVideoInfoResult.fromJson(responseContent); + try(CloseableHttpResponse response = httpclient.execute(httpPost)){ + String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response); + WxError error = WxError.fromJson(responseContent); + if (error.getErrorCode() != 0) { + throw new WxErrorException(error); + } else { + return WxMpMaterialVideoInfoResult.fromJson(responseContent); + } + }finally { + httpPost.releaseConnection(); } } diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/http/MaterialVoiceAndImageDownloadRequestExecutor.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/http/MaterialVoiceAndImageDownloadRequestExecutor.java index 72917a93..a26f3520 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/http/MaterialVoiceAndImageDownloadRequestExecutor.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/http/MaterialVoiceAndImageDownloadRequestExecutor.java @@ -44,22 +44,25 @@ public class MaterialVoiceAndImageDownloadRequestExecutor implements RequestExec Map params = new HashMap<>(); params.put("media_id", materialId); httpPost.setEntity(new StringEntity(WxGsonBuilder.create().toJson(params))); - CloseableHttpResponse response = httpclient.execute(httpPost); - // 下载媒体文件出错 - InputStream inputStream = InputStreamResponseHandler.INSTANCE.handleResponse(response); - byte[] responseContent = IOUtils.toByteArray(inputStream); - String responseContentString = new String(responseContent, "UTF-8"); - if (responseContentString.length() < 100) { - try { - WxError wxError = WxGsonBuilder.create().fromJson(responseContentString, WxError.class); - if (wxError.getErrorCode() != 0) { - throw new WxErrorException(wxError); + try(CloseableHttpResponse response = httpclient.execute(httpPost)){ + // 下载媒体文件出错 + InputStream inputStream = InputStreamResponseHandler.INSTANCE.handleResponse(response); + byte[] responseContent = IOUtils.toByteArray(inputStream); + String responseContentString = new String(responseContent, "UTF-8"); + if (responseContentString.length() < 100) { + try { + WxError wxError = WxGsonBuilder.create().fromJson(responseContentString, WxError.class); + if (wxError.getErrorCode() != 0) { + throw new WxErrorException(wxError); + } + } catch (com.google.gson.JsonSyntaxException ex) { + return new ByteArrayInputStream(responseContent); } - } catch (com.google.gson.JsonSyntaxException ex) { - return new ByteArrayInputStream(responseContent); } + return new ByteArrayInputStream(responseContent); + }finally { + httpPost.releaseConnection(); } - return new ByteArrayInputStream(responseContent); } } diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/http/QrCodeRequestExecutor.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/http/QrCodeRequestExecutor.java index 12606096..bde25cfb 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/http/QrCodeRequestExecutor.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/http/QrCodeRequestExecutor.java @@ -58,8 +58,9 @@ public class QrCodeRequestExecutor implements RequestExecutor { + + @Override + public WxMpCard deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext + jsonDeserializationContext) throws JsonParseException { + WxMpCard card = new WxMpCard(); + JsonObject jsonObject = jsonElement.getAsJsonObject(); + + card.setCardId(GsonHelper.getString(jsonObject, "card_id")); + card.setBeginTime(GsonHelper.getLong(jsonObject, "begin_time")); + card.setEndTime(GsonHelper.getLong(jsonObject, "end_time")); + + return card; + } + +} diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/json/WxMpCardResultGsonAdapter.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/json/WxMpCardResultGsonAdapter.java new file mode 100644 index 00000000..be1a12da --- /dev/null +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/json/WxMpCardResultGsonAdapter.java @@ -0,0 +1,45 @@ +package me.chanjar.weixin.mp.util.json; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.reflect.TypeToken; +import me.chanjar.weixin.common.util.json.GsonHelper; +import me.chanjar.weixin.mp.bean.WxMpCard; +import me.chanjar.weixin.mp.bean.result.WxMpCardResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Type; +import java.text.ParseException; +import java.util.List; + +/** + * Created by YuJian on 15/11/11. + * + * @author YuJian + * @version 15/11/11 + */ +public class WxMpCardResultGsonAdapter implements JsonDeserializer { + @Override + public WxMpCardResult deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException { + WxMpCardResult cardResult = new WxMpCardResult(); + JsonObject jsonObject = jsonElement.getAsJsonObject(); + + cardResult.setOpenId(GsonHelper.getString(jsonObject, "openid")); + cardResult.setErrorCode(GsonHelper.getString(jsonObject, "errcode")); + cardResult.setErrorMsg(GsonHelper.getString(jsonObject, "errmsg")); + cardResult.setCanConsume(GsonHelper.getBoolean(jsonObject, "can_consume")); + cardResult.setUserCardStatus(GsonHelper.getString(jsonObject, "user_card_status")); + + WxMpCard card = WxMpGsonBuilder.INSTANCE.create().fromJson(jsonObject.get("card"), + new TypeToken() { + }.getType()); + + cardResult.setCard(card); + + return cardResult; + } +} diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/json/WxMpGsonBuilder.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/json/WxMpGsonBuilder.java index 20f8d9d4..316f22f5 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/json/WxMpGsonBuilder.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/json/WxMpGsonBuilder.java @@ -38,6 +38,8 @@ public class WxMpGsonBuilder { INSTANCE.registerTypeAdapter(WxMpMaterialNewsBatchGetResult.WxMaterialNewsBatchGetNewsItem.class, new WxMpMaterialNewsBatchGetGsonItemAdapter()); INSTANCE.registerTypeAdapter(WxMpMaterialFileBatchGetResult.class, new WxMpMaterialFileBatchGetGsonAdapter()); INSTANCE.registerTypeAdapter(WxMpMaterialFileBatchGetResult.WxMaterialFileBatchGetNewsItem.class, new WxMpMaterialFileBatchGetGsonItemAdapter()); + INSTANCE.registerTypeAdapter(WxMpCardResult.class, new WxMpCardResultGsonAdapter()); + INSTANCE.registerTypeAdapter(WxMpCard.class, new WxMpCardGsonAdapter()); } public static Gson create() { diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/json/WxMpMaterialNewsArticleGsonAdapter.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/json/WxMpMaterialNewsArticleGsonAdapter.java index 141baeab..96cc6aab 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/json/WxMpMaterialNewsArticleGsonAdapter.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/json/WxMpMaterialNewsArticleGsonAdapter.java @@ -20,6 +20,7 @@ public class WxMpMaterialNewsArticleGsonAdapter implements JsonSerializer xml * diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/WxMpXmlOutTransferCustomerServiceMessageTest.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/WxMpXmlOutTransferCustomerServiceMessageTest.java new file mode 100644 index 00000000..012a0986 --- /dev/null +++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/WxMpXmlOutTransferCustomerServiceMessageTest.java @@ -0,0 +1,70 @@ +package me.chanjar.weixin.mp.bean; + +import org.testng.Assert; +import org.testng.annotations.Test; + +/** + * Created by ben on 2015/12/29. + */ +public class WxMpXmlOutTransferCustomerServiceMessageTest { + @Test + public void test() { + WxMpXmlOutTransferCustomerServiceMessage m = new WxMpXmlOutTransferCustomerServiceMessage(); + m.setCreateTime(1399197672L); + m.setFromUserName("fromuser"); + m.setToUserName("touser"); + + String expected = "" + + "" + + "" + + "1399197672" + + "" + + ""; + System.out.println(m.toXml()); + Assert.assertEquals(m.toXml().replaceAll("\\s", ""), expected.replaceAll("\\s", "")); + + expected = " " + + "" + + "" + + "1399197672" + + "" + + "" + + "" + + "" + + ""; + WxMpXmlOutTransferCustomerServiceMessage.TransInfo transInfo = new WxMpXmlOutTransferCustomerServiceMessage.TransInfo(); + transInfo.setKfAccount("test1@test"); + m.setTransInfo(transInfo); + System.out.println(m.toXml()); + Assert.assertEquals(m.toXml().replaceAll("\\s", ""), expected.replaceAll("\\s", "")); + } + + @Test + public void testBuild() { + WxMpXmlOutTransferCustomerServiceMessage m = WxMpXmlOutMessage.TRANSFER_CUSTOMER_SERVICE().fromUser("fromuser").toUser("touser").build(); + m.setCreateTime(1399197672L); + String expected = "" + + "" + + "" + + "1399197672" + + "" + + ""; + System.out.println(m.toXml()); + Assert.assertEquals(m.toXml().replaceAll("\\s", ""), expected.replaceAll("\\s", "")); + + + expected = " " + + "" + + "" + + "1399197672" + + "" + + "" + + "" + + "" + + ""; + m = WxMpXmlOutMessage.TRANSFER_CUSTOMER_SERVICE().kfAccount("test1@test").fromUser("fromuser").toUser("touser").build(); + m.setCreateTime(1399197672L); + System.out.println(m.toXml()); + Assert.assertEquals(m.toXml().replaceAll("\\s", ""), expected.replaceAll("\\s", "")); + } +} \ No newline at end of file