先给一个请求支付接口的实际例子
- 用户通过小程序服务查询商品
- 小程序服务调用售货机服务查询商品信息返回
- 小程序服务将商品信息返回给用户
- 用户支付下单
- 订单服务执行支付完成逻辑,向售货机下达出货指令
- 售货机出货,并响应出货结果给订单服务
- 订单更新订单状态
其中订单服务执行支付完成逻辑是我们现阶段所需要重点关注的,这里的逻辑较为复杂,需要我们具体分析,我们先针对小程序请求支付这一环节深入剖析
我们先来再看一张图
从上面的交互图可以看出微信小程序端需要:
- 调用后台接口请求下单支付
- 后台在调用微信统一下单接口之前先要获取openId
- 获取openId时需要客户端调用微信登录接口获取微信端返回的jsCode,并将该code传入后台,后台将该code传入微信端接口从而才能得到openId
- 后台调用微信统一下单接口将相关数据传入到微信接口,此时微信平台会返回相关数据(其中包括prepay_id,也叫预付单信息)
- 后台将得到的数据再次签名之后返回到前端
- 前端根据后台返回的数据向微信端调起鉴权支付请求,如果通过则在小程序内部呼起微信支付
前面3步,不做赘述。
从第4步开始,我们需要传参与微信接口进行交互,所以我们从第四步仔细分析:
分析一:后台调用微信统一下单接口将相关数据传入到微信接口
这里我们要明确几件事:
一是后台调用的微信统一下单接口具体是什么?
二是相关数据是什么?
- 小程序(JSAPI)统一下单文档:https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=9_1
从官方文档中,我们能找到答案:
小程序支付接口链接
URL地址:https://api.mch.weixin.qq.com/pay/unifiedorder
请求参数非常之多,这里就不CV过来了(详情见官方文档),太占篇幅,我们挑点必要的(必填的),捋一捋
字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
公众账号ID | appid | 是 | String(32) | wxd678efh567hg6787 | 微信支付分配的公众账号ID(企业号corpid即为此appid) |
商户号 | mch_id | 是 | String(32) | 1230000109 | 微信支付分配的商户号 |
随机字符串 | nonce_str | 是 | String(32) | 5K8264ILTKCH16CQ2502SI8ZNMTM67VS | 随机字符串,长度要求在32位以内。推荐随机数生成算法 |
签名 | sign | 是 | String(32) | C380BEC2BFD727A4B6845133519F3AD6 | 通过签名算法计算得出的签名值,详见签名生成算法 |
商品描述 | body | 是 | String(127) | 腾讯充值中心-QQ会员充值 | 商品简单描述,该字段请按照规范传递,具体请见参数规定 |
商户订单号 | out_trade_no | 是 | String(32) | 20150806125346 | 商户系统内部订单号,要求32个字符内(最少6个字符),只能是数字、大小写字母_-|*且在同一个商户号下唯一。详见商户订单号 |
标价金额 | total_fee | 是 | int | 88 | 订单总金额,单位为分,详见支付金额 |
终端IP | spbill_create_ip | 是 | String(64) | 123.12.12.123 | 支持IPV4和IPV6两种格式的IP地址。用户的客户端IP |
通知地址 | notify_url | 是 | String(256) | https://www.weixin.qq.com/wxpay/pay.php | body 异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。 公网域名必须为https,如果是走专线接入,使用专线NAT IP或者私有回调域名可使用http |
交易类型 | trade_type | 是 | String(16) | JSAPI | JSAPI -JSAPI支付 NATIVE -Native支付 APP -APP支付 说明详见参数规定 |
商品ID | product_id | 否 | String(32) | 12235413214070356458058 | trade_type=NATIVE时,此参数必传。此参数为二维码中包含的商品ID,商户自行定义。 |
用户标识 | openid | 否 | String(128) | oUpF8uMuAJO_M2pxb1Q9zNjWeS6o | trade_type=JSAPI时(即JSAPI支付),此参数必传,此参数为微信用户在商户对应appid下的唯一标识。openid如何获取,可参考【获取openid 】。企业号请使用【企业号OAuth2.0接口 】获取企业号内成员userid,再调用【企业号userid转openid接口 】进行转换 |
入参说明:
- 紫色字体-微信客户端程序会生成 或 配置文件配置
- 红色字体-代码实现需要赋值
分析二:此时微信平台会返回相关数据(其中包括prepay_id,也叫预付单信息)
首先我们要清楚,这里返回的相关数据是叫预付单信息,返回的内容主要有三个:
- 商户身份信息的校验结果
- 发放预付款交易会话标识符
- 发放支付二维码连接(小程序无)
这里我们主要不清楚的是返回的相关数据是什么?
当然官方文档中也详细说明了
这里我们可以C一下,并简单说明一下
返回结果
A表
字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
返回状态码 | return_code | 是 | String(16) | SUCCESS | SUCCESS/FAIL 此字段是通信标识,非交易标识,交易是否成功需要查看result_code来判断 |
返回信息 | return_msg | 否 | String(128) | 签名失败 | 返回信息,如非空,为错误原因 签名失败 参数格式校验错误 |
以下字段在return_code为SUCCESS的时候有返回
B表
字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
小程序ID | appid | 是 | String(32) | wx8888888888888888 | 调用接口提交的小程序ID |
商户号 | mch_id | 是 | String(32) | 1900000109 | 调用接口提交的商户号 |
设备号 | device_info | 否 | String(32) | 013467007045764 | 自定义参数,可以为请求支付的终端设备号等 |
随机字符串 | nonce_str | 是 | String(32) | 5K8264ILTKCH16CQ2502SI8ZNMTM67VS | 微信返回的随机字符串 |
签名 | sign | 是 | String(64) | C380BEC2BFD727A4B6845133519F3AD6 | 微信返回的签名值,详见签名算法 |
业务结果 | result_code | 是 | String(16) | SUCCESS | SUCCESS/FAIL |
错误代码 | err_code | 否 | String(32) | SYSTEMERROR | 详细参见下文错误列表 |
错误代码描述 | err_code_des | 否 | String(128) | 系统错误 | 错误信息描述 |
以下字段在return_code 和result_code都为SUCCESS的时候有返回
C表
字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
交易类型 | trade_type | 是 | String(16) | JSAPI | 交易类型,取值为:JSAPI,NATIVE,APP等,说明详见参数规定 |
预支付交易会话标识 | prepay_id | 是 | String(64) | wx201410272009395522657a690389285100 | 微信生成的预支付会话标识,用于后续接口调用中使用,该值有效期为2小时 |
二维码链接 | code_url | 否 | String(64) | weixin://wxpay/bizpayurl/up?pr=NwY5Mz9&groupid=00 | trade_type=NATIVE时有返回,此url用于生成支付二维码,然后提供给用户进行扫码支付。 注意:code_url的值并非固定,使用时按照URL格式转成二维码即可。时效性为2小时 |
这组参数分为三种情况:
一是只返回A的情况 当A中return_code参数不为SUCCESS的时候,只返回A中参数
二是返回AB的情况 当A中return_code参数为SUCCESS的时候,且B中result_code不为SUCCESS
三是返回ABC的情况 当A中return_code参数为SUCCESS的时候,且B中result_code为SUCCESS
分析三:后台将得到的数据再次签名之后返回到前端
注意此时的返回前端才是本次请求的结束!!!
注意此时的返回前端才是本次请求的结束!!!
注意此时的返回前端才是本次请求的结束!!!
我们得到预付单信息后,进行算法加密(这里我们用的是MD5加密算法),生成签名,然后再次组装数据,返回给前端
下面引用github上大神封装好的微信支付工具,可以仔细分析下面的代码,加深理解
package com.github.wxpay.plus;
import com.github.wxpay.sdk.*;
import com.google.common.base.Strings;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.InputStream;
import java.util.Map;
/**
* 微信支付工具
*/
@Component
@Slf4j
public class WxPayTemplate {
@Autowired
private WXConfig wxConfig;
@Autowired
private WxPaySdkConfig wxPaySdkConfig;
/**
* 下单
* @param wxPayParam 微信支付参数
* @return
*/
public Map<String,String> requestPay( WxPayParam wxPayParam) {
try{
String nonce_str = WXPayUtil.generateNonceStr();
//1.封装请求参数
Map<String,String> map= Maps.newHashMap();
map.put("appid",wxPaySdkConfig.getAppID());//公众账号ID
map.put("mch_id",wxPaySdkConfig.getMchID());//商户号
map.put("nonce_str", nonce_str);//随机字符串
map.put("body",wxPayParam.getBody());//商品描述
map.put("out_trade_no",wxPayParam.getOutTradeNo());//订单号
map.put("total_fee",wxPayParam.getTotalFee()+"");//金额
map.put("spbill_create_ip","127.0.0.1");//终端IP
map.put("notify_url",wxConfig.getNotifyUrl());//回调地址
if(wxPayParam.getOpenid().equals("")){
map.put("trade_type","NATIVE");//交易类型 : 本地
}else{
map.put("trade_type","JSAPI");//交易类型: 小程序
map.put("openid",wxPayParam.getOpenid());//openId
}
String xmlParam = WXPayUtil.generateSignedXml(map, wxPaySdkConfig.getKey());
log.info("封装参数:{}",xmlParam);
//2.发送请求
WXPayRequest wxPayRequest=new WXPayRequest(wxPaySdkConfig);
String xmlResult = wxPayRequest.requestWithCert("/pay/unifiedorder", null, xmlParam, false);
//3.解析返回结果
Map<String, String> mapResult = WXPayUtil.xmlToMap(xmlResult);
log.info("返回结果:{}",mapResult);
//返回给移动端需要的参数
Map<String, String> result = Maps.newHashMap();
if("SUCCESS".equals(mapResult.get("return_code"))){ //返回状态码成功
//如果result-code为失败
if("SUCCESS".equals(mapResult.get("result_code"))){ //如果成功
if(wxPayParam.getOpenid().equals("")) { //本地支付
result.put("codeUrl", mapResult.get("code_url")); //如果本地支付,会显示二维码
}else{ //小程序支付
result.put("appId",wxPaySdkConfig.getAppID());
result.put("package", "prepay_id=" + mapResult.get("prepay_id")); //预付单信息
result.put("signType","MD5");
result.put("nonceStr", WXPayUtil.generateNonceStr());
Long timeStamp = System.currentTimeMillis() / 1000;
result.put("timeStamp", timeStamp + "");//要将返回的时间戳转化成字符串,不然小程序端调用wx.requestPayment方法会报签名错误
//再次签名,这个签名用于小程序端调用wx.requesetPayment方法
String sign = WXPayUtil.generateSignature(result,wxConfig.getPartnerKey());
result.put("paySign", sign);
result.put("appId","");
}
result.put("orderNo",wxPayParam.getOutTradeNo());
result.put("code","SUCCESS"); //成功
return result;
}else {
result.put("msg", mapResult.get("err_code_des" )); //错误描述
result.put("code","RESULT_FAIL");//失败
return result;
}
}else {
log.info("调用微信统一下单接口失败",mapResult.get("return_msg"));
result.put("msg", mapResult.get("return_msg" )); //错误描述
result.put("code","RETURN_FAIL");//返回失败
return result;
}
}catch (Exception ex){
log.info("调用微信统一下单接口失败",ex);
return null;
}
}
/**
* 验证支付结果
* @param inputStream
* @return 订单号
* @throws Exception
*/
public Map<String,String> validPay(InputStream inputStream) throws Exception {
String notifyResult = ConvertUtils.convertToString( inputStream);
Map<String,String> result=Maps.newHashMap();
//解析
Map<String, String> map = WXPayUtil.xmlToMap( notifyResult ); //验签
boolean signatureValid = WXPayUtil.isSignatureValid(map, wxConfig.getPartnerKey());
if(signatureValid){
if("SUCCESS".equals(map.get("result_code"))){
result.put("code","SUCCESS");//成功
result.put("order_sn", map.get("out_trade_no") );//订单号
//返回订单号
return result;
}else {
log.info("支付回调出错:"+notifyResult);
result.put("code","FAIL");//失败
return null;
}
}else {
log.info("支付回调验签失败:"+notifyResult);
result.put("code","FAIL");//失败
return result;
}
}
}