# API v2

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

以下代码以单独使用展开示例，供商户参考。关于链式 `->`，请先阅读 [链式 URI Template](README.md#链式-uri-template)。

## V2初始化

```php
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 = []` 初始化参数，均融合在一个型参上。

## 企业付款到零钱

[官方开发文档地址](https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_2)

```php
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公钥

[官方开发文档地址](https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay_yhk.php?chapter=24_7&index=4)

```php
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);
```

## 付款到银行卡

[官方开发文档地址](https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay_yhk.php?chapter=24_2)

```php
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](https://www.php.net/manual/zh/class.stringable.php) 值的转换，对于 [Scalar](https://www.php.net/manual/zh/function.is-scalar.php) 标量值及实现了 [__toString](https://www.php.net/manual/zh/language.types.string.php#language.types.string.casting) 方法的对象，均支持直接转换，详细可参考 [TransformerTest.php](tests/TransformerTest.php) 的用例用法示例。

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

[官方开发文档地址](https://pay.weixin.qq.com/wiki/doc/wxfacepay/develop/android/faceuser.html)

```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

[官方开发文档地址](https://pay.weixin.qq.com/wiki/doc/api/tools/sp_coupon.php?chapter=23_1&index=2)

```php
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通知应答

```php
use WeChatPay\Transformer;

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

echo $xml;
```

## 数据签名

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

[官方开发文档地址](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter9_3_1.shtml)

```php
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密钥签名

[官方开发文档地址](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter9_4_1.shtml)

```php
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`内置方法，根据[签名算法](https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3)做本地数据签名计算，然后与通知文本的`sign`做`Hash::equals`对比验签；
4. 消息体需要解密的，调用`SDK`内置方法解密；
5. 如遇到问题，请拿`Request-ID`点击[这里](https://support.pay.weixin.qq.com/online-service?utm_source=github&utm_medium=wechatpay-php&utm_content=apiv2)，联系官方在线技术支持；

样例代码如下：

```php
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);// 打印解密后的结果
}
```
