diff --git a/pom.xml b/pom.xml index fa0737c44..6ff5be4fc 100644 --- a/pom.xml +++ b/pom.xml @@ -225,6 +225,13 @@ ${jetty.version} test + + org.assertj + assertj-guava + 3.0.0 + test + + redis.clients jedis diff --git a/weixin-java-pay/pom.xml b/weixin-java-pay/pom.xml index d5e418f70..bc4469935 100644 --- a/weixin-java-pay/pom.xml +++ b/weixin-java-pay/pom.xml @@ -27,7 +27,6 @@ org.jodd jodd-http - provided @@ -45,6 +44,11 @@ testng test + + org.assertj + assertj-guava + test + com.google.inject guice diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/constant/WxPayConstants.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/constant/WxPayConstants.java index 8e9eea95f..7cafd4ab7 100644 --- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/constant/WxPayConstants.java +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/constant/WxPayConstants.java @@ -36,7 +36,17 @@ public class WxPayConstants { } /** - * 订单类型 + * 压缩账单的类型 + */ + public static class TarType { + /** + * 固定值:GZIP,返回格式为.gzip的压缩包账单 + */ + public static final String GZIP = "GZIP"; + } + + /** + * 账单类型 */ public static class BillType { /** diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceAbstractImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceAbstractImpl.java index 0eccb7272..d30ac9b57 100644 --- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceAbstractImpl.java +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceAbstractImpl.java @@ -11,21 +11,47 @@ import com.github.binarywang.wxpay.bean.order.WxPayNativeOrderResult; import com.github.binarywang.wxpay.bean.request.*; import com.github.binarywang.wxpay.bean.result.*; import com.github.binarywang.wxpay.config.WxPayConfig; +import com.github.binarywang.wxpay.constant.WxPayConstants; import com.github.binarywang.wxpay.constant.WxPayConstants.BillType; import com.github.binarywang.wxpay.constant.WxPayConstants.SignType; import com.github.binarywang.wxpay.constant.WxPayConstants.TradeType; import com.github.binarywang.wxpay.exception.WxPayException; import com.github.binarywang.wxpay.service.WxPayService; import com.github.binarywang.wxpay.util.SignUtils; +import com.google.common.base.Joiner; import com.google.common.collect.Maps; +import jodd.io.ZipUtil; +import jodd.util.Base64; import org.apache.commons.lang3.StringUtils; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +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.HttpPost; +import org.apache.http.conn.ssl.DefaultHostnameVerifier; +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.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.net.ssl.SSLContext; import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.*; +import java.util.zip.ZipException; import static com.github.binarywang.wxpay.constant.WxPayConstants.QUERY_COMMENT_DATE_FORMAT; +import static com.github.binarywang.wxpay.constant.WxPayConstants.TarType; /** *
@@ -61,7 +87,17 @@ public abstract class WxPayServiceAbstractImpl implements WxPayService {
   }
 
   /**
-   * 发送post请求
+   * 发送post请求,得到响应字节数组
+   *
+   * @param url        请求地址
+   * @param requestStr 请求信息
+   * @param useKey     是否使用证书
+   * @return 返回请求结果字节数组
+   */
+  protected abstract byte[] postForBytes(String url, String requestStr, boolean useKey) throws WxPayException;
+
+  /**
+   * 发送post请求,得到响应字符串
    *
    * @param url        请求地址
    * @param requestStr 请求信息
@@ -410,6 +446,10 @@ public abstract class WxPayServiceAbstractImpl implements WxPayService {
 
   @Override
   public WxPayBillResult downloadBill(String billDate, String billType, String tarType, String deviceInfo) throws WxPayException {
+    if (!BillType.ALL.equals(billType)) {
+      throw new WxPayException("目前仅支持ALL类型的对账单下载");
+    }
+
     WxPayDownloadBillRequest request = new WxPayDownloadBillRequest();
     request.setBillType(billType);
     request.setBillDate(billDate);
@@ -419,15 +459,52 @@ public abstract class WxPayServiceAbstractImpl implements WxPayService {
     request.checkAndSign(this.getConfig(), false);
 
     String url = this.getPayBaseUrl() + "/pay/downloadbill";
-    String responseContent = this.post(url, request.toXML(), false);
-    if (responseContent.startsWith("<")) {
-      throw WxPayException.from(WxPayBaseResult.fromXML(responseContent, WxPayCommonResult.class));
+
+    String responseContent;
+    if (TarType.GZIP.equals(tarType)) {
+      responseContent = this.handleGzipBill(url, request.toXML());
+    } else {
+      responseContent = this.post(url, request.toXML(), false);
+      if (responseContent.startsWith("<")) {
+        throw WxPayException.from(WxPayBaseResult.fromXML(responseContent, WxPayCommonResult.class));
+      }
     }
 
-    return this.handleBillInformation(responseContent);
+    return this.handleBill(billType, responseContent);
   }
 
-  private WxPayBillResult handleBillInformation(String responseContent) {
+  private WxPayBillResult handleBill(String billType, String responseContent) {
+    if (!BillType.ALL.equals(billType)) {
+      return null;
+    }
+
+    return this.handleAllBill(responseContent);
+  }
+
+  private String handleGzipBill(String url, String requestStr) throws WxPayException {
+    try {
+      byte[] responseBytes = this.postForBytes(url, requestStr, false);
+      Path tempDirectory = Files.createTempDirectory("bill");
+      Path path = Paths.get(tempDirectory.toString(), System.currentTimeMillis() + ".gzip");
+      Files.write(path, responseBytes);
+      try {
+        List allLines = Files.readAllLines(ZipUtil.ungzip(path.toFile()).toPath(), StandardCharsets.UTF_8);
+        return Joiner.on("\n").join(allLines);
+      } catch (ZipException e) {
+        if (e.getMessage().contains("Not in GZIP format")) {
+          throw WxPayException.from(WxPayBaseResult.fromXML(new String(responseBytes, StandardCharsets.UTF_8),
+            WxPayCommonResult.class));
+        } else {
+          this.log.error("解压zip文件出错", e);
+        }
+      }
+    }   catch (IOException e) {
+      e.printStackTrace();
+    }
+    return null;
+  }
+
+  private WxPayBillResult handleAllBill(String responseContent) {
     WxPayBillResult wxPayBillResult = new WxPayBillResult();
 
     String listStr = "";
@@ -444,11 +521,16 @@ public abstract class WxPayServiceAbstractImpl implements WxPayService {
      * 参考以上格式进行取值
      */
     List wxPayBillBaseResultLst = new LinkedList<>();
-    String newStr = listStr.replaceAll(",", " "); // 去空格
-    String[] tempStr = newStr.split("`"); // 数据分组
-    String[] t = tempStr[0].split(" ");// 分组标题
-    int j = tempStr.length / t.length; // 计算循环次数
-    int k = 1; // 纪录数组下标
+    // 去空格
+    String newStr = listStr.replaceAll(",", " ");
+    // 数据分组
+    String[] tempStr = newStr.split("`");
+    // 分组标题
+    String[] t = tempStr[0].split(" ");
+    // 计算循环次数
+    int j = tempStr.length / t.length;
+    // 纪录数组下标
+    int k = 1;
     for (int i = 0; i < j; i++) {
       WxPayBillBaseResult wxPayBillBaseResult = new WxPayBillBaseResult();
 
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceApacheHttpImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceApacheHttpImpl.java
index 653c1a8d6..dc0a1a06e 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceApacheHttpImpl.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceApacheHttpImpl.java
@@ -2,6 +2,7 @@ package com.github.binarywang.wxpay.service.impl;
 
 import com.github.binarywang.wxpay.bean.WxPayApiData;
 import com.github.binarywang.wxpay.exception.WxPayException;
+import jodd.util.Base64;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.http.auth.AuthScope;
 import org.apache.http.auth.UsernamePasswordCredentials;
@@ -19,11 +20,12 @@ import org.apache.http.impl.client.HttpClients;
 import org.apache.http.util.EntityUtils;
 
 import javax.net.ssl.SSLContext;
+import java.io.UnsupportedEncodingException;
 import java.nio.charset.StandardCharsets;
 
 /**
  * 
- * 微信支付请求实现类,apache httpclient实现
+ * 微信支付请求实现类,apache httpclient实现.
  * Created by Binary Wang on 2016/7/28.
  * 
* @@ -31,41 +33,35 @@ import java.nio.charset.StandardCharsets; */ public class WxPayServiceApacheHttpImpl extends WxPayServiceAbstractImpl { + @Override + protected byte[] postForBytes(String url, String requestStr, boolean useKey) throws WxPayException { + try { + HttpClientBuilder httpClientBuilder = createHttpClientBuilder(useKey); + HttpPost httpPost = this.createHttpPost(url, requestStr); + try (CloseableHttpClient httpclient = httpClientBuilder.build()) { + try (CloseableHttpResponse response = httpclient.execute(httpPost)) { + final byte[] bytes = EntityUtils.toByteArray(response.getEntity()); + final String responseData = Base64.encodeToString(bytes); + this.log.info("\n【请求地址】:{}\n【请求数据】:{}\n【响应数据(Base64编码后)】:{}", url, requestStr, responseData); + wxApiData.set(new WxPayApiData(url, requestStr, responseData, null)); + return bytes; + } + } finally { + httpPost.releaseConnection(); + } + } catch (Exception e) { + this.log.error("\n【请求地址】:{}\n【请求数据】:{}\n【异常信息】:{}", url, requestStr, e.getMessage()); + wxApiData.set(new WxPayApiData(url, requestStr, null, e.getMessage())); + throw new WxPayException(e.getMessage(), e); + } + } + @Override protected String post(String url, String requestStr, boolean useKey) throws WxPayException { try { - HttpClientBuilder httpClientBuilder = HttpClients.custom(); - if (useKey) { - SSLContext sslContext = this.getConfig().getSslContext(); - if (null == sslContext) { - sslContext = this.getConfig().initSSLContext(); - } - - SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, - new String[]{"TLSv1"}, null, new DefaultHostnameVerifier()); - httpClientBuilder.setSSLSocketFactory(sslsf); - } - - HttpPost httpPost = new HttpPost(url); - - httpPost.setConfig(RequestConfig.custom() - .setConnectionRequestTimeout(this.getConfig().getHttpConnectionTimeout()) - .setConnectTimeout(this.getConfig().getHttpConnectionTimeout()) - .setSocketTimeout(this.getConfig().getHttpTimeout()) - .build()); - - if (StringUtils.isNotBlank(this.getConfig().getHttpProxyHost()) - && this.getConfig().getHttpProxyPort() > 0) { - // 使用代理服务器 需要用户认证的代理服务器 - CredentialsProvider provider = new BasicCredentialsProvider(); - provider.setCredentials( - new AuthScope(this.getConfig().getHttpProxyHost(), this.getConfig().getHttpProxyPort()), - new UsernamePasswordCredentials(this.getConfig().getHttpProxyUsername(), this.getConfig().getHttpProxyPassword())); - httpClientBuilder.setDefaultCredentialsProvider(provider); - } - + HttpClientBuilder httpClientBuilder = this.createHttpClientBuilder(useKey); + HttpPost httpPost = this.createHttpPost(url, requestStr); try (CloseableHttpClient httpclient = httpClientBuilder.build()) { - httpPost.setEntity(new StringEntity(new String(requestStr.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1))); try (CloseableHttpResponse response = httpclient.execute(httpPost)) { String responseString = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); this.log.info("\n【请求地址】:{}\n【请求数据】:{}\n【响应数据】:{}", url, requestStr, responseString); @@ -82,4 +78,56 @@ public class WxPayServiceApacheHttpImpl extends WxPayServiceAbstractImpl { } } + private StringEntity createEntry(String requestStr) { + try { + return new StringEntity(new String(requestStr.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1)); + } catch (UnsupportedEncodingException e) { + //cannot happen + this.log.error(e.getMessage(),e); + return null; + } + } + + private HttpClientBuilder createHttpClientBuilder(boolean useKey) throws WxPayException { + HttpClientBuilder httpClientBuilder = HttpClients.custom(); + if (useKey) { + this.setKey(httpClientBuilder); + } + + if (StringUtils.isNotBlank(this.getConfig().getHttpProxyHost()) + && this.getConfig().getHttpProxyPort() > 0) { + // 使用代理服务器 需要用户认证的代理服务器 + CredentialsProvider provider = new BasicCredentialsProvider(); + provider.setCredentials( + new AuthScope(this.getConfig().getHttpProxyHost(), this.getConfig().getHttpProxyPort()), + new UsernamePasswordCredentials(this.getConfig().getHttpProxyUsername(), this.getConfig().getHttpProxyPassword())); + httpClientBuilder.setDefaultCredentialsProvider(provider); + } + return httpClientBuilder; + } + + private HttpPost createHttpPost(String url, String requestStr) { + HttpPost httpPost = new HttpPost(url); + httpPost.setEntity(this.createEntry(requestStr)); + + httpPost.setConfig(RequestConfig.custom() + .setConnectionRequestTimeout(this.getConfig().getHttpConnectionTimeout()) + .setConnectTimeout(this.getConfig().getHttpConnectionTimeout()) + .setSocketTimeout(this.getConfig().getHttpTimeout()) + .build()); + + return httpPost; + } + + private void setKey(HttpClientBuilder httpClientBuilder) throws WxPayException { + SSLContext sslContext = this.getConfig().getSslContext(); + if (null == sslContext) { + sslContext = this.getConfig().initSSLContext(); + } + + SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, + new String[]{"TLSv1"}, null, new DefaultHostnameVerifier()); + httpClientBuilder.setSSLSocketFactory(sslsf); + } + } diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceJoddHttpImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceJoddHttpImpl.java index b342c5cda..3ac29a926 100644 --- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceJoddHttpImpl.java +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceJoddHttpImpl.java @@ -9,48 +9,40 @@ import jodd.http.ProxyInfo; import jodd.http.ProxyInfo.ProxyType; import jodd.http.net.SSLSocketHttpConnectionProvider; import jodd.http.net.SocketHttpConnectionProvider; +import jodd.util.Base64; import org.apache.commons.lang3.StringUtils; import javax.net.ssl.SSLContext; import java.nio.charset.StandardCharsets; /** - * 微信支付请求实现类,jodd-http实现 + * 微信支付请求实现类,jodd-http实现. * Created by Binary Wang on 2016/7/28. * * @author Binary Wang */ public class WxPayServiceJoddHttpImpl extends WxPayServiceAbstractImpl { + @Override + protected byte[] postForBytes(String url, String requestStr, boolean useKey) throws WxPayException { + try { + HttpRequest request = this.buildHttpRequest(url, requestStr, useKey); + byte[] responseBytes = request.send().bodyBytes(); + final String responseString = Base64.encodeToString(responseBytes); + this.log.info("\n【请求地址】:{}\n【请求数据】:{}\n【响应数据(Base64编码后)】:{}", url, requestStr, responseString); + wxApiData.set(new WxPayApiData(url, requestStr, responseString, null)); + return responseBytes; + } catch (Exception e) { + this.log.error("\n【请求地址】:{}\n【请求数据】:{}\n【异常信息】:{}", url, requestStr, e.getMessage()); + wxApiData.set(new WxPayApiData(url, requestStr, null, e.getMessage())); + throw new WxPayException(e.getMessage(), e); + } + } + @Override protected String post(String url, String requestStr, boolean useKey) throws WxPayException { try { - HttpRequest request = HttpRequest - .post(url) - .timeout(this.getConfig().getHttpTimeout()) - .connectionTimeout(this.getConfig().getHttpConnectionTimeout()) - .bodyText(requestStr); - - if (useKey) { - SSLContext sslContext = this.getConfig().getSslContext(); - if (null == sslContext) { - sslContext = this.getConfig().initSSLContext(); - } - final SSLSocketHttpConnectionProvider provider = new SSLSocketHttpConnectionProvider(sslContext); - request.withConnectionProvider(provider); - } - - if (StringUtils.isNotBlank(this.getConfig().getHttpProxyHost()) && this.getConfig().getHttpProxyPort() > 0) { - ProxyInfo httpProxy = new ProxyInfo(ProxyType.HTTP, this.getConfig().getHttpProxyHost(), this.getConfig().getHttpProxyPort(), - this.getConfig().getHttpProxyUsername(), this.getConfig().getHttpProxyPassword()); - HttpConnectionProvider provider = request.connectionProvider(); - if (null == provider) { - provider = new SocketHttpConnectionProvider(); - } - provider.useProxy(httpProxy); - request.withConnectionProvider(provider); - } - + HttpRequest request = this.buildHttpRequest(url, requestStr, useKey); String responseString = this.getResponseString(request.send()); this.log.info("\n【请求地址】:{}\n【请求数据】:{}\n【响应数据】:{}", url, requestStr, responseString); @@ -63,6 +55,35 @@ public class WxPayServiceJoddHttpImpl extends WxPayServiceAbstractImpl { } } + private HttpRequest buildHttpRequest(String url, String requestStr, boolean useKey) throws WxPayException { + HttpRequest request = HttpRequest + .post(url) + .timeout(this.getConfig().getHttpTimeout()) + .connectionTimeout(this.getConfig().getHttpConnectionTimeout()) + .bodyText(requestStr); + + if (useKey) { + SSLContext sslContext = this.getConfig().getSslContext(); + if (null == sslContext) { + sslContext = this.getConfig().initSSLContext(); + } + final SSLSocketHttpConnectionProvider provider = new SSLSocketHttpConnectionProvider(sslContext); + request.withConnectionProvider(provider); + } + + if (StringUtils.isNotBlank(this.getConfig().getHttpProxyHost()) && this.getConfig().getHttpProxyPort() > 0) { + ProxyInfo httpProxy = new ProxyInfo(ProxyType.HTTP, this.getConfig().getHttpProxyHost(), this.getConfig().getHttpProxyPort(), + this.getConfig().getHttpProxyUsername(), this.getConfig().getHttpProxyPassword()); + HttpConnectionProvider provider = request.connectionProvider(); + if (null == provider) { + provider = new SocketHttpConnectionProvider(); + } + provider.useProxy(httpProxy); + request.withConnectionProvider(provider); + } + return request; + } + private String getResponseString(HttpResponse response) throws WxPayException { try { this.log.debug("【微信服务器响应头信息】:\n{}", response.toString(false)); diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/WxPayServiceAbstractImplTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/WxPayServiceAbstractImplTest.java index 9c293103b..819cd2d2e 100644 --- a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/WxPayServiceAbstractImplTest.java +++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/WxPayServiceAbstractImplTest.java @@ -26,6 +26,8 @@ import java.nio.file.Path; import java.util.Calendar; import java.util.Date; +import static com.github.binarywang.wxpay.constant.WxPayConstants.*; +import static org.assertj.core.api.Assertions.assertThat; import static org.testng.Assert.*; /** @@ -139,7 +141,13 @@ public class WxPayServiceAbstractImplTest { @DataProvider public Object[][] billingData() { return new Object[][]{ -// {"20170831", BillType.ALL, null, "deviceInfo"}, + {"20170831", BillType.ALL, TarType.GZIP, "deviceInfo"}, + {"20170831", BillType.RECHARGE_REFUND, TarType.GZIP, "deviceInfo"}, + {"20170831", BillType.REFUND, TarType.GZIP, "deviceInfo"}, + {"20170831", BillType.SUCCESS, TarType.GZIP, "deviceInfo"}, + {"20170831", BillType.ALL, null, "deviceInfo"}, + {"20170831", BillType.RECHARGE_REFUND, null, "deviceInfo"}, + {"20170831", BillType.REFUND, null, "deviceInfo"}, {"20170831", BillType.SUCCESS, null, "deviceInfo"} }; } @@ -148,11 +156,11 @@ public class WxPayServiceAbstractImplTest { public void testDownloadBill(String billDate, String billType, String tarType, String deviceInfo) throws Exception { WxPayBillResult billResult = this.payService.downloadBill(billDate, billType, tarType, deviceInfo); - assertNotNull(billResult); + assertThat(billResult).isNotNull(); this.logger.info(billResult.toString()); } - @Test + @Test(expectedExceptions = WxPayException.class) public void testDownloadBill_withNoParams() throws Exception { //必填字段为空时,抛出异常 this.payService.downloadBill("", "", "", null); @@ -247,7 +255,7 @@ public class WxPayServiceAbstractImplTest { .openid("ojOQA0y9o-Eb6Aep7uVTdbkJqrP4") .amount(1) .spbillCreateIp("10.10.10.10") - .checkName(WxPayConstants.CheckNameOption.NO_CHECK) + .checkName(CheckNameOption.NO_CHECK) .description("描述信息") .build();