php微信商家转账到零钱实现


原有的企业转账到零钱已不支持开通,升级后采用微信商家转账到零钱实现用户提现分销发放奖励等场景。

为了快速实现功能,本文讲解使用第三方库方式对接转账到零钱,wechatpay/wechatpay , 基于 Guzzle HTTP Client 的微信支付 PHP 开发库。

功能介绍
  1. 微信支付 APIv2 和 APIv3 的 Guzzle HTTP 客户端,支持 同步 或 异步 发送请求,并自动进行请求签名和应答验签
  2. 链式实现的 URI Template
  3. 敏感信息加解密
  4. 回调通知的验签和解密

详细实现步骤,参考wechatpay

链接地址:https://github.com/wechatpay-apiv3/wechatpay-php

开发前需要到微信商户平台申请api证书,获取证书序列号,生成平台证书,完善好配置参数。

Certificate Downloader 是 PHP版 微信支付 APIv3 平台证书的命令行下载工具。该工具可从 https://api.mch.weixin.qq.com/v3/certificates 接口获取商户可用证书,并使用 APIv3 密钥 和 AES_256_GCM 算法进行解密,并把解密后证书下载到指定位置。链接地址:https://github.com/wechatpay-apiv3/wechatpay-php/blob/main/bin/README.md

商户转账到零钱和回调通知代码如下:

private function pay($outBatchNo = '', $outDetailNo = '', $total_amount = 0, $openid = '',$remark='', $notifyUrl = '', $userName = ''){
        // 设置参数,获取后台配置信息根据实际情况修改
        $payConfig = \addons\epay\library\Service::getConfig('miniapp');
        //var_dump($payConfig);exit;
        //参考 https://packagist.org/packages/wechatpay/wechatpay
        //vendor/bin/CertificateDownloader.php -k ${apiV3key} -m ${mchId} -f ${mchPrivateKeyFilePath} -s ${mchSerialNo} -o ${outputFilePath}
        
        //示例 ./bin/CertificateDownloader.php -k ek00riolullp0fh0pxs22v0z4gzq -m 1669097 -f /www/wwwroot/web/addons/epay/certs/apiclient_key.pem -s 2424BCF93CDD4CD2E4 -o /www/wwwroot/web/addons/epay/certs/
        // 商户号
        $merchantId = $payConfig['mch_id'];
        
        // 从本地文件中加载「商户API私钥」,「商户API私钥」会用来生成请求的签名,注意file://不可省略
        $merchantPrivateKeyFilePath = 'file://'.$payConfig['cert_key'];//'file:///path/to/merchant/apiclient_key.pem';
        $merchantPrivateKeyInstance = Rsa::from($merchantPrivateKeyFilePath, Rsa::KEY_TYPE_PRIVATE);
        
        // 「商户API证书」的「证书序列号」,可改为参数形式
        $merchantCertificateSerial = '24245FFA92E4';//不同证书注意替换该参数
        
        // 从本地文件中加载「微信支付平台证书」,用来验证微信支付应答的签名,
        $platformCertificateFilePath = 'file://'.ROOT_PATH.'addons/epay/certs/wechDAFC.pem';
        $platformPublicKeyInstance = Rsa::from($platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);
        
        // 从「微信支付平台证书」中获取「证书序列号」
        $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;
        
        
        $transferDetail = [[      //注意二位数组
                    'out_detail_no' => $outDetailNo,                  // 明细单号
                    'transfer_amount' => intval($total_amount),   // 转账总金额
                    'transfer_remark' => $remark,                   // 单条转账备注
                    'openid' => $openid,                   // 收款方openid
                ]];
        //m敏感信息加密
        $encryptor = static function(string $msg) use ($platformPublicKeyInstance): string {
            return Rsa::encrypt($msg, $platformPublicKeyInstance);
        };
        // 转账金额 >= 2,000元,收款用户姓名必填,明细转账金额<0.3元时,不允许填写收款用户姓
        if($total_amount >= 200000){
            $transferDetail[0]['user_name'] = $encryptor($userName);
        }
        
        try {
            $resp = $instance
            ->chain('v3/transfer/batches')
            ->post(['json' => [
                'appid'        => $payConfig['app_id'],
                'out_batch_no' => $outBatchNo,
                'batch_name'        => $remark,
                'batch_remark'  => $remark,
                'total_amount' => intval($total_amount),
                'total_num'  => 1,
                'transfer_detail_list'       => $transferDetail,
                'notify_url'   => $notifyUrl
            ]]);
            
                $res = ['code' => $resp->getStatusCode()];
                    $body = (array)json_decode($resp->getBody(),true);
                    [
                    'batch_id'      => $batch_id,
                    'batch_status'  => $batch_status,
                    'out_batch_no' => $out_batch_no
                    ] = $body;
                    $res['data'] = [
                    'batch_id'      => $batch_id,
                    'batch_status'  => $batch_status,
                    'out_batch_no' => $out_batch_no
                    ];
                
                return $res;
            
            
            // echo $resp->getStatusCode(), PHP_EOL;
            // echo $resp->getBody(), PHP_EOL;
            
            //200 {"batch_id":"131006950931882","batch_status":"ACCEPTED","create_time":"2024-05-10T23:38:12+08:00","out_batch_no":"ECxq3526"}

        } catch (\Exception $e) {
            // 进行错误处理
            //echo $e->getMessage(), PHP_EOL;
            if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
                $r = $e->getResponse();
                $body = (array)json_decode($r->getBody(),true);
                $res = ['code' => $r->getStatusCode(), 'data' => $body];
                return $res;
                // echo $r->getStatusCode() . ' ' . $r->getReasonPhrase(), PHP_EOL;
                // echo $r->getBody(), PHP_EOL, PHP_EOL, PHP_EOL;
            }
            //echo $e->getTraceAsString(), PHP_EOL;
        }
    }
    
    public function callback(){
        //获取后台配置参数,根据实际情况修改
        $payConfig = \addons\epay\library\Service::getConfig('miniapp');
        
        $headers = $this->request->header();
        $inWechatpaySignature = $headers['wechatpay-signature'];// 请根据实际情况获取
        $inWechatpayTimestamp = $headers['wechatpay-timestamp'];// 请根据实际情况获取
        $inWechatpaySerial = $headers['wechatpay-serial'];// 请根据实际情况获取
        $inWechatpayNonce = $headers['wechatpay-nonce'];// 请根据实际情况获取
        $inBody = $this->request->post();// 请根据实际情况获取,例如: file_get_contents('php://input');
        
        
        $apiv3Key = $payConfig['key_v3'];// 在商户平台上设置的APIv3密钥
        
        // 根据通知的平台证书序列号,查询本地平台证书文件,
        // 假定为 `/path/to/wechatpay/inWechatpaySerial.pem`
        $platformPublicKeyInstance = Rsa::from('file://'.ROOT_PATH.'addons/epay/certs/wecAD7DAFC.pem', 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数组
            $inBodyResourceArray = (array)json_decode($inBodyResource, true);
            print_r($inBodyResourceArray);// 打印解密后的结果
            //更新支付结果,根据实际业务修改
            //$this->model->where(['out_batch_no' => $outBatchNo])->update(['status' => 1, 'fedback' => $batch_status]);
        }
        
        //(1)如果未处理,进行处理;(2)如果已处理,则直接返回结果成功;
        //若通知回调的签名验证失败,商户系统应返回失败(即应答 4xx 或 5xx 的状态码
        
    }

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注