php对接微信小程序支付
2018-11-17 本文已影响0人
小小的一只_01
注:个人注册微信小程序不支持微信支付功能
开发前流程:
- 1.申请商户平台账号
https://pay.weixin.qq.com/index.php/core/home/login?return_url=%2F - 微信小程序绑定已有商户号并开通微信支付
http://kf.qq.com/faq/140225MveaUz161230yqiIby.html
- 微信小程序绑定已有商户号并开通微信支付
- 3.登录商户平台对小程序授权,下载支付证书,记录商户号,支付密钥
- 4.阅读微信支付官方文档,完成接口的对接编码https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=7_3&index=1
开发流程
注意:支付按钮要加上锁避免用户多次点击
- 1.用户点击下单按钮
- 2.微信小程序用wx.login方法获取用户登录凭证code,code有效期为五分钟
- 3.微信小程序调用服务器接口,传入code,money
- 4.服务器接收到code,money后,调用微信api的code2Session方法进行登录凭证校验,获取到用户唯一标识open_id
- 5.服务器验证open_id,并创建支付订单
- 6.服务器将数据进行签名调用统一下单api,获取到prepay_id
- 7.服务器再次生成签名信息,返回微信小程序
- 8.微信小程序调用wx.requestPayment方法调用微信支付窗口
- 9.服务器异步接收支付通知结果,进行签名验证,并校验用户和返回的订单金额是否与商户的订单金额一致,修改数据库并生成日志
签名流程:
- 1.参数名ASCII码从小到大排序
- 2.格式化参数,将数组转换成key1=valve1&key2=value2...的形式
- 3.追加key,key1=valve1&key2=value2...&key=*******
- 4.md5加密
- 5.转化为大写
异步回调处理流程
支付异步回调处理流程开发流程代码
-
1.用户点击下单按钮
wxml代码
<view class="container">
<text class="name">当前选择:《不抱怨的世界》 ¥0.01</text>
<button class="pay" hidden="{{!pay}}" catch:tap="onTap">确认支付</button>
<button class="pay2" hidden="{{pay}}">确认支付</button>
</view>
wxss代码
.container {
padding: 50rpx;
}
.pay {
margin-top: 30rpx;
color: #fff;
background-color: #1fb922;
}
.pay2 {
margin-top: 30rpx;
color: #fff;
background-color: #dedede;
}
js代码
Page({
/**
* 页面的初始数据
*/
data: {
pay: true,
},
onTap () {
let _self = this;
_self._togglePay();
wx.login({
success(res) {
if (res.code) {
//发起网络请求
wx.request({
url: 'http://api.djny.com/v1/pay/pay-sign',
method: "POST",
data: {
code: res.code,
money:0.01
},
header: {
"content-type": "application/x-www-form-urlencoded"
},
success(res) {
var params = res.data.data;
wx.requestPayment({
'timeStamp': String(params['timeStamp']),
'nonceStr': params['nonceStr'],
'package': params['package'],
'signType': params['signType'],
'paySign': params['paySign'],
'success': function (res) {
wx.showToast({
title: '支付成功',
icon: 'success',
duration: 2000
})
_self._togglePay();
},
'fail': function (res) {
wx.showToast({
title: '支付失败',
icon: 'none',
duration: 2000
})
_self._togglePay();
},
})
}
})
} else {
this._togglePay();
console.log('登录失败!' + res.errMsg)
}
}
})
},
_togglePay() {
this.setData({
pay: !this.data.pay
});
}
})
-
2.基本配置
'wx_pay' => [
'app_id' => 'wx2**********2965c', //小程序appid
'app_secret' => '55913********************6574c6b', //小程序secret
'mch_id' => '15******71', //商户平台商户号
'key' => 'sQm*******************aVQkca', //商户平台密钥key
'notify_url' => 'http://www.test.cn/v1/notify-pay', //支付异步回调地址
'name' => '测试支付', //商品简单描述
]
-
3.工具类方法
/**
* curl get
* @param $url 请求路径
* @param array $params 参数
* @return mixed
*/
public static function get($url, array $params)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url . "?" . http_build_query($params));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // 获取数据返回
curl_setopt($ch, CURLOPT_BINARYTRANSFER, true); // 在启用 CURLOPT_RETURNTRANSFER 时候将获取数据返回
$output = curl_exec($ch);
curl_close($ch);
return $output;
}
/**
* curl post xml
* @param $xml 参数
* @param $url 请求地址
* @param int $second 设置超时
* @return mixed
*/
public static function postXml($xml, $url, $second = 60)
{
$ch = curl_init();
//设置超时
curl_setopt($ch, CURLOPT_TIMEOUT, $second);
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);
//post提交方式
curl_setopt($ch, CURLOPT_POST, TRUE);
curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 20);
curl_setopt($ch, CURLOPT_TIMEOUT, 40);
set_time_limit(0);
//运行curl
$data = curl_exec($ch);
curl_close($ch);
return $data;
}
/**
* array to xml
* @param $arr
* @return string
*/
public static function arrayToXml($arr)
{
$xml = "<xml>";
foreach ($arr as $key => $val) {
if (is_array($val)) {
$xml .= "<" . $key . ">" . _arrayToXml($val) . "</" . $key . ">";
} else {
$xml .= "<" . $key . ">" . $val . "</" . $key . ">";
}
}
$xml .= "</xml>";
return $xml;
}
/**
* xml to array
* @param $xml
* @return mixed
*/
public static function xmlToArray($xml)
{
//禁止引用外部xml实体
libxml_disable_entity_loader(true);
$xmlstring = simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA);
$val = json_decode(json_encode($xmlstring), true);
return $val;
}
/**
* 生产随机字符串 默认32位
* @param int $length
* @return string
*/
public static function randStr($length = 32)
{
$chars = "abcdefghijklmnopqrstuvwxyz0123456789";
$str = "";
for ($i = 0; $i < $length; $i++) {
$str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
}
return $str;
}
/**
* 格式化参数 array to key1=valve1&key2=value2
* @param $params
* @param $url_encode
* @return string
*/
public static function formatParams($params, $url_encode)
{
if (!$params) {
return '';
}
$paramUrl = "";
ksort($params);
foreach ($params as $k => $v) {
if ($url_encode) {
$v = urlencode($v);
}
$paramUrl .= $k . "=" . $v . "&";
}
if (strlen($paramUrl) > 0) {
$paramUrl = substr($paramUrl, 0, strlen($paramUrl) - 1);
}
return $paramUrl;
}
-
4.控制层代码
/**
* 获取支付签名
*/
public function actionPaySign()
{
$code = Yii::$app->request->post('code');
$money = Yii::$app->request->post('money');
if (!$code || $money <= 0) {
$this->render_json(self::STATUS_CODE_CLIENT_ERROR, '参数错误');
}
$wxPay = new WxPay();
$openId = $wxPay->getOpenId($code);
if (!$openId) {
$this->render_json(self::STATUS_CODE_CLIENT_ERROR, $wxPay->getError());
}
$userInfo = WxMember::findOne(['openid' => $openId]);
if (!$userInfo) {
$this->render_json(self::STATUS_CODE_CLIENT_ERROR, '用户信息获取失败');
}
$wxPayOrder = new WxPayorder();
if (!$wxPayOrder->createPayOrder($userInfo, $money)) {
$this->render_json(self::STATUS_CODE_CLIENT_ERROR, $wxPayOrder->getFirstError('id'));
}
$paySign = $wxPay->paySign($openId, $wxPayOrder->out_trade_no, $wxPayOrder->order_amount);
if (!$paySign) {
$this->render_json(self::STATUS_CODE_CLIENT_ERROR, $wxPay->getError());
}
$this->render_json(self::STATUS_CODE_SUCCESS, '成功', $paySign);
}
/**
* 小程序支付异步回调
*/
public function actionNotifyPay()
{
$xml = file_get_contents('php://input', 'r');
$resData = Utility::xmlToArray($xml);
$wxPay = new WxPay();
if ($wxPay->checkNotifySign($resData)) {
$wxPayOrder = new WxPayorder();
$result = $wxPayOrder->updateNotifyPayOrder($resData['return_code'], $resData['openid'], $resData['out_trade_no'], $resData['total_fee'] / 100, $resData['transaction_id'], $resData);
if ($result) {
$resStr = $wxPay->notifyReturnSuccess();
} else {
$resStr = $wxPay->notifyReturnFail($wxPayOrder->getFirstError('id'));
}
} else {
$resStr = $wxPay->notifyReturnFail('签名验证失败');
}
echo $resStr;
}
-
5.微信支付接口类
class WxPay
{
/**
* 小程序appid
* @var string
*/
private $_appId;
/**
* 小程序secret
* @var string
*/
private $_appSecret;
/**
* 商户平台商户id
* @var string
*/
private $_mchId;
/**
* 商户平台密钥key
* @var string
*/
private $_key;
/**
* 支付回掉地址
* @var string
*/
private $_notifyUrl;
/**
* 获取用户唯一标识open_id api 接口地址
* @var string
*/
private $_code2SessionApiUrl;
/**
* 统一下单api 接口地址
* @var string
*/
private $_unifiedOrderApiUrl;
/**
* 错误信息
* @var string
*/
public $errorInfo;
/**
* WxPay constructor.
*/
public function __construct()
{
$this->_appId = Yii::$app->params['wx_pay']['app_id'];
$this->_appSecret = Yii::$app->params['wx_pay']['app_secret'];
$this->_mchId = Yii::$app->params['wx_pay']['mch_id'];
$this->_key = Yii::$app->params['wx_pay']['key'];
$this->_notifyUrl = Yii::$app->params['wx_pay']['notify_url'];
$this->_code2SessionApiUrl = 'https://api.weixin.qq.com/sns/jscode2session';
$this->_unifiedOrderApiUrl = 'https://api.mch.weixin.qq.com/pay/unifiedorder';
}
/**
* 添加错误信息
* @param $err
*/
public function setError($err)
{
$this->errorInfo = $err;
}
/**
* 获取错误信息
* @return string
*/
public function getError()
{
return $this->errorInfo;
}
/**
* 获取用户唯一标识open_id
* @param $code
* @return string
*/
public function getOpenId($code)
{
if (!$code) {
return '';
}
$params = [
'appid' => $this->_appId,
'secret' => $this->_appSecret,
'js_code' => $code,
'grant_type' => 'authorization_code'
];
$resJson = Curl::get($this->_code2SessionApiUrl, $params);
$res = json_decode($resJson);
if (isset($res->errcode)) {
$this->errorInfo = '获取用户open_id失败';
return '';
}
return $res->openid;
}
/**
* 获取支付签名
* @param $open_id
* @param $order_no
* @param $money
* @return array
*/
public function paySign($open_id, $order_no, $money)
{
if (!$open_id || !$order_no || !$money) {
$this->setError('参数错误');
return [];
}
$prepay_id = $this->_unifiedorder($open_id, $order_no, $money);
if (!$prepay_id) {
return [];
}
$params = array(
'appId' => $this->_appId,
'timeStamp' => time(),
'nonceStr' => Utility::randStr(),
'package' => 'prepay_id=' . $prepay_id,
'signType' => 'MD5'
);
$params['paySign'] = $this->_getSign($params);
return $params;
}
/**
* 异步签名验证
* @param $data
* @return bool
*/
public function checkNotifySign($data)
{
if (!$data) {
return false;
}
$sign = $data['sign'];
unset($data['sign']);
if ($sign == $this->_getSign($data)) {
return true;
}
return false;
}
/**
* 异步回调处理成功时返回内容
* @param $msg
* @return string
*/
public function notifyReturnSuccess($msg = 'OK')
{
return '<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[' . $msg . ']]></return_msg></xml>';
}
/**
* 异步回调处理失败时返回内容
* @param $msg
* @return string
*/
public function notifyReturnFail($msg = 'FAIL')
{
return '<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[' . $msg . ']]></return_msg></xml>';
}
/**
* 统一下单
* @param $open_id
* @param $order_no
* @param $money
* @return string
*/
private function _unifiedOrder($open_id, $order_no, $money)
{
$params = [
'appid' => $this->_appId,
'mch_id' => $this->_mchId,
'nonce_str' => Utility::randStr(),
'body' => Yii::$app->params['wx_pay']['name'], //商品简单描述
'out_trade_no' => $order_no, //商户系统内部订单号
'total_fee' => $money * 100, //订单总金额,单位为分
'spbill_create_ip' => Utility::getClientIp(), //用户端ips
'notify_url' => $this->_notifyUrl, //通知地址
'trade_type' => 'JSAPI', //交易类型
'openid' => $open_id //用户标识
];
$params['sign'] = $this->_getSign($params);
$xmlData = Utility::arrayToXml($params);
$returnXml = Curl::postXml($xmlData, $this->_unifiedOrderApiUrl);
$returnArr = Utility::xmlToArray($returnXml);
if ($returnArr['return_code'] == 'FAIL') {
$this->setError($returnArr['return_msg']);
return '';
}
return $returnArr['prepay_id'];
}
/**
* 获取签名
* @param $params
* @return string
*/
private function _getSign($params)
{
if (!$params) {
return '';
}
//step1: 排序
ksort($params);
//step2:格式化参数
$paramUrl = Utility::formatParams($params, false);
//step3:追加key
$paramUrl = $paramUrl . '&key=' . $this->_key;
//step4: md5加密
$paramUrl = md5($paramUrl);
//step5:转化为大写
$sign = strtoupper($paramUrl);
return $sign;
}
}
-
6.模型层
class WxPayorder extends \common\models\base\BaseMain
{
/**
* 充值类型
*/
const TYPE_XCX = 1;
const TYPE_GZH = 2;
/**
* 充值状态: 1未审核 2失败 3成功 4失效 5其他 6处理中
*/
const STATUS_WAITING = 1;
const STATUS_FAILED = 2;
const STATUS_SUCCESS = 3;
const STATUS_LOSE = 4;
const STATUS_OTHER = 5;
const STATUS_PROCESSING = 6;
/**
* 充值时最大金额 单位 元
*/
const MONEY_MAX_RECHARGE = 5000;
/**
* 账户最大金额 单位 元
*/
const MONEY_MAX_ACCOUNT = 6000;
/**
* @inheritdoc
*/
public static function tableName()
{
return '{{%wx_payorder}}';
}
/**
* @inheritdoc
*/
public function rules()
{
return [
[['memberid', 'out_trade_no', 'type', 'status', 'order_amount', 'createtime'], 'required', 'on' => 'insert'],
[['status', 'operation_time'], 'required', 'on' => 'updateStatus'],
[['payamount', 'remark', 'operation_time'], 'required', 'on' => 'updatePaySuccess'],
[['remark', 'operation_time'], 'required', 'on' => 'updatePayFail'],
[['memberid', 'type', 'status', 'operation_time', 'createtime'], 'integer'],
[['payamount', 'order_amount'], 'number'],
[['type', 'status'], 'required'],
[['out_trade_no'], 'string', 'max' => 50],
[['remark'], 'string', 'max' => 100],
];
}
/**
* @inheritdoc
*/
public function attributeLabels()
{
return [
'id' => 'ID',
'memberid' => '会员ID',
'out_trade_no' => '订单号',
'payamount' => '充电金额',
'type' => '终端充值类型(1小程序2公众号)',
'status' => '充值状态(1未审核2失败3成功4失效5其他)',
'order_amount' => '订单充值金额',
'remark' => '备注',
'operation_time' => '响应操作时间(充值成功或失败时需要更新时间)',
'createtime' => '创建时间',
];
}
/**
* before validate
* @return bool
*/
public function beforeValidate()
{
parent::afterValidate();
if ($this->getIsNewRecord()) {
$this->createtime = time();
} else {
$this->operation_time = time();
}
return true;
}
/**
* 添加订单
* @param $userInfo 用户对象
* @param $money
* @return bool
*/
public function createPayOrder($userInfo, $money)
{
$moneyArr = [0.01, 10, 50, 100, 200, 500, 1000];
if (!in_array($money, $moneyArr)) {
$this->addError('id', '充值金额有误');
return false;
}
if ($userInfo->balance >= self::MONEY_MAX_RECHARGE) {
$this->addError('id', '账户金额已经上限,不能再充值了');
return false;
}
if ($userInfo->balance + $money >= self::MONEY_MAX_ACCOUNT) {
$this->addError('id', '充值金额已经上限');
return false;
}
$this->memberid = $userInfo->id;
$this->out_trade_no = $this->_getOrderId($userInfo->id);
$this->type = self::TYPE_XCX;
$this->status = self::STATUS_WAITING;
$this->order_amount = $money;
$this->setScenario('insert');
if (!$this->save()) {
$this->addError('id', '创建订单失败');
return false;
}
return true;
}
/**
* 支付成功修改数据库
* @param $result 支付状态
* @param $open_id open_id
* @param $out_trade_no 客户订单号
* @param $money 金额
* @param $wx_trade_no 微信支付订单号
* @param $data
* @return bool
* @throws \Exception
*/
public function updateNotifyPayOrder($result, $open_id, $out_trade_no, $money, $wx_trade_no, $data)
{
//记录异步通知日志
$wxPayOrderLog = new WxPayorderLog();
$wxPayOrderLog->setScenario('insert');
$wxPayOrderLog->open_id = $open_id;
$wxPayOrderLog->type = WxPayorderLog::TYPE_XCX;
$wxPayOrderLog->out_trade_no = $out_trade_no;
$wxPayOrderLog->money = $money;
$wxPayOrderLog->wx_trade_no = $wx_trade_no;
$wxPayOrderLog->remark = '异步请求';
$wxPayOrderLog->remark_back = json_encode($data);
if (!$wxPayOrderLog->save()) {
$this->addError('id', '订单日志保存失败');
return false;
}
//订单查询
$wxPayOrder = WxPayorder::findOne(['out_trade_no' => $out_trade_no]);
if (!$wxPayOrder) {
$this->addError('id', '订单不存在');
return false;
}
//如果订单支付状态为成功直接返回
if ($wxPayOrder->status == WxPayorder::STATUS_SUCCESS) {
return true;
}
//订单支付状态为未处理
if ($wxPayOrder->status == WxPayorder::STATUS_WAITING) {
//修订订单状态为处理中
$wxPayOrder->status = WxPayorder::STATUS_PROCESSING;
$wxPayOrder->setScenario('updateStatus');
if (!$wxPayOrder->save()) {
$this->addError('id', '订单状态修改失败');
return false;
}
//验证用户
$wxMember = WxMember::findOne(['openid' => $open_id]);
if (!$wxMember) {
$this->addError('id', '用户不存在');
return false;
}
if ($wxMember->id != $wxPayOrder->memberid) {
$this->addError('id', '用户信息有误');
return false;
}
//验证金额
if ($wxPayOrder->order_amount != $money) {
$this->addError('id', '订单金额有误');
return false;
}
//判断订单支付状态
if ($result == 'SUCCESS') {
//修改订单状态为成功
$wxPayOrder->status = self::STATUS_SUCCESS;
$wxPayOrder->setScenario('updateStatus');
if (!$wxPayOrder->save()) {
$this->addError('id', '订单状态修改失败');
return false;
}
//修改订单信息
$wxPayOrder->payamount = $wxPayOrder->order_amount;
$wxPayOrder->remark = '支付成功';
$wxPayOrder->setScenario('updatePaySuccess');
if (!$wxPayOrder->save()) {
$this->addError('id', '订单信息保存失败');
return false;
}
//保存账户余额表
$wxMember->balance = $wxMember->balance + $wxPayOrder->payamount;
if (!$wxMember->save()) {
$this->addError('id', '用户账户信息保存失败');
return false;
}
return true;
} else {
//保存订单表
$wxPayOrder->status = self::STATUS_FAILED;
$wxPayOrder->remark = '支付失败';
$wxPayOrder->setScenario('updatePayFail');
if (!$wxPayOrder->save()) {
$this->addError('id', '数据错误');
return false;
}
}
}
$this->addError('id', '订单处理失败');
return false;
}
/**
* 获取用户唯一订单号 最大32位
* @param $userId
* @return string
*/
private function _getOrderId($userId)
{
return 'XCX' . (time() . $userId . rand(1000, 9000) . rand(1000, 9000));
}
}