小程序微信支付和退款2024版

一、概述

小程序支付需要后端配合,这里以后端php的ThinkPhP6.0框架为例,其他语言的可以参考,流程都是一样的。商户类型有服务商和直连商户两种,对接方式不一样,这里是直连商户。

二、相关文档地址

//微信支付文档
https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_8_1.shtml

//官网支付SDK(php版本),官方还有java和go语言版本,其他语言就得自己去网上找民间大佬自制版本了
https://github.com/wechatpay-apiv3/wechatpay-php

//小程序调起支付文档
https://developers.weixin.qq.com/miniprogram/dev/api/payment/wx.requestPayment.html

三、平台配置

1、小程序微信认证,每年需要缴费,如果同主体其他公众号已经认证缴费了,可以关联自动认证,就不需要再缴费了。

在这里插入图片描述

2、小程序平台关联商户号

在这里插入图片描述

3、小程序平台配置接口地址

在这里插入图片描述

4、商户平台配置密钥和证书,因为我们用的APIv3接口,所以APIv2密钥不用管。证书申请按照页面提示流程来就行了,这里就不截图了,可以看官网文档(https://kf.qq.com/faq/161222NneAJf161222U7fARv.html),注意证书申请之后,点击"管理证书",查看证书序列号,后面代码中要用。

在这里插入图片描述

在这里插入图片描述

四、引入SDK

composer require wechatpay/wechatpay

五、生成微信支付平台证书

有好几种方式,这里介绍一种通过接口生成,然后手动复制粘贴到文件中的方式,其他方式可以参考官方文档。

    //获取微信平台证书
    public function getCert()
    {
        WxPayService::certificates(); //这个类的代码在下面
    }

通过网页访问这个接口,手动创建cert.pem文件,然后将证书部分复制粘贴到cert.pem中

在这里插入图片描述

到目前为止,你应该有3个证书了,其中2个是通过商户平台配置下载的,再加上上面这,一共三个,代码中会用到的是apiclient_key.pem和cert.pem,apiclient_cert.pem暂时没用到

在这里插入图片描述

六、对接微信支付

有如下api,但是不是每个都需要对接,如果仅仅是对接支付的话,小程序下单->小程序调起支付->支付通知,这个流程就够了
在这里插入图片描述

1、小程序下单

    //小程序微信支付
    public function xcxWxPay()
    {
        $payData = [
            'out_trade_no' => '唯一的随机字符串',
            'description' => '支付测试',
            'amount' => 1, //单位分
            'openid' => 'xxxxxxxxxx',
            'notify_url' => 'https://www.xxx.com/api/WxPay/wxNotify' //支付成功回调地址
        ];

        list($code, $param) = WxPayService::init()->xcxPay($payData);

        return $this->result($param);
    }
    /**
     * 小程序微信支付
     * @param $payParam
     * @return array
     */
    public function xcxPay($payParam = [])
    {
        $merchantId = $this->wxPayConfig['mchid'];
        $appid = $this->wxPayConfig['appid'];
        $payJsonData = [
            'mchid' => $merchantId,
            'appid' => $appid,
            'out_trade_no' => $payParam['out_trade_no'],
            'description' => $payParam['description'],
            'notify_url' => $payParam['notify_url'],
            'amount' => [
                'total' => $payParam['amount'],  // 分
                'currency' => 'CNY'
            ],
            'payer' => [
                'openid' => $payParam['openid']
            ]
        ];
        try {
            $resp = $this->instance
                ->chain('v3/pay/transactions/jsapi')
                ->post(['json' => $payJsonData]);

            if ($resp->getStatusCode() == 200) {
                $wo = json_decode($resp->getBody(), true); //prepay_id
                $params = [
                    'appid' => $appid,
                    'timeStamp' => (string)Formatter::timestamp(),
                    'nonceStr' => Formatter::nonce(),
                    'package' => 'prepay_id=' . $wo["prepay_id"],
                ];
                $params += ['paySign' => Rsa::sign(
                    Formatter::joinedByLineFeed(...array_values($params)),
                    $this->merchantPrivateKeyInstance
                )];
                return [1, $params];
            } else {
                return [0, '微信支付失败'];
            }
        } catch (\Exception $e) {
            $code = $e->getCode();
            $data = '';
            if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
                $r = $e->getResponse();
                $data = json_decode($r->getBody(), true);
            }
        }
        return [$code, $data];
    }

2、小程序调起支付

    xcxWxPay().then(res => {
      console.log(res);
      var param = res.data.data
      wx.requestPayment({
        nonceStr: param.nonceStr,
        package: param.package,
        paySign: param.paySign,
        timeStamp: param.timeStamp,
        signType: 'RSA',
        success(res) {
          console.log(res)
        },
        fail(res) {
          console.log(res)
        }
      })
    })

3、支付通知

    //小程序微信支付回调(支付/退款)
    public function wxNotify()
    {
        WxPayService::notify(Request::header(), file_get_contents('php://input'));
    }
    /**
     * 支付/退款回调
     * @param $header
     * @param $inBody
     * @return array|string[]|void
     */
    public static function notify($header, $inBody)
    {
        $inWechatpaySignature = $header['wechatpay-signature'];
        $inWechatpayTimestamp = $header['wechatpay-timestamp'];
        $inWechatpaySerial = $header['wechatpay-serial'];
        $inWechatpayNonce = $header['wechatpay-nonce'];
        if (!$inWechatpaySignature or !$inWechatpayTimestamp or !$inWechatpaySerial or !$inWechatpayNonce) {
            //请求头错误,不是来自微信调用
            exit;
        }
        $wxPayConfig = config('wxpay');
        $merchantId = $wxPayConfig['mchid'];
        $apiv3Key = $wxPayConfig['api_v3_key'];
        $certPath = 'file://' . ROOT_PATH . 'cert/wx/' . $merchantId;
        // 从本地文件中加载「微信支付平台证书」,用来验证微信支付应答的签名
        $platformCertificateFilePath = $certPath . '/cert.pem';
        // 根据通知的平台证书序列号,查询本地平台证书文件,这里是自己生成的证书
        $platformPublicKeyInstance = Rsa::from(file_get_contents($platformCertificateFilePath), Rsa::KEY_TYPE_PUBLIC);
        // 检查通知时间偏移量,允许5分钟之内的偏移
        $timeOffsetStatus = 300 >= abs(Formatter::timestamp() - (int)$inWechatpayTimestamp);
        $verifiedStatus = Rsa::verify(
        // 构造验签名串
            Formatter::joinedByLineFeed($inWechatpayTimestamp, $inWechatpayNonce, $inBody),
            $inWechatpaySignature,
            $platformPublicKeyInstance
        );
        if ($timeOffsetStatus && $verifiedStatus) {
            // 转换通知的JSON文本消息为PHP Array数组
            $inBodyArray = (array)json_decode($inBody, true);
            // 使用PHP7的数据解构语法,从Array中解构并赋值变量
            ['resource' => [
                'ciphertext' => $ciphertext,
                'nonce' => $nonce,
                'associated_data' => $aad
            ]] = $inBodyArray;
            // 加密文本消息解密
            $inBodyResource = AesGcm::decrypt($ciphertext, $apiv3Key, $nonce, $aad);
            // 把解密后的文本转换为PHP Array数组
            return (array)json_decode($inBodyResource, true);
        } else {
            return ['trade_state' => 'FAIL'];
        }
    }

退款、查询账单接口都差不多,就不单独贴出来了,下面一起发不出来

七、源码

1、配置代码config/wxpay.php

<?php

return [
    // +----------------------------------------------------------------------
    // | 微信支付配置
    // +----------------------------------------------------------------------

    'mchid' => '', //商户ID
    'appid' => '', //小程序appid
    'cert_serial' => '', //商户api证书序列号
    'api_v3_key' => '', //在商户平台上设置的APIv3密钥
];

2、接口代码WxPay.php

<?php

namespace app\api\controller;

use app\BaseController;
use app\common\conf\ErrConfig;
use app\common\service\IotService;
use app\common\service\WxPayService;
use think\facade\Request;

class WxPay extends BaseController
{
    //小程序微信支付
    public function xcxWxPay()
    {
        $payData = [
            'out_trade_no' => '唯一的随机字符串',
            'description' => '支付测试',
            'amount' => 1,
            'openid' => 'xx',
            'notify_url' => 'https://www.xxx.com/api/WxPay/wxNotify'
        ];

        list($code, $param) = WxPayService::init()->xcxPay($payData);

        return $this->result($param);
    }

    //小程序微信退款
    public function xcxWxRefund()
    {
        $refundParam = [
            'transaction_id' => 'xxx',
            'out_refund_no' => '唯一的随机字符串',
            'reason' => '退款测试',
            'notify_url' => 'https://www.xxx.com/api/WxPay/wxNotify',
            'amount' => 1,
        ];

        list($code, $res) = WxPayService::init()->refundOrder($refundParam);
    }

    //支付账单查询
    public function getPayBill()
    {
        list($code, $res) = WxPayService::init()->getPayBill('xxx');
    }

    //退款账单查询
    public function getRefundBill()
    {
        list($code, $res) = WxPayService::init()->getRefundBill('xxx');
    }

    //获取微信平台证书
    public function getCert()
    {
        WxPayService::certificates();
    }

    //小程序微信支付回调(支付/退款)
    public function wxNotify()
    {
        WxPayService::notify(Request::header(), file_get_contents('php://input'));
    }
}

3、微信支付服务类WxPayService.php

<?php

namespace app\common\service;

use WeChatPay\Builder;
use WeChatPay\Crypto\AesGcm;
use WeChatPay\Crypto\Rsa;
use WeChatPay\Formatter;
use WeChatPay\Util\PemUtil;

//微信支付
class WxPayService
{
    private $merchantPrivateKeyInstance;

    private static $wxPayService = null;

    private $instance = null;

    private $wxPayConfig = null;

    private function __construct()
    {
    }

    private function __clone()
    {
    }

    public static function Init()
    {
        if (!self::$wxPayService instanceof self) {
            self::$wxPayService = new self();
            self::$wxPayService->wxPayConfig = config('wxpay');
            $mchId = self::$wxPayService->wxPayConfig['mchid'];     // 商户号
            $serial = self::$wxPayService->wxPayConfig['cert_serial'];   //「商户API证书」的「证书序列号」
            $certPath = 'file://' . ROOT_PATH . 'cert/wx/' . $mchId;
            // 从本地文件中加载「商户API私钥」,「商户API私钥」会用来生成请求的签名
            $merchantPrivateKeyFilePath = $certPath . '/apiclient_key.pem';
            self::$wxPayService->merchantPrivateKeyInstance = Rsa::from($merchantPrivateKeyFilePath, Rsa::KEY_TYPE_PRIVATE);
            // 从本地文件中加载「微信支付平台证书」,用来验证微信支付应答的签名
            $platformCertificateFilePath = $certPath . '/cert.pem';
            $platformPublicKeyInstance = Rsa::from($platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);
            // 从「微信支付平台证书」中获取「证书序列号」
            $platformCertificateSerial = PemUtil::parseCertificateSerialNo($platformCertificateFilePath);
            // 构造一个 APIv3 客户端实例
            self::$wxPayService->instance = Builder::factory([
                'mchid' => $mchId,
                'serial' => $serial,
                'privateKey' => self::$wxPayService->merchantPrivateKeyInstance,
                'certs' => [
                    $platformCertificateSerial => $platformPublicKeyInstance,
                ],
            ]);
        }

        return self::$wxPayService;
    }

    /**
     * 小程序微信支付
     * @param $payParam
     * @return array
     */
    public function xcxPay($payParam = [])
    {
        $merchantId = $this->wxPayConfig['mchid'];
        $appid = $this->wxPayConfig['appid'];
        $payJsonData = [
            'mchid' => $merchantId,
            'appid' => $appid,
            'out_trade_no' => $payParam['out_trade_no'],
            'description' => $payParam['description'],
            'notify_url' => $payParam['notify_url'],
            'amount' => [
                'total' => $payParam['amount'],  // 分
                'currency' => 'CNY'
            ],
            'payer' => [
                'openid' => $payParam['openid']
            ]
        ];
        try {
            $resp = $this->instance
                ->chain('v3/pay/transactions/jsapi')
                ->post(['json' => $payJsonData]);

            if ($resp->getStatusCode() == 200) {
                $wo = json_decode($resp->getBody(), true); //prepay_id
                $params = [
                    'appid' => $appid,
                    'timeStamp' => (string)Formatter::timestamp(),
                    'nonceStr' => Formatter::nonce(),
                    'package' => 'prepay_id=' . $wo["prepay_id"],
                ];
                $params += ['paySign' => Rsa::sign(
                    Formatter::joinedByLineFeed(...array_values($params)),
                    $this->merchantPrivateKeyInstance
                )];
                return [1, $params];
            } else {
                return [0, '微信支付失败'];
            }
        } catch (\Exception $e) {
            $code = $e->getCode();
            $data = '';
            if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
                $r = $e->getResponse();
                $data = json_decode($r->getBody(), true);
            }
        }
        return [$code, $data];
    }

    /**
     * 查询支付账单
     * @param $transactionId
     * @return array
     */
    public function getPayBill($transactionId)
    {
        try {
            $response = $this->instance
                ->chain('v3/pay/transactions/id/' . $transactionId)
                ->get([
                    'query' => ['mchid' => $this->wxPayConfig['mchid']],
                ]);

            $code = $response->getStatusCode();
            $response = json_decode($response->getBody(), true);
            $status = $response['status'];
            return [1, $response];
        } catch (\Exception $e) {
            if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
                $r = $e->getResponse();
                $code = $r->getStatusCode();
                $response = json_decode($r->getBody(), true);
                $subCode = $response['code'];
                $subMsg = $response['message'];
                return [$code, $response];
            }
        }
        return [0, '查询失败'];
    }

    /**
     * 支付/退款回调
     * @param $header
     * @param $inBody
     * @return array|string[]|void
     */
    public static function notify($header, $inBody)
    {
        $inWechatpaySignature = $header['wechatpay-signature'];
        $inWechatpayTimestamp = $header['wechatpay-timestamp'];
        $inWechatpaySerial = $header['wechatpay-serial'];
        $inWechatpayNonce = $header['wechatpay-nonce'];
        if (!$inWechatpaySignature or !$inWechatpayTimestamp or !$inWechatpaySerial or !$inWechatpayNonce) {
            //请求头错误,不是来自微信调用
            exit;
        }
        $wxPayConfig = config('wxpay');
        $merchantId = $wxPayConfig['mchid'];
        $apiv3Key = $wxPayConfig['api_v3_key'];
        $certPath = 'file://' . ROOT_PATH . 'cert/wx/' . $merchantId;
        // 从本地文件中加载「微信支付平台证书」,用来验证微信支付应答的签名
        $platformCertificateFilePath = $certPath . '/cert.pem';
        // 根据通知的平台证书序列号,查询本地平台证书文件,这里是自己生成的证书
        $platformPublicKeyInstance = Rsa::from(file_get_contents($platformCertificateFilePath), Rsa::KEY_TYPE_PUBLIC);
        // 检查通知时间偏移量,允许5分钟之内的偏移
        $timeOffsetStatus = 300 >= abs(Formatter::timestamp() - (int)$inWechatpayTimestamp);
        $verifiedStatus = Rsa::verify(
        // 构造验签名串
            Formatter::joinedByLineFeed($inWechatpayTimestamp, $inWechatpayNonce, $inBody),
            $inWechatpaySignature,
            $platformPublicKeyInstance
        );
        if ($timeOffsetStatus && $verifiedStatus) {
            // 转换通知的JSON文本消息为PHP Array数组
            $inBodyArray = (array)json_decode($inBody, true);
            // 使用PHP7的数据解构语法,从Array中解构并赋值变量
            ['resource' => [
                'ciphertext' => $ciphertext,
                'nonce' => $nonce,
                'associated_data' => $aad
            ]] = $inBodyArray;
            // 加密文本消息解密
            $inBodyResource = AesGcm::decrypt($ciphertext, $apiv3Key, $nonce, $aad);
            // 把解密后的文本转换为PHP Array数组
            return (array)json_decode($inBodyResource, true);
        } else {
            return ['trade_state' => 'FAIL'];
        }
    }

    /**
     * 退款
     * @param $refundParam
     * @return array|mixed array [$code,$data] $code=1 成功
     */
    public function refundOrder($refundParam = [])
    {
        try {
            $response = $this->instance
                ->chain('v3/refund/domestic/refunds')
                ->post([
                    'json' => [
                        'transaction_id' => $refundParam['transaction_id'],
                        'out_refund_no' => $refundParam['out_refund_no'],
                        'reason' => $refundParam['reason'],
                        'notify_url' => $refundParam['notify_url'],
                        'amount' => [
                            'refund' => $refundParam['amount'],
                            'total' => $refundParam['amount'],
                            'currency' => 'CNY',
                        ],
                    ],
                ]);
            $code = $response->getStatusCode();
            $response = json_decode($response->getBody(), true);
            $status = $response['status'] ?? '';
            return [1, $response];
        } catch (\Exception $e) {
            if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
                $r = $e->getResponse();
                $code = $r->getStatusCode();
                $response = json_decode($r->getBody(), true);
                $subCode = $response['code'];
                $subMsg = $response['message'];
                $listErrorCode = [
                    'NOT_ENOUGH',  //	余额不足',
                    'MCH_NOT_EXISTS',  //MCHID不存在',
                    'NO_AUTH',  //没有退款权限',
                ];
                if (in_array($subCode, $listErrorCode)) {
                   //失败处理代码
                }
                return [$code, $response];
            }
        }
        return [0, '退款失败'];
    }

    /**
     * 查询退款账单
     * @param $outRefundNo
     * @return array
     */
    public function getRefundBill($outRefundNo)
    {
        try {
            $response = $this->instance
                ->chain('v3/refund/domestic/refunds/' . $outRefundNo)
                ->get();
            $code = $response->getStatusCode();
            $response = json_decode($response->getBody(), true);
            $status = $response['status'];
            return [1, $response];
        } catch (\Exception $e) {
            if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
                $r = $e->getResponse();
                $code = $r->getStatusCode();
                $response = json_decode($r->getBody(), true);
                $subCode = $response['code'];
                $subMsg = $response['message'];
                return [$code, $response];
            }
        }
        return [0, '查询失败'];
    }

    /**
     * 获取证书
     * @return mixed
     */
    public static function certificates()
    {
        $wxPayConfig = config('wxpay');
        $api_v3_key = $wxPayConfig['api_v3_key'];
        $result = self::wxCurlQuery('v3/certificates','GET');
        $result = json_decode($result, true);
        $ciphertext = $result['data'][0]['encrypt_certificate']['ciphertext'] ?? '';
        $nonce = $result['data'][0]['encrypt_certificate']['nonce'] ?? '';
        $associated_data = $result['data'][0]['encrypt_certificate']['associated_data'] ?? '';
        $decryptedMsg = AesGcm::decrypt($ciphertext, $api_v3_key, $nonce, $associated_data);
        $result['decryptedMsg'] = ($decryptedMsg); //解密后的内容,就是证书内容
        dump($result);
    }

    /**
     * get请求
     * @param string $urlV3
     * @param string $http_method
     * @param string $body
     * @return bool|string
     */
    public static function wxCurlQuery($urlV3 = '', $http_method = 'POST', $body = '')
    {
        $url = 'https://api.mch.weixin.qq.com/' . $urlV3;
        $config = config('wxpay');
        $mchId = $config['mchid'];
        $serial_no = $config['cert_serial'];
        $certPath = 'file://' . ROOT_PATH . 'cert/wx/' . $mchId;
        // 从本地文件中加载「商户API私钥」,「商户API私钥」会用来生成请求的签名
        $merchantPrivateKeyFilePath = $certPath . '/apiclient_key.pem';
        $mch_private_key = openssl_get_privatekey(file_get_contents($merchantPrivateKeyFilePath));//私钥
        if(is_array($body)){
            $body = json_encode($body); // 接口参数
        }
        // 生成token 验签
        $timestamp = time();//时间戳
        $nonce = Formatter::nonce(32);//随机串
        $url_parts = parse_url($url);
        $canonical_url = ($url_parts['path'] . (!empty($url_parts['query']) ? "?${url_parts['query']}" : ""));
        //构造签名串
        $message = $http_method . "\n" .
            $canonical_url . "\n" .
            $timestamp . "\n" .
            $nonce . "\n" .
            $body . "\n";//报文主体
        //计算签名值
        openssl_sign($message, $raw_sign, $mch_private_key, 'sha256WithRSAEncryption');
        $sign = base64_encode($raw_sign);
        //设置HTTP头
        $token = sprintf('WECHATPAY2-SHA256-RSA2048 mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"',
            $mchId, $nonce, $timestamp, $serial_no, $sign);
        $headers = [
            'Accept: application/json',
            'User-Agent: */*',
            'Content-Type: application/json; charset=utf-8',
            'Authorization: ' . $token,
            "Wechatpay-Serial:{$serial_no}"
        ];

        $info = curl_init();
        curl_setopt($info, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($info, CURLOPT_HEADER, 0);
        curl_setopt($info, CURLOPT_NOBODY, 0);
        curl_setopt($info, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($info, CURLOPT_SSL_VERIFYHOST, false);
        if ($http_method == 'POST') {
            curl_setopt($info, CURLOPT_POST, 1);
            curl_setopt($info, CURLOPT_POSTFIELDS, json_encode($body));
        }
        //设置header头
        curl_setopt($info, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($info, CURLOPT_URL, $url);
        $output = curl_exec($info);
        curl_close($info);
        return $output;
    }
}

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值