漫谈微信支付


本文对微信支付的应用场景、支付模式、准备工作、官方SDK等进行简要介绍。

概述

微信支付目前在生活中应用的越来越广泛,目前微信支付的应用模式有刷卡支付、公共号支付、扫码支付以及APP支付,每种支付模式都有不同的应用场景与之对应。

支付模式

刷卡支付

刷卡支付是用户展示微信钱包内的“刷卡条码/二维码”给商户系统扫描后直接完成支付的模式。主要应用线下面对面收银的场景。

扫码支付

扫码支付是商户系统按微信支付协议生成支付二维码,用户再用微信“扫一扫”完成支付的模式。该模式适用于PC网站支付、实体店单品或订单支付、媒体广告支付等场景。

公共号支付

公众号支付是用户在微信中打开商户的H5页面,商户在H5页面通过调用微信支付提供的JSAPI接口调起微信支付模块完成支付。应用场景有:
◆ 用户在微信公众账号内进入商家公众号,打开某个主页面,完成支付
◆ 用户的好友在朋友圈、聊天窗口等分享商家页面连接,用户点击链接打开商家页面,完成支付
◆ 将商户页面转换成二维码,用户扫描二维码后在微信浏览器中打开页面后完成支付。

APP支付

APP支付又称移动端支付,是商户通过在移动端应用APP中集成开放SDK调起微信支付模块完成支付的模式。

本系列文章主要介绍后三种支付模式,分别介绍三种不同支付模式的实现以及汇总实现过程中可能遇到问题。


前期准备

使用微信支付前需要进行微信支付接入,商户根据实际需求可以申请接入以上介绍的四种不同支付模式,其中APP支付模式需要首先在微信开放平台注册成为开发者,创建APP后才能继续申请,申请步骤可以参考APP微信商户申请步骤
扫码支付和公共号支付需要先在微信公共平台注册公共号并进行认证,然后再继续申请微信支付,申请步骤可以参考公众平台微信支付商户申请步骤。申请成功后,微信支付开发相关的账号信息会被发送至申请邮箱中,包括:
前期准备

邮件中参数 API参数名 说明
APPID appid 微信公众账号或开放平台APP的唯一标识
商户号 mch_id 微信支付分配的商户收款账号
API密钥 key 交易过程生成签名的密钥,仅保留在商户系统和微信支付后台,不会在网络中传播
Appsecret secret APPID对应的接口密码,用于获取接口调用凭证access_token时使用

得到这些参数后就可以进行微信支付的开发了。


微信支付PHP-SDK

目录结构

微信提供了JAVA、.NET/C#、PHP三种类型的SDK,PHP版本的SDK目录结构如下所示:

1
SDK目录结构
|-- cert
|   |-- apiclient_cert.pem
|   |-- apiclient_key.pem
|-- index.php
|-- lib
|   |-- WxPay.Api.php
|   |-- WxPay.Config.php
|   |-- WxPay.Data.php
|   |-- WxPay.Exception.php
|   |-- WxPay.Notify.php
|-- logs
|-- example
    |-- WxPay.JsApiPay.php
    |-- WxPay.MicroPay.php
    |-- WxPay.NativePay.php
	|-- download.php
	|-- micropay.php
	|-- native.php
	|-- native_notify.php
	|-- notify.php
	|-- orderquery.php
	|-- qrcode.php
	|-- refund.php
	|-- refundquery.php
	|-- jsapi.php
    |-- log.php
    |-- phpqrcode

各个目录以及文件的说明如下:
cert:证书的存放路径
商户证书是微信提供的二进制文件,商户系统发起与微信支付后台服务器通信请求的时候,作为微信支付后台识别商户真实身份的凭据。
商户证书在涉及资金回滚的微信支付接口中使用,包括:申请退款、撤销订单接口。在使用的过程中服务器要做好病毒和木马的防护措施,防止证书被窃取
微信提供了四种商户证书:pkcs12格式、证书pem格式、证书密钥pem格式、CA证书,登录微信商户平台–>账户设置–>API安全–>证书下载,可以进行证书下载。PHP开发只需要用到证书pem格式、证书密钥pem格式,即apiclient_cert.pem和apiclient_key.pem。
index.php:SDK的入口文件
lib:微信支付API库
lib目录下的文件是微信支付开发用到的主要文件,提供了对微信支付的各种接口的封装。其中:
WxPay.Config.php:封装了WxPayConfig类,提供了微信支付所需的基本配置信息包括:APPID、MCHID、KEY、APPSECRET、证书路径、代理设置、微信支付接口上报配置。
WxPay.Data.php:接口输入参数的封装,包括:WxPayDataBase(数据对象基础类)、WxPayResults(接口调用结果类)、WxPayNotifyReply(微信支付回调基础类)、WxPayUnifiedOrder(微信支付统一下单类)、WxPayOrderQuery(查询订单类)、WxPayCloseOrder(关闭订单类)、WxPayRefund(申请退款类)、WxPayRefundQuery(查询退款类)、WxPayDownloadBill(下载对账单类)、WxPayReport(接口测试上报类)、WxPayShortUrl(短连接转换类)、WxPayMicroPay(刷卡支付类)、WxPayReverse(撤销订单类)、WxPayJsApiPay(公共号支付类)以及WxPayBizPayUrl(扫码支付模式一)
logs:日志文件
example:样例程序

源码分析

WxPayDataBase类

位置:lib/WxPay.Data.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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
/**
*
* 数据对象基础类,该类中定义数据类最基本的行为,包括:
* 计算/设置/获取签名、输出xml格式的参数、从xml读取数据对象等
*
*/

class WxPayDataBase
{

protected $values = array();

/**
* 设置签名,详见签名生成算法
* @param string $value
**/

public function SetSign()
{

$sign = $this->MakeSign();
$this->values['sign'] = $sign;
return $sign;
}

/**
* 获取签名,详见签名生成算法的值
* @return
**/

public function GetSign()
{

return $this->values['sign'];
}

/**
* 判断签名,详见签名生成算法是否存在
* @return true 或 false
**/

public function IsSignSet()
{

return array_key_exists('sign', $this->values);
}

/**
* 输出xml字符
* @throws WxPayException
**/

public function ToXml()
{

if(!is_array($this->values)
|| count($this->values) <= 0)
{
throw new WxPayException("数组数据异常!");
}

$xml = "<xml>";
foreach ($this->values as $key=>$val)
{
if (is_numeric($val)){
$xml.="<".$key.">".$val."</".$key.">";
}else{
//CDATA 部分中的所有内容都会被解析器忽略。
$xml.="<".$key."><![CDATA[".$val."]]></".$key.">";
}
}
$xml.="</xml>";
return $xml;
}

/**
* 将xml转为array
* @param string $xml
* @throws WxPayException
*/

public function FromXml($xml)
{

if(!$xml){
throw new WxPayException("xml数据异常!");
}
//将XML转为array
//禁止引用外部xml实体
libxml_disable_entity_loader(true);
$this->values = json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
return $this->values;
}

/**
* 格式化参数格式化成url参数
*/

public function ToUrlParams()
{

$buff = "";
foreach ($this->values as $k => $v)
{
if($k != "sign" && $v != "" && !is_array($v)){
$buff .= $k . "=" . $v . "&";
}
}

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

/**
* 生成签名
* @return 签名,本函数不覆盖sign成员变量,如要设置签名需要调用SetSign方法赋值
*/

public function MakeSign()
{

//签名步骤一:按字典序排序参数
ksort($this->values);
$string = $this->ToUrlParams();
//签名步骤二:在string后加入KEY
$string = $string . "&key=".WxPayConfig::KEY;
//签名步骤三:MD5加密
$string = md5($string);
//签名步骤四:所有字符转为大写
$result = strtoupper($string);
return $result;
}

/**
* 获取设置的值
*/

public function GetValues()
{

return $this->values;
}
}

WxPayDataBase是WxPay.Data.php文件中其他类的基类,定义了数据类的基本行为,包括:计算/设置/获取签名、输出XML格式的参数、从XML读取数据对象等。
变量$values存储各个接口参数,$values输出为XML时,对于非数字类型的变量使用CDATA进行标记,防止XML解析器进行解析,同理将XML对象转换为$value数组时,使用LIBXML_NOCDATA的方式:$this->values = json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
微信支付签名生成规则:
微信支付的签名生成由四个步骤组成:
◆ 将接口参数名按照字典顺序排序并格式化为URL参数
◆ 末尾拼接key
◆ MD5加密
◆ 所有字符转为大写

WxPayResults类

位置:lib/WxPay.Data.php
WxPayResults类继承自数据对象基础类WxPayDataBase,主要用来处理接口调用结果,方法InitInitFromArray均是初始化一个WxPayResults对象,Init方法使用接口调用返回的XML数据进行初始化并强制验证签名,返回数组;InitFromArray方法使用数组初始化WxPayResults对象的values,返回实例化后的WxPayResults对象。

WxPayNotifyReply类

位置:lib/WxPay.Data.php
WxPayNotifyReply类同样继承自数据对象基础类WxPayDataBase,用来处理授权回调,主要获取/设置return_code和return_msg。
WxPay.Data.php文件中定义的剩余其他类均是对各相应业务接口参数的合法性检测,包括获取参数、设置参数以及检测参数是否存在(例如:SetAppid、GetAppid、IsAppidSet)。

WxPayApi类

位置:lib/WxPay.Api.php
WxPayApi包含了所有微信支付API的封装,各个API实现的模式均为:
◆ 设置/校验接口参数
◆ 生成签名
◆ 接口参数转换为XML字符串并记录请求开始时间
◆ POST方式提交XML至接口URL
◆ 返回的XML结果转换为数组
◆ 记录请求结束时间并将请求结果上报微信
统一下单API为例:

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
/**
*
* 统一下单,WxPayUnifiedOrder中out_trade_no、body、total_fee、trade_type必填
* appid、mchid、spbill_create_ip、nonce_str不需要填入
* @param WxPayUnifiedOrder $inputObj
* @param int $timeOut
* @throws WxPayException
* @return 成功时返回,其他抛异常
*/

public static function unifiedOrder($inputObj, $timeOut = 6)
{

$url = "https://api.mch.weixin.qq.com/pay/unifiedorder";
//检测必填参数
if(!$inputObj->IsOut_trade_noSet()) {
throw new WxPayException("缺少统一支付接口必填参数out_trade_no!");
}else if(!$inputObj->IsBodySet()){
throw new WxPayException("缺少统一支付接口必填参数body!");
}else if(!$inputObj->IsTotal_feeSet()) {
throw new WxPayException("缺少统一支付接口必填参数total_fee!");
}else if(!$inputObj->IsTrade_typeSet()) {
throw new WxPayException("缺少统一支付接口必填参数trade_type!");
}

//关联参数
if($inputObj->GetTrade_type() == "JSAPI" && !$inputObj->IsOpenidSet()){
throw new WxPayException("统一支付接口中,缺少必填参数openid!trade_type为JSAPI时,openid为必填参数!");
}
if($inputObj->GetTrade_type() == "NATIVE" && !$inputObj->IsProduct_idSet()){
throw new WxPayException("统一支付接口中,缺少必填参数product_id!trade_type为JSAPI时,product_id为必填参数!");
}

//异步通知url未设置,则使用配置文件中的url
if(!$inputObj->IsNotify_urlSet()){
//坑!WxPayConfig中没有该配置项
$inputObj->SetNotify_url(WxPayConfig::NOTIFY_URL);//异步通知url
}

$inputObj->SetAppid(WxPayConfig::APPID);//公众账号ID
$inputObj->SetMch_id(WxPayConfig::MCHID);//商户号
$inputObj->SetSpbill_create_ip($_SERVER['REMOTE_ADDR']);//终端ip
//$inputObj->SetSpbill_create_ip("1.1.1.1");
$inputObj->SetNonce_str(self::getNonceStr());//随机字符串

//签名
$inputObj->SetSign();
$xml = $inputObj->ToXml();

$startTimeStamp = self::getMillisecond();//请求开始时间
$response = self::postXmlCurl($xml, $url, false, $timeOut);
$result = WxPayResults::Init($response);
self::reportCostTime($url, $startTimeStamp, $result);//上报请求花费时间

return $result;
}

扫码支付、公共号支付以及APP支付都会使用统一下单API,即它是这三种支付模式的公共接口。
统一下单API接收2个参数:$inputObj和$timeOut,$inputObj是WxPayUnifiedOrder类的对象,WxPayUnifiedOrder类存在于文件lib/WxPay.Data.php中,用于验证统一下单接口参数的合法性;$timeOut用来设置post请求的超时时间。
统一下单接口官方WIKI文档中给出的必填参数有appid、mch_id、nonce_str、sign、body、out_trade_no、total_fee、spbill_create_ip、notify_url、trade_type;对于扫码支付模式还必须传参数product_id;对于公共号支付(JSAPI)openid参数也为必填项,所以API接口中首先对需要用户主动传递的参数body、out_trade_no、total_fee、trade_type进行存在性校验;对关联参数product_id和openid需要根据支付模式的不同进行关联验证。
异步通知回调地址参数notify_url,此处官方API中存在错误:

1
2
3
4
if(!$inputObj->IsNotify_urlSet()){
//坑!WxPayConfig中没有该配置项
$inputObj->SetNotify_url(WxPayConfig::NOTIFY_URL);//异步通知url
}

因为WxPayConfig文件中不存在常量NOTIFY_URL,所以可以自行在WxPayConfig文件中添加NOTIFY_URL常量或者在每次调用统一下单接口时全部主动传递异步回调地址。
生成随机字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
*
* 产生随机字符串,不长于32位
* @param int $length
* @return 产生的随机字符串
*/

public static function getNonceStr($length = 32)
{

$chars = "abcdefghijklmnopqrstuvwxyz0123456789";
$str ="";
for ( $i = 0; $i < $length; $i++ ) {
$str .= substr($chars, mt_rand(0, strlen($chars)-1), 1);
}
return $str;
}

原理也比较简单,首先列举所有小写字母和数字,然后使用mt_rand方法从字符串中随机得到一个开始位置并截取一个字符,根据所需要产生的随机字符串长度进行拼接。
设置完接口所需的各个参数后,最后调用生成签名方法,需要注意的是微信支付生成签名时,字段名称区分大小写
所有接口数据最终会通过postXmlCurl方法发送到微信服务器:

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
/**
* 以post方式提交xml到对应的接口url
* @param string $xml 需要post的xml数据
* @param string $url url
* @param bool $useCert 是否需要证书,默认不需要
* @param int $second url执行超时时间,默认30s
* @throws WxPayException
*/

private static function postXmlCurl($xml, $url, $useCert = false, $second = 30)
{

$ch = curl_init();
//设置超时
curl_setopt($ch, CURLOPT_TIMEOUT, $second);

//如果有配置代理这里就设置代理
if(WxPayConfig::CURL_PROXY_HOST != "0.0.0.0"
&& WxPayConfig::CURL_PROXY_PORT != 0){
curl_setopt($ch,CURLOPT_PROXY, WxPayConfig::CURL_PROXY_HOST);
curl_setopt($ch,CURLOPT_PROXYPORT, WxPayConfig::CURL_PROXY_PORT);
}
curl_setopt($ch,CURLOPT_URL, $url);
curl_setopt($ch,CURLOPT_SSL_VERIFYPEER,FALSE);
curl_setopt($ch,CURLOPT_SSL_VERIFYHOST,FALSE);
//设置header
curl_setopt($ch, CURLOPT_HEADER, FALSE);
//要求结果为字符串且输出到屏幕上
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);

if($useCert == true){
//设置证书
//使用证书:cert 与 key 分别属于两个.pem文件
curl_setopt($ch,CURLOPT_SSLCERTTYPE,'PEM');
curl_setopt($ch,CURLOPT_SSLCERT, WxPayConfig::SSLCERT_PATH);
curl_setopt($ch,CURLOPT_SSLKEYTYPE,'PEM');
curl_setopt($ch,CURLOPT_SSLKEY, WxPayConfig::SSLKEY_PATH);
}
//post提交方式
curl_setopt($ch, CURLOPT_POST, TRUE);
curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
//运行curl
$data = curl_exec($ch);
//返回结果
if($data){
curl_close($ch);
return $data;
} else {
$error = curl_errno($ch);
curl_close($ch);
throw new WxPayException("curl出错,错误码:$error");
}
}

所有接口参数以XML格式发送给微信服务器,微信服务器同样以XML格式返回接口调用结果。$result = WxPayResults::Init($response);将微信返回的XML结果转换为数组形式并返回。

WxPayNotify类

位置:lib/WxPay.Notify.php
WxPayNotify类是进行业务回调处理的基础类,它继承自WxPayNotifyReply类。WxPayNotify类包含的方法有Handle、NotifyProcess、NotifyCallBack以及ReplyNotify。其中Handle方法是回调处理的入口函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
*
* 回调入口
* @param bool $needSign 是否需要签名输出
*/

final public function Handle($needSign = true)
{

$msg = "OK";
//当返回false的时候,表示notify中调用NotifyCallBack回调失败获取签名校验失败,此时直接回复失败
$result = WxpayApi::notify(array($this, 'NotifyCallBack'), $msg);
if($result == false){
$this->SetReturn_code("FAIL");
$this->SetReturn_msg($msg);
$this->ReplyNotify(false);
return;
} else {
//该分支在成功回调到NotifyCallBack方法,处理完成之后流程
$this->SetReturn_code("SUCCESS");
$this->SetReturn_msg("OK");
}
$this->ReplyNotify($needSign);
}

WxpayApi类的notify方法为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
*
* 支付结果通用通知
* @param function $callback
* 直接回调函数使用方法: notify(you_function);
* 回调类成员函数方法:notify(array($this, you_function));
* $callback 原型为:function function_name($data){}
*/

public static function notify($callback, &$msg)
{

//获取通知的数据
$xml = $GLOBALS['HTTP_RAW_POST_DATA'];
//如果返回成功则验证签名
try {
$result = WxPayResults::Init($xml);
} catch (WxPayException $e){
$msg = $e->errorMessage();
return false;
}

return call_user_func($callback, $result);
}

首先分析方法notify$GLOBALS['HTTP_RAW_POST_DATA'];获取到微信服务器返回的XML结果,通过WxPayResults的Init方法得到返回结果数组,以参数的方式传递给call_user_func方法。如果使用类方法作为call_user_func的参数,需要使用:array($className, $functionName)的方式传递参数,因此$result = WxpayApi::notify(array($this, 'NotifyCallBack'), $msg);的含义为:将微信返回的结果数组作为参数传递给函数NotifyCallBack,$result为NotifyCallBack方法的返回结果。
在NotifyCallBack方法中调用NotifyProcess,该方法需要用户在继承WxPayNotify类时进行重写(后续微信支付实现时需要重写该方法)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
*
* 回复通知
* @param bool $needSign 是否需要签名输出
*/

final private function ReplyNotify($needSign = true)
{

//如果需要签名
if($needSign == true &&
//$this->GetReturn_code($return_code) == "SUCCESS")
//又现一处SDK错误,$return_code多余
$this->GetReturn_code() == "SUCCESS")
{
$this->SetSign();
}
WxpayApi::replyNotify($this->ToXml());
}

ReplyNotify直接输出回调处理完成后的XML数据。因为是回调系统,直接输出XML后,微信后台可以获取到该输出内容,所以不需要开发者主动将结果发送给微信后台。
此处官方SDK又出现一处错误:GetReturn_code方法不需要传递参数,应该把$return_code删除。

微信支付相关的准备工作已介绍完毕,在下一节漫谈微信支付-扫码支付将会介绍扫码支付的实现过程。
转载请注明出处:http://yurixu.com/blog/2016/06/28/漫谈微信支付/

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