5

可链式调用微信支付OpenAPI的PHP版SDK

 2 years ago
source link: https://thenorthmemory.github.io/post/async-sync-chainable-wechatpay-v2-and-v3-openapi-sdk-for-php/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

可链式调用微信支付OpenAPI的PHP版SDK

2021-06-17

微信支付 APIv2&APIv3 的 Guzzle HttpClient 封装组合, APIv2已内置请求数据签名及XML转换器,应答做了数据签名验签,转换提供有WeChatPay\Transformer::toArray静态方法,按需转换; APIv3已内置 请求签名应答验签 两个middleware中间件,创新性地实现了链式面向对象同步/异步调用远程接口。

如果你是使用 Guzzle 的商户开发者,可以使用 WeChatPay\Builder 工厂方法直接创建一个 GuzzleHttp\Client 的链式调用封装器, 实例在执行请求时将自动携带身份认证信息,并检查应答的微信支付签名。

当前版本为 1.0.2 测试版本。请商户的专业技术人员在使用时注意系统和软件的正确性和兼容性,以及带来的风险。

我们开发和测试使用的环境如下:

  • PHP >=7.2
  • guzzlehttp/guzzle ^7.0

推荐使用PHP包管理工具composer引入SDK到项目中:

在项目目录中,通过composer命令行添加:

composer require wechatpay/wechatpay

在项目的composer.json中加入以下配置:

"require": {
    "wechatpay/wechatpay": "^1.0.2"
}

添加配置后,执行安装

composer install

本类库是以 OpenAPI 对应的接入点 URL.pathname/做切分,映射成segments,编码书写方式有如下约定:

  1. 请求 pathname 切分后的每个segment,可直接以对象获取形式串接,例如 v3/pay/transactions/native 即串成 v3->pay->transactions->native;
  2. 每个 pathname 所支持的 HTTP METHOD,即作为被串接对象的末尾执行方法,例如: v3->pay->transactions->native->post(['json' => []]);
  3. 每个 pathname 所支持的 HTTP METHOD,同时支持Async语法糖,例如: v3->pay->transactions->native->postAsync(['json' => []]);
  4. 每个 segment 有中线(dash)分隔符的,可以使用驼峰camelCase风格书写,例如: merchant-service可写成 merchantService,或如 {'merchant-service'};
  5. 每个 segment 中,若有uri_template动态参数,例如 business_code/{business_code} 推荐以business_code->{'{business_code}'}形式书写,其格式语义与pathname基本一致,阅读起来比较自然;
  6. SDK内置以 v2 特殊标识为 APIv2 的起始 segmemt,之后串接切分后的 segments,如源 pay/micropay 即串成 v2->pay->micropay->post(['xml' => []]) 即以XML形式请求远端接口;
  7. 在IDE集成环境下,也可以按照内置的chain($segment)接口规范,直接以pathname作为变量$segment,来获取OpenAPI接入点的endpoints串接对象,驱动末尾执行方法(填入对应参数),发起请求,例如 chain('v3/pay/transactions/jsapi')->post(['json' => []])

以下示例用法,以异步(Async/PromiseA+)同步(Sync)结合此种编码模式展开。

Note of the segments: See RFC3986 #section-3.3 > A path consists of a sequence of path segments separated by a slash (“/”) character.

Note of the uri_template: See RFC6570

首先,通过 WeChatPay\Builder 工厂方法构建一个实例,然后如上述约定,链式同步异步请求远端OpenAPI接口。

use WeChatPay\Builder;
use WeChatPay\Util\PemUtil;

// 工厂方法构造一个实例
$instance = Builder::factory([
    // 商户号
    'mchid' => '1000100',
    // 商户证书序列号
    'serial' => 'XXXXXXXXXX',
    // 商户API私钥 PEM格式的文本字符串或者文件resource
    'privateKey' => PemUtil::loadPrivateKey('/path/to/mch/apiclient_key.pem'),
    'certs' => [
        // 可由内置的平台证书下载器 `./bin/CertificateDownloader.php` 生成
        'YYYYYYYYYY' => PemUtil::loadCertificate('/path/to/wechatpay/cert.pem')
    ],
    // APIv2密钥(32字节)--不使用APIv2可选
    'secret' => 'ZZZZZZZZZZ',
    'merchant' => [// --不使用APIv2可选
        // 商户证书 文件路径 --不使用APIv2可选
        'cert' => '/path/to/mch/apiclient_cert.pem',
        // 商户API私钥 文件路径 --不使用APIv2可选
        'key' => '/path/to/mch/apiclient_key.pem',
    ],
]);

初始化字典说明如下:

  • mchid 为你的商户号,一般是10字节纯数字
  • serial 为你的商户证书序列号,一般是40字节字符串
  • privateKey 为你的商户API私钥,一般是通过官方证书生成工具生成的文件名是apiclient_key.pem文件,支持纯字符串或者文件resource格式
  • certs[$serial_number => #resource] 为通过下载工具下载的平台证书key/value键值对,键为平台证书序列号,值为平台证书pem格式的纯字符串或者文件resource格式
  • secret 为APIv2版的密钥,商户平台上设置的32字节字符串
  • merchant[cert => $path] 为你的商户证书,一般是文件名为apiclient_cert.pem文件路径,接受[$path, $passphrase] 格式,其中$passphrase为证书密码
  • merchant[key => $path] 为你的商户API私钥,一般是通过官方证书生成工具生成的文件名是apiclient_key.pem文件路径,接受[$path, $passphrase] 格式,其中$passphrase为私钥密码

注: APIv3, APIv2 以及 GuzzleHttp\Client$config = [] 初始化参数,均融合在一个型参上; 另外初始化参数说明中的平台证书下载器可阅读使用说明文档

APIv3

Native下单

try {
    $resp = $instance->v3->pay->transactions->native->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() .' ' . $resp->getReasonPhrase()."\n";
    echo $resp->getBody() . "\n";
} catch (RequestException $e) {
    // 进行错误处理
    echo $e->getMessage()."\n";
    if ($e->hasResponse()) {
        echo $e->getResponse()->getStatusCode().' '.$e->getResponse()->getReasonPhrase()."\n";
        echo $e->getResponse()->getBody();
    }
}
$res = $instance->v3->pay->transactions->id->{'{transaction_id}'}
->getAsync([
    // 查询参数结构
    'query' => ['mchid' => '1230000109'],
    // uri_template 字面量参数
    'transaction_id' => '1217752501201407033233368018',
])
->then(function($response) {
    // 正常逻辑回调处理
    echo $response->getBody()->getContents(), PHP_EOL;
    return $response;
})
->otherwise(function($exception) {
    // 异常错误处理
    $body = $exception->getResponse()->getBody();
    echo $body->getContents(), PHP_EOL, PHP_EOL, PHP_EOL;
    echo $exception->getTraceAsString(), PHP_EOL;
})
->wait();
$res = $instance->v3->pay->transactions->outTradeNo->{'{out_trade_no}'}->close
->postAsync([
    // 请求参数结构
    'json' => ['mchid' => '1230000109'],
    // uri_template 字面量参数
    'out_trade_no' => '1217752501201407033233368018',
])
->then(function($response) {
    // 正常逻辑回调处理
    echo $response->getBody()->getContents(), PHP_EOL;
    return $response;
})
->otherwise(function($exception) {
    // 异常错误处理
    $body = $exception->getResponse()->getBody();
    echo $body->getContents(), PHP_EOL, PHP_EOL, PHP_EOL;
    echo $exception->getTraceAsString(), PHP_EOL;
})
->wait();
$res = $instance->chain('v3/refund/domestic/refunds')
->postAsync([
    'json' => [
        'transaction_id' => '1217752501201407033233368018',
        'out_refund_no' => '1217752501201407033233368018',
        'amount' => [
            'refund' => 888,
            'total' => 888,
            'currency' => 'CNY',
        ],
    ],
])
->then(function($response) {
    // 正常逻辑回调处理
    echo $response->getBody()->getContents(), PHP_EOL;
    return $response;
})
->otherwise(function($exception) {
    // 异常错误处理
    $body = $exception->getResponse()->getBody();
    echo $body->getContents(), PHP_EOL, PHP_EOL, PHP_EOL;
    echo $exception->getTraceAsString(), PHP_EOL;
})
->wait();

视频文件上传

// 参考上述指引说明,并引入 `MediaUtil` 正常初始化,无额外条件
use WeChatPay\Util\MediaUtil;
// 实例化一个媒体文件流,注意文件后缀名需符合接口要求
$media = new MediaUtil('/your/file/path/with.extension');

try {
    $resp = $instance['v3/merchant/media/video_upload']->post([
        'body'    => $media->getStream(),
        'headers' => [
            'content-type' => $media->getContentType(),
        ]
    ]);
    echo $resp->getStatusCode().' '.$resp->getReasonPhrase()."\n";
    echo $resp->getBody()."\n";
} catch (Exception $e) {
    echo $e->getMessage()."\n";
    if ($e->hasResponse()) {
        echo $e->getResponse()->getStatusCode().' '.$e->getResponse()->getReasonPhrase()."\n";
        echo $e->getResponse()->getBody();
    }
}
$resp = $instance->v3->marketing->favor->media->imageUpload
->postAsync([
    'body'    => $media->getStream(),
    'headers' => [
        'content-type' => $media->getContentType(),
    ]
])
->then(function($response) {
    echo $response->getBody()->getContents(), PHP_EOL;
    return $response;
})
->otherwise(function($exception) {
    $body = $exception->getResponse()->getBody();
    echo $body->getContents(), PHP_EOL, PHP_EOL, PHP_EOL;
    echo $exception->getTraceAsString(), PHP_EOL;
})
->wait();

敏感信息加/解密

// 参考上上述说明,引入 `WeChatPay\Crypto\Rsa`
use WeChatPay\Crypto\Rsa;
// 加载最新的平台证书
$publicKey = PemUtil::loadCertificate('/path/to/wechatpay/cert.pem');
// 做一个匿名方法,供后续方便使用
$encryptor = function($msg) use ($publicKey) { return Rsa::encrypt($msg, $publicKey); };

// 正常使用Guzzle发起API请求
try {
    // POST 语法糖
    $resp = $instance->chain('v3/applyment4sub/applyment/')->post([
        'json' => [
            'business_code' => 'APL_98761234',
            'contact_info'  => [
                'contact_name'      => $encryptor('value of `contact_name`'),
                'contact_id_number' => $encryptor('value of `contact_id_number'),
                'mobile_phone'      => $encryptor('value of `mobile_phone`'),
                'contact_email'     => $encryptor('value of `contact_email`'),
            ],
            //...
        ],
        'headers' => [
            // 命令行获取证书序列号
            // openssl x509 -in /path/to/wechatpay/cert.pem -noout -serial | awk -F= '{print $2}'
            // 或者使用工具类获取证书序列号 `PemUtil::parseCertificateSerialNo($certificate)`
            'Wechatpay-Serial' => '下载的平台证书序列号',
        ],
    ]);
    echo $resp->getStatusCode().' '.$resp->getReasonPhrase()."\n";
    echo $resp->getBody()."\n";
} catch (Exception $e) {
    echo $e->getMessage()."\n";
    if ($e->hasResponse()) {
        echo $e->getResponse()->getStatusCode().' '.$e->getResponse()->getReasonPhrase()."\n";
        echo $e->getResponse()->getBody();
    }
    return;
}

APIv2

末尾驱动的 HTTP METHOD 方法入参 array $options,接受两个自定义参数,释义如下:

  • $options['nonceless'] - 标量 scalar 任意值,语义上即,本次请求不用自动添加nonce_str参数,推荐 boolean(True)
  • $options['security'] - 布尔量True,语义上即,本次请求需要加载ssl证书,对应的是初始化 array $config['merchant'] 结构体

企业付款到零钱

use WeChatPay\Transformer;
$res = $instance->v2->mmpaymkttransfers->promotion->transfers
->postAsync([
    'xml' => [
      'appid' => 'wx8888888888888888',
      'mch_id' => '1900000109',
      'partner_trade_no' => '10000098201411111234567890',
      'openid' => 'oxTWIuGaIt6gTKsQRLau2M0yL16E',
      'check_name' => 'FORCE_CHECK',
      're_user_name' => '王小王',
      'amount' => 10099,
      'desc' => '理赔',
      'spbill_create_ip' => '192.168.0.1',
    ],
    'security' => true,
    'debug' => true //开启调试模式
])
->then(static function($response) { return Transformer::toArray($response->getBody()->getContents()); })
->otherwise(static function($exception) { return Transformer::toArray($exception->getResponse()->getBody()->getContents()); })
->wait();
print_r($res);

SDK包的设计是开放式的,目前已知可以采用不下十种方式编程编码方式均可触达,同步/异步、模版字面量、驼峰、链式,甚至企业微信之企业支付,均可按开放规范正常工作。

项目链接地址:https://github.com/TheNorthMemory/wechatpay-php 如果喜欢,欢迎star。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK