漫谈微信支付-扫码支付


本文对微信扫码支付的实现过程进行详细介绍。

概述

扫码支付应用于在各种场景下进行二维码支付,支付流程一般为:

  • 商户根据微信支付规则为不同商品生成二维码;
  • 用户使用微信扫一扫功能,扫码获取商品支付信息;
  • 用户输入支付密码,完成支付;
  • 用户微信客户端收到支付成功通知,微信后台将支付结果异步回调给商户后台。

支付模式

根据应用场景的不同,微信提供了两种扫码支付模式:
模式一:
商户将productid(可定义为产品标识或订单号)作为参数根据微信支付规则事先生成固定的二维码,商户可将此二维码打印张贴,方便用户线下扫码支付。
用户扫码后,微信支付系统会将用户openid和商户定义的productid返回给商户在微信公共平台配置的回调地址,商户根据productid生成微信支付交易,最后微信支付系统发起用户支付流程。
模式二:
商户后台主动发起调用微信支付统一下单接口生成预付交易,并将接口返回的短连接生成二维码,用户扫码后输入密码完成支付交易。


实现过程

模式一:

  • 准备工作:
    模式一需要在微信公共平台配置回调地址:微信公共平台->微信支付->开发配置
    模式一回调配置
  • 开发流程:
    ◆ 商户后台根据微信支付规则生成二维码,展示给用户;
    ◆ 用户扫描二维码,微信支付系统将用户openid和productid以参数方式回调商户在公共平台设置的回调地址;
    ◆ 商户后台系统接收到微信回调请求,根据接收到openid和productid生成商户订单;
    ◆ 商户后台调用微信支付统一下单接口获取预付交易id(prepay_id);
    ◆ 微信支付系统根据商户统一下单请求生成预支付交易,返回prepay_id;
    ◆ 商户系统得到prepay_id后返回给微信支付系统;
    ◆ 微信支付系统接收到prepay_id后发起用户端微信支付流程(拉起微信支付,输入密码,完成扣款);
    ◆ 微信支付系统完成交易后向用户微信客户端发送支付结果通知;
    ◆ 微信支付系统通过发送异步消息,将支付结果发送至商户统一下单时填写的回调地址;
    ◆ 商户确认微信支付结果并更新订单状态,如商户后台未收到微信支付结果通知,可以通过调用查询订单接口主动查询订单支付结果。

  • 实现
    ◆ 所需文件
    lib/* :PHP-SDK lib文件夹;
    WxPay.NativePay.php:扫码支付实现类;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    <?php
    require_once "wxPay/WxPay.Api.php";

    /**
    * 扫码支付实现类
    */

    class NativePay
    {

    /**
    *
    * 生成扫描支付URL,模式一
    * @param BizPayUrlInput $bizUrlInfo
    */

    public function GetPrePayUrl($productId)
    {

    $biz = new WxPayBizPayUrl();
    $biz->SetProduct_id($productId);
    $values = WxpayApi::bizpayurl($biz);
    $url = "weixin://wxpay/bizpayurl?" . $this->ToUrlParams($values);
    return $url;
    }

    /**
    *
    * 参数数组转换为url参数
    * @param array $urlObj
    */

    private function ToUrlParams($urlObj)
    {

    $buff = "";
    foreach ($urlObj as $k => $v)
    {
    $buff .= $k . "=" . $v . "&";
    }

    $buff = trim($buff, "&");
    return $buff;
    }

    /**
    *
    * 生成直接支付url,支付url有效期为2小时,模式二
    * @param UnifiedOrderInput $input
    */

    public function GetPayUrl($input)
    {

    if($input->GetTrade_type() == "NATIVE")
    {
    $result = WxPayApi::unifiedOrder($input);
    return $result;
    }
    }
    }

◆ 生成二维码

1
2
3
$productid = '123456789';
$notify = new NativePay();
$url = $notify->GetPrePayUrl($productid);

返回的二维码链接格式为:

1
weixin://wxpay/bizpayurl?appid=wx426b3015555a46be&mch_id=1225312702&nonce_str=hscvjb7ljo9h53hsn97qhszd5wh5aja6&product_id=123456789&time_stamp=1468641555&sign=47FE32AA7DF1249D92353F1B6D79391A

使用QRcode将链接转换为二维码。

◆ 扫码支付回调
用户扫码完成后,微信支付系统会将用户openid和productid发送至该回调接口,该回调接口接收到微信回调请求后需要完成:
1) 调用微信支付统一下单接口获得prepay_id;
2) 将prepay_id返回给微信服务器。
首先创建实现微信扫码支付模式一的回调接口类NativeNotifyCallBack:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class NativeNotifyCallBack extends WxPayNotify
{

public function unifiedorder($openId, $product_id)
{

//统一下单
$input = new WxPayUnifiedOrder();
$input->SetBody("test");
$input->SetAttach("test");
$input->SetOut_trade_no(WxPayConfig::MCHID.date("YmdHis"));
$input->SetTotal_fee("1");
$input->SetTime_start(date("YmdHis"));
$input->SetTime_expire(date("YmdHis", time() + 600));
$input->SetGoods_tag("test");
//设置微信支付结果通知回调地址
$input->SetNotify_url("http://XXX.com/test/WxPayCallBack");
//扫码支付的trade_type要设置为NATIVE
$input->SetTrade_type("NATIVE");
$input->SetOpenid($openId);
$input->SetProduct_id($product_id);
$result = WxPayApi::unifiedOrder($input);
return $result;
}

public function NotifyProcess($data, &$msg)
{

//echo "处理回调";

if(!array_key_exists("openid", $data) ||
!array_key_exists("product_id", $data))
{
$msg = "回调数据异常";
return false;
}

$openid = $data["openid"];
$product_id = $data["product_id"];

//统一下单
$result = $this->unifiedorder($openid, $product_id);
if(!array_key_exists("appid", $result) ||
!array_key_exists("mch_id", $result) ||
!array_key_exists("prepay_id", $result))
{
$msg = "统一下单失败";
return false;
}

$this->SetData("appid", $result["appid"]);
$this->SetData("mch_id", $result["mch_id"]);
$this->SetData("nonce_str", WxPayApi::getNonceStr());
$this->SetData("prepay_id", $result["prepay_id"]);
$this->SetData("result_code", "SUCCESS");
$this->SetData("err_code_des", "OK");
return true;
}
}

在上一节漫谈微信支付中介绍过微信回调基础类WxPayNotify,支付模式一的回调类NativeNotifyCallBack继承自WxPayNotify,NativeNotifyCallBack需要实现基类中定义的方法NotifyProcess进行回调逻辑处理。
NativeNotifyCallBackNotifyProcess首先获取openid和product_id,然后传递给unifiedorder方法完成统一下单:
$result = $this->unifiedorder($openid, $product_id);接收统一下单返回的结果,并将appid、mch_id、nonce_str、prepay_id等信息返回给微信。

回调方法:
假设商户在公共号平台设置的回调地址为:http://XXX.com/test/NativeCallBack

1
2
3
4
public function actionNativeCallBack() {
$notify = new NativeNotifyCallBack();
$notify->Handle(true);
}

◆ 支付结果回调
支付结果回调用于接收微信服务器发送的支付结果通知,商户后台可能还需要执行更新订单状态以及其他操作。在统一下单接口中通过SetNotify_url进行设置。
首先定义处理微信支付结果的回调类PayNotifyCallBack:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class PayNotifyCallBack extends WxPayNotify
{

//查询订单
public function Queryorder($transaction_id)
{

$input = new WxPayOrderQuery();
$input->SetTransaction_id($transaction_id);
$result = WxPayApi::orderQuery($input);
if(array_key_exists("return_code", $result)
&& array_key_exists("result_code", $result)
&& $result["return_code"] == "SUCCESS"
&& $result["result_code"] == "SUCCESS")
{
return true;
}
return false;
}

//重写回调处理函数
public function NotifyProcess($data, &$msg)
{

$notfiyOutput = array();

if(!array_key_exists("transaction_id", $data)){
$msg = "输入参数不正确";
return false;
}
//查询订单,判断订单真实性
if(!$this->Queryorder($data["transaction_id"])){
$msg = "订单查询失败";
return false;
}
return true;
}
}

PayNotifyCallBack同样继承自WxPayNotify,需要重写NotifyProcess方法。在微信支付结果回调中,NotifyProcess需要完成的操作是调用查询订单接口,主动查询微信端订单状态。
◆ 回调并发控制
假设统一下单接口中定义的回调地址为:http://XXX.com/test/WxPayCallBack,

1
2
3
4
5
6
7
8
9
/**
* 微信支付回调函数
* @author yuri 2015/6/26
*/

public function actionWxPayCallBack(){
require_once "WxPay.PayNotifyCallBack.php";
$notify = new PayNotifyCallBack();
$notify->Handle(true);
}

多数情况下商户后台在接收到微信支付结果通知后会进行订单状态变更以及执行一些业务逻辑操作,因此需要对SDK做一定修改。通过阅读SDK我们知道后台最终都是通过WxpayApi::replyNotify方法将结果以XML的格式直接输出,方便微信服务器接收,修改WxpayApi::replyNotify,将XML作为结果返回:

1
2
3
4
5
6
7
8
9
10
/**
* 直接输出xml
* @param string $xml
*/

public static function replyNotify($xml)
{

//echo $xml;
//modify by yuri 2015/6/26
return $xml;
}

此时WxpayApi::replyNotify修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 微信支付回调函数
* @author yuri 2015/6/26
*/

public function actionWxPayCallBack(){
require_once "WxPay.PayNotifyCallBack.php";
$notify = new PayNotifyCallBack();
$xml = $notify->Handle(true);
$result = simplexml_load_string($xml);

//支付成功
if($result->return_code == 'SUCCESS' && $result->return_msg == 'OK') {
$postStr = $GLOBALS["HTTP_RAW_POST_DATA"];
$postStr = simplexml_load_string($postStr);
if($postStr) {
$out_trade_no = $postStr->out_trade_no; //订单号
$transaction_id = $postStr->transaction_id; //交易id
//处理更新订单状态等其他业务逻辑
$this->orderHandle($out_trade_no, $transaction_id);
}
}
echo $xml;
}

orderHandle方法用于处理商户后台订单逻辑,由于微信异步通知可能会多次发送给商户系统,因此商户后台业务处理逻辑必须能够正确处理重复的通知,如果是更新订单状态,在进行处理之前需要先判断订单状态,防止重复更新;如果处理逻辑中涉及到数据表插入等操作,要采用数据锁进行并发控制。例如使用Redis进行并发控制,key设置为用户ID+订单号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//设置redis防止并发写入
$redis_key = USERID_ORDERID.':'.$userid.':'.$outtradeno;
$set_order = Helper::setRedisCache('setnx', array($redis_key, 1), Mod::app()->params->redisExpire['useridOrderid']);
if($set_order && $set_order[0]) {
//执行更新订单、插入数据表等操作,在处理过程中如果遇到失败,需要及时删除redis,防止后续回调不能正常执行。
$order->paystate = 1;
$nret = $order->save();
if ($nret) {
//进行后续处理
} else {
//保存失败,要及时删除redis防止后续回调操作被阻塞
Helper::delRedisCache('del', array($redis_key));
}
}

$set_order = Helper::setRedisCache('setnx', array($redis_key, 1), Mod::app()->params->redisExpire使用setnx命令设置key的值为1,且缓存时间设置为24小时。SETNX,是「SET if Not eXists」的缩写,只有缓存不存在的时候才设置,可以利用它来实现锁的效果,但是使用setnx时有一些陷阱,具体可参考谈谈Redis的SETNX这篇博文。
Helper::setRedisCache是封装的添加/修改Redis缓存的公共方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static function setRedisCache($command, array $params, $expired) {
$ret = array();

if ($command && $params && !stristr($command, 'del') && !stristr($command, 'rem')) {
if (count($params) == 1) {
$ret = Mod::app()->redis->multi(Redis::PIPELINE)->$command($params[0])->expire($params[0], $expired)->exec();
} elseif (count($params) == 2) {
$ret = Mod::app()->redis->multi(Redis::PIPELINE)->$command($params[0], $params[1])->expire($params[0], $expired)->exec();
} elseif (count($params) == 3) {
$ret = Mod::app()->redis->multi(Redis::PIPELINE)->$command($params[0], $params[1], $params[2])->expire($params[0], $expired)->exec();
}
}

return $ret;
}

在实际支付过程中可能会出现这样一种情况:
用户通过扫码已经完成了支付,但是商户后台没有及时收到微信服务器发送的回调通知,导致商户后台订单状态无法及时更新,因此用户端看到的状态还是“未支付”,用户可能还会继续进行扫码尝试支付,这时在调用统一下单接口时,微信服务器返回的数据中result_code为FAIL,且提示该订单已经支付,此时商户后台需要主动去查询微信端订单状态并更新商户后台订单支付状态。
我在处理这种情况是的逻辑是:
1)查询商户后台订单状态,如果为未支付,执行步骤2,否则结束;
2)主动调用微信查询订单接口,获取订单状态及订单信息,如订单状态为已支付,执行步骤3;
3)商户后台主动更新订单状态以及相应业务逻辑处理。因为在业务逻辑处理中我们使用了数据锁机制,如果在主动更新订单状态的同时收到了微信回调请求,此时也不会发生重复处理的操作,保证了数据的一致性。

  • 总结:
    ◆ 模式一需要事先在公共号平台配置支付回调URL,而且整个支付流程中涉及两部分微信回调过程,每个微信回调过程对于支付结果通知的内容一定要做签名验证,防止数据泄漏导致出现“假通知”,造成资金损失。
    ◆ 微信回调处理逻辑要采用数据锁进行并发控制,防止数据重复写入;

模式二:

模式二要比模式一简单,不需要在公共号平台设置回调URL,直接调用统一下单接口得到code_url用于生成二维码,用户扫描二维码完成支付,同时微信将支付结果异步通知商户后台。

  • 开发流程:
    ◆ 商户后台根据商品生成商户订单;
    ◆ 商户后台调用微信支付统一下单接口生成预支付交易,并返回二维码链接code_url;
    ◆ 商户后台根据code_url生成二维码;
    ◆ 用户扫描二维码后微信端发起用户端微信支付流程(拉起微信支付,输入密码,完成扣款);
    ◆ 微信支付系统完成交易后向用户微信客户端发送支付结果通知;
    ◆ 微信支付系统通过发送异步消息,将支付结果发送至商户统一下单时填写的回调地址;
    ◆ 商户确认微信支付结果并更新订单状态,如商户后台未收到微信支付结果通知,可以通过调用查询订单接口主动查询订单支付结果。

◆ 所需文件
lib/* :PHP-SDK lib文件夹;
WxPay.NativePay.php:扫码支付实现类;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
require_once "wxPay/WxPay.Api.php";
require_once "WxPay.NativePay.php";
$notify = new NativePay();

$input = new WxPayUnifiedOrder();
$input->SetBody($title);
$input->SetAttach($attach);
$input->SetOut_trade_no($orderid);
$input->SetTotal_fee($total_fee);
$input->SetTime_start(date("YmdHis"));
$input->SetTime_expire(date("YmdHis", time() + 600));
$input->SetNotify_url("http://XXX.com/test/WxPayCallBack");
$input->SetTrade_type("NATIVE");
$input->SetProduct_id($orderid);
$url = $notify->GetPayUrl($input);

GetPayUrl方法完成了统一下单操作,并返回微信端接口调用结果。
注意:生成的code_url有效期为2小时。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
*
* 生成直接支付url,支付url有效期为2小时,模式二
* @param UnifiedOrderInput $input
*/

public function GetPayUrl($input)
{

if($input->GetTrade_type() == "NATIVE")
{
$result = WxPayApi::unifiedOrder($input);
return $result;
}
}

模式二支付结果回调流程与模式一相同。


支付模式区别

◆ 应用场景:
模式一生成的是固定二维码,方便打印,更适用于用户线下扫码支付;
模式二是商户后台先下单再生成支付二维码,具有时效性,更适用于线上扫码支付;
◆ 开发流程
模式一比模式二在支付过程中多一次回调流程,而且模式二流程更为简单;


总结

扫码支付在四种支付方式中算是比较简单的一种,而且微信官方SDK已经对支付流程中需要的各种接口作了很好的封装,开发起来相对比较简单。
任何一种支付方式都少不了处理异步回调支付结果过程,在该过程中商户后台一定要做好签名验证,正确处理重复通知,保持数据的一致性,防止数据重入造成的数据混乱。

有关扫码支付的实现已介绍完毕,在下一节漫谈微信支付-APP支付将会介绍APP支付的实现过程。
转载请注明出处:http://yurixu.com/blog/2016/07/15/漫谈微信支付-扫码支付/

分享 本文总阅读量
< !-- add by yurixu 替换Google的jquery并且添加判断逻辑 -->