近期项目,有一个涉及到微信小程序支付功能,踩了不少坑,记录一哈子。
开发环境:php 7.0.12 + Apache
框架:Laravel5.3
微信官方的流程示意图:
作为phper,要做的部分就是用前端传递过来的code换取openid,生成商户订单,再调用支付统一下单API换取预付单信息,将预付单信息再次签名后返回给前端。
code换取openid:
/**
* code换取openid
* @param $code
* @return bool
*/
function getOpenID($code)
{
//从配置文件读取小程序的appid&secret
$appid = config('miniapp_id');
$secret = config('mini_secret');
$url = "https://api.weixin.qq.com/sns/jscode2session?appid=$appid&secret=$secret&js_code=$code&grant_type=authorization_code";
$weixin = file_get_contents($url);//通过code换取网页授权access_token
$jsondecode = json_decode($weixin); //对JSON格式的字符串进行编码
$array = get_object_vars($jsondecode);//转换成数组
if (!isset($array['openid'])) {
throw new \Exception('code错误T^T');
}
$openid = $array['openid'];//输出openid
return $openid;
}
获取随机字符串:
/**
* 产生随机字符串,不长于32位
* @param int $length
* @return string 产生的随机字符串
*/
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;
}
xml与array转换:
/**
* 将一个数组转换为 XML 结构的字符串
* @param array $arr 要转换的数组
* @param int $level 节点层级, 1 为 Root.
* @return string XML 结构的字符串
*/
function arraytoXml($arr, $level = 1)
{
$s = $level == 1 ? "<xml>" : '';
foreach ($arr as $tagname => $value) {
if (is_numeric($tagname)) {
$tagname = $value['TagName'];
unset($value['TagName']);
}
if (!is_array($value)) {
$s .= "<{$tagname}>" . (!is_numeric($value) ? '<![CDATA[' : '') . $value . (!is_numeric($value) ? ']]>' : '') . "</{$tagname}>";
} else {
$s .= "<{$tagname}>" . $this->arraytoXml($value, $level + 1) . "</{$tagname}>";
}
}
$s = preg_replace("/([\x01-\x08\x0b-\x0c\x0e-\x1f])+/", ' ', $s);
return $level == 1 ? $s . "</xml>" : $s;
}
/**
* 将xml转为array
* @param string $xml xml字符串
* @return array 转换得到的数组
*/
function xmltoArray($xml)
{
//禁止引用外部xml实体
libxml_disable_entity_loader(true);
$result = json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
return $result;
}
生成签名:
/**
* 生成签名
* @param $data
* @return string
*/
function makeSign($data)
{
//获取微信支付秘钥
$key = config('key');
//去空
$data = array_filter($data);
//签名步骤一:按字典序排序参数
ksort($data);
$string_a = http_build_query($data);
$string_a = urldecode($string_a);
//签名步骤二:在string后加入KEY
$string_sign_temp = $string_a . "&key=$key";
//签名步骤三:MD5加密
$sign = md5($string_sign_temp);
//签名步骤四:所有字符转为大写
return strtoupper($sign);
}
curl发送请求:
/**
* 微信支付发起请求
* @param $url
* @param $xmldata
* @param int $second
* @param array $aHeader
* @return bool|mixed
*/
protected function curl_post_ssl($url, $xmldata, $second = 30, $aHeader = array())
{
$ch = curl_init();
//超时时间
curl_setopt($ch, CURLOPT_TIMEOUT, $second);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
//这里设置代理,如果有的话
//curl_setopt($ch,CURLOPT_PROXY, '10.206.30.98');
//curl_setopt($ch,CURLOPT_PROXYPORT, 8080);
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
if (count($aHeader) >= 1) {
curl_setopt($ch, CURLOPT_HTTPHEADER, $aHeader);
}
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $xmldata);
$data = curl_exec($ch);
if ($data) {
curl_close($ch);
return $data;
} else {
$error = curl_errno($ch);
echo "call faild, errorCode:$error\n";
curl_close($ch);
return false;
}
}
预支付:
/**
* @param $unifiedorder
* @return array
* @throws \Exception
*/
function prepay($unifiedorder)
{
$unifiedorder['sign'] = $this->makeSign($unifiedorder);
$xmldata = $this->arraytoXml($unifiedorder);
$url = config('pay_url');
$res = $this->curl_post_ssl($url, $xmldata);
if (!$res) {
throw new \Exception('链接Wechat服务器失败 (キ`゚Д゚´)');
}
$content = $this->xmltoArray($res);
if (strval($content['return_code']) == 'FAIL') {
throw new \Exception('生成签名数据失败 ( ̄□ ̄;)');
}
//拼接小程序的接口数据
$result = [
'appId' => strval($content['appid']),
'timeStamp' => time(),
'nonceStr' => $this->getNonceStr(),
'package' => 'prepay_id=' . strval($content['prepay_id']),
'signType' => 'MD5',
];
//加密签名
$result['paySign'] = $this->makeSign($resData);
return $result;
}
业务逻辑部分:
function WeChatOrder(Request $request)
{
$code = $request->get('code');
$payHelper = new WechatHelper();
$openId = $payHelper->getOpenID($code);
try {
DB::beginTransaction();
//生成业务订单
$order = [
'orderNum' => 123456789,
'price' => 2333,
...
];
Order::create($order);
$unifiedorder = [
'openid' => $openId,
'appid' => config('miniapp_id'),
'mch_id' => config('mch_id'),
'nonce_str' => $payHelper->getNonceStr(),//获取随机字符串
'body' => '商品讯息',
'out_trade_no' => $order['orderNum'],
'total_fee' => $order['price'],//单位为"分"
'spbill_create_ip' => $request->ip(),
'notify_url' => config('notify_url'),
'trade_type' => 'JSAPI',//小程序均为"JSAPI"
];
//再次签名返回
$signature = $payHelper->prepay($unifiedorder);
DB::commit();
} catch (QueryException $e) {
DB::rollback();
throw new \Exception('提交订单失败 T^T');
} catch (\Exception $e) {
DB::rollback();
throw new \Exception('出错了 T^T');
}
//返回$signature给前端
}
PS:
小程序的appid与secret与公众号的是不同的;
请确保各项配置无误,我会说因为需求方给的key错误耽误了一天时间ヽ(`Д´)ノ︵ ┻━┻ ┻━┻;
一般错误都会告知原因,个人猜测因为key比较私密所以因为key错误,返回的讯息一直都是 “签名错误”,并未给出具体原因;
微信提供了签名校验,可以设置好参数去校验下自己的签名生成算法是否OK;
生成预支付订单后,返回给前端的时候需要再次签名,切记.
参考链接:
微信小程序支付流程
统一支付下单API参数