PHP 微信 v3 商家转账到零钱

一文搞懂微信支付 Api-v3 规则实现(附源码) https://developers.weixin.qq.com/community/develop/article/doc/000040ff8642e0555b0afe78951813

https://blog.csdn.net/qq_23564667/article/details/132169681
https://www.cnblogs.com/wjh0521/articles/16740612.html

使用条件

    1、商户号已入驻 90 日且截止今日回推 30 天商户号保持连续不间断的交易。

2、登录微信支付商户平台 – 产品中心,开通付款到零钱。

付款资金
付款到零钱资金使用商户号余额资金。

根据商户号的账户开通情况,实际出款账户有做区别:

默认情况下,付款到零钱使用商户号基本户(或余额账户)余额。如商户号已开通运营账户,则付款到零钱使用运营账户内的资金。
基本户(或上述其他出款账户)的资金来源,可能是交易结算款项(仅基本户),或给账户充值的资金。当出款账户余额不足时,付款将因余额不足而付款失败。

1、开通 微信 商家转账到零钱;
。申请api v3秘钥(和api秘钥可以是一样的,需要分开在平台申请);

2、微信sdk安装:composer:composer require wechatpay/wechatpay;

3、证书生成:

 

 

4、PHP demo源码:

<?php
namespace app\common\extend;
use think\Exception;
use WeChatPay\Builder;
use WeChatPay\Crypto\Rsa;
use WeChatPay\Util\PemUtil;

/**
 * 实测 可用 相关配置弄好
 * date:   2022/09/19 18:10
 * desc:
 */
class WechatPayV3
{
    private $v3_key;
    private $merchantId;
    private $merchantCertificateSerial;
    private $merchantPrivateKeyFilePath;
    private $platformCertificateFilePath;
    private $platformPublicKeyInstance;
    private $instance;

    const AUTH_TAG_LENGTH_BYTE = 16;

    public function __construct()
    {
     //证书生成命令(生成的证书在sdk目录 或 自己指定)
        //php ./bin/CertificateDownloader.php -k jiaxin**************uzhou20 -m 1614781491 -f /www/wwwroot/项目目录/server/public/cert/apiclient_key.pem -s 629BD177D7******************8CE43244 -o ./

        $pay_config = PayConfig::find(2);
        $pay_config = $pay_config->config;
        // 商户号
        $this->merchantId = $pay_config['mch_id'];
        // v3api秘钥
        $this->v3_key = $pay_config['pay_sign_key'];
        // 「商户API证书」的「证书序列号」 商户平台【API安全】->【API证书】->【查看证书】,可查看商户API证书序列号
        $this->merchantCertificateSerial = '629BD177D7BB33*******13620F8CE43244';
        // 商户API私钥
        //$this->merchantPrivateKeyFilePath = $pay_config['apiclient_key']; //"file://"
        $this->merchantPrivateKeyFilePath = "file://" . root_path() . "public/upload/cert/apiclient_key.pem"; //
        ;
        // 微信支付平台证书 首次通过命令行生成  https://github.com/wechatpay-apiv3/wechatpay-php/tree/main/bin
        //$this->platformCertificateFilePath = $pay_config['apiclient_cert'];
        $this->platformCertificateFilePath = "file://" . root_path() . "public/upload/cert/wechatpay_547228274D64DE93A481DEAB6F44CC22CB604784.pem";
        // 构建客户端实例
        $this->instance = $this->buildInstance();
        
        
        //var_dump($this->merchantPrivateKeyFilePath,$this->platformCertificateFilePath);exit;
    }

    /**
     * 商家付款到零钱
     * @param string $appid     申请商户号的appid或商户号绑定的appid(企业号corpid即为此appid),小程序、公众号、APP
     * @param string $batch_sn  商户系统内部的商家批次单号
     * @param string $order_sn  账明细单的唯一标识
     * @param float $money      转账金额,单位:元
     * @param string $openid    用户openid
     * @param string $remark    转账备注
     * @param string $user_name 用户姓名,转账金额大于2000元,必填
     * @return array
     * @throws Exception
     */
    public function transfer(string $appid, string $batch_sn, string $order_sn, float $money, string $openid, string $remark, string $user_name = ''): array
    {
        $trans_detail = [
            'out_detail_no'   => $order_sn,
            'transfer_amount' => $money * 100,
            'transfer_remark' => $remark,
            'openid'          => $openid,
        ];
        if ($money >= 2000) {
            if (empty($user_name)) {
                throw new Exception('提现金额大于2000元,真实姓名不能为空');
            }
            $trans_detail['user_name'] = $this->encrypt($user_name);
        }
        $response = $this->instance->chain('v3/transfer/batches')->post([
            'json' => [
                'appid'                => $appid,
                'out_batch_no'         => $batch_sn,
                'batch_name'           => '提现转账',
                'batch_remark'         => '提现转账',
                'total_amount'         => $money * 100,
                'total_num'            => 1,
                'transfer_detail_list' => [
                    $trans_detail
                ]
            ]
        ]);
        return json_decode($response->getBody(), true);
    }

    /**
     * 转账明细查询
     * @param string $out_batch_no  商户系统内部的商家批次单号
     * @param string $out_detail_no 账明细单的唯一标识
     * @return array
     */
    public function transferQuery(string $out_batch_no, string $out_detail_no = ''): array
    {
        $api = 'v3/transfer/batches/out-batch-no/' . $out_batch_no;
        $query = [];
        if ($out_detail_no) {
            $api .= '/details/out-detail-no/' . $out_detail_no;
        } else {
            $query = [
                'query' => [
                    'need_query_detail' => true,
                    'detail_status'     => 'ALL'
                ]
            ];
        }
        $response = $this->instance->chain($api)->get($query);
        return json_decode($response->getBody(), true);
    }

    // 自动更新平台证书
    public function autoUpdatePlatformCertificateFile()
    {
        $result = $this->v3Certificates()['data'];
        $data   = end($result);
        $effective_time = strtotime($data['effective_time']);
        if ($effective_time - time() > 0 && $effective_time - time() <= 86400) {
            $encrypt_certificate = $data['encrypt_certificate'];
            $cert_content = $this->decryptToString($encrypt_certificate['associated_data'], $encrypt_certificate['nonce'], $encrypt_certificate['ciphertext']);
            file_exists($this->platformCertificateFilePath) && file_put_contents($this->platformCertificateFilePath, $cert_content);
        }
    }

    // 获取平台证书
    private function v3Certificates()
    {
        $response = $this->instance->chain('v3/certificates')->get();
        return json_decode($response->getBody(), true);
    }

    /**
     * Decrypt AEAD_AES_256_GCM ciphertext
     * 用于解密v3Certificates接口数据获取证书
     * https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_2.shtml
     * @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
     */
    private 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->v3_key);
        }

        // 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->v3_key);
        }

        // 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->v3_key, \OPENSSL_RAW_DATA, $nonceStr, $authTag, $associatedData);
        }
        throw new \RuntimeException('AEAD_AES_256_GCM需要PHP 7.1以上或者安装libsodium-php');
    }


    // 加密
    private function encrypt(string $msg): string
    {
        return Rsa::encrypt($msg, $this->platformPublicKeyInstance);
    }

    private function buildInstance(): \WeChatPay\BuilderChainable
    {
        // 从本地文件中加载「商户API私钥」,「商户API私钥」会用来生成请求的签名
        $merchantPrivateKeyInstance = Rsa::from($this->merchantPrivateKeyFilePath, Rsa::KEY_TYPE_PRIVATE);
        // 从本地文件中加载 微信支付平台证书
        $this->platformPublicKeyInstance   = Rsa::from($this->platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);
        // 从「微信支付平台证书」中获取「证书序列号」
        $platformCertificateSerial = PemUtil::parseCertificateSerialNo($this->platformCertificateFilePath);
        // 构造一个 APIv3 客户端实例
        return Builder::factory([
            'mchid'      => $this->merchantId,
            'serial'     => $this->merchantCertificateSerial,
            'privateKey' => $merchantPrivateKeyInstance,
            'certs'      => [
                $platformCertificateSerial => $this->platformPublicKeyInstance,
            ],
        ]);
    }
}

 ————————————–方法2

https://blog.csdn.net/Logical_storm/article/details/123797074

1.使用 PHP 包管理工具 Composer 安装 SDK:

composer require wechatpay/wechatpay

2.获取微信商户证书

参考地址:什么是商户API证书?如何获取商户API证书?

3.获取微信支付平台证书

在服务器上进入PHP项目,进入vendor/bin/目录

    -k:apiv3秘钥
    -m:商户号
    -f:微信商户API私钥文件目录
    -s:证书序列号
    -o:生成后的证书保存地址
     
    php CertificateDownloader.php -k ${apiV3key} -m ${mchId} -f ${mchPrivateKeyFilePath} -s ${mchSerialNo} -o ${outputFilePath}
    例如:
    php CertificateDownloader.php -k 241054wsd5we14586esfwqsfjke25344 -m 1600789654 -f /app/file/wxCert/apiclient_key.pem -s WB0E676A11B907E25875FCCBB15151637E4 -o  /app/file/wxCert/

4.构造一个APIV3客户端实例

    <?php
     
    require_once('vendor/autoload.php');
     
    use WeChatPay\Builder;
    use WeChatPay\Crypto\Rsa;
    use WeChatPay\Util\PemUtil;
     
    // 设置参数
     
    // 商户号
    $merchantId = '190000****';
     
    // 从本地文件中加载「商户API私钥」,步骤2生成的API私钥文件
    $merchantPrivateKeyFilePath = 'file:///path/to/merchant/apiclient_key.pem';
    $merchantPrivateKeyInstance = Rsa::from($merchantPrivateKeyFilePath, Rsa::KEY_TYPE_PRIVATE);
     
    // 「商户API证书」的「证书序列号」,步骤2生成的序列号
    $merchantCertificateSerial = '3775B6A45ACD588826D15E583A95F5DD********';
     
    //「微信支付平台证书」,步骤3生成的证书
    $platformCertificateFilePath = 'file:///path/to/wechatpay/cert.pem';
    $platformPublicKeyInstance = Rsa::from($platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);
     
    // 从「微信支付平台证书」中获取「证书序列号」,步骤3生成的证书
    $platformCertificateSerial = PemUtil::parseCertificateSerialNo($platformCertificateFilePath);
     
    // 构造一个 APIv3 客户端实例
    $instance = Builder::factory([
        'mchid'      => $merchantId,
        'serial'     => $merchantCertificateSerial,
        'privateKey' => $merchantPrivateKeyInstance,
        'certs'      => [
            $platformCertificateSerial => $platformPublicKeyInstance,
        ],
    ]);
     
    // 发送请求
    $resp = $instance->chain('v3/certificates')->get(
        ['debug' => true] // 调试模式,https://docs.guzzlephp.org/en/stable/request-options.html#debug
    );
    echo $resp->getBody(), PHP_EOL;

5.APP支付下单为例

    try {
        $resp = $instance
        ->chain('v3/pay/transactions/app')
        ->post(['json' => [
            'mchid'        => '1900006XXX',
            'out_trade_no' => 'native12177525012014070332333',
            'appid'        => 'wxdace645e0bc2cXXX',
            'description'  => 'Image形象店-深圳腾大-QQ公仔',
            'notify_url'   => 'https://weixin.qq.com/',
            'amount'       => [
                'total'    => 1,
                'currency' => 'CNY'
            ],
        ]]);
     
        echo $resp->getStatusCode(), PHP_EOL;
        echo $resp->getBody(), PHP_EOL;
    } catch (\Exception $e) {
        // 进行错误处理
        echo $e->getMessage(), PHP_EOL;
        if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
            $r = $e->getResponse();
            echo $r->getStatusCode() . ' ' . $r->getReasonPhrase(), PHP_EOL;
            echo $r->getBody(), PHP_EOL, PHP_EOL, PHP_EOL;
        }
        echo $e->getTraceAsString(), PHP_EOL;
    }

6.注意

1.若运行中出现如下错误:Cannot load privateKey from(string), please take care about the \\$thing input.

需要在文件前面添加 file://

例如:

$merchantPrivateKeyFilePath = 'file://'.私钥文件目录;

2.若出现签名错误:

先自己查看商户API证书,API序列号,APIV3秘钥,商户号,微信平台证书是否一致,确保一致后还是出现签名错误,则可以验签。


发表评论

电子邮件地址不会被公开。 必填项已用*标注