本文没有与申请微信支付相关的内容(申请微信支付过于复杂…..) 本文前提: 已经申请好了对应的微信支付商户号,并且已经签约了对应的支付产品

在Java中,集成微信支付,十分的简单,主要分为如下几步

1. 导入依赖

2. 编写微信支付的配置

3. 编写下单,进行测试


一、导入依赖

  1. <dependency>
  2. <groupId>com.github.wxpay</groupId>
  3. <artifactId>wxpay-sdk</artifactId>
  4. <version>0.0.3</version>
  5. </dependency>

二、编写微信支付的配置

这里,通过一个配置类,读取application.yml 中的配置,再通过 @Configuration+@Bean的方式,注入到容器中。

  1. public class WxPayScanConfig implements WXPayConfig {
  2. private PayInfoConfig payInfoConfig;
  3. private byte[] certData;
  4. /**
  5. * 使用类加载的方式,取出证书,否则,当发布打成Jar包的时候,会读取不到Jar包中的东西
  6. */
  7. public WxPayScanConfig(PayInfoConfig payInfoConfig) {
  8. this.payInfoConfig = payInfoConfig;
  9. try {
  10. InputStream certStream = this.getClass().getResourceAsStream("/apiclient_cert.p12");
  11. this.certData = new byte[certStream.available()];
  12. certStream.read(this.certData);
  13. certStream.close();
  14. } catch (IOException e) {
  15. e.printStackTrace();
  16. }
  17. }
  18. @Override
  19. public String getAppID() {
  20. return payInfoConfig.getWxpay().getAppId();
  21. }
  22. @Override
  23. public String getMchID() {
  24. return payInfoConfig.getWxpay().getMchId();
  25. }
  26. @Override
  27. public String getKey() {
  28. return payInfoConfig.getWxpay().getMchKey();
  29. }
  30. @Override
  31. public InputStream getCertStream() {
  32. return new ByteArrayInputStream(this.certData);
  33. }
  34. @Override
  35. public int getHttpConnectTimeoutMs() {
  36. return 8000;
  37. }
  38. @Override
  39. public int getHttpReadTimeoutMs() {
  40. return 100000;
  41. }
  42. }

三、编写下单

3.1 编写支付工具类 WxPayUtil

  1. @Slf4j
  2. @Component
  3. public class WxPayUtil {
  4. @Autowired
  5. private WXPay wxPay;
  6. @Autowired
  7. private PayInfoConfig payInfoConfig;
  8. @Autowired
  9. private PayCommonUtil payCommonUtil;
  10. public Map<String,String> appNotify(String body,String price){
  11. SortedMap<String, String> data = new TreeMap<>();
  12. String outTradeNo = getRandomString(32);
  13. data.put("body", body);
  14. data.put("out_trade_no", outTradeNo);
  15. data.put("total_fee", price);
  16. data.put("spbill_create_ip", "58.23.48.202");
  17. data.put("notify_url", "http://localhost:8080/");
  18. data.put("trade_type", "APP");
  19. Map<String, String> resp = null;
  20. try {
  21. resp = wxPay.unifiedOrder(data);
  22. resp.put("out_trade_no",outTradeNo);
  23. } catch (Exception e) {
  24. log.info("微信支付出现错误!" +e.getMessage());
  25. e.printStackTrace();
  26. }
  27. return resp;
  28. }
  29. public Map<String,String> refund(String outTradeNo){
  30. SortedMap<String,String> data = new TreeMap<>();
  31. data.put("appid", payInfoConfig.getWxpay().getAppId());
  32. data.put("mch_id", payInfoConfig.getWxpay().getMchId());
  33. data.put("nonce_str", getRandomString(32));
  34. data.put("out_trade_no",outTradeNo);
  35. data.put("out_refund_no", getRandomString(32));
  36. Map<String, String> orderQuery = queryOrder(outTradeNo);
  37. data.put("total_fee",orderQuery.get("total_fee"));
  38. data.put("refund_fee",orderQuery.get("total_fee"));
  39. data.put("sign", payCommonUtil.createSign("UTF-8",data));
  40. try {
  41. Map<String, String> resp = wxPay.refund(data);
  42. System.out.println(resp);
  43. return resp;
  44. } catch (Exception e) {
  45. e.printStackTrace();
  46. }
  47. return null;
  48. }
  49. public Map<String,String> queryRefund(String outTradeNo){
  50. Map<String, String> data = new HashMap<>(0);
  51. data.put("out_trade_no", outTradeNo);
  52. try {
  53. return wxPay.refundQuery(data);
  54. } catch (Exception e) {
  55. e.printStackTrace();
  56. }
  57. return null;
  58. }
  59. /**
  60. * 获取app二次签名数据
  61. * @param resp 微信统一下单之后返回的data
  62. * @return Map<String,String>
  63. */
  64. public Map<String,String> appPaySignDouble(Map<String,String> resp){
  65. log.info("response=" + resp);
  66. SortedMap<String, String> signParam = new TreeMap<>();
  67. //app_id
  68. String prepayid = resp.get("prepay_id");
  69. signParam.put("appid", resp.get("appid"));
  70. signParam.put("partnerid", resp.get("mch_id"));
  71. signParam.put("prepayid", prepayid);
  72. signParam.put("package", "Sign=WXPay");
  73. signParam.put("noncestr", resp.get("nonce_str"));
  74. //北京时间时间戳
  75. signParam.put("timestamp", System.currentTimeMillis()/1000 + "");
  76. signParam.put("paySign", payCommonUtil.createSign("UTF-8", signParam));
  77. signParam.put("outTradeNo",resp.get("out_trade_no"));
  78. return signParam;
  79. }
  80. public Map<String,String> queryOrder(String outTradeNo){
  81. Map<String, String> data = new HashMap<>(0);
  82. data.put("out_trade_no", outTradeNo);
  83. try {
  84. return wxPay.orderQuery(data);
  85. } catch (Exception e) {
  86. e.printStackTrace();
  87. }
  88. return null;
  89. }
  90. public String getRandomString(int length){
  91. //定义一个字符串(A-Z,a-z,0-9)即62位;
  92. String str="zxcvbnmlkjhgfdsaqwertyuiopQWERTYUIOPASDFGHJKLZXCVBNM1234567890";
  93. //由Random生成随机数
  94. Random random=new Random();
  95. StringBuffer sb=new StringBuffer();
  96. //长度为几就循环几次
  97. for(int i=0; i<length; ++i){
  98. //产生0-61的数字
  99. int number=random.nextInt(62);
  100. //将产生的数字通过length次承载到sb中
  101. sb.append(str.charAt(number));
  102. }
  103. //将承载的字符转换成字符串
  104. return sb.toString();
  105. }
  106. }

其中用到的工具类:

PayCommonUtil 主要作用为生成签名以及解析xml

  1. @Component
  2. public class PayCommonUtil {
  3. @Autowired
  4. private PayInfoConfig payInfoConfig;
  5. /**
  6. * 定义签名,微信根据参数字段的ASCII码值进行排序 加密签名,故使用SortMap进行参数排序
  7. * @param characterEncoding 字符编码
  8. * @param parameters 需要签名的参数
  9. * @return sign string。class
  10. */
  11. public String createSign(String characterEncoding,SortedMap<String,String> parameters){
  12. StringBuilder sb = new StringBuilder();
  13. Set<Map.Entry<String, String>> es = parameters.entrySet();
  14. for (Map.Entry<String, String> entry : es) {
  15. String k = entry.getKey();
  16. Object v = entry.getValue();
  17. if (null != v && !"".equals(v)
  18. && !"sign".equals(k) && !"key".equals(k)) {
  19. sb.append(k).append("=").append(v).append("&");
  20. }
  21. }
  22. //最后加密时添加商户密钥,由于key值放在最后,所以不用添加到SortMap里面去,单独处理,编码方式采用UTF-8
  23. sb.append("key=").append(payInfoConfig.getWxpay().getMchKey());
  24. return MD5Util.MD5Encode(sb.toString(), characterEncoding).toUpperCase();
  25. }
  26. /**
  27. * 将封装好的参数转换成Xml格式类型的字符串
  28. * @param parameters 封装好的参数
  29. * @return String xml类型的字符串
  30. */
  31. public String getRequestXml(SortedMap<String,String> parameters){
  32. StringBuilder sb = new StringBuilder();
  33. sb.append("<xml>");
  34. Set<Map.Entry<String, String>> es = parameters.entrySet();
  35. for (Map.Entry<String, String> entry : es) {
  36. String k = entry.getKey();
  37. String v = entry.getValue();
  38. if ("sign".equalsIgnoreCase(k)) {
  39. } else if ("attach".equalsIgnoreCase(k) || "body".equalsIgnoreCase(k)) {
  40. sb.append("<").append(k).append(">").append("<![CDATA[").append(v).append("]]></").append(k).append(">");
  41. } else {
  42. sb.append("<").append(k).append(">").append(v).append("</").append(k).append(">");
  43. }
  44. }
  45. sb.append("<" + "sign" + ">" + "<![CDATA[").append(parameters.get("sign")).append("]]></").append("sign").append(">");
  46. sb.append("</xml>");
  47. return sb.toString();
  48. }
  49. /**
  50. * 验证回调签名
  51. * @return Boolean, 是否正确
  52. */
  53. public boolean isTenpaySign(Map<String, String> map) {
  54. String characterEncoding="utf-8";
  55. String charset = "utf-8";
  56. String signFromAPIResponse = map.get("sign");
  57. if (signFromAPIResponse == null || "".equals(signFromAPIResponse)) {
  58. System.out.println("API返回的数据签名数据不存在,有可能被第三方篡改!!!");
  59. return false;
  60. }
  61. System.out.println("服务器回包里面的签名是:" + signFromAPIResponse);
  62. //过滤空 设置 TreeMap
  63. SortedMap<String,String> packageParams = new TreeMap<>();
  64. for (String parameter : map.keySet()) {
  65. String parameterValue = map.get(parameter);
  66. String v = "";
  67. if (null != parameterValue) {
  68. v = parameterValue.trim();
  69. }
  70. packageParams.put(parameter, v);
  71. }
  72. StringBuilder sb = new StringBuilder();
  73. Set<Map.Entry<String, String>> es = packageParams.entrySet();
  74. for (Object e1 : es) {
  75. @SuppressWarnings("unchecked")
  76. Map.Entry<String, String> entry = (Map.Entry<String, String>) e1;
  77. String k = entry.getKey();
  78. String v = entry.getValue();
  79. if (!"sign".equals(k) && null != v && !"".equals(v)) {
  80. sb.append(k).append("=").append(v).append("&");
  81. }
  82. }
  83. sb.append("key=").append(payInfoConfig.getWxpay().getMchKey());
  84. //将API返回的数据根据用签名算法进行计算新的签名,用来跟API返回的签名进行比较
  85. //算出签名
  86. String resultSign = "";
  87. String tobesign = sb.toString();
  88. if (null == charset || "".equals(charset)) {
  89. resultSign = MD5Util.MD5Encode(tobesign, characterEncoding).toUpperCase();
  90. }else{
  91. try{
  92. resultSign = MD5Util.MD5Encode(tobesign, characterEncoding).toUpperCase();
  93. }catch (Exception e) {
  94. resultSign = MD5Util.MD5Encode(tobesign, characterEncoding).toUpperCase();
  95. }
  96. }
  97. String tenpaySign = (packageParams.get("sign")).toUpperCase();
  98. return tenpaySign.equals(resultSign);
  99. }
  100. public String httpsRequest(String requestUrl, String requestMethod, String outputStr)
  101. {
  102. try
  103. {
  104. // 创建SSLContext对象,并使用我们指定的信任管理器初始化
  105. TrustManager[] tm =
  106. { null };
  107. SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
  108. sslContext.init(null, tm, new java.security.SecureRandom());
  109. // 从上述SSLContext对象中得到SSLSocketFactory对象
  110. SSLSocketFactory ssf = sslContext.getSocketFactory();
  111. URL url = new URL(requestUrl);
  112. HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
  113. conn.setDoOutput(true);
  114. conn.setDoInput(true);
  115. conn.setUseCaches(false);
  116. // 设置请求方式(GET/POST)
  117. conn.setRequestMethod(requestMethod);
  118. conn.setRequestProperty("content-type", "application/x-www-form-urlencoded");
  119. // 当outputStr不为null时向输出流写数据
  120. if (null != outputStr)
  121. {
  122. OutputStream outputStream = conn.getOutputStream();
  123. // 注意编码格式
  124. outputStream.write(outputStr.getBytes(StandardCharsets.UTF_8));
  125. outputStream.close();
  126. }
  127. // 从输入流读取返回内容
  128. InputStream inputStream = conn.getInputStream();
  129. InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
  130. BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
  131. String str = null;
  132. StringBuilder buffer = new StringBuilder();
  133. while ((str = bufferedReader.readLine()) != null)
  134. {
  135. buffer.append(str);
  136. }
  137. // 释放资源
  138. bufferedReader.close();
  139. inputStreamReader.close();
  140. inputStream.close();
  141. inputStream = null;
  142. conn.disconnect();
  143. return buffer.toString();
  144. } catch (ConnectException ce) {
  145. // log.error("连接超时:{}", ce);
  146. } catch (Exception e) {
  147. }
  148. return null;
  149. }
  150. }

MD5Util 主要作用,用于数据的加密,默认方式为MD5加密

  1. public class MD5Util {
  2. private static String byteArrayToHexString(byte b[]) {
  3. StringBuffer resultSb = new StringBuffer();
  4. for (int i = 0; i < b.length; i++)
  5. resultSb.append(byteToHexString(b[i]));
  6. return resultSb.toString();
  7. }
  8. private static String byteToHexString(byte b) {
  9. int n = b;
  10. if (n < 0)
  11. n += 256;
  12. int d1 = n / 16;
  13. int d2 = n % 16;
  14. return hexDigits[d1] + hexDigits[d2];
  15. }
  16. public static String MD5Encode(String origin, String charsetname) {
  17. String resultString = null;
  18. try {
  19. resultString = new String(origin);
  20. MessageDigest md = MessageDigest.getInstance("MD5");
  21. if (charsetname == null || "".equals(charsetname))
  22. resultString = byteArrayToHexString(md.digest(resultString
  23. .getBytes()));
  24. else
  25. resultString = byteArrayToHexString(md.digest(resultString
  26. .getBytes(charsetname)));
  27. } catch (Exception exception) {
  28. }
  29. return resultString;
  30. }
  31. private static final String hexDigits[] = { "0", "1", "2", "3", "4", "5",
  32. "6", "7", "8", "9", "a", "b", "c", "d", "e", "f" };
  33. }

3.2 编写Controller,调用封装的工具类方法

这里需要注意的是,需要进行两步操作,分别为

  • 统一下单(appNotify)
  • 二次签名(将统一下单中的一部分数据,进行二次的签名,详见 微信支付开发文档

需要注意的是,wxpay和alipay不同的是,wxpay的price,是按照分来计算的(即1=1分,输入为100,代表收取1块钱),且不能包含小数点,所以在传入值的时候,需要将Double强转成int

  1. @Autowired
  2. private WxPayUtil wxPayUtil;
  3. @GetMapping("/test")
  4. public ResponseModel test(String title, Double price) {
  5. Map<String, String> response = wxPayUtil.appNotify(title, ((int)(price * 100)) + "");
  6. Map<String, String> result = wxPayUtil.appPaySignDouble(response);
  7. return ResponseModel.success("签名成功!", result);
  8. }

二次签名之后返回的数据如下所示

  1. {
  2. "appid": "",
  3. "noncestr": "s3rziO04IYbvhcvU",
  4. "outTradeNo": "a14As0d1zqmnVWvN1FMriQmPNu3lPW5P",
  5. "package": "Sign=WXPay",
  6. "partnerid": "",
  7. "paySign": "F549A5ADE9E767C49A8432AA2F9297E8",
  8. "prepayid": "wx09165100326315c5XXXXXXXXXXXXXXXXXX",
  9. "timestamp": "1599641460"
  10. },

如果,二次签名返回的数据中,都是不为null的,说明,有很大可能,是签名成功的,或者,可以通过 微信支付接口签名校验工具 来进行验证,如果验证成功,此时就靠前端朋友,来测试微信支付了,看能否成功了

如下,验证应该的sign值为 F549A5ADE9E767C49A8432AA2F9297E8,实际返回给前端的paySign也是 F549A5ADE9E767C49A8432AA2F9297E8,即代表签名成功,可以考虑发起支付了
image.png

四、APP进行支付测试

APP 端的测试,详见另一篇文章