评论

可链式调用微信支付OpenAPI的 wechatpay-php SDK README

一款你用起来,会觉得原来程序还可以这么自由编写的微信支付SDK——[A]Sync Chainable WeChatPay v2&v3's OpenAPI SDK for PHP

概览

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

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

项目状态

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

环境要求

  • PHP ^7.2.5 || ^8.0
  • guzzlehttp/guzzle ^7.0

phpstan代码静态分析可达6级

约定

本类库是以 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->postAsycn(['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)结合此种编码模式展开。

Notes: RFC3986 #section-3.3 A path consists of a sequence of path segments separated by a slash ("/") character.

开始

首先,通过 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 -m {商户号} -s {商户证书序列号} -f {商户API私钥文件路径} -k {APIv3密钥} -o {保存地址}` 生成
        '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文件路径
  • merchant[key => $path] 为你的商户API私钥,一般是通过官方证书生成工具生成的文件名是apiclient_key.pem文件路径

注: APIv3, APIv2 以及 GuzzleHttp\Client$config = [] 初始化参数,均融合在一个型参上。

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' => 'must be the serial number via the downloaded pem file of `/v3/certificates`',
        ],
    ]);
    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

企业付款到零钱

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。

最后一次编辑于  2021-12-26  
点赞 4
收藏
评论

2 个评论

  • 今天睡地板
    今天睡地板
    2021-08-21

    大哥 能告诉我 sdk内置的验签怎么用吗?

    2021-08-21
    赞同
    回复
  • 蔡婷
    蔡婷
    发表于移动端
    2021-06-17
    膜拜💯💯💯
    2021-06-17
    赞同
    回复
登录 后发表内容