本以为没有机会接触鼎鼎大名的支付宝和微信接口(公司本身是做第三方支付的),最近由于一个售货机项目需要对接银联,支付宝和微信接口,因为我自身已经对接了银联,之后根据安排,由我对接微信的相关接口。话不多说,让我们开启踩坑之旅。
对接微信支付接口准备工作:
1.微信支付文档地址:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_1
2.微信支付签名验证地址:https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=20_1
3.下载微信支付官方demo(根据自身需求):https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=11_1
4.申请相应的账号,包括:微信支付商户号,公众号ID:appid,appsecret,key,以及业务需要有可能用到的证书文件
接下来所有的示例都用统一下单接口来展现
- 第一步:阅读文档
进入微信支付文档首页,根据我们使用微信支付的不同场景选择不同的文档来进行阅读,否则,你就有可能因为文档阅读错误导致demo跑不通而到处找错。
仔细阅读方框中的文档说明后,我们就可以进入到API列表中的统一下单接口中,准备对接此接口
请务必注意传递字段的描述,微信支付文档中有很多地方在传递参数中,如果某个必传参数触发某个条件,那就需要多传递另外的参数,务必看仔细后再用demo去进行调用。
- 第二步:修改官方demo进行测试
此处为什么说要修改官方demo,因为官方demo只提供了必要的结构和代码,相关我们自己的参数还需要自己去实现,打比方说:微信demo中有一个类:WXPayConfig,是一个抽象类,你就必须先实现里面的方法,然后自己写一个测试用例进行调用和调试。
和官方demo相比,我只增加了这两个类来分别实现相应的接口,统一下单接口中的测试和修改基本都在WXPay中完成,下面我贴上这两个类的代码,仅供参考。
WxPayConfigImpl类代码:
package com.github.wxpay.sdk;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
public class WxPayConfigImpl extends WXPayConfig{
private byte[] certData;
private static WxPayConfigImpl INSTANCE;
//公众号AppID
private String appId = "";
//商户号
private String mchId = "";
//key
private String key = "";
private WxPayConfigImpl() throws Exception{
// String certPath = "D://CERT/common/apiclient_cert.p12";
// File file = new File(certPath);
// InputStream certStream = new FileInputStream(file);
// this.certData = new byte[(int) file.length()];
// certStream.read(this.certData);
// certStream.close();
}
public static WxPayConfigImpl getInstance() throws Exception{
if (INSTANCE == null) {
synchronized (WxPayConfigImpl.class) {
if (INSTANCE == null) {
INSTANCE = new WxPayConfigImpl();
}
}
}
return INSTANCE;
}
public String getAppID() {
return this.appId;
}
public String getMchID() {
return this.mchId;
}
public String getKey() {
return this.key;
}
public InputStream getCertStream() {
ByteArrayInputStream certBis;
certBis = new ByteArrayInputStream(this.certData);
return certBis;
}
public int getHttpConnectTimeoutMs() {
return 2000;
}
public int getHttpReadTimeoutMs() {
return 10000;
}
IWXPayDomain getWXPayDomain() {
return WXPayDomainSimpleImpl.instance();
}
public String getPrimaryDomain() {
return "api.mch.weixin.qq.com";
}
public String getAlternateDomain() {
return "api2.mch.weixin.qq.com";
}
@Override
public int getReportWorkerNum() {
return 1;
}
@Override
public int getReportBatchSize() {
return 2;
}
}
WXPayDomainSimpleImp类代码:
package com.github.wxpay.sdk;
import org.apache.http.conn.ConnectTimeoutException;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Map;
/**
* Created by blaketang on 2017/6/16.
*/
public class WXPayDomainSimpleImpl implements IWXPayDomain {
private WXPayDomainSimpleImpl(){}
private static class WxpayDomainHolder{
private static IWXPayDomain holder = new WXPayDomainSimpleImpl();
}
public static IWXPayDomain instance(){
return WxpayDomainHolder.holder;
}
public synchronized void report(final String domain, long elapsedTimeMillis, final Exception ex) {
DomainStatics info = domainData.get(domain);
if(info == null){
info = new DomainStatics(domain);
domainData.put(domain, info);
}
if(ex == null){ //success
if(info.succCount >= 2){ //continue succ, clear error count
info.connectTimeoutCount = info.dnsErrorCount = info.otherErrorCount = 0;
}else{
++info.succCount;
}
}else if(ex instanceof ConnectTimeoutException){
info.succCount = info.dnsErrorCount = 0;
++info.connectTimeoutCount;
}else if(ex instanceof UnknownHostException){
info.succCount = 0;
++info.dnsErrorCount;
}else{
info.succCount = 0;
++info.otherErrorCount;
}
}
public synchronized DomainInfo getDomain(final WXPayConfig config) {
DomainStatics primaryDomain = domainData.get(WXPayConstants.DOMAIN_API);
if(primaryDomain == null ||
primaryDomain.isGood()) {
return new DomainInfo(WXPayConstants.DOMAIN_API, true);
}
long now = System.currentTimeMillis();
if(switchToAlternateDomainTime == 0){ //first switch
switchToAlternateDomainTime = now;
return new DomainInfo(WXPayConstants.DOMAIN_API2, false);
}else if(now - switchToAlternateDomainTime < MIN_SWITCH_PRIMARY_MSEC){
DomainStatics alternateDomain = domainData.get(WXPayConstants.DOMAIN_API2);
if(alternateDomain == null ||
alternateDomain.isGood() ||
alternateDomain.badCount() < primaryDomain.badCount()){
return new DomainInfo(WXPayConstants.DOMAIN_API2, false);
}else{
return new DomainInfo(WXPayConstants.DOMAIN_API, true);
}
}else{ //force switch back
switchToAlternateDomainTime = 0;
primaryDomain.resetCount();
DomainStatics alternateDomain = domainData.get(WXPayConstants.DOMAIN_API2);
if(alternateDomain != null)
alternateDomain.resetCount();
return new DomainInfo(WXPayConstants.DOMAIN_API, true);
}
}
static class DomainStatics {
final String domain;
int succCount = 0;
int connectTimeoutCount = 0;
int dnsErrorCount =0;
int otherErrorCount = 0;
DomainStatics(String domain) {
this.domain = domain;
}
void resetCount(){
succCount = connectTimeoutCount = dnsErrorCount = otherErrorCount = 0;
}
boolean isGood(){ return connectTimeoutCount <= 2 && dnsErrorCount <= 2; }
int badCount(){
return connectTimeoutCount + dnsErrorCount * 5 + otherErrorCount / 4;
}
}
private final int MIN_SWITCH_PRIMARY_MSEC = 3 * 60 * 1000; //3 minutes
private long switchToAlternateDomainTime = 0;
private Map<String, DomainStatics> domainData = new HashMap<String, DomainStatics>();
}
记下来就是展现你有没有仔细看文档的时候了,我们可以写个测试类,也可以直接写个main方法,我这里因为后续要集成到代码中,所以我直接用main方法直接运行
<xml>
<nonce_str>8SwC0RG5WpHXPZrykLxEq7chaYFrKCbd</nonce_str>
<out_trade_no>L0000000000000000026</out_trade_no>
<total_fee>10</total_fee>
<product_id>359</product_id>
<appid></appid>
<sign></sign>
<trade_type>NATIVE</trade_type>
<body>福优售货机</body>
<notify_url>http://222.186.36.87:8086/paytest/weixin/payNotify</notify_url>
<mch_id></mch_id>
<spbill_create_ip>222.186.36.87</spbill_create_ip>
<sign_type>MD5</sign_type>
</xml>
此处为使用NATIVE支付时传递的参数(此处传递的是必传参数,根据业务需要大家可以根据文档来灵活使用),这里需要给大家说一下微信的签名验证工具
一般情况下,只要正确调用了相关的接口,是不会出现签名不过的情况,但就是有一些拿到官方文档后大改特改的人,所以这个时候需要用到微信的签名验证工具,地址在博客开头。
具体用法:拿到上面我展现的xml文件(debug找到输出地方打印即可),放到下图的文本框中进行验证
最终进行签名的校验即可。
- 第三步:解决过程中遇到的bug
1.切记,必须拿到最新的商户参数和密钥,并且保证正确可用
2.拿到demo后全局浏览后,补充必要文件和参数
3.根据文档传递参数,注意文档的备注
4.参数签名为MD5,注意追踪demo默认签名类型
- 第四步:生成二维码进行支付
统一下单接口支付成功后,会返回你一串以weixin.开头的url地址,用二维码生成工具生成后,即可用微信扫一扫功能扫码付款了,注意,传递金额是以分为单位,测试时不要金额传递过多哟。
- 第五步:接收支付成功回调
public void payNotify() {
try {
InputStream inputStream ;
StringBuffer sb = new StringBuffer();
inputStream = request.getInputStream();
String s ;
BufferedReader in = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
while ((s = in.readLine()) != null){
sb.append(s);
}
in.close();
inputStream.close();
//解析xml成map
Map<String, String> m = new HashMap<String, String>();
m = XMLUtil.doXMLParse(sb.toString());
System.out.println("微信支付,支付通知数据:"+m.toString());
//过滤空 设置 TreeMap
SortedMap<Object,Object> packageParams = new TreeMap<Object,Object>();
Iterator it = m.keySet().iterator();
while (it.hasNext()) {
String parameter = (String) it.next();
String parameterValue = m.get(parameter);
String v = "";
if(null != parameterValue) {
v = parameterValue.trim();
}
packageParams.put(parameter, v);
}//判断签名是否正确
if(PayCommonUtil.isTenpaySign("UTF-8", packageParams,key)) {
//------------------------------
//处理业务开始
//------------------------------
String resXml = "";
if(VendorConstant.Weixin.SUCCESS.equals((String)packageParams.get("result_code"))){
// 这里是支付成功
//执行自己的业务逻辑
String mch_id = (String)packageParams.get("mch_id");
String openid = (String)packageParams.get("openid");
String is_subscribe = (String)packageParams.get("is_subscribe");
String out_trade_no = (String)packageParams.get("out_trade_no");
String result_code = (String)packageParams.get("result_code");
String total_fee = (String)packageParams.get("total_fee");
String transaction_id = (String)packageParams.get("transaction_id");
System.out.println("mch_id:"+mch_id);
System.out.println("openid:"+openid);
log.info("is_subscribe:"+is_subscribe);
log.info("out_trade_no:"+out_trade_no);
log.info("result_code:"+result_code);
log.info("total_fee:"+total_fee);
log.info("transaction_id:",transaction_id);
payCombineService.notifyPayResult("4",divideAmount(total_fee),out_trade_no,result_code,transaction_id);
//执行自己的业务逻辑
log.info("支付成功");
//通知微信.异步确认成功.必写.不然会一直通知后台.八次之后就认为交易失败了.
resXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>"
+ "<return_msg><![CDATA[OK]]></return_msg>" + "</xml> ";
} else {
log.info("支付失败,错误信息:" + packageParams.get("err_code"));
resXml = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>"
+ "<return_msg><![CDATA[报文为空]]></return_msg>" + "</xml> ";
}
//------------------------------
//处理业务完毕
//------------------------------
BufferedOutputStream out = new BufferedOutputStream(
response.getOutputStream());
out.write(resXml.getBytes());
out.flush();
out.close();
} else{
log.info("通知签名验证失败");
}
}catch (Exception e){
e.printStackTrace();
}
}
因为此处的回调代码已经集成到公司项目里面了,这里就只贴出相应的代码点到为止,相应的工具类大家可以到支付回调工具类下载,配套使用更佳哦。
总结:实际上,微信支付能对接全国的开发者是有其道理的,我遇到问题的时候在百度的时候碰到很多这样的语句,都说微信支付文档是大坑,确实,很多时候有很多问题都是自己不仔细,然后微信接口提示你签名错误,其实,只要认真看了微信官方文档,绝大多数问题都可以迎刃而解,很多时候都是自己不细心导致的。所以,有些时候,细心最重要。
下一波我将介绍微信退款和退款结果通知的相关内容,剧透一下下:退款结果通知又是一个小坑。