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

326 lines
12 KiB

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