From cd8c4d646954dfafc00d4b70c10623cfe6c44994 Mon Sep 17 00:00:00 2001 From: yang_shj Date: Thu, 3 Mar 2022 10:35:15 +0800 Subject: [PATCH] =?UTF-8?q?feat=200=20=E6=94=AF=E4=BB=98=E5=B7=A5=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- payment-base-app/payment-base-app.iml | 224 ++++++ payment-base-app/pom.xml | 2 + .../aspect/aspect/WebMvcConfiguration.java | 32 + .../springcloud/base/MchConfigEntity.java | 22 + .../com/base/springcloud/bo/CodePayBO.java | 32 + .../java/com/base/springcloud/bo/OrderBO.java | 9 +- .../com/base/springcloud/bo/RefundBO.java | 32 + .../springcloud/constant/PaymentConstant.java | 3 + .../controller/PaymentController.java | 3 +- .../springcloud/entity/WxpayRefundVo.java | 7 +- .../base/springcloud/entity/WxpayTradeVo.java | 5 +- .../service/WecPaymentService.java | 19 + .../service/imp/MerchantServiceImp.java | 61 +- .../service/imp/OrderServiceImp.java | 3 +- .../service/imp/WecPaymentServiceImp.java | 110 ++- .../base/springcloud/util/DecimalUtil.java | 3 + .../base/springcloud/wxsdk/IWXPayDomain.java | 42 ++ .../com/base/springcloud/wxsdk/MyConfig.java | 87 +++ .../com/base/springcloud/wxsdk/WXPay.java | 689 ++++++++++++++++++ .../base/springcloud/wxsdk/WXPayConfig.java | 103 +++ .../springcloud/wxsdk/WXPayConstants.java | 59 ++ .../base/springcloud/wxsdk/WXPayReport.java | 265 +++++++ .../base/springcloud/wxsdk/WXPayRequest.java | 257 +++++++ .../com/base/springcloud/wxsdk/WXPayUtil.java | 298 ++++++++ .../base/springcloud/wxsdk/WXPayXmlUtil.java | 30 + .../{util => wxsdk}/WxpayUtils.java | 8 +- 26 files changed, 2353 insertions(+), 52 deletions(-) create mode 100644 payment-base-app/payment-base-app.iml create mode 100644 payment-base-app/src/main/java/com/base/springcloud/aspect/aspect/WebMvcConfiguration.java create mode 100644 payment-base-app/src/main/java/com/base/springcloud/base/MchConfigEntity.java create mode 100644 payment-base-app/src/main/java/com/base/springcloud/bo/CodePayBO.java create mode 100644 payment-base-app/src/main/java/com/base/springcloud/bo/RefundBO.java create mode 100644 payment-base-app/src/main/java/com/base/springcloud/wxsdk/IWXPayDomain.java create mode 100644 payment-base-app/src/main/java/com/base/springcloud/wxsdk/MyConfig.java create mode 100644 payment-base-app/src/main/java/com/base/springcloud/wxsdk/WXPay.java create mode 100644 payment-base-app/src/main/java/com/base/springcloud/wxsdk/WXPayConfig.java create mode 100644 payment-base-app/src/main/java/com/base/springcloud/wxsdk/WXPayConstants.java create mode 100644 payment-base-app/src/main/java/com/base/springcloud/wxsdk/WXPayReport.java create mode 100644 payment-base-app/src/main/java/com/base/springcloud/wxsdk/WXPayRequest.java create mode 100644 payment-base-app/src/main/java/com/base/springcloud/wxsdk/WXPayUtil.java create mode 100644 payment-base-app/src/main/java/com/base/springcloud/wxsdk/WXPayXmlUtil.java rename payment-base-app/src/main/java/com/base/springcloud/{util => wxsdk}/WxpayUtils.java (97%) diff --git a/payment-base-app/payment-base-app.iml b/payment-base-app/payment-base-app.iml new file mode 100644 index 0000000..e3fda62 --- /dev/null +++ b/payment-base-app/payment-base-app.iml @@ -0,0 +1,224 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/payment-base-app/pom.xml b/payment-base-app/pom.xml index a1bbbe9..8ea2679 100644 --- a/payment-base-app/pom.xml +++ b/payment-base-app/pom.xml @@ -214,6 +214,8 @@ jar-with-dependencies + rbs-pay + ../target diff --git a/payment-base-app/src/main/java/com/base/springcloud/aspect/aspect/WebMvcConfiguration.java b/payment-base-app/src/main/java/com/base/springcloud/aspect/aspect/WebMvcConfiguration.java new file mode 100644 index 0000000..182c36c --- /dev/null +++ b/payment-base-app/src/main/java/com/base/springcloud/aspect/aspect/WebMvcConfiguration.java @@ -0,0 +1,32 @@ +package com.base.springcloud.aspect.aspect; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * Spring Boot 2.0 解决跨域问题 + * + * @Author qinfeng + * + */ +@Configuration +public class WebMvcConfiguration implements WebMvcConfigurer { + + + @Override + public void addCorsMappings(CorsRegistry registry){ + // 设置允许跨域的路由 + registry.addMapping("/**") + // 设置允许跨域请求的域名------------修改此行 + .allowedOrigins("*") + // 是否允许证书(cookies) + .allowCredentials(true) + // 设置允许的方法 + .allowedMethods("*") + // 跨域允许时间 + .maxAge(3600); + } + + +} diff --git a/payment-base-app/src/main/java/com/base/springcloud/base/MchConfigEntity.java b/payment-base-app/src/main/java/com/base/springcloud/base/MchConfigEntity.java new file mode 100644 index 0000000..630f835 --- /dev/null +++ b/payment-base-app/src/main/java/com/base/springcloud/base/MchConfigEntity.java @@ -0,0 +1,22 @@ +package com.base.springcloud.base; + +import lombok.Data; + +@Data +public class MchConfigEntity { + + // 商户编号 + private String mchId; + + // 商户私钥 + private String mchPrivateKey; + + // 微信平台证书 + private String certificate; + + // 证书序列号 + private String serialNo; + + // 公众号Id + private String appId; +} diff --git a/payment-base-app/src/main/java/com/base/springcloud/bo/CodePayBO.java b/payment-base-app/src/main/java/com/base/springcloud/bo/CodePayBO.java new file mode 100644 index 0000000..a9d8bc3 --- /dev/null +++ b/payment-base-app/src/main/java/com/base/springcloud/bo/CodePayBO.java @@ -0,0 +1,32 @@ +package com.base.springcloud.bo; + +import com.base.springcloud.base.MchConfigEntity; +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; + +@Data +public class CodePayBO extends MchConfigEntity implements Serializable { + + // 商品描述 + private String body; + + // 附加数据 + private String attach; + + // 订单号 + private String orderNo; + + // 订单金额 + private BigDecimal amount; + + // 微信付款码 + private String authCode; + + // 设备号 + private String deviceInfo; + + // 终端IP + private String spbillCreateIp; +} diff --git a/payment-base-app/src/main/java/com/base/springcloud/bo/OrderBO.java b/payment-base-app/src/main/java/com/base/springcloud/bo/OrderBO.java index 7b15216..e43578e 100644 --- a/payment-base-app/src/main/java/com/base/springcloud/bo/OrderBO.java +++ b/payment-base-app/src/main/java/com/base/springcloud/bo/OrderBO.java @@ -1,5 +1,6 @@ package com.base.springcloud.bo; +import com.base.springcloud.base.MchConfigEntity; import com.base.springcloud.entity.Order; import com.base.springcloud.entity.SaleBill; import lombok.Data; @@ -7,13 +8,7 @@ import lombok.Data; import java.io.Serializable; @Data -public class OrderBO implements Serializable { - - // 商户编号 - private String mchId; - - // 商户私钥 - private String mchPrivateKey; +public class OrderBO extends MchConfigEntity implements Serializable { // 销售单据号 private String billNo; diff --git a/payment-base-app/src/main/java/com/base/springcloud/bo/RefundBO.java b/payment-base-app/src/main/java/com/base/springcloud/bo/RefundBO.java new file mode 100644 index 0000000..3bdefa1 --- /dev/null +++ b/payment-base-app/src/main/java/com/base/springcloud/bo/RefundBO.java @@ -0,0 +1,32 @@ +package com.base.springcloud.bo; + +import com.base.springcloud.base.MchConfigEntity; +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; + +@Data +public class RefundBO extends MchConfigEntity implements Serializable { + + // 销售退款单据号 + private String billRefundNo; + + // 原交易订单号即支付订单号 + private String origOrderNo; + + // 原订单金额 + private BigDecimal orderAmount; + + // 退款订单号 + private String refundNo; + + // 退款金额 + private BigDecimal refundAmount; + + // 退款通知地址 + private String notifyUrl; + + // 退款原因 + private String reason; +} diff --git a/payment-base-app/src/main/java/com/base/springcloud/constant/PaymentConstant.java b/payment-base-app/src/main/java/com/base/springcloud/constant/PaymentConstant.java index b756fb1..00f8b30 100644 --- a/payment-base-app/src/main/java/com/base/springcloud/constant/PaymentConstant.java +++ b/payment-base-app/src/main/java/com/base/springcloud/constant/PaymentConstant.java @@ -21,6 +21,9 @@ public interface PaymentConstant { String CP = "CP"; // 云闪付 String QRCCP = "QRCCP"; + String HB = "HB"; // 和包 + String QRCHB = "QRCHB"; + String NATIVE = "Native"; // Native支付 String H5 = "H5"; // H5支付 String JSAPI = "Jsapi"; // JSAPI支付 diff --git a/payment-base-app/src/main/java/com/base/springcloud/controller/PaymentController.java b/payment-base-app/src/main/java/com/base/springcloud/controller/PaymentController.java index a03ddea..29ccbfc 100644 --- a/payment-base-app/src/main/java/com/base/springcloud/controller/PaymentController.java +++ b/payment-base-app/src/main/java/com/base/springcloud/controller/PaymentController.java @@ -33,7 +33,8 @@ public class PaymentController { @ApiOperation(value = "native下单", notes = "微信native下单") @GetMapping("/nativePlaceOrder") - public ReportResult nativePlaceOrder(@RequestParam String salesBill,@RequestParam String mchId) { + public ReportResult nativePlaceOrder(@RequestParam String salesBill) { + String mchId = "888027018000004"; OrderBO orderBO = new OrderBO(); orderBO.setBillNo(salesBill); orderBO.setMchId(mchId); diff --git a/payment-base-app/src/main/java/com/base/springcloud/entity/WxpayRefundVo.java b/payment-base-app/src/main/java/com/base/springcloud/entity/WxpayRefundVo.java index 068d10e..d59723f 100644 --- a/payment-base-app/src/main/java/com/base/springcloud/entity/WxpayRefundVo.java +++ b/payment-base-app/src/main/java/com/base/springcloud/entity/WxpayRefundVo.java @@ -3,6 +3,7 @@ package com.base.springcloud.entity; import lombok.Data; import java.io.Serializable; +import java.math.BigDecimal; /** * 微信退款预下单-主体信息 @@ -36,10 +37,10 @@ public class WxpayRefundVo implements Serializable { public static class WxRefundAmountVo { // 退款金额,币种的最小单位,只能为整数,不能超过原订单支付金额。 // 示例值:888 - private long refund; + private BigDecimal refund; // 原支付交易的订单总金额,币种的最小单位,只能为整数。 // 示例值:888 - private long total; + private BigDecimal total; // CNY:人民币,境内商户号仅支持人民币。 // 示例值:CNY private String currency = "CNY"; @@ -48,7 +49,7 @@ public class WxpayRefundVo implements Serializable { super(); } - public WxRefundAmountVo(long refund, long total) { + public WxRefundAmountVo(BigDecimal refund, BigDecimal total) { super(); this.refund = refund; this.total = total; diff --git a/payment-base-app/src/main/java/com/base/springcloud/entity/WxpayTradeVo.java b/payment-base-app/src/main/java/com/base/springcloud/entity/WxpayTradeVo.java index e57629f..751f797 100644 --- a/payment-base-app/src/main/java/com/base/springcloud/entity/WxpayTradeVo.java +++ b/payment-base-app/src/main/java/com/base/springcloud/entity/WxpayTradeVo.java @@ -3,6 +3,7 @@ package com.base.springcloud.entity; import lombok.Data; import java.io.Serializable; +import java.math.BigDecimal; /** * 微信支付预下单-主体信息 @@ -63,7 +64,7 @@ public class WxpayTradeVo implements Serializable { public static class WxTradeAmountVo { // 订单总金额,单位为分。 // 示例值:100 - private long total; + private BigDecimal total; // CNY:人民币,境内商户号仅支持人民币。 // 示例值:CNY private String currency = "CNY"; @@ -71,7 +72,7 @@ public class WxpayTradeVo implements Serializable { public WxTradeAmountVo() { super(); } - public WxTradeAmountVo(long total) { + public WxTradeAmountVo(BigDecimal total) { super(); this.total = total; } diff --git a/payment-base-app/src/main/java/com/base/springcloud/service/WecPaymentService.java b/payment-base-app/src/main/java/com/base/springcloud/service/WecPaymentService.java index 881ddcf..e564616 100644 --- a/payment-base-app/src/main/java/com/base/springcloud/service/WecPaymentService.java +++ b/payment-base-app/src/main/java/com/base/springcloud/service/WecPaymentService.java @@ -1,6 +1,8 @@ package com.base.springcloud.service; +import com.base.springcloud.bo.CodePayBO; import com.base.springcloud.bo.OrderBO; +import com.base.springcloud.bo.RefundBO; import com.base.springcloud.entity.Order; public interface WecPaymentService { @@ -12,6 +14,23 @@ public interface WecPaymentService { */ String nativePay(OrderBO orderBO); + /** + * 微信Native支付 + * @param orderBO + */ + String wecNativePay(OrderBO orderBO); + + /** + * 微信APIv3退款 + * @param refundBO + */ + void wecRefund(RefundBO refundBO); + + /** + * 微信付款码支付 + */ + void codePay(CodePayBO codePayBO); + /** * 查询微信订单结果 * @param orderBO diff --git a/payment-base-app/src/main/java/com/base/springcloud/service/imp/MerchantServiceImp.java b/payment-base-app/src/main/java/com/base/springcloud/service/imp/MerchantServiceImp.java index a0edac2..4e999b2 100644 --- a/payment-base-app/src/main/java/com/base/springcloud/service/imp/MerchantServiceImp.java +++ b/payment-base-app/src/main/java/com/base/springcloud/service/imp/MerchantServiceImp.java @@ -2,20 +2,15 @@ package com.base.springcloud.service.imp; import com.base.springcloud.bo.MerchantConfigBO; import com.base.springcloud.bo.OrderBO; -import com.base.springcloud.constant.PaymentConstant; import com.base.springcloud.dao.MerchantConfigMapper; import com.base.springcloud.entity.MerchantConfig; -import com.base.springcloud.entity.WxpayTradeVo; import com.base.springcloud.service.MerchantService; +import com.base.springcloud.util.DateUtil; import com.base.springcloud.util.FtpUtil; -import com.base.springcloud.util.WxpayUtils; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder; +import com.base.springcloud.wxsdk.MyConfig; +import com.base.springcloud.wxsdk.WXPay; +import com.base.springcloud.wxsdk.WXPayUtil; import org.apache.commons.lang.StringUtils; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.utils.URIBuilder; -import org.apache.http.util.EntityUtils; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -23,10 +18,7 @@ import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.io.*; -import java.util.Date; -import java.util.List; -import java.util.Optional; -import java.util.UUID; +import java.util.*; import java.util.stream.Collectors; @Service @@ -192,22 +184,29 @@ public class MerchantServiceImp implements MerchantService { return null; } - /*public static void main(String[] args) { - WxpayTradeVo wxpayTradeVo = new WxpayTradeVo(); - wxpayTradeVo.setAppid("wxa91984b310053c3d"); - wxpayTradeVo.setMchid("1610494969"); - wxpayTradeVo.setDescription("Image形象店-深圳腾大-QQ公仔"); - wxpayTradeVo.setOut_trade_no("20220301095644"); - wxpayTradeVo.setNotify_url("https://www.weixin.qq.com/wxpay/pay.php"); - WxpayTradeVo.WxTradeAmountVo wxTradeAmountVo = new WxpayTradeVo.WxTradeAmountVo(); - wxTradeAmountVo.setTotal(1l); - wxTradeAmountVo.setCurrency("CNY"); - wxpayTradeVo.setAmount(wxTradeAmountVo); - ObjectNode objectNode = WxpayUtils.getWxpay(wxpayTradeVo, - PaymentConstant.NATIVE, - "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCVVy4CUrRTPJZ49vuK9Tr5Rurd3dE3eiz7lsnWPAYgZtdWsC6TORUaP0Px/ZMtaJYNURMpVKC5koXZSPTNr9ROHa6B6IX4lTMERzHgiVzGIqLlE/twBYXbSuh8yXy3k59adFDYZZNN+2THWdst2Ao7pD+GXlDby8gfF67ax9CbWq5yyx6MYMKHwOVe3H/ON2pcmZR6I809Lwa4zfmUyOez1U5pwNiwv2cyorM5UXzVMsvK5jfIKDaskNVIV/+ax15tf+OQ/6t9iaWzRAV1zeeJd/TuVVGB/QU5slg5xyIwaqKzJM1H3n64er5QNRKpU31Ru0dHzJTW7ljtILWCQZubAgMBAAECggEAVga6QixSJACqzVO44vsQ1be8Y8H7WvHOJ79c1UQOY2vab91ddwFS3qPDCAsCVQ4CIfmqCAzv6PY2lEwqeLHooncGp17Cjcekc7pvSODXyGr7Wup/1K/Zw9EKy5dleHpZOUn3qDAbArjl/NAOKgJhIqoizcxW18YzqXWDDBQ+Xr6kqmjWXWZ3Kj6ghZNoSkdoynTQ7Ti0C2lq0UeXg611ea6Zs3fgjOgYEBMSSI7v4zE/6RN7jFGlZe5Z1NgO15rl8r3YdDnXTCIpEiTV+yHsDkluqJquB0E1Xrr1AXl9AGMuq6lYEB4houJUPtGgQ6bLs93MrnyzgnGTLbLSnXDNwQKBgQDD6Z+0xZ8lApXZylI2N4zuSrCsi6wB6Qp/64jr+C7GAdmC7w9WRoIlIF7Q2s9p5ulGyxD2hAbrajY5CsjV3S0uIDAaULoG3dw5vxYFEdvcKgxphf8j0gcajUnPGzCxF5vm29B9D+o1mrylr04dtWHlP0KpKwQ6HiTX7if+DcXMhwKBgQDDJOFpoCcVdZ0clfpvJPZpgRTu3I4HFTs27elCQ98g1UWav/VY7729YLAKU27d6lG+iqH1X5lVu2lr/du8wdJoMbpBPD34V+V6ye8gV9lCG7wSCgsNnPqM/HoyBSe6+JUgwG+vCRh1KMhNBmqEMCqPRAtbW3wfDdCShfTBHAjxTQKBgCjYrNOj4Gp2/4dJB5sGOfv01qEi/dnoHKhPE10fgaEHAbKq5Se3y6qx9RLWjCgoXLrF2+OJ42dx3ZxHA1iTpLfZPi4+ZorH9wKTNKQZ9R0SJ0RZC7kMMS74BkuH/77sjTS03+MzTkGe4XYp9r968c46QkvDDD6Ryly66tutQ3BBAoGBAJiE/MDncWSuVit/dQrlYso3HztJmqAikfNuoz8tePlad1pp6cKnLi8ToMORTjeX6mf4RnlXTKrbHZxP/yxaeK8DHsXZ5b1yRWNg7MrknmOy9Z1LyDPNm24R7xelO9ZAb533idLOUxVO2K6K8zl5w+nRjubKbAqBa6D5ncBIvxyJAoGAP9cSun39cFuzjdTaWojmbQMM8k5pyvFWr2UdY50GFrFCdajkAxqoTLkFEUP5fx8InIU/X9aSbVE6Va5fdOKiifbx5KaSKFrH/xZS6XykAbIkWDWqOPMovHoMWj3lGO96wDInry9YZNy7JA8AuZsLSBUA7Xb33FqZ2WU5ePeFOAA=", - "MIID9jCCAt6gAwIBAgIUHPQVmPutv7O6ub4lOfSWhLWhflowDQYJKoZIhvcNAQELBQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsTFFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3QgQ0EwHhcNMjEwNjA5MDMzMjQ5WhcNMjYwNjA4MDMzMjQ5WjCBhzETMBEGA1UEAwwKMTYxMDQ5NDk2OTEbMBkGA1UECgwS5b6u5L+h5ZWG5oi357O757ufMTMwMQYDVQQLDCrotLXlt57pgJrnqIvlhYTlvJ/lt6XnqIvmnLrmorDmnInpmZDlhazlj7gxCzAJBgNVBAYMAkNOMREwDwYDVQQHDAhTaGVuWmhlbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJVXLgJStFM8lnj2+4r1OvlG6t3d0Td6LPuWydY8BiBm11awLpM5FRo/Q/H9ky1olg1REylUoLmShdlI9M2v1E4droHohfiVMwRHMeCJXMYiouUT+3AFhdtK6HzJfLeTn1p0UNhlk037ZMdZ2y3YCjukP4ZeUNvLyB8XrtrH0JtarnLLHoxgwofA5V7cf843alyZlHojzT0vBrjN+ZTI57PVTmnA2LC/ZzKiszlRfNUyy8rmN8goNqyQ1UhX/5rHXm1/45D/q32JpbNEBXXN54l39O5VUYH9BTmyWDnHIjBqorMkzUfefrh6vlA1EqlTfVG7R0fMlNbuWO0gtYJBm5sCAwEAAaOBgTB/MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgTwMGUGA1UdHwReMFwwWqBYoFaGVGh0dHA6Ly9ldmNhLml0cnVzLmNvbS5jbi9wdWJsaWMvaXRydXNjcmw/Q0E9MUJENDIyMEU1MERCQzA0QjA2QUQzOTc1NDk4NDZDMDFDM0U4RUJEMjANBgkqhkiG9w0BAQsFAAOCAQEACoG5Uf3X33UlDY6VWxgvN6Kg1W9ZRwcwcxSPuURv7OVO1o1JH0c1lPRqY3H8A+TeVI4/5wt5GPRRkSOtecPhMikgvhUjxJZnM0b30sxEv7hoqgEqNcsu2Kc60W6hGzVW0Fw8qimjOmoREBAaOOpYNr/zIrZZiW4vJd//ESNXvpTlwwK1xE9VW3VJGHW7mMZ+Whr3S13esH7rds5mLE6OQvrL6B2weEcVlzP1oEZAbaZW5bgz2gQBR5WheU8QGySnU/e38nzLsdwY0r7ztaWQ39z7fsOeY2gupB8cZ251pWZ9oYvUu4u0VFflrbJiMdLLEZDej02MYtgO6o9xb8BNmw==", - "1CF41598FBADBFB3BAB9BE2539F49684B5A17E5A"); - System.out.println(objectNode); - }*/ + public static void main(String[] args) throws Exception { + + MyConfig config = new MyConfig(); + WXPay wxPay = new WXPay(config); + + Map data = new HashMap(); + //公众账号ID (微信分配的公众账号ID(企业号corpid即为此appId)) + data.put("appid", "wxa91984b310053c3d"); + //商户号 (微信支付分配的商户号) + data.put("mch_id", "1610494969"); + data.put("device_info", "sb0001"); + //商品描述 (商品简单描述,该字段须严格按照规范传递) 店名-销售商品类目 例 小张南山店-超市 + data.put("body", "Image形象店-深圳腾大-QQ公仔"); + //商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|*且在同一个商户号下唯一 + data.put("out_trade_no", DateUtil.getDateString(25)); + //订单总金额,单位为分,只能为整数 + data.put("total_fee", "1"); + data.put("auth_code", "135080797699902868"); + data.put("spbill_create_ip", "8.8.8.8"); + //付款码 (扫码支付付款码,设备读取用户微信中的条码或者二维码信息(注:用户付款码条形码规则:18位纯数字,以10、11、12、13、14、15开头)) + data.put("sign", WXPayUtil.generateSignature(data,"yDXt3Jczcs2dSKvwUxGkYYfHzCe8SXtY")); + Map responseMap = wxPay.microPay(data); + //判断是否成功 + System.out.println("微信返回响应:" + responseMap.toString()); + } } \ No newline at end of file diff --git a/payment-base-app/src/main/java/com/base/springcloud/service/imp/OrderServiceImp.java b/payment-base-app/src/main/java/com/base/springcloud/service/imp/OrderServiceImp.java index 7eef2c4..70a9cdd 100644 --- a/payment-base-app/src/main/java/com/base/springcloud/service/imp/OrderServiceImp.java +++ b/payment-base-app/src/main/java/com/base/springcloud/service/imp/OrderServiceImp.java @@ -36,7 +36,8 @@ public class OrderServiceImp implements OrderService { order.setTimeStamp(new Date()); order.setMchId(orderBO.getMchId()); order.setOrderState(PaymentConstant.WAIT); - order.setOrderAmount(DecimalUtil.divide(orderBO.getSaleBill().getTotalAmount(),new BigDecimal(100))); + BigDecimal amount = orderBO.getSaleBill().getTotalAmount(); + order.setOrderAmount(DecimalUtil.divide(Optional.ofNullable(amount).orElse(new BigDecimal(0.01)),new BigDecimal(100))); orderMapper.insert(order); orderBO.setOrder(order); } diff --git a/payment-base-app/src/main/java/com/base/springcloud/service/imp/WecPaymentServiceImp.java b/payment-base-app/src/main/java/com/base/springcloud/service/imp/WecPaymentServiceImp.java index da1e9e1..6e7797e 100644 --- a/payment-base-app/src/main/java/com/base/springcloud/service/imp/WecPaymentServiceImp.java +++ b/payment-base-app/src/main/java/com/base/springcloud/service/imp/WecPaymentServiceImp.java @@ -1,11 +1,19 @@ package com.base.springcloud.service.imp; +import com.base.springcloud.bo.CodePayBO; import com.base.springcloud.bo.OrderBO; +import com.base.springcloud.bo.RefundBO; import com.base.springcloud.constant.PaymentConstant; import com.base.springcloud.entity.Order; +import com.base.springcloud.entity.WxpayRefundVo; +import com.base.springcloud.entity.WxpayRefundVo.WxRefundAmountVo; +import com.base.springcloud.entity.WxpayTradeVo; import com.base.springcloud.service.WecPaymentService; import com.base.springcloud.util.DateUtil; import com.base.springcloud.util.DecimalUtil; +import com.base.springcloud.wxsdk.WxpayUtils; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.hisun.iposm.HiCASignUtil2; import com.hisun.iposm.HiiposmUtil; import org.apache.commons.lang.StringUtils; @@ -15,6 +23,9 @@ import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import java.io.IOException; +import java.math.BigDecimal; +import java.util.Map; +import java.util.Optional; @Service public class WecPaymentServiceImp implements WecPaymentService { @@ -41,7 +52,7 @@ public class WecPaymentServiceImp implements WecPaymentService { String storeBrand = "RBS-MD"; String cityBranchNm = "RBS-PP"; String createDate = DateUtil.getYearMonthDay2(); - + String amount = DecimalUtil.multiple(orderBO.getSaleBill().getTotalAmount(),100).setScale(0, BigDecimal.ROUND_DOWN).toPlainString(); // 签名数据 Order order = orderBO.getOrder(); @@ -54,9 +65,12 @@ public class WecPaymentServiceImp implements WecPaymentService { signDate.append(PaymentConstant.SIGN_TYPE); signDate.append(type); signDate.append(version); - signDate.append(DecimalUtil.stripTrailingZeros(orderBO.getSaleBill().getTotalAmount())); + signDate.append(amount); signDate.append(createDate); switch (order.getOrderType()) { + case PaymentConstant.HB: + signDate.append(PaymentConstant.QRCHB); + break; case PaymentConstant.WEC: signDate.append(PaymentConstant.QRCWC); break; @@ -83,10 +97,13 @@ public class WecPaymentServiceImp implements WecPaymentService { requestParam.append("&signType=").append(PaymentConstant.SIGN_TYPE); requestParam.append("&type=").append(type); requestParam.append("&version=").append(version); - requestParam.append("&amount=").append(DecimalUtil.stripTrailingZeros(orderBO.getSaleBill().getTotalAmount())); + requestParam.append("&amount=").append(amount); requestParam.append("&orderDate=").append(createDate); requestParam.append("&payWay="); switch (order.getOrderType()) { + case PaymentConstant.HB: + requestParam.append(PaymentConstant.QRCHB); + break; case PaymentConstant.WEC: requestParam.append(PaymentConstant.QRCWC); break; @@ -112,6 +129,92 @@ public class WecPaymentServiceImp implements WecPaymentService { return StringUtils.isNotBlank(res) ? util.getValue(res, "sessionId") : ""; } + /** + * 微信Native支付 + * @param orderBO + */ + @Override + public String wecNativePay(OrderBO orderBO) { + Order order = Optional.ofNullable(orderBO).map(o->orderBO.getOrder()).orElse(null); + if(order == null) { + // TODO 抛出业务异常 + } + WxpayTradeVo wxpayTradeVo = new WxpayTradeVo(); + wxpayTradeVo.setAppid(orderBO.getAppId()); + wxpayTradeVo.setMchid(orderBO.getMchId()); + wxpayTradeVo.setDescription(order.getGoodName()); + wxpayTradeVo.setOut_trade_no(order.getOrderNo()); + // FIXME 待定为商户系统||RBS系统回调接收 + wxpayTradeVo.setNotify_url("https://www.weixin.qq.com/wxpay/pay.php"); + WxpayTradeVo.WxTradeAmountVo wxTradeAmountVo = new WxpayTradeVo.WxTradeAmountVo(); + wxTradeAmountVo.setTotal(order.getOrderAmount()); + wxpayTradeVo.setAmount(wxTradeAmountVo); + ObjectNode objectNode = WxpayUtils.getWxpay( + wxpayTradeVo, + PaymentConstant.NATIVE, + orderBO.getMchPrivateKey(), + orderBO.getCertificate(), + orderBO.getSerialNo()); + logger.info("Native支付订单 :{} 接收响应数据 :{}",order.getOrderNo(),objectNode.toString()); + JsonNode jsonNode = objectNode.get("code_url"); + return jsonNode.toString(); + } + + /** + * 微信APIv3退款 + * @param refundBO + */ + @Override + public void wecRefund(RefundBO refundBO) { + WxpayRefundVo wxpayRefundVo = new WxpayRefundVo(); + wxpayRefundVo.setTransaction_id(refundBO.getOrigOrderNo()); + wxpayRefundVo.setReason(refundBO.getReason()); + WxRefundAmountVo wxRefundAmountVo = new WxRefundAmountVo(); + wxRefundAmountVo.setTotal(refundBO.getOrderAmount()); + wxRefundAmountVo.setRefund(refundBO.getRefundAmount()); + wxpayRefundVo.setNotify_url(refundBO.getNotifyUrl()); + wxpayRefundVo.setOut_refund_no(refundBO.getRefundNo()); + wxpayRefundVo.setAmount(wxRefundAmountVo); + String refundRsp = WxpayUtils.refund(wxpayRefundVo,refundBO.getMchId(),refundBO.getMchPrivateKey(),refundBO.getCertificate(),refundBO.getSerialNo()); + } + + /** + * 微信付款码支付 + * @param codePayBO + */ + @Override + public void codePay(CodePayBO codePayBO) { + } + + /*public static void main(String[] args) { + CodePayBO codePayBO = new CodePayBO(); + codePayBO.setAppId("wxa91984b310053c3d"); + codePayBO.setMchId("1610494969"); + codePayBO.setDeviceInfo("sb0001"); + codePayBO.setSpbillCreateIp("8.8.8.8"); + codePayBO.setBody("Image形象店-深圳腾大-QQ公仔"); + codePayBO.setOrderNo(DateUtil.getDateString(25)); + codePayBO.setAmount(new BigDecimal(1)); + codePayBO.setMchPrivateKey("MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCVVy4CUrRTPJZ49vuK9Tr5Rurd3dE3eiz7lsnWPAYgZtdWsC6TORUaP0Px/ZMtaJYNURMpVKC5koXZSPTNr9ROHa6B6IX4lTMERzHgiVzGIqLlE/twBYXbSuh8yXy3k59adFDYZZNN+2THWdst2Ao7pD+GXlDby8gfF67ax9CbWq5yyx6MYMKHwOVe3H/ON2pcmZR6I809Lwa4zfmUyOez1U5pwNiwv2cyorM5UXzVMsvK5jfIKDaskNVIV/+ax15tf+OQ/6t9iaWzRAV1zeeJd/TuVVGB/QU5slg5xyIwaqKzJM1H3n64er5QNRKpU31Ru0dHzJTW7ljtILWCQZubAgMBAAECggEAVga6QixSJACqzVO44vsQ1be8Y8H7WvHOJ79c1UQOY2vab91ddwFS3qPDCAsCVQ4CIfmqCAzv6PY2lEwqeLHooncGp17Cjcekc7pvSODXyGr7Wup/1K/Zw9EKy5dleHpZOUn3qDAbArjl/NAOKgJhIqoizcxW18YzqXWDDBQ+Xr6kqmjWXWZ3Kj6ghZNoSkdoynTQ7Ti0C2lq0UeXg611ea6Zs3fgjOgYEBMSSI7v4zE/6RN7jFGlZe5Z1NgO15rl8r3YdDnXTCIpEiTV+yHsDkluqJquB0E1Xrr1AXl9AGMuq6lYEB4houJUPtGgQ6bLs93MrnyzgnGTLbLSnXDNwQKBgQDD6Z+0xZ8lApXZylI2N4zuSrCsi6wB6Qp/64jr+C7GAdmC7w9WRoIlIF7Q2s9p5ulGyxD2hAbrajY5CsjV3S0uIDAaULoG3dw5vxYFEdvcKgxphf8j0gcajUnPGzCxF5vm29B9D+o1mrylr04dtWHlP0KpKwQ6HiTX7if+DcXMhwKBgQDDJOFpoCcVdZ0clfpvJPZpgRTu3I4HFTs27elCQ98g1UWav/VY7729YLAKU27d6lG+iqH1X5lVu2lr/du8wdJoMbpBPD34V+V6ye8gV9lCG7wSCgsNnPqM/HoyBSe6+JUgwG+vCRh1KMhNBmqEMCqPRAtbW3wfDdCShfTBHAjxTQKBgCjYrNOj4Gp2/4dJB5sGOfv01qEi/dnoHKhPE10fgaEHAbKq5Se3y6qx9RLWjCgoXLrF2+OJ42dx3ZxHA1iTpLfZPi4+ZorH9wKTNKQZ9R0SJ0RZC7kMMS74BkuH/77sjTS03+MzTkGe4XYp9r968c46QkvDDD6Ryly66tutQ3BBAoGBAJiE/MDncWSuVit/dQrlYso3HztJmqAikfNuoz8tePlad1pp6cKnLi8ToMORTjeX6mf4RnlXTKrbHZxP/yxaeK8DHsXZ5b1yRWNg7MrknmOy9Z1LyDPNm24R7xelO9ZAb533idLOUxVO2K6K8zl5w+nRjubKbAqBa6D5ncBIvxyJAoGAP9cSun39cFuzjdTaWojmbQMM8k5pyvFWr2UdY50GFrFCdajkAxqoTLkFEUP5fx8InIU/X9aSbVE6Va5fdOKiifbx5KaSKFrH/xZS6XykAbIkWDWqOPMovHoMWj3lGO96wDInry9YZNy7JA8AuZsLSBUA7Xb33FqZ2WU5ePeFOAA="); + codePayBO.setAuthCode("135276524069149568"); + + Map params = MicroPayModel + .builder() + .appid(codePayBO.getAppId()) + .mch_id(codePayBO.getMchId()) + .device_info(codePayBO.getDeviceInfo()) + .nonce_str(WxPayKit.generateStr()) + .body(codePayBO.getBody()) + //.attach(codePayBO.getAttach()) + .out_trade_no(codePayBO.getOrderNo()) + .total_fee(codePayBO.getAmount().toPlainString()) + .spbill_create_ip(codePayBO.getSpbillCreateIp()) + .auth_code(codePayBO.getAuthCode()) + .build() + .createSign(codePayBO.getMchPrivateKey(), SignType.HMACSHA256); + System.out.println(WxPayApi.microPay(false,params)); + } +*/ /** * 订单状态查询 * @@ -167,5 +270,4 @@ public class WecPaymentServiceImp implements WecPaymentService { } return false; } - } diff --git a/payment-base-app/src/main/java/com/base/springcloud/util/DecimalUtil.java b/payment-base-app/src/main/java/com/base/springcloud/util/DecimalUtil.java index d10bbbf..22b44da 100644 --- a/payment-base-app/src/main/java/com/base/springcloud/util/DecimalUtil.java +++ b/payment-base-app/src/main/java/com/base/springcloud/util/DecimalUtil.java @@ -1,9 +1,12 @@ package com.base.springcloud.util; import java.math.BigDecimal; +import java.text.DecimalFormat; public class DecimalUtil { + private DecimalFormat df = new DecimalFormat("#.00"); + /** * 加法计算(result = x + y) * diff --git a/payment-base-app/src/main/java/com/base/springcloud/wxsdk/IWXPayDomain.java b/payment-base-app/src/main/java/com/base/springcloud/wxsdk/IWXPayDomain.java new file mode 100644 index 0000000..da5ae42 --- /dev/null +++ b/payment-base-app/src/main/java/com/base/springcloud/wxsdk/IWXPayDomain.java @@ -0,0 +1,42 @@ +package com.base.springcloud.wxsdk; + +/** + * 域名管理,实现主备域名自动切换 + */ +public abstract interface IWXPayDomain { + /** + * 上报域名网络状况 + * @param domain 域名。 比如:api.mch.weixin.qq.com + * @param elapsedTimeMillis 耗时 + * @param ex 网络请求中出现的异常。 + * null表示没有异常 + * ConnectTimeoutException,表示建立网络连接异常 + * UnknownHostException, 表示dns解析异常 + */ + abstract void report(final String domain, long elapsedTimeMillis, final Exception ex); + + /** + * 获取域名 + * @param config 配置 + * @return 域名 + */ + abstract DomainInfo getDomain(final WXPayConfig config); + + static class DomainInfo{ + public String domain; //域名 + public boolean primaryDomain; //该域名是否为主域名。例如:api.mch.weixin.qq.com为主域名 + public DomainInfo(String domain, boolean primaryDomain) { + this.domain = domain; + this.primaryDomain = primaryDomain; + } + + @Override + public String toString() { + return "DomainInfo{" + + "domain='" + domain + '\'' + + ", primaryDomain=" + primaryDomain + + '}'; + } + } + +} \ No newline at end of file diff --git a/payment-base-app/src/main/java/com/base/springcloud/wxsdk/MyConfig.java b/payment-base-app/src/main/java/com/base/springcloud/wxsdk/MyConfig.java new file mode 100644 index 0000000..1286ae3 --- /dev/null +++ b/payment-base-app/src/main/java/com/base/springcloud/wxsdk/MyConfig.java @@ -0,0 +1,87 @@ +package com.base.springcloud.wxsdk; + +import sun.misc.BASE64Decoder; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +public class MyConfig extends WXPayConfig { + + private String appid = "wxa91984b310053c3d"; + private String mchid = "1610494969"; + private String key = "yDXt3Jczcs2dSKvwUxGkYYfHzCe8SXtY"; + + /** + * 加载证书 这里证书需要到微信商户平台进行下载 + */ + private byte[] certData; + + public MyConfig() throws Exception { + this.certData = new BASE64Decoder().decodeBuffer("MIID9jCCAt6gAwIBAgIUHPQVmPutv7O6ub4lOfSWhLWhflowDQYJKoZIhvcNAQELBQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsTFFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3QgQ0EwHhcNMjEwNjA5MDMzMjQ5WhcNMjYwNjA4MDMzMjQ5WjCBhzETMBEGA1UEAwwKMTYxMDQ5NDk2OTEbMBkGA1UECgwS5b6u5L+h5ZWG5oi357O757ufMTMwMQYDVQQLDCrotLXlt57pgJrnqIvlhYTlvJ/lt6XnqIvmnLrmorDmnInpmZDlhazlj7gxCzAJBgNVBAYMAkNOMREwDwYDVQQHDAhTaGVuWmhlbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJVXLgJStFM8lnj2+4r1OvlG6t3d0Td6LPuWydY8BiBm11awLpM5FRo/Q/H9ky1olg1REylUoLmShdlI9M2v1E4droHohfiVMwRHMeCJXMYiouUT+3AFhdtK6HzJfLeTn1p0UNhlk037ZMdZ2y3YCjukP4ZeUNvLyB8XrtrH0JtarnLLHoxgwofA5V7cf843alyZlHojzT0vBrjN+ZTI57PVTmnA2LC/ZzKiszlRfNUyy8rmN8goNqyQ1UhX/5rHXm1/45D/q32JpbNEBXXN54l39O5VUYH9BTmyWDnHIjBqorMkzUfefrh6vlA1EqlTfVG7R0fMlNbuWO0gtYJBm5sCAwEAAaOBgTB/MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgTwMGUGA1UdHwReMFwwWqBYoFaGVGh0dHA6Ly9ldmNhLml0cnVzLmNvbS5jbi9wdWJsaWMvaXRydXNjcmw/Q0E9MUJENDIyMEU1MERCQzA0QjA2QUQzOTc1NDk4NDZDMDFDM0U4RUJEMjANBgkqhkiG9w0BAQsFAAOCAQEACoG5Uf3X33UlDY6VWxgvN6Kg1W9ZRwcwcxSPuURv7OVO1o1JH0c1lPRqY3H8A+TeVI4/5wt5GPRRkSOtecPhMikgvhUjxJZnM0b30sxEv7hoqgEqNcsu2Kc60W6hGzVW0Fw8qimjOmoREBAaOOpYNr/zIrZZiW4vJd//ESNXvpTlwwK1xE9VW3VJGHW7mMZ+Whr3S13esH7rds5mLE6OQvrL6B2weEcVlzP1oEZAbaZW5bgz2gQBR5WheU8QGySnU/e38nzLsdwY0r7ztaWQ39z7fsOeY2gupB8cZ251pWZ9oYvUu4u0VFflrbJiMdLLEZDej02MYtgO6o9xb8BNmw=="); + } + + @Override + String getAppID() { + return this.appid; + } + + @Override + String getMchID() { + return this.mchid; + } + + @Override + String getKey() { + return this.key; + } + + void setKey(String key){this.key = key;} + + @Override + InputStream getCertStream() { + return new ByteArrayInputStream(this.certData); + } + + @Override + public IWXPayDomain getWXPayDomain() { // 这个方法需要这样实现, 否则无法正常初始化WXPay + IWXPayDomain iwxPayDomain = new IWXPayDomain() { + @Override + public void report(String domain, long elapsedTimeMillis, Exception ex) { + + } + + @Override + public DomainInfo getDomain(WXPayConfig config) { + return new DomainInfo(WXPayConstants.DOMAIN_API, true); + } + }; + return iwxPayDomain; + } + + /** + * 获取沙箱密钥 + * @return + */ + public String getSandboxSignKey() { + try { + WXPay wxPay = new WXPay(this); + Map params = new HashMap(); + params.put("mch_id", this.getMchID()); + params.put("nonce_str", WXPayUtil.generateNonceStr()); + params.put("sign", WXPayUtil.generateSignature(params, this.getKey())); + String strXML = wxPay.requestWithoutCert("https://api.mch.weixin.qq.com/sandboxnew/pay/getsignkey", + params, this.getHttpConnectTimeoutMs(), this.getHttpReadTimeoutMs()); + System.out.println(strXML); + Map result = WXPayUtil.xmlToMap(strXML); + if ("SUCCESS".equals(result.get("return_code"))) { + return result.get("sandbox_signkey"); + } + return null; + } catch (Exception e) { + return null; + } + } + +} \ No newline at end of file diff --git a/payment-base-app/src/main/java/com/base/springcloud/wxsdk/WXPay.java b/payment-base-app/src/main/java/com/base/springcloud/wxsdk/WXPay.java new file mode 100644 index 0000000..2e8df72 --- /dev/null +++ b/payment-base-app/src/main/java/com/base/springcloud/wxsdk/WXPay.java @@ -0,0 +1,689 @@ +package com.base.springcloud.wxsdk; + +import com.base.springcloud.wxsdk.WXPayConstants.SignType; + +import java.util.HashMap; +import java.util.Map; + +public class WXPay { + + private WXPayConfig config; + private SignType signType; + private boolean autoReport; + private boolean useSandbox; + private String notifyUrl; + private WXPayRequest wxPayRequest; + + public WXPay(final WXPayConfig config) throws Exception { + this(config, null, true, false); + } + + public WXPay(final WXPayConfig config, final boolean autoReport) throws Exception { + this(config, null, autoReport, false); + } + + + public WXPay(final WXPayConfig config, final boolean autoReport, final boolean useSandbox) throws Exception{ + this(config, null, autoReport, useSandbox); + } + + public WXPay(final WXPayConfig config, final String notifyUrl) throws Exception { + this(config, notifyUrl, true, false); + } + + public WXPay(final WXPayConfig config, final String notifyUrl, final boolean autoReport) throws Exception { + this(config, notifyUrl, autoReport, false); + } + + public WXPay(final WXPayConfig config, final String notifyUrl, final boolean autoReport, final boolean useSandbox) throws Exception { + this.config = config; + this.notifyUrl = notifyUrl; + this.autoReport = autoReport; + this.useSandbox = useSandbox; + if (useSandbox) { + this.signType = SignType.MD5; // 沙箱环境 + } + else { + this.signType = SignType.HMACSHA256; + } + this.wxPayRequest = new WXPayRequest(config); + } + + private void checkWXPayConfig() throws Exception { + if (this.config == null) { + throw new Exception("config is null"); + } + if (this.config.getAppID() == null || this.config.getAppID().trim().length() == 0) { + throw new Exception("appid in config is empty"); + } + if (this.config.getMchID() == null || this.config.getMchID().trim().length() == 0) { + throw new Exception("appid in config is empty"); + } + if (this.config.getCertStream() == null) { + throw new Exception("cert stream in config is empty"); + } + if (this.config.getWXPayDomain() == null){ + throw new Exception("config.getWXPayDomain() is null"); + } + + if (this.config.getHttpConnectTimeoutMs() < 10) { + throw new Exception("http connect timeout is too small"); + } + if (this.config.getHttpReadTimeoutMs() < 10) { + throw new Exception("http read timeout is too small"); + } + + } + + /** + * 向 Map 中添加 appid、mch_id、nonce_str、sign_type、sign
+ * 该函数适用于商户适用于统一下单等接口,不适用于红包、代金券接口 + * + * @param reqData + * @return + * @throws Exception + */ + public Map fillRequestData(Map reqData) throws Exception { + reqData.put("appid", config.getAppID()); + reqData.put("mch_id", config.getMchID()); + reqData.put("nonce_str", WXPayUtil.generateNonceStr()); + if (SignType.MD5.equals(this.signType)) { + reqData.put("sign_type", WXPayConstants.MD5); + } + else if (SignType.HMACSHA256.equals(this.signType)) { + reqData.put("sign_type", WXPayConstants.HMACSHA256); + } + reqData.put("sign", WXPayUtil.generateSignature(reqData, config.getKey(), this.signType)); + return reqData; + } + + /** + * 判断xml数据的sign是否有效,必须包含sign字段,否则返回false。 + * + * @param reqData 向wxpay post的请求数据 + * @return 签名是否有效 + * @throws Exception + */ + public boolean isResponseSignatureValid(Map reqData) throws Exception { + // 返回数据的签名方式和请求中给定的签名方式是一致的 + return WXPayUtil.isSignatureValid(reqData, this.config.getKey(), this.signType); + } + + /** + * 判断支付结果通知中的sign是否有效 + * + * @param reqData 向wxpay post的请求数据 + * @return 签名是否有效 + * @throws Exception + */ + public boolean isPayResultNotifySignatureValid(Map reqData) throws Exception { + String signTypeInData = reqData.get(WXPayConstants.FIELD_SIGN_TYPE); + SignType signType; + if (signTypeInData == null) { + signType = SignType.MD5; + } + else { + signTypeInData = signTypeInData.trim(); + if (signTypeInData.length() == 0) { + signType = SignType.MD5; + } + else if (WXPayConstants.MD5.equals(signTypeInData)) { + signType = SignType.MD5; + } + else if (WXPayConstants.HMACSHA256.equals(signTypeInData)) { + signType = SignType.HMACSHA256; + } + else { + throw new Exception(String.format("Unsupported sign_type: %s", signTypeInData)); + } + } + return WXPayUtil.isSignatureValid(reqData, this.config.getKey(), signType); + } + + + /** + * 不需要证书的请求 + * @param urlSuffix String + * @param reqData 向wxpay post的请求数据 + * @param connectTimeoutMs 超时时间,单位是毫秒 + * @param readTimeoutMs 超时时间,单位是毫秒 + * @return API返回数据 + * @throws Exception + */ + public String requestWithoutCert(String urlSuffix, Map reqData, + int connectTimeoutMs, int readTimeoutMs) throws Exception { + String msgUUID = reqData.get("nonce_str"); + String reqBody = WXPayUtil.mapToXml(reqData); + System.out.println("请求串:" + reqBody); + String resp = this.wxPayRequest.requestWithoutCert(urlSuffix, msgUUID, reqBody, connectTimeoutMs, readTimeoutMs, autoReport); + return resp; + } + + + /** + * 需要证书的请求 + * @param urlSuffix String + * @param reqData 向wxpay post的请求数据 Map + * @param connectTimeoutMs 超时时间,单位是毫秒 + * @param readTimeoutMs 超时时间,单位是毫秒 + * @return API返回数据 + * @throws Exception + */ + public String requestWithCert(String urlSuffix, Map reqData, + int connectTimeoutMs, int readTimeoutMs) throws Exception { + String msgUUID= reqData.get("nonce_str"); + String reqBody = WXPayUtil.mapToXml(reqData); + + String resp = this.wxPayRequest.requestWithCert(urlSuffix, msgUUID, reqBody, connectTimeoutMs, readTimeoutMs, this.autoReport); + return resp; + } + + /** + * 处理 HTTPS API返回数据,转换成Map对象。return_code为SUCCESS时,验证签名。 + * @param xmlStr API返回的XML格式数据 + * @return Map类型数据 + * @throws Exception + */ + public Map processResponseXml(String xmlStr) throws Exception { + String RETURN_CODE = "return_code"; + String return_code; + Map respData = WXPayUtil.xmlToMap(xmlStr); + if (respData.containsKey(RETURN_CODE)) { + return_code = respData.get(RETURN_CODE); + } + else { + throw new Exception(String.format("No `return_code` in XML: %s", xmlStr)); + } + + if (return_code.equals(WXPayConstants.FAIL)) { + return respData; + } + else if (return_code.equals(WXPayConstants.SUCCESS)) { + if (this.isResponseSignatureValid(respData)) { + return respData; + } + else { + throw new Exception(String.format("Invalid sign value in XML: %s", xmlStr)); + } + } + else { + throw new Exception(String.format("return_code value %s is invalid in XML: %s", return_code, xmlStr)); + } + } + + /** + * 作用:提交刷卡支付
+ * 场景:刷卡支付 + * @param reqData 向wxpay post的请求数据 + * @return API返回数据 + * @throws Exception + */ + public Map microPay(Map reqData) throws Exception { + return this.microPay(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs()); + } + + + /** + * 作用:提交刷卡支付
+ * 场景:刷卡支付 + * @param reqData 向wxpay post的请求数据 + * @param connectTimeoutMs 连接超时时间,单位是毫秒 + * @param readTimeoutMs 读超时时间,单位是毫秒 + * @return API返回数据 + * @throws Exception + */ + public Map microPay(Map reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception { + String url; + if (this.useSandbox) { + url = WXPayConstants.SANDBOX_MICROPAY_URL_SUFFIX; + } + else { + url = WXPayConstants.MICROPAY_URL_SUFFIX; + } + String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs); + return this.processResponseXml(respXml); + } + + /** + * 提交刷卡支付,针对软POS,尽可能做成功 + * 内置重试机制,最多60s + * @param reqData + * @return + * @throws Exception + */ + public Map microPayWithPos(Map reqData) throws Exception { + return this.microPayWithPos(reqData, this.config.getHttpConnectTimeoutMs()); + } + + /** + * 提交刷卡支付,针对软POS,尽可能做成功 + * 内置重试机制,最多60s + * @param reqData + * @param connectTimeoutMs + * @return + * @throws Exception + */ + public Map microPayWithPos(Map reqData, int connectTimeoutMs) throws Exception { + int remainingTimeMs = 60*1000; + long startTimestampMs = 0; + Map lastResult = null; + Exception lastException = null; + + while (true) { + startTimestampMs = WXPayUtil.getCurrentTimestampMs(); + int readTimeoutMs = remainingTimeMs - connectTimeoutMs; + if (readTimeoutMs > 1000) { + try { + lastResult = this.microPay(reqData, connectTimeoutMs, readTimeoutMs); + String returnCode = lastResult.get("return_code"); + if (returnCode.equals("SUCCESS")) { + String resultCode = lastResult.get("result_code"); + String errCode = lastResult.get("err_code"); + if (resultCode.equals("SUCCESS")) { + break; + } + else { + // 看错误码,若支付结果未知,则重试提交刷卡支付 + if (errCode.equals("SYSTEMERROR") || errCode.equals("BANKERROR") || errCode.equals("USERPAYING")) { + remainingTimeMs = remainingTimeMs - (int)(WXPayUtil.getCurrentTimestampMs() - startTimestampMs); + if (remainingTimeMs <= 100) { + break; + } + else { + WXPayUtil.getLogger().info("microPayWithPos: try micropay again"); + if (remainingTimeMs > 5*1000) { + Thread.sleep(5*1000); + } + else { + Thread.sleep(1*1000); + } + continue; + } + } + else { + break; + } + } + } + else { + break; + } + } + catch (Exception ex) { + lastResult = null; + lastException = ex; + } + } + else { + break; + } + } + + if (lastResult == null) { + throw lastException; + } + else { + return lastResult; + } + } + + + + /** + * 作用:统一下单
+ * 场景:公共号支付、扫码支付、APP支付 + * @param reqData 向wxpay post的请求数据 + * @return API返回数据 + * @throws Exception + */ + public Map unifiedOrder(Map reqData) throws Exception { + return this.unifiedOrder(reqData, config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs()); + } + + + /** + * 作用:统一下单
+ * 场景:公共号支付、扫码支付、APP支付 + * @param reqData 向wxpay post的请求数据 + * @param connectTimeoutMs 连接超时时间,单位是毫秒 + * @param readTimeoutMs 读超时时间,单位是毫秒 + * @return API返回数据 + * @throws Exception + */ + public Map unifiedOrder(Map reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception { + String url; + if (this.useSandbox) { + url = WXPayConstants.SANDBOX_UNIFIEDORDER_URL_SUFFIX; + } + else { + url = WXPayConstants.UNIFIEDORDER_URL_SUFFIX; + } + if(this.notifyUrl != null) { + reqData.put("notify_url", this.notifyUrl); + } + String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs); + return this.processResponseXml(respXml); + } + + + /** + * 作用:查询订单
+ * 场景:刷卡支付、公共号支付、扫码支付、APP支付 + * @param reqData 向wxpay post的请求数据 + * @return API返回数据 + * @throws Exception + */ + public Map orderQuery(Map reqData) throws Exception { + return this.orderQuery(reqData, config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs()); + } + + + /** + * 作用:查询订单
+ * 场景:刷卡支付、公共号支付、扫码支付、APP支付 + * @param reqData 向wxpay post的请求数据 int + * @param connectTimeoutMs 连接超时时间,单位是毫秒 + * @param readTimeoutMs 读超时时间,单位是毫秒 + * @return API返回数据 + * @throws Exception + */ + public Map orderQuery(Map reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception { + String url; + if (this.useSandbox) { + url = WXPayConstants.SANDBOX_ORDERQUERY_URL_SUFFIX; + } + else { + url = WXPayConstants.ORDERQUERY_URL_SUFFIX; + } + String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs); + return this.processResponseXml(respXml); + } + + + /** + * 作用:撤销订单
+ * 场景:刷卡支付 + * @param reqData 向wxpay post的请求数据 + * @return API返回数据 + * @throws Exception + */ + public Map reverse(Map reqData) throws Exception { + return this.reverse(reqData, config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs()); + } + + + /** + * 作用:撤销订单
+ * 场景:刷卡支付
+ * 其他:需要证书 + * @param reqData 向wxpay post的请求数据 + * @param connectTimeoutMs 连接超时时间,单位是毫秒 + * @param readTimeoutMs 读超时时间,单位是毫秒 + * @return API返回数据 + * @throws Exception + */ + public Map reverse(Map reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception { + String url; + if (this.useSandbox) { + url = WXPayConstants.SANDBOX_REVERSE_URL_SUFFIX; + } + else { + url = WXPayConstants.REVERSE_URL_SUFFIX; + } + String respXml = this.requestWithCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs); + return this.processResponseXml(respXml); + } + + + /** + * 作用:关闭订单
+ * 场景:公共号支付、扫码支付、APP支付 + * @param reqData 向wxpay post的请求数据 + * @return API返回数据 + * @throws Exception + */ + public Map closeOrder(Map reqData) throws Exception { + return this.closeOrder(reqData, config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs()); + } + + + /** + * 作用:关闭订单
+ * 场景:公共号支付、扫码支付、APP支付 + * @param reqData 向wxpay post的请求数据 + * @param connectTimeoutMs 连接超时时间,单位是毫秒 + * @param readTimeoutMs 读超时时间,单位是毫秒 + * @return API返回数据 + * @throws Exception + */ + public Map closeOrder(Map reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception { + String url; + if (this.useSandbox) { + url = WXPayConstants.SANDBOX_CLOSEORDER_URL_SUFFIX; + } + else { + url = WXPayConstants.CLOSEORDER_URL_SUFFIX; + } + String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs); + return this.processResponseXml(respXml); + } + + + /** + * 作用:申请退款
+ * 场景:刷卡支付、公共号支付、扫码支付、APP支付 + * @param reqData 向wxpay post的请求数据 + * @return API返回数据 + * @throws Exception + */ + public Map refund(Map reqData) throws Exception { + return this.refund(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs()); + } + + + /** + * 作用:申请退款
+ * 场景:刷卡支付、公共号支付、扫码支付、APP支付
+ * 其他:需要证书 + * @param reqData 向wxpay post的请求数据 + * @param connectTimeoutMs 连接超时时间,单位是毫秒 + * @param readTimeoutMs 读超时时间,单位是毫秒 + * @return API返回数据 + * @throws Exception + */ + public Map refund(Map reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception { + String url; + if (this.useSandbox) { + url = WXPayConstants.SANDBOX_REFUND_URL_SUFFIX; + } + else { + url = WXPayConstants.REFUND_URL_SUFFIX; + } + String respXml = this.requestWithCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs); + return this.processResponseXml(respXml); + } + + + /** + * 作用:退款查询
+ * 场景:刷卡支付、公共号支付、扫码支付、APP支付 + * @param reqData 向wxpay post的请求数据 + * @return API返回数据 + * @throws Exception + */ + public Map refundQuery(Map reqData) throws Exception { + return this.refundQuery(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs()); + } + + + /** + * 作用:退款查询
+ * 场景:刷卡支付、公共号支付、扫码支付、APP支付 + * @param reqData 向wxpay post的请求数据 + * @param connectTimeoutMs 连接超时时间,单位是毫秒 + * @param readTimeoutMs 读超时时间,单位是毫秒 + * @return API返回数据 + * @throws Exception + */ + public Map refundQuery(Map reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception { + String url; + if (this.useSandbox) { + url = WXPayConstants.SANDBOX_REFUNDQUERY_URL_SUFFIX; + } + else { + url = WXPayConstants.REFUNDQUERY_URL_SUFFIX; + } + String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs); + return this.processResponseXml(respXml); + } + + + /** + * 作用:对账单下载(成功时返回对账单数据,失败时返回XML格式数据)
+ * 场景:刷卡支付、公共号支付、扫码支付、APP支付 + * @param reqData 向wxpay post的请求数据 + * @return API返回数据 + * @throws Exception + */ + public Map downloadBill(Map reqData) throws Exception { + return this.downloadBill(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs()); + } + + + /** + * 作用:对账单下载
+ * 场景:刷卡支付、公共号支付、扫码支付、APP支付
+ * 其他:无论是否成功都返回Map。若成功,返回的Map中含有return_code、return_msg、data, + * 其中return_code为`SUCCESS`,data为对账单数据。 + * @param reqData 向wxpay post的请求数据 + * @param connectTimeoutMs 连接超时时间,单位是毫秒 + * @param readTimeoutMs 读超时时间,单位是毫秒 + * @return 经过封装的API返回数据 + * @throws Exception + */ + public Map downloadBill(Map reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception { + String url; + if (this.useSandbox) { + url = WXPayConstants.SANDBOX_DOWNLOADBILL_URL_SUFFIX; + } + else { + url = WXPayConstants.DOWNLOADBILL_URL_SUFFIX; + } + String respStr = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs).trim(); + Map ret; + // 出现错误,返回XML数据 + if (respStr.indexOf("<") == 0) { + ret = WXPayUtil.xmlToMap(respStr); + } + else { + // 正常返回csv数据 + ret = new HashMap(); + ret.put("return_code", WXPayConstants.SUCCESS); + ret.put("return_msg", "ok"); + ret.put("data", respStr); + } + return ret; + } + + + /** + * 作用:交易保障
+ * 场景:刷卡支付、公共号支付、扫码支付、APP支付 + * @param reqData 向wxpay post的请求数据 + * @return API返回数据 + * @throws Exception + */ + public Map report(Map reqData) throws Exception { + return this.report(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs()); + } + + + /** + * 作用:交易保障
+ * 场景:刷卡支付、公共号支付、扫码支付、APP支付 + * @param reqData 向wxpay post的请求数据 + * @param connectTimeoutMs 连接超时时间,单位是毫秒 + * @param readTimeoutMs 读超时时间,单位是毫秒 + * @return API返回数据 + * @throws Exception + */ + public Map report(Map reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception { + String url; + if (this.useSandbox) { + url = WXPayConstants.SANDBOX_REPORT_URL_SUFFIX; + } + else { + url = WXPayConstants.REPORT_URL_SUFFIX; + } + String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs); + return WXPayUtil.xmlToMap(respXml); + } + + + /** + * 作用:转换短链接
+ * 场景:刷卡支付、扫码支付 + * @param reqData 向wxpay post的请求数据 + * @return API返回数据 + * @throws Exception + */ + public Map shortUrl(Map reqData) throws Exception { + return this.shortUrl(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs()); + } + + + /** + * 作用:转换短链接
+ * 场景:刷卡支付、扫码支付 + * @param reqData 向wxpay post的请求数据 + * @return API返回数据 + * @throws Exception + */ + public Map shortUrl(Map reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception { + String url; + if (this.useSandbox) { + url = WXPayConstants.SANDBOX_SHORTURL_URL_SUFFIX; + } + else { + url = WXPayConstants.SHORTURL_URL_SUFFIX; + } + String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs); + return this.processResponseXml(respXml); + } + + + /** + * 作用:授权码查询OPENID接口
+ * 场景:刷卡支付 + * @param reqData 向wxpay post的请求数据 + * @return API返回数据 + * @throws Exception + */ + public Map authCodeToOpenid(Map reqData) throws Exception { + return this.authCodeToOpenid(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs()); + } + + + /** + * 作用:授权码查询OPENID接口
+ * 场景:刷卡支付 + * @param reqData 向wxpay post的请求数据 + * @param connectTimeoutMs 连接超时时间,单位是毫秒 + * @param readTimeoutMs 读超时时间,单位是毫秒 + * @return API返回数据 + * @throws Exception + */ + public Map authCodeToOpenid(Map reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception { + String url; + if (this.useSandbox) { + url = WXPayConstants.SANDBOX_AUTHCODETOOPENID_URL_SUFFIX; + } + else { + url = WXPayConstants.AUTHCODETOOPENID_URL_SUFFIX; + } + String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs); + return this.processResponseXml(respXml); + } + + +} // end class diff --git a/payment-base-app/src/main/java/com/base/springcloud/wxsdk/WXPayConfig.java b/payment-base-app/src/main/java/com/base/springcloud/wxsdk/WXPayConfig.java new file mode 100644 index 0000000..47e3427 --- /dev/null +++ b/payment-base-app/src/main/java/com/base/springcloud/wxsdk/WXPayConfig.java @@ -0,0 +1,103 @@ +package com.base.springcloud.wxsdk; + +import java.io.InputStream; + +public abstract class WXPayConfig { + + + + /** + * 获取 App ID + * + * @return App ID + */ + abstract String getAppID(); + + + /** + * 获取 Mch ID + * + * @return Mch ID + */ + abstract String getMchID(); + + + /** + * 获取 API 密钥 + * + * @return API密钥 + */ + abstract String getKey(); + + + /** + * 获取商户证书内容 + * + * @return 商户证书内容 + */ + abstract InputStream getCertStream(); + + /** + * HTTP(S) 连接超时时间,单位毫秒 + * + * @return + */ + public int getHttpConnectTimeoutMs() { + return 6*1000; + } + + /** + * HTTP(S) 读数据超时时间,单位毫秒 + * + * @return + */ + public int getHttpReadTimeoutMs() { + return 8*1000; + } + + /** + * 获取WXPayDomain, 用于多域名容灾自动切换 + * @return + */ + abstract IWXPayDomain getWXPayDomain(); + + /** + * 是否自动上报。 + * 若要关闭自动上报,子类中实现该函数返回 false 即可。 + * + * @return + */ + public boolean shouldAutoReport() { + return true; + } + + /** + * 进行健康上报的线程的数量 + * + * @return + */ + public int getReportWorkerNum() { + return 6; + } + + + /** + * 健康上报缓存消息的最大数量。会有线程去独立上报 + * 粗略计算:加入一条消息200B,10000消息占用空间 2000 KB,约为2MB,可以接受 + * + * @return + */ + public int getReportQueueMaxSize() { + return 10000; + } + + /** + * 批量上报,一次最多上报多个数据 + * + * @return + */ + public int getReportBatchSize() { + return 10; + } + +} diff --git a/payment-base-app/src/main/java/com/base/springcloud/wxsdk/WXPayConstants.java b/payment-base-app/src/main/java/com/base/springcloud/wxsdk/WXPayConstants.java new file mode 100644 index 0000000..e725363 --- /dev/null +++ b/payment-base-app/src/main/java/com/base/springcloud/wxsdk/WXPayConstants.java @@ -0,0 +1,59 @@ +package com.base.springcloud.wxsdk; + +import org.apache.http.client.HttpClient; + +/** + * 常量 + */ +public class WXPayConstants { + + public enum SignType { + MD5, HMACSHA256 + } + + public static final String DOMAIN_API = "api.mch.weixin.qq.com"; + public static final String DOMAIN_API2 = "api2.mch.weixin.qq.com"; + public static final String DOMAIN_APIHK = "apihk.mch.weixin.qq.com"; + public static final String DOMAIN_APIUS = "apius.mch.weixin.qq.com"; + + + public static final String FAIL = "FAIL"; + public static final String SUCCESS = "SUCCESS"; + public static final String HMACSHA256 = "HMAC-SHA256"; + public static final String MD5 = "MD5"; + + public static final String FIELD_SIGN = "sign"; + public static final String FIELD_SIGN_TYPE = "sign_type"; + + public static final String WXPAYSDK_VERSION = "WXPaySDK/3.0.9"; + public static final String USER_AGENT = WXPAYSDK_VERSION + + " (" + System.getProperty("os.arch") + " " + System.getProperty("os.name") + " " + System.getProperty("os.version") + + ") Java/" + System.getProperty("java.version") + " HttpClient/" + HttpClient.class.getPackage().getImplementationVersion(); + + public static final String MICROPAY_URL_SUFFIX = "/pay/micropay"; + public static final String UNIFIEDORDER_URL_SUFFIX = "/pay/unifiedorder"; + public static final String ORDERQUERY_URL_SUFFIX = "/pay/orderquery"; + public static final String REVERSE_URL_SUFFIX = "/secapi/pay/reverse"; + public static final String CLOSEORDER_URL_SUFFIX = "/pay/closeorder"; + public static final String REFUND_URL_SUFFIX = "/secapi/pay/refund"; + public static final String REFUNDQUERY_URL_SUFFIX = "/pay/refundquery"; + public static final String DOWNLOADBILL_URL_SUFFIX = "/pay/downloadbill"; + public static final String REPORT_URL_SUFFIX = "/payitil/report"; + public static final String SHORTURL_URL_SUFFIX = "/tools/shorturl"; + public static final String AUTHCODETOOPENID_URL_SUFFIX = "/tools/authcodetoopenid"; + + // sandbox + public static final String SANDBOX_MICROPAY_URL_SUFFIX = "/sandboxnew/pay/micropay"; + public static final String SANDBOX_UNIFIEDORDER_URL_SUFFIX = "/sandboxnew/pay/unifiedorder"; + public static final String SANDBOX_ORDERQUERY_URL_SUFFIX = "/sandboxnew/pay/orderquery"; + public static final String SANDBOX_REVERSE_URL_SUFFIX = "/sandboxnew/secapi/pay/reverse"; + public static final String SANDBOX_CLOSEORDER_URL_SUFFIX = "/sandboxnew/pay/closeorder"; + public static final String SANDBOX_REFUND_URL_SUFFIX = "/sandboxnew/secapi/pay/refund"; + public static final String SANDBOX_REFUNDQUERY_URL_SUFFIX = "/sandboxnew/pay/refundquery"; + public static final String SANDBOX_DOWNLOADBILL_URL_SUFFIX = "/sandboxnew/pay/downloadbill"; + public static final String SANDBOX_REPORT_URL_SUFFIX = "/sandboxnew/payitil/report"; + public static final String SANDBOX_SHORTURL_URL_SUFFIX = "/sandboxnew/tools/shorturl"; + public static final String SANDBOX_AUTHCODETOOPENID_URL_SUFFIX = "/sandboxnew/tools/authcodetoopenid"; + +} + diff --git a/payment-base-app/src/main/java/com/base/springcloud/wxsdk/WXPayReport.java b/payment-base-app/src/main/java/com/base/springcloud/wxsdk/WXPayReport.java new file mode 100644 index 0000000..13067d0 --- /dev/null +++ b/payment-base-app/src/main/java/com/base/springcloud/wxsdk/WXPayReport.java @@ -0,0 +1,265 @@ +package com.base.springcloud.wxsdk; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.config.RegistryBuilder; +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.entity.StringEntity; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.conn.BasicHttpClientConnectionManager; +import org.apache.http.util.EntityUtils; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; + +/** + * 交易保障 + */ +public class WXPayReport { + + public static class ReportInfo { + + /** + * 布尔变量使用int。0为false, 1为true。 + */ + + // 基本信息 + private String version = "v1"; + private String sdk = WXPayConstants.WXPAYSDK_VERSION; + private String uuid; // 交易的标识 + private long timestamp; // 上报时的时间戳,单位秒 + private long elapsedTimeMillis; // 耗时,单位 毫秒 + + // 针对主域名 + private String firstDomain; // 第1次请求的域名 + private boolean primaryDomain; //是否主域名 + private int firstConnectTimeoutMillis; // 第1次请求设置的连接超时时间,单位 毫秒 + private int firstReadTimeoutMillis; // 第1次请求设置的读写超时时间,单位 毫秒 + private int firstHasDnsError; // 第1次请求是否出现dns问题 + private int firstHasConnectTimeout; // 第1次请求是否出现连接超时 + private int firstHasReadTimeout; // 第1次请求是否出现连接超时 + + public ReportInfo(String uuid, long timestamp, long elapsedTimeMillis, String firstDomain, boolean primaryDomain, int firstConnectTimeoutMillis, int firstReadTimeoutMillis, boolean firstHasDnsError, boolean firstHasConnectTimeout, boolean firstHasReadTimeout) { + this.uuid = uuid; + this.timestamp = timestamp; + this.elapsedTimeMillis = elapsedTimeMillis; + this.firstDomain = firstDomain; + this.primaryDomain = primaryDomain; + this.firstConnectTimeoutMillis = firstConnectTimeoutMillis; + this.firstReadTimeoutMillis = firstReadTimeoutMillis; + this.firstHasDnsError = firstHasDnsError?1:0; + this.firstHasConnectTimeout = firstHasConnectTimeout?1:0; + this.firstHasReadTimeout = firstHasReadTimeout?1:0; + } + + @Override + public String toString() { + return "ReportInfo{" + + "version='" + version + '\'' + + ", sdk='" + sdk + '\'' + + ", uuid='" + uuid + '\'' + + ", timestamp=" + timestamp + + ", elapsedTimeMillis=" + elapsedTimeMillis + + ", firstDomain='" + firstDomain + '\'' + + ", primaryDomain=" + primaryDomain + + ", firstConnectTimeoutMillis=" + firstConnectTimeoutMillis + + ", firstReadTimeoutMillis=" + firstReadTimeoutMillis + + ", firstHasDnsError=" + firstHasDnsError + + ", firstHasConnectTimeout=" + firstHasConnectTimeout + + ", firstHasReadTimeout=" + firstHasReadTimeout + + '}'; + } + + /** + * 转换成 csv 格式 + * + * @return + */ + public String toLineString(String key) { + String separator = ","; + Object[] objects = new Object[] { + version, sdk, uuid, timestamp, elapsedTimeMillis, + firstDomain, primaryDomain, firstConnectTimeoutMillis, firstReadTimeoutMillis, + firstHasDnsError, firstHasConnectTimeout, firstHasReadTimeout + }; + StringBuffer sb = new StringBuffer(); + for(Object obj: objects) { + sb.append(obj).append(separator); + } + try { + String sign = WXPayUtil.HMACSHA256(sb.toString(), key); + sb.append(sign); + return sb.toString(); + } + catch (Exception ex) { + return null; + } + + } + + } + + private static final String REPORT_URL = "http://report.mch.weixin.qq.com/wxpay/report/default"; + // private static final String REPORT_URL = "http://127.0.0.1:5000/test"; + + + private static final int DEFAULT_CONNECT_TIMEOUT_MS = 6*1000; + private static final int DEFAULT_READ_TIMEOUT_MS = 8*1000; + + private LinkedBlockingQueue reportMsgQueue = null; + private WXPayConfig config; + private ExecutorService executorService; + + private volatile static WXPayReport INSTANCE; + + private WXPayReport(final WXPayConfig config) { + this.config = config; + reportMsgQueue = new LinkedBlockingQueue(config.getReportQueueMaxSize()); + + // 添加处理线程 + executorService = Executors.newFixedThreadPool(config.getReportWorkerNum(), new ThreadFactory() { + public Thread newThread(Runnable r) { + Thread t = Executors.defaultThreadFactory().newThread(r); + t.setDaemon(true); + return t; + } + }); + + if (config.shouldAutoReport()) { + WXPayUtil.getLogger().info("report worker num: {}", config.getReportWorkerNum()); + for (int i = 0; i < config.getReportWorkerNum(); ++i) { + executorService.execute(new Runnable() { + public void run() { + while (true) { + // 先用 take 获取数据 + try { + StringBuffer sb = new StringBuffer(); + String firstMsg = reportMsgQueue.take(); + WXPayUtil.getLogger().info("get first report msg: {}", firstMsg); + String msg = null; + sb.append(firstMsg); //会阻塞至有消息 + int remainNum = config.getReportBatchSize() - 1; + for (int j=0; jcreate() + .register("http", PlainConnectionSocketFactory.getSocketFactory()) + .register("https", SSLConnectionSocketFactory.getSocketFactory()) + .build(), + null, + null, + null + ); + HttpClient httpClient = HttpClientBuilder.create() + .setConnectionManager(connManager) + .build(); + + HttpPost httpPost = new HttpPost(REPORT_URL); + + RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(readTimeoutMs).setConnectTimeout(connectTimeoutMs).build(); + httpPost.setConfig(requestConfig); + + StringEntity postEntity = new StringEntity(data, "UTF-8"); + httpPost.addHeader("Content-Type", "text/xml"); + httpPost.addHeader("User-Agent", WXPayConstants.USER_AGENT); + httpPost.setEntity(postEntity); + + HttpResponse httpResponse = httpClient.execute(httpPost); + HttpEntity httpEntity = httpResponse.getEntity(); + return EntityUtils.toString(httpEntity, "UTF-8"); + } + +} diff --git a/payment-base-app/src/main/java/com/base/springcloud/wxsdk/WXPayRequest.java b/payment-base-app/src/main/java/com/base/springcloud/wxsdk/WXPayRequest.java new file mode 100644 index 0000000..cad3463 --- /dev/null +++ b/payment-base-app/src/main/java/com/base/springcloud/wxsdk/WXPayRequest.java @@ -0,0 +1,257 @@ +package com.base.springcloud.wxsdk; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.ConnectTimeoutException; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.conn.socket.PlainConnectionSocketFactory; +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.HttpClientBuilder; +import org.apache.http.impl.conn.BasicHttpClientConnectionManager; +import org.apache.http.util.EntityUtils; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import java.io.InputStream; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; +import java.security.KeyStore; +import java.security.SecureRandom; +import static com.base.springcloud.wxsdk.WXPayConstants.USER_AGENT; + +public class WXPayRequest { + private WXPayConfig config; + public WXPayRequest(WXPayConfig config) throws Exception{ + + this.config = config; + } + + /** + * 请求,只请求一次,不做重试 + * @param domain + * @param urlSuffix + * @param uuid + * @param data + * @param connectTimeoutMs + * @param readTimeoutMs + * @param useCert 是否使用证书,针对退款、撤销等操作 + * @return + * @throws Exception + */ + private String requestOnce(final String domain, String urlSuffix, String uuid, String data, int connectTimeoutMs, int readTimeoutMs, boolean useCert) throws Exception { + BasicHttpClientConnectionManager connManager; + if (useCert) { + // 证书 + char[] password = config.getMchID().toCharArray(); + InputStream certStream = config.getCertStream(); + KeyStore ks = KeyStore.getInstance("PKCS12"); + ks.load(certStream, password); + + // 实例化密钥库 & 初始化密钥工厂 + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(ks, password); + + // 创建 SSLContext + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(kmf.getKeyManagers(), null, new SecureRandom()); + + SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory( + sslContext, + new String[]{"TLSv1"}, + null, + new DefaultHostnameVerifier()); + + connManager = new BasicHttpClientConnectionManager( + RegistryBuilder.create() + .register("http", PlainConnectionSocketFactory.getSocketFactory()) + .register("https", sslConnectionSocketFactory) + .build(), + null, + null, + null + ); + } + else { + connManager = new BasicHttpClientConnectionManager( + RegistryBuilder.create() + .register("http", PlainConnectionSocketFactory.getSocketFactory()) + .register("https", SSLConnectionSocketFactory.getSocketFactory()) + .build(), + null, + null, + null + ); + } + + HttpClient httpClient = HttpClientBuilder.create() + .setConnectionManager(connManager) + .build(); + + String url = "https://" + domain + urlSuffix; + HttpPost httpPost = new HttpPost(url); + + RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(readTimeoutMs).setConnectTimeout(connectTimeoutMs).build(); + httpPost.setConfig(requestConfig); + + StringEntity postEntity = new StringEntity(data, "UTF-8"); + httpPost.addHeader("Content-Type", "text/xml"); + httpPost.addHeader("User-Agent", USER_AGENT + " " + config.getMchID()); + httpPost.setEntity(postEntity); + + HttpResponse httpResponse = httpClient.execute(httpPost); + HttpEntity httpEntity = httpResponse.getEntity(); + return EntityUtils.toString(httpEntity, "UTF-8"); + + } + + + private String request(String urlSuffix, String uuid, String data, int connectTimeoutMs, int readTimeoutMs, boolean useCert, boolean autoReport) throws Exception { + Exception exception = null; + long elapsedTimeMillis = 0; + long startTimestampMs = WXPayUtil.getCurrentTimestampMs(); + boolean firstHasDnsErr = false; + boolean firstHasConnectTimeout = false; + boolean firstHasReadTimeout = false; + IWXPayDomain.DomainInfo domainInfo = config.getWXPayDomain().getDomain(config); + if(domainInfo == null){ + throw new Exception("WXPayConfig.getWXPayDomain().getDomain() is empty or null"); + } + try { + String result = requestOnce(domainInfo.domain, urlSuffix, uuid, data, connectTimeoutMs, readTimeoutMs, useCert); + elapsedTimeMillis = WXPayUtil.getCurrentTimestampMs()-startTimestampMs; + config.getWXPayDomain().report(domainInfo.domain, elapsedTimeMillis, null); + WXPayReport.getInstance(config).report( + uuid, + elapsedTimeMillis, + domainInfo.domain, + domainInfo.primaryDomain, + connectTimeoutMs, + readTimeoutMs, + firstHasDnsErr, + firstHasConnectTimeout, + firstHasReadTimeout); + return result; + } + catch (UnknownHostException ex) { // dns 解析错误,或域名不存在 + exception = ex; + firstHasDnsErr = true; + elapsedTimeMillis = WXPayUtil.getCurrentTimestampMs()-startTimestampMs; + WXPayUtil.getLogger().warn("UnknownHostException for domainInfo {}", domainInfo); + WXPayReport.getInstance(config).report( + uuid, + elapsedTimeMillis, + domainInfo.domain, + domainInfo.primaryDomain, + connectTimeoutMs, + readTimeoutMs, + firstHasDnsErr, + firstHasConnectTimeout, + firstHasReadTimeout + ); + } + catch (ConnectTimeoutException ex) { + exception = ex; + firstHasConnectTimeout = true; + elapsedTimeMillis = WXPayUtil.getCurrentTimestampMs()-startTimestampMs; + WXPayUtil.getLogger().warn("connect timeout happened for domainInfo {}", domainInfo); + WXPayReport.getInstance(config).report( + uuid, + elapsedTimeMillis, + domainInfo.domain, + domainInfo.primaryDomain, + connectTimeoutMs, + readTimeoutMs, + firstHasDnsErr, + firstHasConnectTimeout, + firstHasReadTimeout + ); + } + catch (SocketTimeoutException ex) { + exception = ex; + firstHasReadTimeout = true; + elapsedTimeMillis = WXPayUtil.getCurrentTimestampMs()-startTimestampMs; + WXPayUtil.getLogger().warn("timeout happened for domainInfo {}", domainInfo); + WXPayReport.getInstance(config).report( + uuid, + elapsedTimeMillis, + domainInfo.domain, + domainInfo.primaryDomain, + connectTimeoutMs, + readTimeoutMs, + firstHasDnsErr, + firstHasConnectTimeout, + firstHasReadTimeout); + } + catch (Exception ex) { + exception = ex; + elapsedTimeMillis = WXPayUtil.getCurrentTimestampMs()-startTimestampMs; + WXPayReport.getInstance(config).report( + uuid, + elapsedTimeMillis, + domainInfo.domain, + domainInfo.primaryDomain, + connectTimeoutMs, + readTimeoutMs, + firstHasDnsErr, + firstHasConnectTimeout, + firstHasReadTimeout); + } + config.getWXPayDomain().report(domainInfo.domain, elapsedTimeMillis, exception); + throw exception; + } + + + /** + * 可重试的,非双向认证的请求 + * @param urlSuffix + * @param uuid + * @param data + * @return + */ + public String requestWithoutCert(String urlSuffix, String uuid, String data, boolean autoReport) throws Exception { + return this.request(urlSuffix, uuid, data, config.getHttpConnectTimeoutMs(), config.getHttpReadTimeoutMs(), false, autoReport); + } + + /** + * 可重试的,非双向认证的请求 + * @param urlSuffix + * @param uuid + * @param data + * @param connectTimeoutMs + * @param readTimeoutMs + * @return + */ + public String requestWithoutCert(String urlSuffix, String uuid, String data, int connectTimeoutMs, int readTimeoutMs, boolean autoReport) throws Exception { + return this.request(urlSuffix, uuid, data, connectTimeoutMs, readTimeoutMs, false, autoReport); + } + + /** + * 可重试的,双向认证的请求 + * @param urlSuffix + * @param uuid + * @param data + * @return + */ + public String requestWithCert(String urlSuffix, String uuid, String data, boolean autoReport) throws Exception { + return this.request(urlSuffix, uuid, data, config.getHttpConnectTimeoutMs(), config.getHttpReadTimeoutMs(), true, autoReport); + } + + /** + * 可重试的,双向认证的请求 + * @param urlSuffix + * @param uuid + * @param data + * @param connectTimeoutMs + * @param readTimeoutMs + * @return + */ + public String requestWithCert(String urlSuffix, String uuid, String data, int connectTimeoutMs, int readTimeoutMs, boolean autoReport) throws Exception { + return this.request(urlSuffix, uuid, data, connectTimeoutMs, readTimeoutMs, true, autoReport); + } +} diff --git a/payment-base-app/src/main/java/com/base/springcloud/wxsdk/WXPayUtil.java b/payment-base-app/src/main/java/com/base/springcloud/wxsdk/WXPayUtil.java new file mode 100644 index 0000000..e43d756 --- /dev/null +++ b/payment-base-app/src/main/java/com/base/springcloud/wxsdk/WXPayUtil.java @@ -0,0 +1,298 @@ +package com.base.springcloud.wxsdk; + +import com.base.springcloud.wxsdk.WXPayConstants.SignType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.StringWriter; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.util.*; + + +public class WXPayUtil { + + private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + private static final Random RANDOM = new SecureRandom(); + + /** + * XML格式字符串转换为Map + * + * @param strXML XML字符串 + * @return XML数据转换后的Map + * @throws Exception + */ + public static Map xmlToMap(String strXML) throws Exception { + try { + Map data = new HashMap(); + DocumentBuilder documentBuilder = WXPayXmlUtil.newDocumentBuilder(); + InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8")); + org.w3c.dom.Document doc = documentBuilder.parse(stream); + doc.getDocumentElement().normalize(); + NodeList nodeList = doc.getDocumentElement().getChildNodes(); + for (int idx = 0; idx < nodeList.getLength(); ++idx) { + Node node = nodeList.item(idx); + if (node.getNodeType() == Node.ELEMENT_NODE) { + org.w3c.dom.Element element = (org.w3c.dom.Element) node; + data.put(element.getNodeName(), element.getTextContent()); + } + } + try { + stream.close(); + } catch (Exception ex) { + // do nothing + } + return data; + } catch (Exception ex) { + WXPayUtil.getLogger().warn("Invalid XML, can not convert to map. Error message: {}. XML content: {}", ex.getMessage(), strXML); + throw ex; + } + + } + + /** + * 将Map转换为XML格式的字符串 + * + * @param data Map类型数据 + * @return XML格式的字符串 + * @throws Exception + */ + public static String mapToXml(Map data) throws Exception { + org.w3c.dom.Document document = WXPayXmlUtil.newDocument(); + org.w3c.dom.Element root = document.createElement("xml"); + document.appendChild(root); + for (String key: data.keySet()) { + String value = data.get(key); + if (value == null) { + value = ""; + } + value = value.trim(); + org.w3c.dom.Element filed = document.createElement(key); + filed.appendChild(document.createTextNode(value)); + root.appendChild(filed); + } + TransformerFactory tf = TransformerFactory.newInstance(); + Transformer transformer = tf.newTransformer(); + DOMSource source = new DOMSource(document); + transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + StringWriter writer = new StringWriter(); + StreamResult result = new StreamResult(writer); + transformer.transform(source, result); + String output = writer.getBuffer().toString(); //.replaceAll("\n|\r", ""); + try { + writer.close(); + } + catch (Exception ex) { + } + return output; + } + + + /** + * 生成带有 sign 的 XML 格式字符串 + * + * @param data Map类型数据 + * @param key API密钥 + * @return 含有sign字段的XML + */ + public static String generateSignedXml(final Map data, String key) throws Exception { + return generateSignedXml(data, key, SignType.MD5); + } + + /** + * 生成带有 sign 的 XML 格式字符串 + * + * @param data Map类型数据 + * @param key API密钥 + * @param signType 签名类型 + * @return 含有sign字段的XML + */ + public static String generateSignedXml(final Map data, String key, SignType signType) throws Exception { + String sign = generateSignature(data, key, signType); + data.put(WXPayConstants.FIELD_SIGN, sign); + return mapToXml(data); + } + + + /** + * 判断签名是否正确 + * + * @param xmlStr XML格式数据 + * @param key API密钥 + * @return 签名是否正确 + * @throws Exception + */ + public static boolean isSignatureValid(String xmlStr, String key) throws Exception { + Map data = xmlToMap(xmlStr); + if (!data.containsKey(WXPayConstants.FIELD_SIGN) ) { + return false; + } + String sign = data.get(WXPayConstants.FIELD_SIGN); + return generateSignature(data, key).equals(sign); + } + + /** + * 判断签名是否正确,必须包含sign字段,否则返回false。使用MD5签名。 + * + * @param data Map类型数据 + * @param key API密钥 + * @return 签名是否正确 + * @throws Exception + */ + public static boolean isSignatureValid(Map data, String key) throws Exception { + return isSignatureValid(data, key, SignType.MD5); + } + + /** + * 判断签名是否正确,必须包含sign字段,否则返回false。 + * + * @param data Map类型数据 + * @param key API密钥 + * @param signType 签名方式 + * @return 签名是否正确 + * @throws Exception + */ + public static boolean isSignatureValid(Map data, String key, SignType signType) throws Exception { + if (!data.containsKey(WXPayConstants.FIELD_SIGN) ) { + return false; + } + String sign = data.get(WXPayConstants.FIELD_SIGN); + return generateSignature(data, key, signType).equals(sign); + } + + /** + * 生成签名 + * + * @param data 待签名数据 + * @param key API密钥 + * @return 签名 + */ + public static String generateSignature(final Map data, String key) throws Exception { + return generateSignature(data, key, SignType.MD5); + } + + /** + * 生成签名. 注意,若含有sign_type字段,必须和signType参数保持一致。 + * + * @param data 待签名数据 + * @param key API密钥 + * @param signType 签名方式 + * @return 签名 + */ + public static String generateSignature(final Map data, String key, SignType signType) throws Exception { + Set keySet = data.keySet(); + String[] keyArray = keySet.toArray(new String[keySet.size()]); + Arrays.sort(keyArray); + StringBuilder sb = new StringBuilder(); + for (String k : keyArray) { + if (k.equals(WXPayConstants.FIELD_SIGN)) { + continue; + } + if (data.get(k).trim().length() > 0) // 参数值为空,则不参与签名 + sb.append(k).append("=").append(data.get(k).trim()).append("&"); + } + sb.append("key=").append(key); + //String signDate = sb.toString().substring(0,sb.toString().length() -1); + //System.out.println("签名串:" + signDate); + if (SignType.MD5.equals(signType)) { + return MD5(sb.toString()).toUpperCase(); + } + else if (SignType.HMACSHA256.equals(signType)) { + return HMACSHA256(sb.toString(), key); + } + else { + throw new Exception(String.format("Invalid sign_type: %s", signType)); + } + } + + + /** + * 获取随机字符串 Nonce Str + * + * @return String 随机字符串 + */ + public static String generateNonceStr() { + char[] nonceChars = new char[32]; + for (int index = 0; index < nonceChars.length; ++index) { + nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length())); + } + return new String(nonceChars); + } + + + /** + * 生成 MD5 + * + * @param data 待处理数据 + * @return MD5结果 + */ + public static String MD5(String data) throws Exception { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] array = md.digest(data.getBytes("UTF-8")); + StringBuilder sb = new StringBuilder(); + for (byte item : array) { + sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3)); + } + return sb.toString().toUpperCase(); + } + + /** + * 生成 HMACSHA256 + * @param data 待处理数据 + * @param key 密钥 + * @return 加密结果 + * @throws Exception + */ + public static String HMACSHA256(String data, String key) throws Exception { + System.out.println("签名串:"+data); + Mac sha256_HMAC = Mac.getInstance("HmacSHA256"); + SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256"); + sha256_HMAC.init(secret_key); + byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8")); + StringBuilder sb = new StringBuilder(); + for (byte item : array) { + sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3)); + } + return sb.toString().toUpperCase(); + } + + /** + * 日志 + * @return + */ + public static Logger getLogger() { + Logger logger = LoggerFactory.getLogger("wxpay java sdk"); + return logger; + } + + /** + * 获取当前时间戳,单位秒 + * @return + */ + public static long getCurrentTimestamp() { + return System.currentTimeMillis()/1000; + } + + /** + * 获取当前时间戳,单位毫秒 + * @return + */ + public static long getCurrentTimestampMs() { + return System.currentTimeMillis(); + } + +} diff --git a/payment-base-app/src/main/java/com/base/springcloud/wxsdk/WXPayXmlUtil.java b/payment-base-app/src/main/java/com/base/springcloud/wxsdk/WXPayXmlUtil.java new file mode 100644 index 0000000..9b3e8dd --- /dev/null +++ b/payment-base-app/src/main/java/com/base/springcloud/wxsdk/WXPayXmlUtil.java @@ -0,0 +1,30 @@ +package com.base.springcloud.wxsdk; + +import org.w3c.dom.Document; + +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +/** + * 2018/7/3 + */ +public final class WXPayXmlUtil { + public static DocumentBuilder newDocumentBuilder() throws ParserConfigurationException { + DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + documentBuilderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + documentBuilderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false); + documentBuilderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + documentBuilderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); + documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + documentBuilderFactory.setXIncludeAware(false); + documentBuilderFactory.setExpandEntityReferences(false); + + return documentBuilderFactory.newDocumentBuilder(); + } + + public static Document newDocument() throws ParserConfigurationException { + return newDocumentBuilder().newDocument(); + } +} diff --git a/payment-base-app/src/main/java/com/base/springcloud/util/WxpayUtils.java b/payment-base-app/src/main/java/com/base/springcloud/wxsdk/WxpayUtils.java similarity index 97% rename from payment-base-app/src/main/java/com/base/springcloud/util/WxpayUtils.java rename to payment-base-app/src/main/java/com/base/springcloud/wxsdk/WxpayUtils.java index 8d476af..9281578 100644 --- a/payment-base-app/src/main/java/com/base/springcloud/util/WxpayUtils.java +++ b/payment-base-app/src/main/java/com/base/springcloud/wxsdk/WxpayUtils.java @@ -1,10 +1,11 @@ -package com.base.springcloud.util; +package com.base.springcloud.wxsdk; import com.alibaba.fastjson.JSON; import com.base.springcloud.constant.PaymentConstant; import com.base.springcloud.entity.CallbackBodyVo; import com.base.springcloud.entity.WxpayRefundVo; import com.base.springcloud.entity.WxpayTradeVo; +import com.base.springcloud.util.StringHelper; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder; @@ -100,10 +101,11 @@ public class WxpayUtils { * @param serialNo * @return */ - public static void refund(WxpayRefundVo tradeVo, String mchid, String privateKey, String certificate, String serialNo) { + public static String refund(WxpayRefundVo tradeVo, String mchid, String privateKey, String certificate, String serialNo) { // ... String responseBody = response(JSON.toJSONString(tradeVo), REFUND_URL, mchid, privateKey, certificate, serialNo); log.info("======>>> wxrefund response body: ", responseBody); + return responseBody; } /** @@ -174,7 +176,7 @@ public class WxpayUtils { X509Certificate cert = null; try { cert = (X509Certificate) certificateFactory.generateCertificate(base); - }catch (CertificateException e){ + } catch (CertificateException e) { throw e; } assert cert != null;