You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
yanzong/vendor/wechatpay/wechatpay/README_APIv2.md

12 KiB

API v2

本类库可单独用于APIv2的开发,希望能给商户提供一个过渡,可先平滑迁移至本类库以承接APIv2对接,然后再按需替换升级至APIv3上。

以下代码以单独使用展开示例,供商户参考。关于链式 ->,请先阅读 链式 URI Template

V2初始化

use WeChatPay\Builder;

// 商户号,假定为`1000100`
$merchantId = '1000100';
// APIv2密钥(32字节) 假定为`exposed_your_key_here_have_risks`,使用请替换为实际值
$apiv2Key = 'exposed_your_key_here_have_risks';
// 商户私钥,文件路径假定为 `/path/to/merchant/apiclient_key.pem`
$merchantPrivateKeyFilePath = '/path/to/merchant/apiclient_key.pem';
// 商户证书,文件路径假定为 `/path/to/merchant/apiclient_cert.pem`
$merchantCertificateFilePath = '/path/to/merchant/apiclient_cert.pem';

// 工厂方法构造一个实例
$instance = Builder::factory([
    'mchid'      => $merchantId,
    'serial'     => 'nop',
    'privateKey' => 'any',
    'certs'      => ['any' => null],
    'secret'     => $apiv2Key,
    'merchant' => [
        'cert' => $merchantCertificateFilePath,
        'key'  => $merchantPrivateKeyFilePath,
    ],
]);

初始化字典说明如下:

  • mchid 为你的商户号,一般是10字节纯数字
  • serial 为你的商户证书序列号,不使用APIv3可填任意值
  • privateKey 为你的商户API私钥,不使用APIv3可填任意值
  • certs[$serial_number => #resource] 不使用APIv3可填任意值, $serial_number 注意不要与商户证书序列号serial相同
  • 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 = [] 初始化参数,均融合在一个型参上。

企业付款到零钱

官方开发文档地址

use WeChatPay\Transformer;
$res = $instance
->v2->mmpaymkttransfers->promotion->transfers
->postAsync([
    'xml' => [
      'mch_appid'        => 'wx8888888888888888',
      'mchid'            => '1900000109',// 注意这个商户号,key是`mchid`非`mch_id`
      '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((string)$response->getBody());
})
->otherwise(static function($e) {
    // 更多`$e`异常类型判断是必须的,这里仅列出一种可能情况,请根据实际对接过程调整并增加
    if ($e instanceof \GuzzleHttp\Promise\RejectionException) {
        return Transformer::toArray((string)$e->getReason()->getBody());
    }
    return [];
})
->wait();
print_r($res);

APIv2末尾驱动的 HTTP METHOD(POST) 方法入参 array $options,可接受类库定义的两个参数,释义如下:

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

企业付款到银行卡-获取RSA公钥

官方开发文档地址

use WeChatPay\Transformer;
$res = $instance
->v2->risk->getpublickey
->postAsync([
    'xml' => [
        'mch_id' => '1900000109',
        'sign_type' => 'MD5',
    ],
    'security' => true, //请求需要双向证书
    // 特殊接入点,仅对本次请求有效
    'base_uri' => 'https://fraud.mch.weixin.qq.com/',
])
->then(static function($response) {
    return Transformer::toArray((string)$response->getBody());
})
->wait();
print_r($res);

付款到银行卡

官方开发文档地址

use WeChatPay\Transformer;
use WeChatPay\Crypto\Rsa;
// 做一个匿名方法,供后续方便使用,$rsaPubKeyString 是`risk/getpublickey` 的返回值'pub_key'字符串
$rsaPublicKeyInstance = Rsa::from($rsaPubKeyString, Rsa::KEY_TYPE_PUBLIC);
$encryptor = static function(string $msg) use ($rsaPublicKeyInstance): string {
    return Rsa::encrypt($msg, $rsaPublicKeyInstance);
};
$res = $instance
->v2->mmpaysptrans->pay_bank
->postAsync([
    'xml' => [
        'mch_id'           => '1900000109',
        'partner_trade_no' => '1212121221227',
        'enc_bank_no'      => $encryptor('6225............'),
        'enc_true_name'    => $encryptor('张三'),
        'bank_code'        => '1001',
        'amount'           => '100000',
        'desc'             => '理财',
    ],
    'security' => true, //请求需要双向证书
])
->then(static function($response) {
    return Transformer::toArray((string)$response->getBody());
})
->wait();
print_r($res);

SDK自v1.4.6调整了XML转换函数,支持了Stringable 值的转换,对于 Scalar 标量值及实现了 __toString 方法的对象,均支持直接转换,详细可参考 TransformerTest.php 的用例用法示例。

刷脸支付-人脸识别-获取调用凭证

官方开发文档地址

use WeChatPay\Formatter;
use WeChatPay\Transformer;

$res = $instance
->v2->face->get_wxpayface_authinfo
->postAsync([
    'xml' => [
        'store_id'   => '1234567',
        'store_name' => '云店(广州白云机场店)',
        'device_id'  => 'abcdef',
        'rawdata'    => '从客户端`getWxpayfaceRawdata`方法取得的数据',
        'appid'      => 'wx8888888888888888',
        'mch_id'     => '1900000109',
        'now'        => (string)Formatter::timestamp(),
        'version'    => '1',
        'sign_type'  => 'HMAC-SHA256',
    ],
    // 特殊接入点,仅对本次请求有效
    'base_uri' => 'https://payapp.weixin.qq.com/',
])
->then(static function($response) {
    return Transformer::toArray((string)$response->getBody());
})
->wait();
print_r($res);

v2沙箱环境-获取验签密钥API

官方开发文档地址

use WeChatPay\Transformer;
$res = $instance
->v2->xdc->apiv2getsignkey->sign->getsignkey
->postAsync([
    'xml' => [
        'mch_id' => '1900000109',
    ],
    // 通知SDK不接受沙箱环境重定向,仅对本次请求有效
    'allow_redirects' => false,
])
->then(static function($response) {
    return Transformer::toArray((string)$response->getBody());
})
->wait();
print_r($res);

v2通知应答

use WeChatPay\Transformer;

$xml = Transformer::toXml([
  'return_code' => 'SUCCESS',
  'return_msg' => 'OK',
]);

echo $xml;

数据签名

商家券-小程序发券APIv2密钥签名

官方开发文档地址

use WeChatPay\Formatter;
use WeChatPay\Crypto\Hash;

$apiv2Key = 'exposed_your_key_here_have_risks';

$busiFavorFlat = static function (array $params): array {
    $result = ['send_coupon_merchant' => $params['send_coupon_merchant']];
    foreach ($params['send_coupon_params'] as $index => $item) {
        foreach ($item as $key => $value) {
            $result["{$key}{$index}"] = $value;
        }
    }
    return $result;
};

// 发券小程序所需数据结构
$busiFavor = [
    'send_coupon_params' => [
        ['out_request_no' => '1234567', 'stock_id' => 'abc123'],
        ['out_request_no' => '7654321', 'stock_id' => '321cba'],
    ],
    'send_coupon_merchant' => '10016226'
];

$busiFavor += ['sign' => Hash::sign(
    Hash::ALGO_HMAC_SHA256,
    Formatter::queryStringLike(Formatter::ksort($busiFavorFlat($busiFavor))),
    $apiv2Key
)];

echo json_encode($busiFavor);

商家券-H5发券APIv2密钥签名

官方开发文档地址

use WeChatPay\Formatter;
use WeChatPay\Crypto\Hash;

$apiv2Key = 'exposed_your_key_here_have_risks';

$params = [
  'stock_id'             => '12111100000001',
  'out_request_no'       => '20191204550002',
  'send_coupon_merchant' => '10016226',
  'open_id'              => 'oVvBvwEurkeUJpBzX90-6MfCHbec',
  'coupon_code'          => '75345199',
];

$params += ['sign' => Hash::sign(
    Hash::ALGO_HMAC_SHA256,
    Formatter::queryStringLike(Formatter::ksort($params)),
    $apiv2Key
)];

echo json_encode($params);

v2回调通知

回调通知受限于开发者/商户所使用的WebServer有很大差异,这里只给出开发指导步骤,供参考实现。

  1. 从请求头Headers获取Request-ID,商户侧Web解决方案可能有差异,请求头的Request-ID可能大小写不敏感,请根据自身应用来定;
  2. 获取请求body体的XML纯文本;
  3. 调用SDK内置方法,根据签名算法做本地数据签名计算,然后与通知文本的signHash::equals对比验签;
  4. 消息体需要解密的,调用SDK内置方法解密;
  5. 如遇到问题,请拿Request-ID点击这里,联系官方在线技术支持;

样例代码如下:

use WeChatPay\Transformer;
use WeChatPay\Crypto\Hash;
use WeChatPay\Crypto\AesEcb;
use WeChatPay\Formatter;

$inBody = '';// 请根据实际情况获取,例如: file_get_contents('php://input');

$apiv2Key = '';// 在商户平台上设置的APIv2密钥

$inBodyArray = Transformer::toArray($inBody);

// 部分通知体无`sign_type`,部分`sign_type`默认为`MD5`,部分`sign_type`默认为`HMAC-SHA256`
// 部分通知无`sign`字典
// 请根据官方开发文档确定
['sign_type' => $signType, 'sign' => $sign] = $inBodyArray;

$calculated = Hash::sign(
    $signType ?? Hash::ALGO_MD5,// 如没获取到`sign_type`,假定默认为`MD5`
    Formatter::queryStringLike(Formatter::ksort($inBodyArray)),
    $apiv2Key
);

$signatureStatus = Hash::equals($calculated, $sign);

if ($signatureStatus) {
    // 如需要解密的
    ['req_info' => $reqInfo] = $inBodyArray;
    $inBodyReqInfoXml = AesEcb::decrypt($reqInfo, Hash::md5($apiv2Key));
    $inBodyReqInfoArray = Transformer::toArray($inBodyReqInfoXml);
    // print_r($inBodyReqInfoArray);// 打印解密后的结果
}