1. 程式人生 > >Laravel 微信小程式支付

Laravel 微信小程式支付

近期專案,有一個涉及到微信小程式支付功能,踩了不少坑,記錄一哈子。

開發環境: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;
生成預支付訂單後,返回給前端的時候需要再次簽名,切記.