当前位置: 首页 > article >正文

PHP对接微信支付v3版本

【以下内容是在thinkphp框架中编写的,做简单修改就可使用】

一、扩展

1-1、支付扩展 

扩展路径:extend/wxpay/Wx.php,根据项目实际情况修改。

<?php
/**
 * User: Lee
 * Date: 2025/2/23
 * Time: 22:23
 */

namespace extend\wxpay;


class Wx
{
    private $appid=''; // 小程序id
    private $mch_id=''; // 商户号
    private $serial_no=''; // 商户证书序列号
    private $private_key=''; // 商户私钥
    private $api_key=''; // API密钥
    private $notify_url='https://*******/callback.html'; // 支付结果通知地址
    private $apiclient_cert_url = ''; // apiclient_cert.pem文件在服务器的磁盘路径
    private $apiclient_key_url = ''; // apiclient_key.pem文件在服务器的磁盘路径

    /**
     * 构造函数
     *
     * @param string $mch_id 商户号
     * @param string $serial_no 商户证书序列号
     * @param string $private_key 商户私钥
     * @param string $api_key API密钥
     * @param string $notify_url 支付结果通知地址
     */
    public function __construct()
    {
        $serial_no = $this->getSerialNoFromCert($this->apiclient_cert_url);
        $this->serial_no = $serial_no;
        $this->private_key = file_get_contents($this->apiclient_key_url);
    }

    /**
     * 统一下单接口
     *
     * @param string $openid 用户openid
     * @param string $out_trade_no 商户订单号
     * @param string $description 商品描述
     * @param int $amount 订单金额(单位:分)
     * @return array 返回统一下单结果
     */
    public function unifiedOrder($openid, $out_trade_no, $description, $amount)
    {
        // 统一下单接口URL
        $url = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi";

        // 请求参数
        $params = [
            'appid' => $this->appid, // 小程序或公众号的appid
            'mchid' => $this->mch_id,
            'description' => $description,
            'out_trade_no' => $out_trade_no,
            'notify_url' => $this->notify_url,
            'amount' => [
                'total' => $amount, // 订单金额(单位:分)
                'currency' => 'CNY', // 货币类型
            ],
            'payer' => [
                'openid' => $openid, // 用户openid
            ],
        ];

        // 生成签名
        $authorization = $this->generateAuthorization('POST', $url, $params);

        // 发送HTTP请求
        $response = $this->httpPost($url, json_encode($params), [
            'Authorization: ' . $authorization,
            'Content-Type: application/json',
            'Accept: application/json',
            'User-Agent:yaode/1.0',
        ]);

        // 解析响应
        return json_decode($response, true);
    }

    /**
     * 生成签名
     *
     * @param string $method HTTP方法(GET/POST等)
     * @param string $url 请求URL
     * @param array $body 请求体
     * @return string 返回Authorization头
     */
    private function generateAuthorization($method, $url, $body)
    {
        // 1. 生成随机字符串
        $nonce_str = $this->generateNonceStr();

        // 2. 生成时间戳
        $timestamp = time();

        // 3. 生成签名原文
        $signature = $this->generateSignature($method, $url, $timestamp, $nonce_str, $body);

        // 4. 生成Authorization头
        return sprintf(
            'WECHATPAY2-SHA256-RSA2048 mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"',
            $this->mch_id,
            $nonce_str,
            $timestamp,
            $this->serial_no,
            $signature
        );
    }

    /**
     * 生成签名
     *
     * @param string $method HTTP方法
     * @param string $url 请求URL
     * @param int $timestamp 时间戳
     * @param string $nonce_str 随机字符串
     * @param array $body 请求体
     * @return string 返回签名
     */
    private function generateSignature($method, $url, $timestamp, $nonce_str, $body)
    {
        // 1. 拼接签名原文
        $message = sprintf(
            "%s\n%s\n%d\n%s\n%s\n",
            $method,
            parse_url($url, PHP_URL_PATH),
            $timestamp,
            $nonce_str,
            json_encode($body)
        );

        // 2. 使用私钥生成签名
        openssl_sign($message, $signature, $this->private_key, OPENSSL_ALGO_SHA256);

        // 3. 返回Base64编码的签名
        return base64_encode($signature);
    }

    /**
     * 生成随机字符串
     */
    private function generateNonceStr($length = 32)
    {
        $chars = "abcdefghijklmnopqrstuvwxyz0123456789";
        $str = "";
        for ($i = 0; $i < $length; $i++) {
            $str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
        }
        return $str;
    }

    /**
     * 发送HTTP POST请求
     */
    private function httpPost($url, $data, $headers = [])
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 不验证SSL证书
        $response = curl_exec($ch);
        curl_close($ch);
        return $response;
    }
    public function getSerialNoFromCert($certPath) {
        $cert = file_get_contents($certPath);
        $ssl = openssl_x509_parse($cert);
        return $ssl['serialNumberHex'];
    }

    /**
     * 生成小程序调起支付所需的参数
     *
     * @param string $prepay_id 统一下单接口返回的prepay_id
     * @return array 返回小程序调起支付所需的参数
     */
    public function getPaymentParams($prepay_id)
    {
        // 参数
        $params = [
            'appId' => $this->appid, // 小程序或公众号的appid
            'timeStamp' => (string)time(), // 时间戳
            'nonceStr' => $this->generateNonceStr(), // 随机字符串
            'package' => 'prepay_id=' . $prepay_id, // 预支付交易会话标识
            'signType' => 'RSA', // 签名类型
        ];

        // 生成签名
        $params['paySign'] = $this->generateSignature_($params);

        return $params;
    }

    private function generateSignature_($params)
    {
        $stringToSign = sprintf(
            "%s\n%s\n%s\n%s\n",
            $params['appId'],
            $params['timeStamp'],
            $params['nonceStr'],
            $params['package']
        );

        // 3. 使用商户私钥生成签名
        openssl_sign($stringToSign, $signature, $this->private_key, OPENSSL_ALGO_SHA256);

        // 4. 返回 Base64 编码的签名
        return base64_encode($signature);
    }



}

1-2、回调扩展

 路径:extend/wxpay/AesUtil.php,根据项目实际情况修改。

下面代码是微信支付文档给的代码示例,可用。

<?php
/**
 * User: Lee
 * Date: 2025/2/28
 * Time: 17:10
 */

namespace extend\api\wxpay;

class AesUtil
{
    public $aesKey = 'API密钥';

    const KEY_LENGTH_BYTE = 32;
    const AUTH_TAG_LENGTH_BYTE = 16;

    /**
     * Constructor
     */
    public
    function __construct()
    {
        $aesKey = 'IAMpnZCXj70oRGuS5gA4N00fHVcCXJJp';
        if (strlen($aesKey) != self::KEY_LENGTH_BYTE) {
            throw new InvalidArgumentException('无效的ApiV3Key,长度应为32个字节');
        }
        $this->aesKey = $aesKey;
    }

    /**
     * Decrypt AEAD_AES_256_GCM ciphertext
     *
     * @param string $associatedData AES GCM additional authentication data
     * @param string $nonceStr AES GCM nonce
     * @param string $ciphertext AES GCM cipher text
     *
     * @return string|bool      Decrypted string on success or FALSE on failure
     */
    public
    function decryptToString($associatedData, $nonceStr, $ciphertext)
    {
        $ciphertext = \base64_decode($ciphertext);
        if (strlen($ciphertext) <= self::AUTH_TAG_LENGTH_BYTE) {
            return false;
        }

        // ext-sodium (default installed on >= PHP 7.2)
        if (function_exists('\sodium_crypto_aead_aes256gcm_is_available') && \sodium_crypto_aead_aes256gcm_is_available()) {
            return \sodium_crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $this->aesKey);
        }

        // ext-libsodium (need install libsodium-php 1.x via pecl)
        if (function_exists('\Sodium\crypto_aead_aes256gcm_is_available') && \Sodium\crypto_aead_aes256gcm_is_available()) {
            return \Sodium\crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $this->aesKey);
        }

        // openssl (PHP >= 7.1 support AEAD)
        if (PHP_VERSION_ID >= 70100 && in_array('aes-256-gcm', \openssl_get_cipher_methods())) {
            $ctext = substr($ciphertext, 0, -self::AUTH_TAG_LENGTH_BYTE);
            $authTag = substr($ciphertext, -self::AUTH_TAG_LENGTH_BYTE);

            return \openssl_decrypt($ctext, 'aes-256-gcm', $this->aesKey, \OPENSSL_RAW_DATA, $nonceStr,
                $authTag, $associatedData);
        }

        throw new \RuntimeException('AEAD_AES_256_GCM需要PHP 7.1以上或者安装libsodium-php');
    }
}

二、controller文件

controller文件路径:app/api/controoler/Pay.php,根据项目实际情况修改。

<?php
/**
 * User: Lee
 * Date: 2025/2/23
 * Time: 17:05
 */

namespace app\api\controller;

use extend\api\wxpay\Wx;

class Pay extends BaseApi
{
    public function pay()
    {
        // 订单编号
        $out_trade_no = $this->params['out_trade_no'];
        $token = $this->checkToken();
        if ($token['code'] < 0) return $this->response($token);
        // 这里写业务逻辑
        $total_fee = 1; // 订单金额(单位:分)
        $openid = '用户openid';
        $body = '描述';
        $Wx = new Wx();
        $res = $Wx->unifiedOrder($openid, $out_trade_no, $body, $total_fee);
        if (isset($res['prepay_id'])) {
            // 生成小程序调起支付所需的参数
            $paymentParams = $Wx->getPaymentParams($res['prepay_id']);
            // 根据项目情况修改返回信息格式及参数
            return json(['data'=>array_merge($res,$paymentParams)]);
        }else{
            // 根据项目情况修改返回信息格式及参数
            return json();
        }

    }
}

三、回调

<?php
/**
 * User: Lee
 * Date: 2025/2/23
 * Time: 15:21
 */

namespace app\api\controller;

use extend\wxpay\AesUtil;

class Callback
{
    public $appid='';
    public $mch_id=''; // 商户号
    public $serial_no=''; // 商户证书序列号
    public $private_key=''; // 商户私钥
    public $public_key=''; // 商户公钥
    public $api_key=''; // API密钥
    const AUTH_TAG_LENGTH_BYTE = 16;
    private $notify_url='https://*******/callback.html'; // 支付结果通知地址
    private $apiclient_cert_url = ''; // apiclient_cert.pem文件在服务器的磁盘路径
    private $apiclient_key_url = ''; // apiclient_key.pem文件在服务器的磁盘路径

    public function __construct()
    {
        $serial_no = $this->getSerialNoFromCert($this->apiclient_cert_url);
        $this->serial_no = $serial_no;
        $this->private_key = file_get_contents($this->apiclient_key_url);
    }

    public function getSerialNoFromCert($certPath) {
        $cert = file_get_contents($certPath);
        $ssl = openssl_x509_parse($cert);
        return $ssl['serialNumberHex'];
    }

    public function index()
    {
        $return = file_get_contents('php://input');
        if(!empty($return)){
            $return = json_decode($return, true);
            $all = getallheaders();

            if($return['event_type']!='TRANSACTION.SUCCESS' || empty($return['resource'])){
                return json(['code'=>"FAIL",'message'=>"失败-3"]);
            }

            $associatedData = $return['resource']['associated_data'];  //获取associated_data数据,附加数据
            $nonceStr = $return['resource']['nonce'];                  //获取nonce数据,加密使用的随机串
            $ciphertext = $return['resource']['ciphertext'];           //获取ciphertext数据,base64编码后的数据密文


            $aes = new AesUtil();
            $res = $aes->decryptToString($associatedData,$nonceStr,$ciphertext);
            if(!empty($res)){
                $res = json_decode($res,true);
                if(!empty($res)){
                    $out_trade_no = $res['out_trade_no'];
                    // 这里写业务逻辑
                    return json();
                }else{
                    return json(['code'=>"FAIL",'message'=>"失败-2"]);
                }
            }

        }

        return json(['code'=>"FAIL",'message'=>"失败-1"]);

    }
}


http://www.kler.cn/a/567013.html

相关文章:

  • 从0开始的IMX6ULL学习篇——裸机篇之外设资源分析
  • mysql系列10—mysql锁
  • 如何使用 preg_replace 处理复杂字符串替换
  • 测试向丨多模态大模型能做宠物身份识别吗?
  • Express + MongoDB 实现 VOD 视频点播
  • QT:Echart-折线图
  • JeeWMS cgReportController.do 多个参数SQL注入漏洞(CVE-2024-57760)
  • Jeecg-Boot 开放接口开发实战:在 Jeecg-Boot 的jeecg-system-biz中添加一个controller 实现免鉴权数据接口
  • AcWing 农夫约翰的奶酪块
  • DeepSeek引爆AI浪潮:B站如何成为科技普惠的“新课堂”?
  • Linux Mem -- 关于AArch64 MTE功能的疑问
  • 大数据与金融科技:革新金融行业的动力引擎
  • CSS Selectors
  • unity学习56:旧版legacy和新版TMP文本输入框 InputField学习
  • STM32G431RBT6——(1)芯片命名规则
  • 每天一个Flutter开发小项目 (8) : 掌握Flutter网络请求 - 构建每日名言应用
  • Kafka重复消费问题和解决方式
  • Redis大key
  • 基于JAVA+Spring+mysql_快递管理系统源码+设计文档
  • C++20 Lambda表达式新特性:包扩展与初始化捕获的强强联合