Laravel 微信小程式支付
阿新 • • 發佈:2019-01-06
近期專案,有一個涉及到微信小程式支付功能,踩了不少坑,記錄一哈子。
開發環境: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;
生成預支付訂單後,返回給前端的時候需要再次簽名,切記.