您现在的位置:首页 » 红点经验 » 技术博客

微信小程序、ThinkPHP5 集成微信支付过程

2018-10-22 10:51:29

在微信小程序项目中,需要接入微信支付,用户在小程序中点击【付款】,唤起微信支付的弹窗,输入密码进行支付,虽然支付流程很简单,但真正接入的时候难免会遇到一些问题。

项目环境

客户端:微信小程序
服务器:CentOS 7PHP 7.0NginxMySQL 5.6
后台框架:ThinkPHP 5

准备工作

  1. 申请微信小程序账号,并完成微信支付的事情工作。这些工作按照微信的要求一步步进行即可。
  2. 配置项目环境,需要注意的是接口的请求地址需要是https,可以到阿里云申请一年免费的SSL证书,按照Nginx的配置完成。
  3. 在微信小程序后台管理上完成微信支付的申请之后,就可以使用微信邮件给你的账号密码登陆商户平台https://pay.weixin.qq.com
  4. 在商户平台的【账户中心】->【API安全】中下载证书,以及【设置API密钥】,这两部分在代码配置中需要。
  5. 下载SDK地址。将lib目录下的文件拷贝到项目的extend下的wxpay目录中。
  6. 配置extend/wxpay/WxPay.Config.php,将其中的参数和上面下载的证书路径配置好。

微信支付参考文档:开发文档

微信小程序文档:开发文档

具体开发

创建一个订单

这一步很简单,就是常规的根据用户购买信息创建订单,将数据保存到数据库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 创建订单
*
* @return void
*/

public function create()
 {

$order = new OrderModel();
$order['title'] = $title.'-'.date('Ymd');
$order['out_trade_no'] = \WxPayConfig::MCHID.time();
$order['amount'] = $amount;
$order['status'] = 0;
if ($order->save()) {
return json_encode(['error' => 0, 'msg' => '创建成功']);
}
return json_encode(['error' => 1026, 'msg' => '创建失败']);
}

 

唤起支付窗口

在小程序中发起微信支付请求很简单,需要注意的是,在调用wx.requestPayment({})前需要获取签名数据,如以下的代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
app.requestData('Wxpay/sign', { id: id, openid: openid }, 'GET', function (res) {
if (res.error) {
app.showToast(res.msg, 'success', 2000)
} else {
wx.requestPayment({
timeStamp: res.result.timeStamp,
nonceStr: res.result.nonceStr,
package: res.result.package,
signType: res.result.signType,
paySign: res.result.paySign,
'success': function (res) { },
'fail': function (res) { },
'complete': function (res) { }
})
}
}, function (res) {
app.showToast(res.errMsg, 'success', 2000)
})

 

获取支付签名参数

在上一步的请求之前,需要获取签名参数,需要注意的是获取签名参数是需要当前微信用户的openid的(id是订单的id),先忽略openid如何获取,看签名方法:

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
<?php
 
namespace app\api\controller;
 
use think\Loader;
 
use app\api\controller\Base;
 
use app\common\model\Order as OrderModel;
 
Loader::import('wxpay/WxPay', EXTEND_PATH, '.Api.php');

class Wxpay extends Base
{

public $data = null;
private $curl_timeout = 3000;

/**
* 获取支付参数
*
* @return void
*/

public function sign()
{

$id = input('?get.id') ? trim(input('get.id')) : 0;
$openid = input('?get.openid') ? trim(input('get.openid')) : 0;
if (!$openid) {
return json_encode(['error' => 4001, 'msg' => '用户参数错误']);
}
$order = OrderModel::get($id);
if (!$order) {
return json_encode(['error' => 4002, 'msg' => '订单不存在']);
}
// 统一下单
$input = new \WxPayUnifiedOrder();
$input->SetBody($order->title);
$input->SetAttach($order->id);
$input->SetOut_trade_no($order->out_trade_no);
$input->SetTotal_fee(floatval($order->amount) * 100); // 单位为分
$input->SetTime_start(date("YmdHis"));
$input->SetTime_expire(date("YmdHis", time() + 600));
$input->SetGoods_tag("wx-app");
$input->SetNotify_url(config('wxpay.pay_notify_url'));
$input->SetTrade_type("JSAPI");
$input->SetOpenid($openid);
$order = \WxPayApi::unifiedOrder($input);

$jsApiParameters = $this->GetJsApiParameters($order);
if ($jsApiParameters) {
return json_encode(['error' => 0, 'msg' => '支付参数', 'result' => json_decode($jsApiParameters)]);
}
return json_encode(['error' => 4001, 'msg' => '参数错误']);
}
}

/**
*
* 获取jsapi支付的参数
* @param array $UnifiedOrderResult 统一支付接口返回的数据
* @throws WxPayException
*
* @return json数据,可直接填入js函数作为参数
*/

private function GetJsApiParameters($UnifiedOrderResult)
{

if (!array_key_exists("appid", $UnifiedOrderResult)
|| !array_key_exists("prepay_id", $UnifiedOrderResult)
|| $UnifiedOrderResult['prepay_id'] == "")
{
return null;
}
$jsapi = new \WxPayJsApiPay();
$jsapi->SetAppid($UnifiedOrderResult["appid"]);
$timeStamp = time();
$jsapi->SetTimeStamp("$timeStamp");
$jsapi->SetNonceStr(\WxPayApi::getNonceStr());
$jsapi->SetPackage("prepay_id=" . $UnifiedOrderResult['prepay_id']);
$jsapi->SetSignType("MD5");
$jsapi->SetPaySign($jsapi->MakeSign());
$parameters = json_encode($jsapi->GetValues());
return $parameters;
}

 

以上就是完整的获取支付签名参数的代码,需要注意的notify_url,这个微信支付成功的回调地址,保证浏览器可以正常访问即可。

支付回调

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
/**
* 微信支付回调
*
* @return void
*/

public function callback()
{

$xml = file_get_contents("php://input");
$data = \WxPayResults::Init($xml);
if (!array_key_exists("transaction_id", $data)) {
return $this->toXML(['return_code' => 'FAIL', 'return_msg' => '参数错误']);
}
//查询订单,判断订单真实性
if (!$data["out_trade_no"] || !$this->Queryorder($data["transaction_id"])) {
return $this->toXML(['return_code' => 'FAIL', 'return_msg' => '订单真实性存疑']);
}
$order = OrderModel::where(['out_trade_no' => $data["out_trade_no"]])->find();
if (!$order) {
return $this->toXML(['return_code' => 'FAIL', 'return_msg' => '订单不存在']);
}

$order->status = 1;
if ($order->save()) {
return $this->toXML(['return_code' => 'SUCCESS', 'return_msg' => 'OK']);
}
return $this->toXML(['return_code' => 'FAIL', 'return_msg' => '回调失败']);
}

/**
* 查询订单
*
* @param [type] $transaction_id
* @return void
*/

private 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;
}

/**
* array转xml
*
* @param [type] $value
* @return void
*/

public function toXML($values)
{

if(!is_array($values) || count($values) <= 0) {
return "<xml></xml>";
}

$xml = "<xml>";
foreach ($values as $key => $val) {
if (is_numeric($val)) {
$xml.="<".$key.">".$val."</".$key.">";
}else{
$xml.="<".$key."><![CDATA[".$val."]]></".$key.">";
}
}
$xml.="</xml>";
return $xml;
}

这里主要就是在回调方法中将订单的状态变换一下。

获取用户 openid

回到上面提到的签名方法,在那个方法中需要提供用户的openid,获取的流程在小程序的开发文档中有,其次服务器端的实现在下载的sdksample中也能够找到,如以下所示:
小程序端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//调用登录接口
wx.login({
success: function (res) {
if (res.code) {
if (!wx.getStorageSync('openid')) {
that.requestData('Wxpay/openid', {
code: res.code
}, 'GET', function (res) {
if (res.error) {
that.showToast(res.msg, 'success', 2000)
} else {
wx.setStorageSync('openid', res.result)
}
})
}
} else {
that.showToast("用户登录失败", 'success', 1000)
}
}
})

 

服务器端:

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
/**
* 获取用户的 openid
*
* @return \think\Response
*/

public function openid()
{

$code = input('?get.code') ? trim(input('get.code')) : '';
if (!$code) {
return json_encode(['error' => 2001, 'msg' => '获取用户登录状态失败']);
}
$openid = $this->getOpenidFromMp($code);
return json_encode(['error' => 0, 'msg' => '获取用户登录状态成功', 'result' => $openid]);
}

/**
*
* 通过code从工作平台获取openid机器access_token
* @param string $code 微信跳转回来带上的code
*
* @return openid
*/

private function getOpenidFromMp($code)
{

$url = $this->__CreateOauthUrlForOpenid($code);
//初始化curl
$ch = curl_init();
//设置超时
curl_setopt($ch, CURLOPT_TIMEOUT, $this->curl_timeout);
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);
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,结果以jason形式返回
$res = curl_exec($ch);
curl_close($ch);
//取出openid
$data = json_decode($res, true);
$this->data = $data;
$openid = $data['openid'];
return $openid;
}

/**
*
* 构造获取open和access_toke的url地址
* @param string $code,微信跳转带回的code
*
* @return 请求的url
*/

private function __CreateOauthUrlForOpenid($code)
{

$urlObj["appid"] = \WxPayConfig::APPID;
$urlObj["secret"] = \WxPayConfig::APPSECRET;
$urlObj["code"] = $code;
$urlObj["grant_type"] = "authorization_code";
$bizString = $this->ToUrlParams($urlObj);
return "https://api.weixin.qq.com/sns/oauth2/access_token?".$bizString;
}

/**
*
* 拼接签名字符串
* @param array $urlObj
*
* @return 返回已经拼接好的字符串
*/

private function ToUrlParams($urlObj)
{

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

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

 

退款回调

以上步骤之后微信支付就已经完成了,如果用户付款了后悔,找到平台客户,然后客服在商户平台进行退款,这时候微信会有退款结果通知,在商户平台的【交易中心】->【退款配置】中设置通知url

然后在服务器端完成通知方法:

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
/**
* 退款通知回调
*
* @return void
*/

public function refund()
{

$xml = file_get_contents("php://input");
$obj = $this->fromXml($xml);

if($obj['return_code'] != 'SUCCESS'){
return $this->toXML(['return_code' => 'FAIL', 'return_msg' => '回调失败']);
}

// 退款结果对重要的数据进行了加密,商户需要用商户证书与商户秘钥进行解密后才能获得结果通知的内容
$string = $obj['req_info'];
$data = base64_decode($string);
$md5_key = md5(config('wxpay.key'));
// AES解密
$iv = mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_ECB), MCRYPT_RAND);
$res = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $md5_key, $data, MCRYPT_MODE_ECB, $iv);

// 解密的字符串末尾多了 ^G 或 ^K 等杂乱字符 ,需去掉
$res_arr = explode('</root>', $res);
$req_info = $this->fromXml($res_arr[0].'</root>');

//查询订单,判断订单真实性
$order = OrderModel::where(['out_trade_no' => $req_info["out_trade_no"]])->find();
if (!$order) {
return $this->toXML(['return_code' => 'FAIL', 'return_msg' => '订单不存在']);
}

$order->status = 2;
if ($order->save()) {
// TODO::存储退款信息
return $this->toXML(['return_code' => 'SUCCESS', 'return_msg' => 'OK']);
}
return $this->toXML(['return_code' => 'SUCCESS', 'return_msg' => 'OK']);
}

/**
* 将xml转为array
*
* @param [type] $xml
* @return void
*/

public function fromXml($xml)
{

if(!$xml){
return [];
}
//将XML转为array
//禁止引用外部xml实体
libxml_disable_entity_loader(true);
$values = json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
return $values;
}

 

在通知中主要是对订单的状态变换了一下,其次解析返回的加密数据,可进一步存储退款信息。

扫码添加微信

也许您需要专业的服务,欢迎来电咨询!

132-7725-3377

湖北红点科技有限公司

FAQ常见问题解答
发票申请 设计意向
创新咨询服务
  • 数字品牌咨询 / 商业模式创新咨询 / 服务设计咨询 / 互联网产品咨询

数字营销服务
  • 品牌营销年度服务 / 产品增长营销服务 / 高端定制网站 / 创意H5定制

产品研发服务
  • 品牌电商方案 / SAAS电商 / APP定制开发 / B2C电商方案 / 微信小程序

运营保障服务
  • 年度运维托管服务 / 高并发、大流量运维服务 / 知识产权申请

扫码添加微信

也许您需要专业的服务,欢迎来电咨询!

132-7725-3377

湖北红点科技有限公司

FAQ常见问题解答
发票申请 设计意向
专属客服