微信小程序-微信支付退款
官方接口文档及相关附件
错误集锦
-
调用该
https://api.mch.weixin.qq.com/secapi/pay/refund
接口需要应程序安装API证书才可以,否则会提示以下错误:<head><title>400 No required SSL certificate was sent</title></head> <body bgcolor="white"> <center><h1>400 Bad Request</h1></center> <center>No required SSL certificate was sent</center> <hr><center>nginx</center> </body> </html>
根据官方文档中介绍的证书下载及安装方式,由于本人是Windows环境下进行开发,所以按照它的介绍,下载下来apiclient_cert.p12证书之后,直接双击安装即可。但安装成功之后,请求还是报错。没办法,调试代码吧。具体调试过程如下:
第一步:接受客户端发送的退款请求
// 业务处理步骤省略…… // 发起微信退款 WXPay wxPay = new WXPay(wxPayConfig); // WXPay为微信官方提供的SDK所在类,官方SDK为com.github.wxpay.sdk,链接已在上面给出,需要的童鞋可以去下载。 Map<String, String> res = wxPay.refund(redundData); // redundData为退款申请数据
第二步:调试wxPay.refund方法,发现会走到这个方法
requestWithCert
public Map<String, String> refund(Map<String, String> reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception { String url; if (this.useSandbox) { url = "/sandboxnew/secapi/pay/refund"; } else { url = "/secapi/pay/refund"; } String respXml = this.requestWithCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs); return this.processResponseXml(respXml); } public String requestWithCert(String urlSuffix, Map<String, String> reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception { String msgUUID = (String)reqData.get("nonce_str"); String reqBody = WXPayUtil.mapToXml(reqData); String resp = this.wxPayRequest.requestWithCert(urlSuffix, msgUUID, reqBody, connectTimeoutMs, readTimeoutMs, this.autoReport); return resp; }
第三步:调试
wxPayRequest.requestWithCert
方法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); }
上面方法又调用了自己的
request
方法private String request(String urlSuffix, String uuid, String data, int connectTimeoutMs, int readTimeoutMs, boolean useCert, boolean autoReport) throws Exception { Exception exception = null; long elapsedTimeMillis = 0L; long startTimestampMs = WXPayUtil.getCurrentTimestampMs(); boolean firstHasDnsErr = false; boolean firstHasConnectTimeout = false; boolean firstHasReadTimeout = false; DomainInfo domainInfo = this.config.getWXPayDomain().getDomain(this.config); if (domainInfo == null) { throw new Exception("WXPayConfig.getWXPayDomain().getDomain() is empty or null"); } else { try { String result = this.requestOnce(domainInfo.domain, urlSuffix, uuid, data, connectTimeoutMs, readTimeoutMs, useCert); elapsedTimeMillis = WXPayUtil.getCurrentTimestampMs() - startTimestampMs; this.config.getWXPayDomain().report(domainInfo.domain, elapsedTimeMillis, (Exception)null); WXPayReport.getInstance(this.config).report(uuid, elapsedTimeMillis, domainInfo.domain, domainInfo.primaryDomain, connectTimeoutMs, readTimeoutMs, firstHasDnsErr, firstHasConnectTimeout, firstHasReadTimeout); return result; } catch (UnknownHostException var18) { exception = var18; firstHasDnsErr = true; elapsedTimeMillis = WXPayUtil.getCurrentTimestampMs() - startTimestampMs; WXPayUtil.getLogger().warn("UnknownHostException for domainInfo {}", domainInfo); WXPayReport.getInstance(this.config).report(uuid, elapsedTimeMillis, domainInfo.domain, domainInfo.primaryDomain, connectTimeoutMs, readTimeoutMs, firstHasDnsErr, firstHasConnectTimeout, firstHasReadTimeout); } catch (ConnectTimeoutException var19) { exception = var19; firstHasConnectTimeout = true; elapsedTimeMillis = WXPayUtil.getCurrentTimestampMs() - startTimestampMs; WXPayUtil.getLogger().warn("connect timeout happened for domainInfo {}", domainInfo); WXPayReport.getInstance(this.config).report(uuid, elapsedTimeMillis, domainInfo.domain, domainInfo.primaryDomain, connectTimeoutMs, readTimeoutMs, firstHasDnsErr, firstHasConnectTimeout, firstHasReadTimeout); } catch (SocketTimeoutException var20) { exception = var20; firstHasReadTimeout = true; elapsedTimeMillis = WXPayUtil.getCurrentTimestampMs() - startTimestampMs; WXPayUtil.getLogger().warn("timeout happened for domainInfo {}", domainInfo); WXPayReport.getInstance(this.config).report(uuid, elapsedTimeMillis, domainInfo.domain, domainInfo.primaryDomain, connectTimeoutMs, readTimeoutMs, firstHasDnsErr, firstHasConnectTimeout, firstHasReadTimeout); } catch (Exception var21) { exception = var21; elapsedTimeMillis = WXPayUtil.getCurrentTimestampMs() - startTimestampMs; WXPayReport.getInstance(this.config).report(uuid, elapsedTimeMillis, domainInfo.domain, domainInfo.primaryDomain, connectTimeoutMs, readTimeoutMs, firstHasDnsErr, firstHasConnectTimeout, firstHasReadTimeout); } this.config.getWXPayDomain().report(domainInfo.domain, elapsedTimeMillis, (Exception)exception); throw (Exception)exception; } }
上面方法又调用了同类的
requestOnce
方法private String requestOnce(String domain, String urlSuffix, String uuid, String data, int connectTimeoutMs, int readTimeoutMs, boolean useCert) throws Exception { BasicHttpClientConnectionManager connManager; if (useCert) { char[] password = this.config.getMchID().toCharArray(); InputStream certStream = this.config.getCertStream(); KeyStore ks = KeyStore.getInstance("PKCS12"); ks.load(certStream, password); KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); kmf.init(ks, password); SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(kmf.getKeyManagers(), (TrustManager[])null, new SecureRandom()); SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext, new String[]{"TLSv1"}, (String[])null, new DefaultHostnameVerifier()); connManager = new BasicHttpClientConnectionManager(RegistryBuilder.create().register("http", PlainConnectionSocketFactory.getSocketFactory()).register("https", sslConnectionSocketFactory).build(), (HttpConnectionFactory)null, (SchemePortResolver)null, (DnsResolver)null); } else { connManager = new BasicHttpClientConnectionManager(RegistryBuilder.create().register("http", PlainConnectionSocketFactory.getSocketFactory()).register("https", SSLConnectionSocketFactory.getSocketFactory()).build(), (HttpConnectionFactory)null, (SchemePortResolver)null, (DnsResolver)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", WXPayConstants.USER_AGENT + " " + this.config.getMchID()); httpPost.setEntity(postEntity); HttpResponse httpResponse = httpClient.execute(httpPost); HttpEntity httpEntity = httpResponse.getEntity(); return EntityUtils.toString(httpEntity, "UTF-8"); }
可以发现,证书最终是通过该方法进行导入的。具体导入是由WXPayConfig类进行操作的,该类是在创建WXPay对象时传入的,一般我们都会根据自己的业务场景写一个该类的子类,由于在重写该类方法时,getCertStream方法直接返回了null,所以即使证书安装了,也会提示证书无法找到的错误。
问题解决:
-
将下载好的证书放入工程目录下,注意该证书文件应放在有访问权限控制的目录中,防止被他人下载。
-
重写WXPayConfig的getCertStream方法
private byte[] certData; @Override public InputStream getCertStream() { ByteArrayInputStream certBis = new ByteArrayInputStream(this.certData); return null; } public MyWXPayConfig() throws Exception { URI uri = this.getClass().getClassLoader().getResource("cert/apiclient_cert.p12").toURI(); String certPath =uri.getPath(); File file = new File(certPath); InputStream certStream = new FileInputStream(file); this.certData = new byte[(int) file.length()]; certStream.read(this.certData); certStream.close(); }
-