Laravel 微信小程序支付

近期项目,有一个涉及到微信小程序支付功能,踩了不少坑,记录一哈子。

开发环境:php 7.0.12 + Apache
框架:Laravel5.3
微信官方的流程示意图:

Laravel 微信小程序支付_第1张图片

作为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 ? "" : '';
    foreach ($arr as $tagname => $value) {
        if (is_numeric($tagname)) {
            $tagname = $value['TagName'];
            unset($value['TagName']);
        }
        if (!is_array($value)) {
            $s .= "<{$tagname}>" . (!is_numeric($value) ? ' : '') . $value . (!is_numeric($value) ? ']]>' : '') . "";
        } else {
            $s .= "<{$tagname}>" . $this->arraytoXml($value, $level + 1) . "";
        }
    }
    $s = preg_replace("/([\x01-\x08\x0b-\x0c\x0e-\x1f])+/", ' ', $s);
    return $level == 1 ? $s . "" : $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参数

你可能感兴趣的:(项目总结)