JSAPI支付v3

开发指引:https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_3.shtml

主要步骤

1、用户下单发起支付,商户可通过JSAPI下单创建支付订单。

2、商户可在微信浏览器内通过JSAPI调起支付API调起微信支付,发起支付请求。

3、用户支付成功后,商户可接收到微信支付支付结果通知支付结果通知API

4、商户在没有接收到微信支付结果通知的情况下需要主动调用查询订单API查询支付结果。


接入准备

1、设置v3密钥(用作:下载平台证书,解密支付回调)

2、生成商户证书并下载证书  # 需要用到证书工具(https://kf.qq.com/faq/161222NneAJf161222U7fARv.html),注:证书只能生成的时候下载,丢失后只能重新生成

3、配置商户平台支付目录

4、配置公众号JS SDK 域名

5、下载平台证书:https://github.com/wechatpay-apiv3/wechatpay-php/blob/main/bin/README.md


SDK接入

接入方式说明:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay-1.shtml

SDK:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay6_0.shtml

安装

composer require wechatpay/wechatpay-guzzle-middleware

composer require wechatpay/wechatpay


核心代码

namespace App\Helpers;

use GuzzleHttp\Exception\RequestException;
use Illuminate\Support\Facades\Log;
use WeChatPay\Crypto\AesGcm;
use WeChatPay\Crypto\Rsa;
use WeChatPay\Formatter;
use WechatPay\GuzzleMiddleware\WechatPayMiddleware;
use WechatPay\GuzzleMiddleware\Util\PemUtil;
use GuzzleHttp\HandlerStack;
use GuzzleHttp;
 
class WxPayHelper
{
    protected $client;
    protected $mchid = "";// 商户号
    protected $v3Key = "";// 在商户平台上设置的APIv3密钥
    protected $sn = ""; // 商户API证书序列号
    protected $appid;
    protected $notify_url = ""; //支付回调通知

    public function __construct()
    {
        $this->appid = env('WX_APPID');
        // 商户相关配置
        $merchantId = $this->mchid;
        $merchantSerialNumber = $this->sn;
        $merchantPrivateKey = PemUtil::loadPrivateKey(base_path() . '/wechatpay/cert/apiclient_key.pem'); // 商户私钥文件路径

        // 微信支付平台配置
        $wechatpayCertificate = PemUtil::loadCertificate(base_path() . '/wechatpay/cert/wechatpay_46C3853B00D240EA93FC8FE2FADDFAA9763F6DFE.pem'); // 微信支付平台证书文件路径

        // 构造一个WechatPayMiddleware
        $wechatpayMiddleware = WechatPayMiddleware::builder()
            ->withMerchant($merchantId, $merchantSerialNumber, $merchantPrivateKey) // 传入商户相关配置
            ->withWechatPay([$wechatpayCertificate]) // 可传入多个微信支付平台证书,参数类型为array
            ->build();

        // 将WechatPayMiddleware添加到Guzzle的HandlerStack中
        $stack = GuzzleHttp\HandlerStack::create();
        $stack->push($wechatpayMiddleware, 'wechatpay');

        // 创建Guzzle HTTP Client时,将HandlerStack传入,接下来,正常使用Guzzle发起API请求,WechatPayMiddleware会自动地处理签名和验签
        $this->client = new GuzzleHttp\Client(['handler' => $stack]);
    }

    //下单
    //预支付交易会话标识。用于后续接口调用中使用,该值有效期为2小时
    //body = {"prepay_id":"wx101024560400364d661f85a39172860000"}
    public function order($openid, $amount, $out_trade_no, $description)
    {
        $body = [
            "appid" => $this->appid,//应用ID
            "mchid" => $this->mchid,//商户号
            //商户系统的订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一
            "out_trade_no" => $out_trade_no,
            //商品描述
            "description" => $description,
            //通知地址,异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。 公网域名必须为https,如果是走专线接入,使用专线NAT IP或者私有回调域名可使用http
            "notify_url" => $this->notify_url,
        ];
        $body['payer'] = [
            "openid" => $openid,// 下单用户的Openid
        ];
        $body['amount'] = [
            "total" => $amount,//订单总金额,单位为分。
            "currency" => "CNY",//CNY:人民币,境内商户号仅支持人民币。
        ];

        $jsapi = 'https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi';//下单接口
        try {
            $resp = $this->client->request('POST', $jsapi, [
                'json' => $body,
                'headers' => ['Accept' => 'application/json']
            ]);
            $statusCode = $resp->getStatusCode();
            $content = $resp->getBody()->getContents();
            Log::channel('order_debug')->info($statusCode . '@' . $content);
            if (200 == $statusCode) {
                return json_decode($content, true);
            }
            return ['error' => $statusCode];
        } catch (RequestException $e) {
            $error = $e->getMessage();
            if ($e->hasResponse()) {
                $error .= "failed,resp code = " . $e->getResponse()->getStatusCode() . " return body = " . $e->getResponse()->getBody() . "\n";
            }
            Log::channel('order_debug')->error($error);
            return ['error' => $error];
        }
    }

    //支付签名
    public function jsApiSign($package)
    {
        $merchantPrivateKeyFilePath = 'file://' . base_path() . '/wechatpay/cert/apiclient_key.pem';
        $merchantPrivateKeyInstance = Rsa::from($merchantPrivateKeyFilePath);

        $params = [
            'appId' => $this->appid,
            'timeStamp' => (string)Formatter::timestamp(),
            'nonceStr' => Formatter::nonce(),
            'package' => $package,
        ];
        $paySign = Rsa::sign(Formatter::joinedByLineFeed(...array_values($params)), $merchantPrivateKeyInstance);
        $params['paySign'] = $paySign;
        $params['signType'] = 'RSA';
        return $params;
    }

    //查询订单
    public function queryOrder($order_sn)
    {
        //$api = "https://api.mch.weixin.qq.com/v3/pay/transactions/id/$order_sn?mchid=" . $this->mchid; //利用微信订单号查询
        $api = "https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/$order_sn?mchid=" . $this->mchid; //利用商户订单号查询
        try {
            $resp = $this->client->request('GET', $api, ['headers' => ['Accept' => 'application/json']]);
            $statusCode = $resp->getStatusCode();
            if (200 != $statusCode) {
                return ['error' => $statusCode];
            }
            $content = $resp->getBody()->getContents();
            return json_decode($content, true);
        } catch (RequestException $e) {
            $error = $e->getMessage();
            if ($e->hasResponse()) {
                $error .= "failed,resp code = " . $e->getResponse()->getStatusCode() . " return body = " . $e->getResponse()->getBody() . "\n";
            }
            return ['error' => $error];
        }
    }

    //退款
    public function refund($refund, $out_refund_no, $order)
    {
        /*
        注意:
        1、交易时间超过一年的订单无法提交退款
        2、微信支付退款支持单笔交易分多次退款(不超50次),多次退款需要提交原支付订单的商户订单号和设置不同的退款单号。申请退款总金额不能超过订单金额。 一笔退款失败后重新提交,请不要更换退款单号,请使用原商户退款单号
        3、错误或无效请求频率限制:6qps,即每秒钟异常或错误的退款申请请求不超过6次
        4、每个支付订单的部分退款次数不能超过50次
        5、如果同一个用户有多笔退款,建议分不同批次进行退款,避免并发退款导致退款失败
        6、申请退款接口的返回仅代表业务的受理情况,具体退款是否成功,需要通过退款查询接口获取结果
        7、一个月之前的订单申请退款频率限制为:5000/min
        8、同一笔订单多次退款的请求需相隔1分钟
        * */

        $api = 'https://api.mch.weixin.qq.com/v3/refund/domestic/refunds';
        $body = [
            'out_trade_no' => $order['order_sn'],
            'out_refund_no' => $out_refund_no,//退款单号
            'amount' => [
                'refund' => $refund,//退款金额
                'total' => $order['amount'],//原订单金额
                'currency' => 'CNY',
            ],
        ];

        try {
            $resp = $this->client->request('POST', $api, [
                'json' => $body,
                'headers' => ['Accept' => 'application/json']
            ]);
            $statusCode = $resp->getStatusCode();
            $content = $resp->getBody()->getContents();
            return json_decode($content, true);
        } catch (RequestException $e) {
            $error = $e->getMessage();
            if ($e->hasResponse()) {
                $error .= "failed,resp code = " . $e->getResponse()->getStatusCode() . " return body = " . $e->getResponse()->getBody() . "\n";
            }
            return ['error' => $error];
        }
    }

    //支付回调
    public function payNotify()
    {
        $headers = getallheaders();
        $inWechatpaySignature = $headers['Wechatpay-Signature'] ?? '';
        $inWechatpayTimestamp = $headers['Wechatpay-Timestamp'] ?? '';
        $inWechatpaySerial = $headers['Wechatpay-Serial'] ?? '';
        $inWechatpayNonce = $headers['Wechatpay-Nonce'] ?? '';
        $inBody = file_get_contents('php://input');

        // 检查通知时间偏移量,允许5分钟之内的偏移
        $timeOffsetStatus = 300 >= abs(Formatter::timestamp() - (int)$inWechatpayTimestamp);
        if (!$timeOffsetStatus) {
            Log::channel('order_debug')->error('超时');
            trigger_error('超时');
        }

        // 根据通知的平台证书序列号,查询本地平台证书文件,
        $path = 'file://' . base_path() . '/wechatpay/cert/wechatpay_46C3853B00D240EA93FC8FE2FADDFAA9763F6DFE.pem';
        $platformPublicKeyInstance = Rsa::from($path, Rsa::KEY_TYPE_PUBLIC);
        // 构造验签名串
        $verifiedStatus = Rsa::verify(Formatter::joinedByLineFeed($inWechatpayTimestamp, $inWechatpayNonce, $inBody), $inWechatpaySignature, $platformPublicKeyInstance);
        if (!$verifiedStatus) {
            Log::channel('order_debug')->error('验证失败');
            trigger_error('验证失败');
        }

        try {
            Log::channel('order_debug')->notice('验证通过,解密数据');
            // 转换通知的JSON文本消息为PHP Array数组
            $inBodyArray = (array)json_decode($inBody, true);
            [
                'resource' => [
                    'ciphertext' => $ciphertext,
                    'nonce' => $nonce,
                    'associated_data' => $aad
                ]
            ] = $inBodyArray;
            // 解密加密文本消息
            $inBodyResource = AesGcm::decrypt($ciphertext, $this->v3Key, $nonce, $aad);
            // 把解密后的文本转换为PHP Array数组
            return (array)json_decode($inBodyResource, true);
        } catch (\Throwable $e) {
            Log::channel('order_debug')->error($e->getMessage());
            trigger_error($e->getMessage());
        }
        return null;
    }
}

发表评论

对待女朋友就像对待代码一样,永远没有错,就算你错了,也是我来改。