准备工作
-
调用前需在小程序微信公众平台 -功能-微信支付入口申请接入微信支付
- 微信商户号mch_id
- API密钥api_secret
-
准备一个支付界面
/payment
-
有一个可以点击的支付按钮
<button>支付订单</button>
-
能接收上个页面传进来的订单值
orderInfo
-
-
选择支付接口版本。
v3
和旧版本v2
的鉴权方式和请求参数都有很大区别,但最重要的是,截止到2020.12.31,v2
版本提供更多接口功能,包括退款等。因此下面介绍的是v2
版本的微信支付接口。 -
v2
版本的demo下载地址:SDK与DEMO下载和文档目录:业务流程 -
需要的pom依赖:
<dependency> <groupId>xpp3</groupId> <artifactId>xpp3_min</artifactId> <version>1.1.4c</version> </dependency> <dependency> <groupId>com.thoughtworks.xstream</groupId> <artifactId>xstream</artifactId> <version>1.4.10</version> </dependency>
步骤1:签名,IP等业务无关参数的生成
-
接口中包含一些与业务本身无关的参数,这些参数的生成过程是比较复杂的,很多可以引用DEMO里的方法。在后续的代码中会展示如何使用DEMO的工具类。
-
随机字符串
nonceStr
:只要是32位以内的,由数字和字母组成的随机字符串即可。 -
签名
sign
:官方的签名算法文档。- 将发送的数据中除去签名字段、参数值非空的参数名按照ASCII码从小到大字典序排序,拼接成
key1=value1&key2=value2
的字符串stringA - 在stringA的最后拼接上
&key=
+API密钥api_secret,得到stringTemp - 对stringTemp进行MD5运算,得到signMD5
- 将signMD5所有字符大写,得到最终sign值
- 将发送的数据中除去签名字段、参数值非空的参数名按照ASCII码从小到大字典序排序,拼接成
-
机器IP
spbill_create_ip
:直接人工填写是最快的,当然动态获取也可以(注意反向代理、局域网等) -
时间戳
timestamp
:注意服务器时区是否正确
步骤2:统一下单,获取预支付ID
第一步是要创建预支付交易单
-
在后端创建一个生成预支付交易单的接口,如
Res createWxOrder(Req entity)
-
entity的结构至少包含:
{ orderId:"1", openid:"2" }
-
接口内的实现,使用官方的DEMO方法会变得异常简单:
@Override public Map<String,Object> createWxOrder(WxOrderParam entity) throws Exception { String orderURL="https://api.mch.weixin.qq.com/pay/unifiedorder"; Map<String,String> order=new HashMap<>(); order.put("appid",appId); order.put("mch_id",merchantId); order.put("nonce_str",WXPayUtil.generateNonceStr()); order.put("body","xxxx-xx"); order.put("out_trade_no",entity.getOrderId()); order.put("total_fee","1"); order.put("spbill_create_ip", getLocalIp()); order.put("notify_url","nonono");//onnononono 回调地址 order.put("trade_type","JSAPI"); //调用微信支付api接口(带有签名){统一下单} String orderXml=WXPayUtil.generateSignedXml(order,apiSecret); String resXml=HttpUtil.postWithXml(orderURL,orderXml,"utf-8"); //接收微信支付接口{统一下单}的返回数据 XStream xs = new XStream(); XStream.setupDefaultSecurity(xs); xs.allowTypes(new Class[]{WxPayOrderInfoRes.class}); xs.alias("xml", WxPayOrderInfoRes.class); WxPayOrderInfoRes returnInfo = (WxPayOrderInfoRes)xs.fromXML(resXml); //二次签名 .... }
-
-
微信支付后台接收到这个预付单后,会返回一个非常重要的预支付交易会话标识
prepay_id
,用于后续接口调用,2小时内过期。 -
如果发生异常,则会返回异常码等。
步骤3:二次签名,用于wx.requestPayment鉴权
-
上面我们拿到了标识还不能急着返回,我们需要结合返回的数据再次生成签名,然后才能返回给小程序。因为wx.requestPayment需要后端提供几个重要的参数用于接口鉴权。
-
生成签名的方法与之前类似:
@Override public Map<String,Object> createWxOrder(WxOrderParam entity) throws Exception { //统一下单 //... //二次签名 Map<String,Object> payInfo = new HashMap<>(); if ("SUCCESS".equals(returnInfo.getReturn_code()) && "SUCCESS".equals(returnInfo.getResult_code())) { long time = System.currentTimeMillis()/1000; //生成签名(官方给出来的签名方法) Map<String,String> map2 = new HashMap<>(); map2.put("appId", appId); map2.put("timeStamp", String.valueOf(time)); //这边的随机字符串必须是第一次生成sign时,微信返回的随机字符串,不然小程序支付时会报签名错误 map2.put("nonceStr", returnInfo.getNonce_str()); map2.put("package", "prepay_id=" + returnInfo.getPrepay_id()); map2.put("signType", "MD5"); String sign2 = WXPayUtil.generateSignature(map2, apiSecret); //无效的签名方法 //String sign1 = Signature.getSign(signInfo); payInfo.put("timeStamp", String.valueOf(time)); payInfo.put("nonceStr", returnInfo.getNonce_str()); payInfo.put("prepay_id","prepay_id=" + returnInfo.getPrepay_id()); payInfo.put("signType", "MD5"); payInfo.put("paySign", sign2); } else { payInfo.put("errorBody", resXml); } return payInfo; }
步骤4:小程序调出支付弹窗,输入密码支付订单
-
小程序端应该先同步调用服务端的生成订单,然后等到那些鉴权参数返回后,调用wx.requestPayment发起支付。
wx.request({ url: app.globalData.ServerHost+'dealer/mina/createWxOrder', data: { openid:app.globalData.openid, orderId:"icytest"+Math.random().toString(16).substr(2, 8), }, header: {'content-type':'application/json'}, method: 'POST', dataType: 'json', success: (result)=>{ console.log('统一下单:',result) wx.requestPayment({ timeStamp: result.data.timeStamp, nonceStr: result.data.nonceStr, package: result.data.prepay_id, signType:result.data.signType, paySign: result.data.paySign, success: (result)=>{ console.log('发起微信支付:',result) }, fail: ()=>{}, complete: ()=>{} }); }, fail: ()=>{}, complete: ()=>{} });
步骤5:支付完成,服务端接受回调通知
-
微信在处理完支付订单后,会调用先前
notify_url
传递的接口。 -
这个回调接口我们需要:
- 接收xml报文,并验证是否返回SUCCESS。
- 验证签名及订单金额和实付金额。
- 处理支付完毕后的业务逻辑
- 通知微信平台已经成功收到
-
可能会多次重复通知,要做好能处理重复消息的准备。
-
使用流数据的方式来传输。
@Override public void notifyWxPayment(HttpServletRequest request, HttpServletResponse response) throws Exception { BufferedReader br = new BufferedReader(new InputStreamReader(request.getInputStream())); String line; StringBuilder sb = new StringBuilder(); while((line = br.readLine()) != null){ sb.append(line); } br.close(); //sb为微信返回的xml String notityXml = sb.toString(); String resXml = ""; System.out.println("{微信支付完成回调}接收到的报文:" + notityXml); Map<String, String> map = WXPayUtil.xmlToMap(notityXml); String returnCode = map.get("return_code"); if("SUCCESS".equals(returnCode)){ //验证签名是否正确 Map<String, String> validParams = PayUtil.paraFilter(map); //回调验签时需要去除sign和空值参数 //把数组所有元素,按照“参数=参数值”的模式用“&”字符拼接成字符串 String validStr = PayUtil.createLinkString(validParams); //拼装生成服务器端验证的签名 String sign = PayUtil.sign(validStr, Configure.getKey(), "utf-8").toUpperCase(); // 当第一次回调成功了,那么我们就不再执行逻辑了 //根据微信官网的介绍,此处不仅对回调的参数进行验签,还需要对返回的金额与系统订单的金额进行比对等 if(sign.equals(map.get("sign"))){ System.out.println("我执行我的业务逻辑"); //通知微信服务器已经支付成功 resXml = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>" + "<return_msg><![CDATA[OK]]></return_msg>" + "</xml>"; } else { System.out.println("微信支付回调失败!签名不一致"); } }else{ 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(); }
步骤6:小程序获取支付成功通知解决方案
- 官方实现参考:支付回调和查单实现指引
坑:签名错误
- 签名错误的时候可以先去微信支付接口签名校验工具校验一下。可能是签名算法错误了。
- 有可能是body里包含中文,没有处理好。排查这个的方法是不用中文body就能通过签名验证。解决办法是:
httpPost.setHeader("Content-type", "text/xml;charset=utf-8");
- 有可能是API密钥api_secret错误,需要重置。
MINE MIND系列将在我的GitHub上实时更新,同时精选部分汇总于CSDN专栏
GitHub仓库:https://github.com/IcyLeaves/MINE-MIND
CSDN专栏:https://blog.csdn.net/qq_37398834/category_10975647.html