支付开发填坑记之微信支付

  LeungJZ

微信支付,支持的支付方式比较多:有扫码支付,刷卡支付,APP支付和公众号支付。其中,APP和网站上最常用的就是APP支付和公众号支付。前者集成在APP中,后者主要是为微信用户提供了另一种支付方式(需要在微信的内置浏览器中打开页面,再调起微信支付)。

同样的,微信的APP支付和支付宝的APP支付也是很简单:

APP支付

商户系统和微信支付系统主要交互说明:

步骤1:用户在商户APP中选择商品,提交订单,选择微信支付。

步骤2:商户后台收到用户支付单,调用微信支付统一下单接口。参见【统一下单API】

步骤3:统一下单接口返回正常的prepay_id,再按签名规范重新生成签名后,将数据传输给APP。参与签名的字段名为appIdpartnerIdprepayIdnonceStrtimeStamppackage注意:package的值格式为Sign=WXPay

步骤4:商户APP调起微信支付。

步骤5:商户后台接收支付通知。

步骤6:商户后台查询支付结果。

这里主要的还是后台干活(获取 prepay_id,生成随机字符串 nonceStr 和时间戳 timeStampappIdpartnerId 均能在后台管理中查看。)

后台的步骤也很简洁,就是上述中的步骤1,2。

  1. 获取 prepayId

  2. 设置获取 prepayId 所需参数。

此处需要调用微信的统一下单接口。这个过程,官方文档已经写得十分之详细了,包括调用的接口API地址,需要传递的参数(必要和非必要的参数),还有返回结果也写得很清楚。

以下是我在实际项目开发中传入的参数。
项目中传入的参数

参数解析

  1. 签名。

签名都差不多,都是先将所有的带签名的参数进行字典排序。

ksort($data);

然后将参数以 {key}={value} 的组合形式,用 & 连接。

$a = array();
foreach ($data as $k => $v) {
    if ((string) $v === '') {
        continue;
    }
    $a[] = "{$k}={$v}";
}

$a = implode('&', $a);

最后拼上 &key={Your apiKey} ,然后对整串字符串进行MD5加密即可。

$sign = strtoupper(md5($a));
  1. 将拼好的数据,以 XML 的格式发送给微信,请求 prepayId

没错,就是要转成 XML 格式再发送。

但是,这个XML格式很简单,只需要进行简单的拼接即可:

public function arrayToXml(array $data)
{
    $xml = "<xml>";
    foreach ($data as $k => $v) {
        if (is_numeric($v)) {
            $xml .= "<{$k}>{$v}</{$k}>";
        } else {
            $xml .= "<{$k}><![CDATA[{$v}]]></{$k}>";
        }
    }
    $xml .= "</xml>";
    return $xml;
}

参数值用XML转义即可,CDATA标签用于说明数据不被XML解析器解析。。

然后请求统一下单API即可(url = https://api.mch.weixin.qq.com/pay/unifiedorder

$ch = curl_init();
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
curl_setopt($ch, CURLOPT_HEADER, FALSE);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
curl_setopt($ch, CURLOPT_POST, TRUE);
curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
$response = curl_exec($ch);
if (!$response) {
    throw new Exception('CURL Error: ' . curl_errno($ch));
}
curl_close($ch);

请求回来的数据也为XML格式,只需要简单做下处理,转换成array即可:

public function xmlToArray($xml){
    return json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
}

如果返回值中的 return_coderesult_code 都为 SUCCESS 的时候会返回 交易类型 trade_type 和 预支付交易会话标识 prepayId 。到这里,我们就可以获取到 prepayId

  1. 将获取的 prepayId 与其他参数拼接,返回给APP即可。
$params = array();
//  商户号
$params['appid'] = $this->config->appId;
//  时间戳
$params['timestamp'] = '' . time();
//  随机字符串
$params['noncestr'] = md5(uniqid(mt_rand(), true));
//  固定为 'Sign=WXPay'
$params['package'] = 'Sign=WXPay';
//  步骤3获取的预支付交易会话标识
$params['prepayid'] = $prepayId;
//  合作伙伴id
$params['partnerid'] = $this->config->partnerId;
//  步骤2生成的签名。
$params['sign'] = $this->sign($params);

微信APP支付,后台需要干的活到这里就暂时结束了(因为还有支付成功后的异步通知商户后面再讲)


jsapi支付

下面就是web版的微信支付(公司项目是在微信浏览器内,选择微信支付后,在微信中调起的微信支付)
微信支付

web版微信支付的步骤和APP的大同小异,也是现获取 prepayId ,再在页面中,调用jsapi进行支付。

但是,此处有2个坑

坑1: 支付时出现 appid and openid not match 的报错

原因非常的简单,就是支付时所获取的 openid 在并不属于支付的商户。
这个 openid 为微信用户在商户对应appid下的唯一标识。也就是说,必须根据支付的商户的 appid 去获取用户的 openid

因为业务逻辑需要,项目中用于微信登录用的公众号A用于支付的公众号B(其实还和开放平台用于APP支付的 appId 也是不一样的)是不一样的,虽然所获取unionid是一致,但是 openid 是不!一!样!的!所以,在获取 openid 时,需要使用当前支付时所用到的 appid 去请求用户的 openid ,同时,请求 openid 后的回调也必须是 支付商户 后台所设置好的回调地址,要不然就会报 redirect_uri 参数错误 的错误。

坑2: 参数名大小写不一致。

APP支付的参数

↑ APP支付的参数

web支付的参数

↑ web支付的参数

仔细看看划横线的地方。没错,app中的参数的key全是小写,web支付中的key则为驼峰命名方式。而且,签名方式 signType 是必填的, 签名的字段也变成了 paySign,其中 package 的值也是不一样,APP支付是固定的值,web支付则为 prepayId,这也要注意。当然,官方文档也是很详细的说明,但是需要细心观察(所以说嘛,还是直接拷贝必填项最保险了2333)。

拿到所有参数后,就可以在页面中发起微信支付的请求了。

代码可以直接使用官方提供的js代码

function onBridgeReady(){
   WeixinJSBridge.invoke(
       'getBrandWCPayRequest', YOUR_PARAMS,
       function(res){     
           if(res.err_msg == "get_brand_wcpay_request:ok" ) {
               // success_callback
           }     // 使用以上方式判断前端返回,微信团队郑重提示:res.err_msg将在用户支付成功后返回    ok,但并不保证它绝对可靠。 
       }
   ); 
}
if (typeof WeixinJSBridge == "undefined"){
   if( document.addEventListener ){
       document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
   }else if (document.attachEvent){
       document.attachEvent('WeixinJSBridgeReady', onBridgeReady); 
       document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
   }
}else{
   onBridgeReady();
}

其中 YOUR_PARAMS 是参数转换成json格式直接渲染至页面即可。

如果参数没错的话,那么就可以顺利的调起支付窗口了。

坑3: APP支付和jsapi支付不是同一个号

APP支付是在开放平台中申请下来的,appId和apiKey都是不一样的。而jsapi支付实质就是公众号支付,是在公众平台中申请得到的。所以,在这里,需要注意一下。


重要的来了,能体现后台的重要性的地方终于来了 —

支付结果的异步通知

官方文档写得也很详细了(不得不说,微信的开发文档真的很清晰。很容易找到。就是没有详细的步骤区分)。

首先需要申明的是:异步通知的URL是必须能在公网访问的,而且,必须不能携带参数

也就是说,http://domain.com/payment/wxpay/notify.php 是没问题的,但是 http://domain.com/payment/notify.php?payment_code=wxpay 这样的URL是不行的。如果要想达到这种效果,要不服务器(Nginx , Apache)进行rewrite,要不在notify.php 中,手动修改 $_GET 中的参数。

返回的数据,都是一致的:

这时候,商户后台拿到这些异步通知的数据进行简单的校验即可,然后修改商户中相应订单的支付状态。

  1. 校验返回码是否成功
$d = $this->xmlToArray(file_get_contents('php://input'));
if (empty($d)) {
    throw new Exception(__METHOD__);
}
if ($d['return_code'] != 'SUCCESS') {
    throw new Exception($d['return_msg']);
}
if ($d['result_code'] != 'SUCCESS') {
    throw new Exception("[{$d['err_code']}]{$d['err_code_des']}");
}
  1. 对返回数据进行校验

和请求 prepayId 时处理数据的方式差不多,先取出签名 sign,然后除去签名后,进行字典排序,以 {key}={value} 的方式进行组合,并在最后加上 &key={apiKey}得到待校验字符串,最后,将待校验字符串进行MD5加密,和签名进行比较,若一致则校验成功,并且支付成功,然后后台做相应操作。

if (!$this->verify($d)) {
    throw new Exception("Invalid signature");
}

//  验证函数
if (empty($d['sign'])) {
    return false;
}
$sign = $d['sign'];
unset($d['sign']);
return $sign == $this->sign($d);

微信退款

有支付肯定就会有退款。微信的退款操作也是很简单,而且退款速度非常快,测试时基本都是秒退。

但是退款是有注意事项的:
1. 交易时间超过一年的订单无法提交退款;
2. 微信支付退款支持单笔交易分多次退款,多次退款需要提交原支付订单的商户订单号和设置不同的退款单号。总退款金额不能超过用户实际支付金额。一笔退款失败后重新提交,请不要更换退款单号,请使用原商户退款单号
3. 退款请求需要证书

【证书获取方式:】

微信支付接口中,涉及资金回滚的接口会使用到商户证书,包括退款、撤销接口。商家在申请微信支付成功后,收到的相应邮件后,可以按照指引下载API证书,也可以按照以下路径下载:微信商户平台(pay.weixin.qq.com)–>账户中心–>账户设置–>API安全–>证书下载。

微信退款程序流程:
退款流程

  1. 设置退款时得参数。

请求的参数有:
1. appid : 公众账号ID
2. mch_id : 商户号
3. nonce_str : 随机字符串
4. sign: 签名
5. transaction_id / out_trade_no :微信订单号 / 商户订单号 二者中传其中一个即可。
6. out_refund_no: 商户退款单号(由商户自行生成的唯一标识)
7. total_fee:订单金额(单位为分)
8. refund_fee:退款金额(单位为分),退款金额不能大于订单金额。
9. op_user_id:操作员帐号, 默认为商户号

签名还是老规矩(默认是MD5方式),先将所有参数进行字典排序,然后以$key=$value的形式用&字符拼接成字符串,最后将拼上 &key=YOUR_APIKEY 的待签名字符串进行MD5加密即可。
签名方法

  1. 将参数列表转换成XML格式。
    将参数列表转换成XML格式
  2. 发送退款请求。

退款请求需要携带微信上下载的证书,请保证证书存放路径外网不能直接访问。
发送退款请求

  1. 解析请求结果。

当返回的结果中, return_coderesult_code 均为 SUCCESS ,即为退款申请成功。更多返回结果,请移步至 官网

结尾

微信支付的官方开发文档其实算是很详细了,传递的参数,返回结果,如果判断是否成功,都写的很好。只是,开发中的逻辑过程需要自己慢慢摸索,理清思路后,开发起来其实都是很迅速的。

但是,开发微信支付时,需要留个心,需要将所有涉及到的微信后台提供的数据小心保存(比如AppSecret,一当忘记只能重置。)

祝各位开发过程顺利进行。