Commit da14de52 authored by wangzhengwen's avatar wangzhengwen

wehcatpay

parent dd416904
composer exec CertificateDownloader.php -- -k "VUCbXcDXZxMM9E2lBi5626qdVvp3tKxA" -m "1716540652" -f "D:/work/code/projecttwo/config/cert/wechat/apiclient_key.pem" -s "113BF40FB7EFF4C2BA69166A211A4570E969AC01" -o "D:/work/code/projecttwo/config/cert/wechat"
php ./bin/CertificateDownloader.php -k "VUCbXcDXZxMM9E2lBi5626qdVvp3tKxA" -m "1716540652" -f "D:/work/code/projecttwo/config/cert/wechat/apiclient_key.pem" -s "113BF40FB7EFF4C2BA69166A211A4570E969AC01" -o "D:/work/code/projecttwo/config/cert/wechat"
/www/server/php/82/bin/php/ ./bin/CertificateDownloader.php ./bin/CertificateDownloader.php -k "VUCbXcDXZxMM9E2lBi5626qdVvp3tKxA" -m "1716540652" -f "/www/wwwroot/api.kjxl.zhubei.cn/config/cert/wechat/apiclient_key.pem" -s "113BF40FB7EFF4C2BA69166A211A4570E969AC01" -o "/www/wwwroot/api.kjxl.zhubei.cn/config/cert/wechat"
......@@ -9,10 +9,11 @@ use Yansongda\Pay\Pay;
use app\api\service\PayService;
use think\facade\Db;
use think\App;
use app\api\service\WeChatPayService;
class PayController
{
//--ignore-platform-req=ext-swoole
/**
* 创建支付订单
......@@ -107,7 +108,7 @@ class PayController
if (!$payment || $payment['pay_method'] != PayService::PAY_METHOD_WECHAT) {
throw new \Exception("支付订单不存在或支付方式不匹配");
}
// halt(intval($payment['pay_amount'] * 100));
// 构造微信订单
$order = [
'out_trade_no' => $orderNo,
......@@ -117,17 +118,10 @@ class PayController
'total' => intval($payment['pay_amount'] * 100), // 转为分
],
];
$wechatPay = new WeChatPayService();
$result = $wechatPay->createNativeOrder($order['out_trade_no'], $order['amount']['total'], $order['description']);
return json(['code' => 1, 'msg' =>'success','data'=>$result]);
// 发起支付
$response = Pay::wechat(PayService::getPayConfig(PayService::PAY_METHOD_WECHAT))->scan($order);
return json([
'code' => 200,
'data' => [
'code_url' => $response->code_url,
'order_no' => $orderNo
]
]);
} catch (\Exception $e) {
return json(['code' => 500, 'msg' => $e->getMessage()]);
}
......@@ -162,22 +156,19 @@ class PayController
*/
public function wechatNotify()
{
$wechat = Pay::wechat(PayService::getPayConfig(PayService::PAY_METHOD_WECHAT));
try {
$data = $wechat->verify();
// 处理支付回调
$result = PayService::handlePaymentNotify($data->out_trade_no, $data->all());
if ($result) {
return $wechat->success();
} else {
return json(['code' => 500, 'msg' => '处理失败']);
$wechatPayService = new WeChatPayService();
$res = $wechatPayService->handleNotify();
if($res)
{
return json(['code' => 'success', 'msg' => '成功']);
}
return json(['code' => 'FAIL', 'msg' => '失败'],500);
} catch (\Exception $e) {
Log::error('微信回调异常: ' . $e->getMessage());
return json(['code' => 500, 'msg' => $e->getMessage()]);
return json(['code' => 'FAIL', 'msg' => '失败'],500);
}
}
......
<?php
namespace app\api\controller;
use app\api\middleware\Auth;
use app\BaseController;
use think\facade\Log;
use think\facade\View;
use Yansongda\Pay\Pay;
use app\api\service\PayService;
use think\facade\Db;
use think\App;
class PayController_old
{
//--ignore-platform-req=ext-swoole
/**
* 创建支付订单
*/
public function create($params)
{
// $params = request()->only([
// 'order_id',
// 'order_type',
// 'pay_method',
// 'amount',
// 'user_id'
// ]);
//
// // 验证参数
// $validate = validate('Pay');
// if (!$validate->scene('create')->check($params)) {
// return json(['code' => 400, 'msg' => $validate->getError()]);
// }
// 创建支付订单
$result = PayService::createPayment(
$params['user_id'],
$params['order_id'],
$params['amount'],
$params['pay_method'],
$params['order_type'],
request()->param()
);
if (!$result['status']) {
return json(['code' => 0, 'msg' => $result['msg']]);
}
return json([
'code' => 1,
'data' => [
'payment_id' => $result['data']['payment_id'],
'order_no' => $result['data']['order_no']
]
]);
}
/**
* 发起支付宝支付
*/
public function alipay()
{
$orderNo = request()->param('order_no');
try {
// 查询支付订单
$payment = Db::name('payment')
->where('order_no', $orderNo)
->find();
if (!$payment || $payment['pay_method'] != PayService::PAY_METHOD_ALIPAY) {
throw new \Exception("支付订单不存在或支付方式不匹配");
}
// 构造支付宝订单
$order = [
'out_trade_no' => $orderNo,
'total_amount' => $payment['pay_amount'],
'subject' => $payment['order_type'] == PayService::ORDER_TYPE_COURSE ?
'课程购买' : '证书购买',
];
// 发起支付
$alipay = Pay::alipay(PayService::getPayConfig(PayService::PAY_METHOD_ALIPAY))->web($order);
halt($alipay);
return redirect($alipay->getTargetUrl());
} catch (\Exception $e) {
Log::error("支付创建错误: {$orderNo} - " . $e->getMessage());
return json(['code' => 500, 'msg' => $e->getMessage()]);
}
}
/**
* 发起微信支付
*/
public function wechat()
{
$orderNo = request()->param('order_no');
try {
// 查询支付订单
$payment = Db::name('payment')
->where('order_no', $orderNo)
->find();
if (!$payment || $payment['pay_method'] != PayService::PAY_METHOD_WECHAT) {
throw new \Exception("支付订单不存在或支付方式不匹配");
}
// 构造微信订单
$order = [
'out_trade_no' => $orderNo,
'description' => $payment['order_type'] == PayService::ORDER_TYPE_COURSE ?
'课程购买' : '证书购买',
'amount' => [
'total' => intval($payment['pay_amount'] * 100), // 转为分
],
];
Pay::setContainer(\Illuminate\Container\Container::getInstance());
// Pay::config(config('pay.wechat'));
$result = Pay::wechat()->scan($order);
halt($result);
// halt(config('pay.wechat'));
// halt(PayService::getPayConfig(PayService::PAY_METHOD_WECHAT)['default']);
// 发起支付
// $response = Pay::wechat(config('pay.wechat'))->scan($order);
$pay = Pay::wechat()->scan($order);
dd($pay); // 查看最终生成的配置
return json([
'code' => 200,
'data' => [
'code_url' => $response->code_url,
'order_no' => $orderNo
]
]);
} catch (\Exception $e) {
return json(['code' => 500, 'msg' => $e->getMessage()]);
}
}
/**
* 支付宝回调处理
*/
public function alipayNotify()
{
$alipay = Pay::alipay(PayService::getPayConfig(PayService::PAY_METHOD_ALIPAY));
try {
$data = Pay::alipay()->callback();
// 处理支付回调
$result = PayService::handlePaymentNotify($data->out_trade_no, $data->all());
if ($result) {
return $alipay->success();
} else {
return json(['code' => 500, 'msg' => '处理失败']);
}
} catch (\Exception $e) {
Log::error('支付宝回调异常: ' . $e->getMessage());
return json(['code' => 500, 'msg' => $e->getMessage()]);
}
}
/**
* 微信支付回调处理
*/
public function wechatNotify()
{
$wechat = Pay::wechat(PayService::getPayConfig(PayService::PAY_METHOD_WECHAT));
try {
$data = $wechat->verify();
// 处理支付回调
$result = PayService::handlePaymentNotify($data->out_trade_no, $data->all());
if ($result) {
return $wechat->success();
} else {
return json(['code' => 500, 'msg' => '处理失败']);
}
} catch (\Exception $e) {
Log::error('微信回调异常: ' . $e->getMessage());
return json(['code' => 500, 'msg' => $e->getMessage()]);
}
}
/**
* 查询支付状态
*/
public function query()
{
$orderNo = request()->param('order_no');
try {
$payment = Db::name('fj_payment')
->where('order_no', $orderNo)
->find();
if (!$payment) {
throw new \Exception("支付订单不存在");
}
return json([
'code' => 200,
'data' => [
'order_no' => $payment['order_no'],
'pay_status' => $payment['pay_status'],
'pay_time' => $payment['pay_time'],
'pay_amount' => $payment['pay_amount']
]
]);
} catch (\Exception $e) {
return json(['code' => 500, 'msg' => $e->getMessage()]);
}
}
}
\ No newline at end of file
......@@ -9,11 +9,7 @@ use app\model\system\SystemArea;
class Util extends BaseController
{
protected $middleware = [
[
'Auth' => ['except' => ['sums']]
],
];
public function getAreaJson()
{
......
......@@ -88,10 +88,11 @@ class PayService
*/
public static function handlePaymentNotify($orderNo, $notifyData)
{
Db::startTrans();
try {
// 查询支付订单
$payment = Db::name('fj_payment')
$payment = Db::name('payment')
->where('order_no', $orderNo)
->lock(true)
->find();
......@@ -108,7 +109,7 @@ class PayService
// 验证金额
$amount = $payment['pay_method'] == self::PAY_METHOD_ALIPAY ?
$notifyData['total_amount'] : ($notifyData['total_fee'] / 100);
$notifyData['amount']['total'] : ($notifyData['amount']['total'] / 100);
if (bccomp($amount, $payment['pay_amount'], 2) !== 0) {
throw new \Exception("金额不一致: 订单{$payment['pay_amount']}, 回调{$amount}");
......@@ -122,7 +123,7 @@ class PayService
'updatetime' => time()
];
$result = Db::name('fj_payment')
$result = Db::name('payment')
->where('order_no', $orderNo)
->update($updateData);
......
This diff is collapsed.
......@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "abeeceb9a3eed0f7bab8445271a50739",
"content-hash": "94ac66d0de24bd6012b59701a6bcfc94",
"packages": [
{
"name": "adbario/php-dot-notation",
......@@ -4943,6 +4943,73 @@
},
"time": "2021-02-24T11:53:01+00:00"
},
{
"name": "wechatpay/wechatpay",
"version": "1.4.12",
"source": {
"type": "git",
"url": "https://github.com/wechatpay-apiv3/wechatpay-php.git",
"reference": "bd2148e0456f560df4d1c857d6cd1f8ad9f5222e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/wechatpay-apiv3/wechatpay-php/zipball/bd2148e0456f560df4d1c857d6cd1f8ad9f5222e",
"reference": "bd2148e0456f560df4d1c857d6cd1f8ad9f5222e",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-libxml": "*",
"ext-openssl": "*",
"ext-simplexml": "*",
"guzzlehttp/guzzle": "^6.5 || ^7.0",
"guzzlehttp/uri-template": "^0.2 || ^1.0",
"php": ">=7.1.2"
},
"require-dev": {
"phpstan/phpstan": "^0.12.89 || ^1.0",
"phpunit/phpunit": "^7.5 || ^8.5.16 || ^9.3.5"
},
"bin": [
"bin/CertificateDownloader.php"
],
"type": "library",
"autoload": {
"psr-4": {
"WeChatPay\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "James ZHANG",
"homepage": "https://github.com/TheNorthMemory"
},
{
"name": "WeChatPay Community",
"homepage": "https://developers.weixin.qq.com/community/pay"
}
],
"description": "[A]Sync Chainable WeChatPay v2&v3's OpenAPI SDK for PHP",
"homepage": "https://pay.weixin.qq.com/",
"keywords": [
"AES-GCM",
"aes-ecb",
"openapi-chainable",
"rsa-oaep",
"wechatpay",
"xml-builder",
"xml-parser"
],
"support": {
"issues": "https://github.com/wechatpay-apiv3/wechatpay-php/issues",
"source": "https://github.com/wechatpay-apiv3/wechatpay-php/tree/v1.4.12"
},
"time": "2025-01-26T14:16:41+00:00"
},
{
"name": "workerman/gateway-worker",
"version": "v3.0.22",
......
-----BEGIN CERTIFICATE-----
MIIEITCCAwmgAwIBAgIUETv0D7fv9MK6aRZqIRpFcOlprAEwDQYJKoZIhvcNAQEL
BQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsT
FFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3Qg
Q0EwHhcNMjUwNTI2MDMzOTU1WhcNMzAwNTI1MDMzOTU1WjB7MRMwEQYDVQQDDAox
NzE2NTQwNjUyMRswGQYDVQQKDBLlvq7kv6HllYbmiLfns7vnu58xJzAlBgNVBAsM
HumHjeW6hua1gemAn+enkeaKgOaciemZkOWFrOWPuDELMAkGA1UEBhMCQ04xETAP
BgNVBAcMCFNoZW5aaGVuMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
1jC9WzCraMewjDHfCjyQcCTpv7MFSSXvntfQY2TniPyrXa3x9WNVRCF7bTMHR/jd
VAykR70SrVmY4PgNhvgCw3boFfu6qzecThy4oiuWuFdJ5jw/sJzkAW8P+pFcqZFS
h08b8QJKmExb7izEbt6JKURObzr3DxmMRU5lJoG1fZIgVa3PuI8q9qgUIAtB3JjN
NGR4XRUtDjvthAr/Li0dbLF320RJ7f2Pi5LdIlZQp/1P0+Cnt+bzV9xMxUrXxv3t
BejU/bVbBgvZBCUjUPg4lXmsz+uKl60IipH/RyVeI+17UMeIuoRcD2wEvCaJkGvz
kdCezrlKHs2PqVFf09DqvwIDAQABo4G5MIG2MAkGA1UdEwQCMAAwCwYDVR0PBAQD
AgP4MIGbBgNVHR8EgZMwgZAwgY2ggYqggYeGgYRodHRwOi8vZXZjYS5pdHJ1cy5j
b20uY24vcHVibGljL2l0cnVzY3JsP0NBPTFCRDQyMjBFNTBEQkMwNEIwNkFEMzk3
NTQ5ODQ2QzAxQzNFOEVCRDImc2c9SEFDQzQ3MUI2NTQyMkUxMkIyN0E5RDMzQTg3
QUQxQ0RGNTkyNkUxNDAzNzEwDQYJKoZIhvcNAQELBQADggEBAGfr6HeuQBJfnlCD
IxTe2CfENF1Hk7A3hGlMqSAij+FaH4ccuvitXJ4Zq1bRKQFoa1wfBxPSASr0U46I
SZxO71Lk77MsqIFDxN6GUCoKRbX8ZyydMZ5R6OB1rVHJmh2BftwCEMes5RdM6gOH
1QceLU5Wraygt7icCoJJrmXIEEcT0bC/C+GNt4pxURyeMCVx8gzCCwXTTWrgXEdd
5gih1mV4olM37yXKVfJzSGLIMs+18uGyjy32C5v2VmqG8/6SJvOPcDaEWrISo2Xh
72KUGHRsAPWWT8E2GMYGcXpNqdXEWrbWOy29eI5Gl5X1EaLB5PK5zQFUMl+J9yk1
+wGxFyU=
-----END CERTIFICATE-----
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDWML1bMKtox7CM
Md8KPJBwJOm/swVJJe+e19BjZOeI/KtdrfH1Y1VEIXttMwdH+N1UDKRHvRKtWZjg
+A2G+ALDdugV+7qrN5xOHLiiK5a4V0nmPD+wnOQBbw/6kVypkVKHTxvxAkqYTFvu
LMRu3okpRE5vOvcPGYxFTmUmgbV9kiBVrc+4jyr2qBQgC0HcmM00ZHhdFS0OO+2E
Cv8uLR1ssXfbREnt/Y+Lkt0iVlCn/U/T4Ke35vNX3EzFStfG/e0F6NT9tVsGC9kE
JSNQ+DiVeazP64qXrQiKkf9HJV4j7XtQx4i6hFwPbAS8JomQa/OR0J7OuUoezY+p
UV/T0Oq/AgMBAAECggEAapfw9HvhIhEW3H5JOIfxfc3xAjTucXvOp2dRztU9oN/V
zJYvbuaTXYeoLC0T945zX0u3SfhfXiPTwEnSOZQdk/cOAzq2qFTRldIXVKWKqYzP
OyRKsfMySUBjXFiRG9Y1kx8ckbGJPAfsTDi9PUvESOQ0gIiAwWP+edNM5X/xuV5g
9hy35INkPfVvq424i5JfG3mtgs2g5/GCuzZis5h2l6Tjf8Ebs/3hhcy6yIVRN12y
2RpnEXEqkUKh4NTP+xr3DiU5eo6cr0ej025CI7s3jK+G7WTaryyfMLVH3LBTaBUV
rmBS0fqg8WhnHli6AK5JUAJHMuOyWeQHTygEZsSLKQKBgQD44ws9g+Pryx+q4/js
dt78rDJH7tL9D7tVWcMJ1/qVvIgQXpa0R/mXOGXsZmd+HuJ2tNspcyF0TNAGH49g
j3vDE72eNB+vz+QwqHYKA+esWx71vt5UyHzAtkNqjbMvsz006wC9ws3A0ooqY1pF
guE73se4cAYA8HRyUPyyfi9mywKBgQDcT9efnDp22WLdGJUhiyi5v0RySAIwE3TO
ug2TuY73zfYP1J17fIs6+Xs5I/r9mzisV+T5Ddlgyggpc+iHdRXgL8hNTvDjw/iF
zj3czsbhnJwB4qQsGYaGMC92Yz7Ey5fzzB8eJomtsN3Mcq+E/ln7dXAXTNopKug+
BwbXLhRZXQKBgQCOa229L8G25i8i3P1OYt9K+0ZyfylhAiWSu6Ct+1c7Y/0AUQAv
/ZfHftBkLF1AgG/aubdHysf0AxhuyJSFDtYlVSCGbRFMy8uqRFv3czCZIjNfMG+N
WIR6ylFdoeRNgWWe6HMuI1EV6+SASQYZDMHSPrNOyVvGIdKgr9NKWIbLbQKBgQCo
QOXZBNR1nfHuDu8d7gxNR3C7opjhJIrJsrfZwRYZ1Jb+Yg9flq8yfAQKkJsIAfqX
TI0XU+dXDxSKq/XDNb1eOL/NouM/35O2hNsj/ltPRG601eUxtNDTPIuS65qtaxuC
WrrNKPtuxiuuD0xS0nngHEFd2him5hj1/iHQRmXOFQKBgCLIKrWZ35IoqbNYVZPD
Ijy95iYSBaVsBsV5fXXoY6FsZyThXb9GMb5bnAs6zSb0txqwG/pQx+oAMIls2JKr
Jr31W9vMRnuFkAsw/CySGdLVwpz/DYrkNxrXIXNvgpxLV5CVIR9VTPeFKN5edtUy
LFCGV53MF75I0FESq0jUhOrd
-----END PRIVATE KEY-----
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApccwlLAcFz8BxMOFAE8o
BksEHwHOeNcFG/V0t7ijy9PBNrvGfWsHYPZPXO2Q08jSAWdUeJznM9QntI0v1Oag
qNPf7LNAb3wJhL19kN4Ug53t4RPWz0XoZJIjJI4/ZnE7tlFdXzKDrCtk2jKWuOiz
6Fq+bWy578tXVHOiV5/M79sAAxQn+vbnPpWgVyUZ7yaZ5MhAAxEuHgoP7bAz+22h
Vo58FqF2UewQ/Ikg4A9DnpGhkB2DHCuGtyMsCJ5IHB9HW2XQ3erzVv8DiuExquaQ
SXnfC72+0gOrgJhLQmYNfCtDcz19vmoLKOXBQOiqR9Zt3hEPjlU7SXxfy8em/zYv
AQIDAQAB
-----END PUBLIC KEY-----
-----BEGIN CERTIFICATE-----
MIIEFDCCAvygAwIBAgIUWBs/CqlY8N8QWs5MRSMjmfV2qY4wDQYJKoZIhvcNAQEL
BQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsT
FFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3Qg
Q0EwHhcNMjUwNTI2MDMzOTU1WhcNMzAwNTI1MDMzOTU1WjBuMRgwFgYDVQQDDA9U
ZW5wYXkuY29tIHNpZ24xEzARBgNVBAoMClRlbnBheS5jb20xHTAbBgNVBAsMFFRl
bnBheS5jb20gQ0EgQ2VudGVyMQswCQYDVQQGEwJDTjERMA8GA1UEBwwIU2hlblpo
ZW4wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDlh4zBSXUWlmnWgHhY
AZX1/HXqoJZs5NqODvuBClvvmLCF7tl6ht3ygoDaGOF5HzVxAv0QQdpcos5yHHKX
y/pjcMJJ6rDS59y/A5wqIIpWoRnRa3wdNHOfIKlU8AWGmqUR+zg9b6w+9wNRffuz
XKdLIGDG5zUTM0bLTJQkYBNl8HXtt8c5OtvNiZ0y68ZTNMm+M+UJbqrgVoOcKiTJ
xLNkfP+60QL1CJIEdTKjc8KX0f9uDQ+HcTzlbk3Jp+lj/dxJaHALFqn7cPgD70if
SKjvB34KmZ5gMixaM/fRyVZ2lL10q/9Jz72ycCqZvaGi3KSpVyukG650xQwMlC0u
vFMBAgMBAAGjgbkwgbYwCQYDVR0TBAIwADALBgNVHQ8EBAMCA/gwgZsGA1UdHwSB
kzCBkDCBjaCBiqCBh4aBhGh0dHA6Ly9ldmNhLml0cnVzLmNvbS5jbi9wdWJsaWMv
aXRydXNjcmw/Q0E9MUJENDIyMEU1MERCQzA0QjA2QUQzOTc1NDk4NDZDMDFDM0U4
RUJEMiZzZz1IQUNDNDcxQjY1NDIyRTEyQjI3QTlEMzNBODdBRDFDREY1OTI2RTE0
MDM3MTANBgkqhkiG9w0BAQsFAAOCAQEAhEVAKCvwHRCkqbvGwsmzW6WYhK/DrDtK
9J9/RNXn4eubwjkPiiMPH3Nvvi7SToZIAVQiWDVJ12te3MPBqOZmfedaVYULE2qA
bzUE8ET10hO/QQcUN6OlbGkvrafqFjEsnDeJ4wwLTmXcd3iXHewec7JuaTFC8dFQ
h00qfNxxlrJDskfhABTirdKHHDwFDrEQIy3RcpgFKa3U0YQoQghXT2oSMI/r0RKl
9cLBMdZumlVPSr0i131tFflZ88xiN6/l3gALSIWtBUBe1x+xGqwFiMWzAap0rDdK
axkHXvHzese6Kj/0GVaF28RwUeXojugZW2Yap8epoTUOqjQrrEgGlg==
-----END CERTIFICATE-----
\ No newline at end of file
......@@ -27,48 +27,30 @@ return [
],
'wechat' => [
'default' => [
// 「必填」商户号,服务商模式下为服务商商户号
// 可在 https://pay.weixin.qq.com/ 账户中心->商户信息 查看
'mch_id' => '',
// 「选填」v2商户私钥
'mch_secret_key_v2' => '',
// 「必填」v3 商户秘钥
// 即 API v3 密钥(32字节,形如md5值),可在 账户中心->API安全 中设置
'mch_secret_key' => '',
// 「必填」商户私钥 字符串或路径
// 即 API证书 PRIVATE KEY,可在 账户中心->API安全->申请API证书 里获得
// 文件名形如:apiclient_key.pem
'mch_secret_cert' => '',
// 「必填」商户公钥证书路径
// 即 API证书 CERTIFICATE,可在 账户中心->API安全->申请API证书 里获得
// 文件名形如:apiclient_cert.pem
'mch_public_cert_path' => '',
// 「必填」微信回调url
// 不能有参数,如?号,空格等,否则会无法正确回调
'notify_url' => 'https://yansongda.cn/wechat/notify',
// 「选填」公众号 的 app_id
// 可在 mp.weixin.qq.com 设置与开发->基本配置->开发者ID(AppID) 查看
'mp_app_id' => '2016082000291234',
// 「选填」小程序 的 app_id
'mini_app_id' => '',
// 「选填」app 的 app_id
'app_id' => '',
// 「选填」服务商模式下,子公众号 的 app_id
'sub_mp_app_id' => '',
// 「选填」服务商模式下,子 app 的 app_id
'sub_app_id' => '',
// 「选填」服务商模式下,子小程序 的 app_id
'sub_mini_app_id' => '',
// 「选填」服务商模式下,子商户id
'sub_mch_id' => '',
// 「选填」(适用于 2024-11 及之前开通微信支付的老商户)微信支付平台证书序列号及证书路径,强烈建议 php-fpm 模式下配置此参数
// 「必填」微信支付公钥ID及证书路径,key 填写形如 PUB_KEY_ID_0000000000000024101100397200000006 的公钥id,见 https://pay.weixin.qq.com/doc/v3/merchant/4013053249
'wechat_public_cert_path' => [
'45F59D4DABF31918AFCEC556D5D2C6E376675D57' => __DIR__.'/Cert/wechatPublicKey.crt',
'PUB_KEY_ID_0000000000000024101100397200000006' => __DIR__.'/Cert/publickey.pem',
],
// 「选填」默认为正常模式。可选为: MODE_NORMAL, MODE_SERVICE
'mode' => \Yansongda\Pay\Pay::MODE_NORMAL,
// 商户号
'mchid' => env('WECHATPAY_MCHID', '1716540652'),
// 商户证书序列号
'serial_no' => env('WECHATPAY_SERIAL_NO', '113BF40FB7EFF4C2BA69166A211A4570E969AC01'),
// APIv3密钥
'apiv3_key' => env('WECHATPAY_APIV3_KEY', 'VUCbXcDXZxMM9E2lBi5626qdVvp3tKxA'),
// AppID
'appid' => env('WECHATPAY_APPID', 'wx24528a99c58e1919'),
// 商户私钥文件路径
'private_key_path' =>__DIR__.'/cert/wechat/apiclient_key.pem',
// 商户证书文件路径
'certificate_path' => __DIR__.'/cert/wechat/apiclient_cert.pem',
// 通知地址
'notify_url' => env('WECHATPAY_NOTIFY_URL', 'https://api.kuajingxl.com/api/payController/wechatNotify'),
// 微信支付公钥路径
'publick_key_path' =>__DIR__.'/cert/wechat/publickey.pem',
'publick_key_id' =>'PUB_KEY_ID_0117165406522025052600322092003400',
//平台证书
'wechatpay_platform_path' => __DIR__.'/cert/wechat/wechatpay_581B3F0AA958F0DF105ACE4C45232399F576A98E.pem',
'wechatpay_platform_id' => '581B3F0AA958F0DF105ACE4C45232399F576A98E'
// 'certificate_path' => __DIR__ . '/cert/wechat/wechatpay_platform.pem', // 微信平台证书(新下载)
]
],
];
\ No newline at end of file
......@@ -14,7 +14,7 @@ use AlibabaCloud\Client\Exception\ClientException;
use AlibabaCloud\Client\Exception\ServerException;
use AlibabaCloud\Dysmsapi\Dysmsapi;
use think\facade\Cache;
use think\Facade\Db;
use think\facade\Db;
/**
* 短信发送类
......
File added
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../wechatpay/wechatpay/bin/CertificateDownloader.php)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
return include("phpvfscomposer://" . __DIR__ . '/..'.'/wechatpay/wechatpay/bin/CertificateDownloader.php');
}
}
return include __DIR__ . '/..'.'/wechatpay/wechatpay/bin/CertificateDownloader.php';
@ECHO OFF
setlocal DISABLEDELAYEDEXPANSION
SET BIN_TARGET=%~dp0/CertificateDownloader.php
SET COMPOSER_RUNTIME_BIN_DIR=%~dp0
php "%BIN_TARGET%" %*
......@@ -10,10 +10,10 @@ return array(
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
'7448f3465e10b5f033e4babb31eb0b06' => $vendorDir . '/topthink/think-orm/src/helper.php',
'35fab96057f1bf5e7aba31a8a6d5fdde' => $vendorDir . '/topthink/think-orm/stubs/load_stubs.php',
'15ec93fa4ce4b2d53816a1a5f2c514e2' => $vendorDir . '/topthink/think-validate/src/helper.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
'662a729f963d39afe703c9d9b7ab4a8c' => $vendorDir . '/symfony/polyfill-php83/bootstrap.php',
'6b998e7ad3182c0d21d23780badfa07b' => $vendorDir . '/yansongda/supports/src/Functions.php',
......
......@@ -23,6 +23,7 @@ return array(
'Yansongda\\Pay\\' => array($vendorDir . '/yansongda/pay/src'),
'Yansongda\\Artful\\' => array($vendorDir . '/yansongda/artful/src'),
'Workerman\\' => array($vendorDir . '/workerman/workerman'),
'WeChatPay\\' => array($vendorDir . '/wechatpay/wechatpay/src'),
'Symfony\\Polyfill\\Php83\\' => array($vendorDir . '/symfony/polyfill-php83'),
'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'),
'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'),
......
......@@ -11,10 +11,10 @@ class ComposerStaticInit118552ef2b3077b276422e248ece3c62
'7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php',
'6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php',
'7448f3465e10b5f033e4babb31eb0b06' => __DIR__ . '/..' . '/topthink/think-orm/src/helper.php',
'35fab96057f1bf5e7aba31a8a6d5fdde' => __DIR__ . '/..' . '/topthink/think-orm/stubs/load_stubs.php',
'15ec93fa4ce4b2d53816a1a5f2c514e2' => __DIR__ . '/..' . '/topthink/think-validate/src/helper.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php',
'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
'662a729f963d39afe703c9d9b7ab4a8c' => __DIR__ . '/..' . '/symfony/polyfill-php83/bootstrap.php',
'6b998e7ad3182c0d21d23780badfa07b' => __DIR__ . '/..' . '/yansongda/supports/src/Functions.php',
......@@ -75,6 +75,7 @@ class ComposerStaticInit118552ef2b3077b276422e248ece3c62
'W' =>
array (
'Workerman\\' => 10,
'WeChatPay\\' => 10,
),
'S' =>
array (
......@@ -240,6 +241,10 @@ class ComposerStaticInit118552ef2b3077b276422e248ece3c62
array (
0 => __DIR__ . '/..' . '/workerman/workerman',
),
'WeChatPay\\' =>
array (
0 => __DIR__ . '/..' . '/wechatpay/wechatpay/src',
),
'Symfony\\Polyfill\\Php83\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php83',
......
......@@ -5352,6 +5352,76 @@
},
"install-path": "../veitool/admin"
},
{
"name": "wechatpay/wechatpay",
"version": "1.4.12",
"version_normalized": "1.4.12.0",
"source": {
"type": "git",
"url": "https://github.com/wechatpay-apiv3/wechatpay-php.git",
"reference": "bd2148e0456f560df4d1c857d6cd1f8ad9f5222e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/wechatpay-apiv3/wechatpay-php/zipball/bd2148e0456f560df4d1c857d6cd1f8ad9f5222e",
"reference": "bd2148e0456f560df4d1c857d6cd1f8ad9f5222e",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-libxml": "*",
"ext-openssl": "*",
"ext-simplexml": "*",
"guzzlehttp/guzzle": "^6.5 || ^7.0",
"guzzlehttp/uri-template": "^0.2 || ^1.0",
"php": ">=7.1.2"
},
"require-dev": {
"phpstan/phpstan": "^0.12.89 || ^1.0",
"phpunit/phpunit": "^7.5 || ^8.5.16 || ^9.3.5"
},
"time": "2025-01-26T14:16:41+00:00",
"bin": [
"bin/CertificateDownloader.php"
],
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"WeChatPay\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "James ZHANG",
"homepage": "https://github.com/TheNorthMemory"
},
{
"name": "WeChatPay Community",
"homepage": "https://developers.weixin.qq.com/community/pay"
}
],
"description": "[A]Sync Chainable WeChatPay v2&v3's OpenAPI SDK for PHP",
"homepage": "https://pay.weixin.qq.com/",
"keywords": [
"AES-GCM",
"aes-ecb",
"openapi-chainable",
"rsa-oaep",
"wechatpay",
"xml-builder",
"xml-parser"
],
"support": {
"issues": "https://github.com/wechatpay-apiv3/wechatpay-php/issues",
"source": "https://github.com/wechatpay-apiv3/wechatpay-php/tree/v1.4.12"
},
"install-path": "../wechatpay/wechatpay"
},
{
"name": "workerman/gateway-worker",
"version": "v3.0.22",
......
......@@ -1750,6 +1750,15 @@
'aliases' => array(),
'dev_requirement' => false,
),
'wechatpay/wechatpay' => array(
'pretty_version' => '1.4.12',
'version' => '1.4.12.0',
'reference' => 'bd2148e0456f560df4d1c857d6cd1f8ad9f5222e',
'type' => 'library',
'install_path' => __DIR__ . '/../wechatpay/wechatpay',
'aliases' => array(),
'dev_requirement' => false,
),
'workerman/gateway-worker' => array(
'pretty_version' => 'v3.0.22',
'version' => '3.0.22.0',
......
<?php
// This file is automatically generated at:2025-05-23 17:01:16
// This file is automatically generated at:2025-05-26 14:08:29
declare (strict_types = 1);
return array (
0 => 'think\\captcha\\CaptchaService',
......
......@@ -21,13 +21,13 @@ class QRcode
{
/**
* 初始化配置信息
* @var array
* @var array
*/
protected $config = [
'cache_dir' => 'static/qrcode',
'background' => ''
];
protected $cache_dir = ''; //二维码缓存
protected $outfile = ''; //输出二维码文件
......@@ -53,12 +53,36 @@ class QRcode
* @param int $size 图片大小
* @return Obj $this
*/
public function png($url,$outfile=false,$evel='H',$size=5){
$this->outfile = $outfile ?: $this->cache_dir.'/'.time().'.png';
\QRcode::png($url,$this->outfile,$evel,$size,2);
public function png($url, $outfile = null, $level = 'H', $size = 5) {
// 使用系统临时目录
$tempDir = sys_get_temp_dir() . '/qrcodes/';
if (!is_dir($tempDir)) {
mkdir($tempDir, 0777, true);
}
$this->outfile = $tempDir . uniqid('wxpay_') . '.png';
// 生成二维码
\QRcode::png($url, $this->outfile, $level, $size, 2);
// 验证文件
if (!file_exists($this->outfile)) {
throw new Exception("文件生成失败,路径:{$this->outfile}");
}
return $this;
}
public function getBase64() {
if (!file_exists($this->outfile)) {
throw new Exception('请先调用png()方法生成二维码');
}
$imageData = file_get_contents($this->outfile);
unlink($this->outfile); // 清理临时文件
return 'data:image/png;base64,' . base64_encode($imageData);
}
/**
* 添加logo到二维码中
* @param $logo
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
# Security Policy
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| 1.x | :white_check_mark: |
## Reporting a Vulnerability
Please do not open GitHub issues or pull requests - this makes the problem immediately visible to everyone, including malicious actors.
Security issues in this open source project can be safely reported to [TSRC](https://security.tencent.com).
## 报告漏洞
请不要使用 GitHub issues 或 pull request —— 这会让漏洞立即暴露给所有人,包括恶意人员。
请将本开源项目的安全问题报告给 [腾讯安全应急响应中心](https://security.tencent.com).
---
另外,你可能需要关注影响本SDK运行时行为的主要的PHP扩展缺陷列表:
+ [OpenSSL](https://www.openssl.org/news/vulnerabilities.html)
+ [libxml2](https://gitlab.gnome.org/GNOME/libxml2/-/blob/master/NEWS)
+ [curl](https://curl.se/docs/security.html)
当你准备在报告安全问题时,请先对照如上列表,确认是否存在已知运行时环境安全问题。
当你尝试升级主要扩展至最新版本之后,如若问题依旧存在,请将本开源项目的安全问题报告给 [TSRC腾讯安全应急响应中心](https://security.tencent.com),致谢。
This diff is collapsed.
#!/usr/bin/env php
<?php declare(strict_types=1);
// load autoload.php
$possibleFiles = [__DIR__.'/../vendor/autoload.php', __DIR__.'/../../../autoload.php', __DIR__.'/../../autoload.php'];
$file = null;
foreach ($possibleFiles as $possibleFile) {
if (\file_exists($possibleFile)) {
$file = $possibleFile;
break;
}
}
if (null === $file) {
throw new \RuntimeException('Unable to locate autoload.php file.');
}
require_once $file;
unset($possibleFiles, $possibleFile, $file);
use GuzzleHttp\Middleware;
use GuzzleHttp\Exception\RequestException;
use Psr\Http\Message\ResponseInterface;
use WeChatPay\Builder;
use WeChatPay\ClientDecoratorInterface;
use WeChatPay\Crypto\AesGcm;
/**
* CertificateDownloader class
*/
class CertificateDownloader
{
private const DEFAULT_BASE_URI = 'https://api.mch.weixin.qq.com/';
public function run(): void
{
$opts = $this->parseOpts();
if (!$opts || isset($opts['help'])) {
$this->printHelp();
return;
}
if (isset($opts['version'])) {
self::prompt(ClientDecoratorInterface::VERSION);
return;
}
$this->job($opts);
}
/**
* Before `verifier` executing, decrypt and put the platform certificate(s) into the `$certs` reference.
*
* @param string $apiv3Key
* @param array<string,?string> $certs
*
* @return callable(ResponseInterface)
*/
private static function certsInjector(string $apiv3Key, array &$certs): callable {
return static function(ResponseInterface $response) use ($apiv3Key, &$certs): ResponseInterface {
$body = (string) $response->getBody();
$json = \json_decode($body);
$data = \is_object($json) && isset($json->data) && \is_array($json->data) ? $json->data : [];
\array_map(static function($row) use ($apiv3Key, &$certs) {
$cert = $row->encrypt_certificate;
$certs[$row->serial_no] = AesGcm::decrypt($cert->ciphertext, $apiv3Key, $cert->nonce, $cert->associated_data);
}, $data);
return $response;
};
}
/**
* @param array<string,string|true> $opts
*
* @return void
*/
private function job(array $opts): void
{
static $certs = ['any' => null];
$outputDir = $opts['output'] ?? \sys_get_temp_dir();
$apiv3Key = (string) $opts['key'];
$instance = Builder::factory([
'mchid' => $opts['mchid'],
'serial' => $opts['serialno'],
'privateKey' => \file_get_contents((string)$opts['privatekey']),
'certs' => &$certs,
'base_uri' => (string)($opts['baseuri'] ?? self::DEFAULT_BASE_URI),
]);
/** @var \GuzzleHttp\HandlerStack $stack */
$stack = $instance->getDriver()->select(ClientDecoratorInterface::JSON_BASED)->getConfig('handler');
// The response middle stacks were executed one by one on `FILO` order.
$stack->after('verifier', Middleware::mapResponse(self::certsInjector($apiv3Key, $certs)), 'injector');
$stack->before('verifier', Middleware::mapResponse(self::certsRecorder((string) $outputDir, $certs)), 'recorder');
$instance->chain('v3/certificates')->getAsync(
['debug' => true]
)->otherwise(static function($exception) {
self::prompt($exception->getMessage());
if ($exception instanceof RequestException && $exception->hasResponse()) {
/** @var ResponseInterface $response */
$response = $exception->getResponse();
self::prompt((string) $response->getBody(), '', '');
}
self::prompt($exception->getTraceAsString());
})->wait();
}
/**
* After `verifier` executed, wrote the platform certificate(s) onto disk.
*
* @param string $outputDir
* @param array<string,?string> $certs
*
* @return callable(ResponseInterface)
*/
private static function certsRecorder(string $outputDir, array &$certs): callable {
return static function(ResponseInterface $response) use ($outputDir, &$certs): ResponseInterface {
$body = (string) $response->getBody();
$json = \json_decode($body);
$data = \is_object($json) && isset($json->data) && \is_array($json->data) ? $json->data : [];
\array_walk($data, static function($row, $index, $certs) use ($outputDir) {
$serialNo = $row->serial_no;
$outpath = $outputDir . \DIRECTORY_SEPARATOR . 'wechatpay_' . $serialNo . '.pem';
self::prompt(
'Certificate #' . $index . ' {',
' Serial Number: ' . self::highlight($serialNo),
' Not Before: ' . (new \DateTime($row->effective_time))->format(\DateTime::W3C),
' Not After: ' . (new \DateTime($row->expire_time))->format(\DateTime::W3C),
' Saved to: ' . self::highlight($outpath),
' You may confirm the above infos again even if this library already did(by Crypto\Rsa::verify):',
' ' . self::highlight(\sprintf('openssl x509 -in %s -noout -serial -dates', $outpath)),
' Content: ', '', $certs[$serialNo] ?? '', '',
'}'
);
\file_put_contents($outpath, $certs[$serialNo]);
}, $certs);
return $response;
};
}
/**
* @param string $thing
*/
private static function highlight(string $thing): string
{
return \sprintf("\x1B[1;32m%s\x1B[0m", $thing);
}
/**
* @param string $messages
*/
private static function prompt(...$messages): void
{
\array_walk($messages, static function (string $message): void { \printf('%s%s', $message, \PHP_EOL); });
}
/**
* @return ?array<string,string|true>
*/
private function parseOpts(): ?array
{
$opts = [
[ 'key', 'k', true ],
[ 'mchid', 'm', true ],
[ 'privatekey', 'f', true ],
[ 'serialno', 's', true ],
[ 'output', 'o', false ],
// baseuri can be one of 'https://api2.mch.weixin.qq.com/', 'https://apihk.mch.weixin.qq.com/'
[ 'baseuri', 'u', false ],
];
$shortopts = 'hV';
$longopts = [ 'help', 'version' ];
foreach ($opts as $opt) {
[$key, $alias] = $opt;
$shortopts .= $alias . ':';
$longopts[] = $key . ':';
}
$parsed = \getopt($shortopts, $longopts);
if (!$parsed) {
return null;
}
$args = [];
foreach ($opts as $opt) {
[$key, $alias, $mandatory] = $opt;
if (isset($parsed[$key]) || isset($parsed[$alias])) {
/** @var string|string[] $possible */
$possible = $parsed[$key] ?? $parsed[$alias] ?? '';
$args[$key] = \is_array($possible) ? $possible[0] : $possible;
} elseif ($mandatory) {
return null;
}
}
if (isset($parsed['h']) || isset($parsed['help'])) {
$args['help'] = true;
}
if (isset($parsed['V']) || isset($parsed['version'])) {
$args['version'] = true;
}
return $args;
}
private function printHelp(): void
{
self::prompt(
'Usage: 微信支付平台证书下载工具 [-hV]',
' -f=<privateKeyFilePath> -k=<apiv3Key> -m=<merchantId>',
' -s=<serialNo> -o=[outputFilePath] -u=[baseUri]',
'Options:',
' -m, --mchid=<merchantId> 商户号',
' -s, --serialno=<serialNo> 商户证书的序列号',
' -f, --privatekey=<privateKeyFilePath>',
' 商户的私钥文件',
' -k, --key=<apiv3Key> APIv3密钥',
' -o, --output=[outputFilePath]',
' 下载成功后保存证书的路径,可选,默认为临时文件目录夹',
' -u, --baseuri=[baseUri] 接入点,可选,默认为 ' . self::DEFAULT_BASE_URI,
' -V, --version Print version information and exit.',
' -h, --help Show this help message and exit.', ''
);
}
}
// main
(new CertificateDownloader())->run();
# Certificate Downloader
Certificate Downloader 是 PHP版 微信支付 APIv3 平台证书的命令行下载工具。该工具可从 `https://api.mch.weixin.qq.com/v3/certificates` 接口获取商户可用证书,并使用 [APIv3 密钥](https://wechatpay-api.gitbook.io/wechatpay-api-v3/ren-zheng/api-v3-mi-yao) 和 AES_256_GCM 算法进行解密,并把解密后证书下载到指定位置。
## 使用
使用方法与 [Java版Certificate Downloader](https://github.com/wechatpay-apiv3/CertificateDownloader) 一致,参数与常见问题请参考[其文档](https://github.com/wechatpay-apiv3/CertificateDownloader/blob/master/README.md)
```shell
> bin/CertificateDownloader.php
Usage: 微信支付平台证书下载工具 [-hV]
-f=<privateKeyFilePath> -k=<apiV3key> -m=<merchantId>
-s=<serialNo> -o=[outputFilePath] -u=[baseUri]
Options:
-m, --mchid=<merchantId> 商户号
-s, --serialno=<serialNo> 商户证书的序列号
-f, --privatekey=<privateKeyFilePath>
商户的私钥文件
-k, --key=<apiV3key> ApiV3Key
-o, --output=[outputFilePath]
下载成功后保存证书的路径,可选参数,默认为临时文件目录夹
-u, --baseuri=[baseUri] 接入点,默认为 https://api.mch.weixin.qq.com/
-V, --version Print version information and exit.
-h, --help Show this help message and exit.
```
完整命令示例:
```shell
./bin/CertificateDownloader.php -k ${apiV3key} -m ${mchId} -f ${mchPrivateKeyFilePath} -s ${mchSerialNo} -o ${outputFilePath}
```
```shell
php -f ./bin/CertificateDownloader.php -- -k ${apiV3key} -m ${mchId} -f ${mchPrivateKeyFilePath} -s ${mchSerialNo} -o ${outputFilePath}
```
```shell
php ./bin/CertificateDownloader.php -k ${apiV3key} -m ${mchId} -f ${mchPrivateKeyFilePath} -s ${mchSerialNo} -o ${outputFilePath}
```
使用`composer`安装的软件包,可以通过如下命令下载:
```shell
vendor/bin/CertificateDownloader.php -k ${apiV3key} -m ${mchId} -f ${mchPrivateKeyFilePath} -s ${mchSerialNo} -o ${outputFilePath}
```
```shell
composer exec CertificateDownloader.php -- -k ${apiV3key} -m ${mchId} -f ${mchPrivateKeyFilePath} -s ${mchSerialNo} -o ${outputFilePath}
```
使用源码克隆版本,也可以使用`composer`通过以下命令下载:
```shell
composer v3-certificates -k ${apiV3key} -m ${mchId} -f ${mchPrivateKeyFilePath} -s ${mchSerialNo} -o ${outputFilePath}
```
支持从海外接入点下载,命令如下:
```shell
composer v3-certificates -k ${apiV3key} -m ${mchId} -f ${mchPrivateKeyFilePath} -s ${mchSerialNo} -o ${outputFilePath} -u https://apihk.mch.weixin.qq.com/
```
**注:** 示例命令行上的`${}`是变量表达方法,运行时请替换(包括`${}`)为对应的实际值。
## 常见问题
### 如何保证证书正确
请参见CertificateDownloader文档中[关于如何保证证书正确的说明](https://github.com/wechatpay-apiv3/CertificateDownloader#%E5%A6%82%E4%BD%95%E4%BF%9D%E8%AF%81%E8%AF%81%E4%B9%A6%E6%AD%A3%E7%A1%AE)
### 如何使用信任链验证平台证书
请参见CertificateDownloader文档中[关于如何使用信任链验证平台证书的说明](https://github.com/wechatpay-apiv3/CertificateDownloader#%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8%E4%BF%A1%E4%BB%BB%E9%93%BE%E9%AA%8C%E8%AF%81%E5%B9%B3%E5%8F%B0%E8%AF%81%E4%B9%A6)
### 第一次下载证书
请参见CertificateDownloader文档中[相关说明](https://github.com/wechatpay-apiv3/CertificateDownloader#%E7%AC%AC%E4%B8%80%E6%AC%A1%E4%B8%8B%E8%BD%BD%E8%AF%81%E4%B9%A6)
{
"name": "wechatpay/wechatpay",
"version": "1.4.12",
"description": "[A]Sync Chainable WeChatPay v2&v3's OpenAPI SDK for PHP",
"type": "library",
"keywords": [
"wechatpay",
"openapi-chainable",
"xml-parser",
"xml-builder",
"aes-ecb",
"aes-gcm",
"rsa-oaep"
],
"authors": [
{
"name": "James ZHANG",
"homepage": "https://github.com/TheNorthMemory"
},
{
"name": "WeChatPay Community",
"homepage": "https://developers.weixin.qq.com/community/pay"
}
],
"homepage": "https://pay.weixin.qq.com/",
"license": "Apache-2.0",
"require": {
"php": ">=7.1.2",
"ext-curl": "*",
"ext-libxml": "*",
"ext-simplexml": "*",
"ext-openssl": "*",
"guzzlehttp/uri-template": "^0.2 || ^1.0",
"guzzlehttp/guzzle": "^6.5 || ^7.0"
},
"require-dev": {
"phpunit/phpunit": "^7.5 || ^8.5.16 || ^9.3.5",
"phpstan/phpstan": "^0.12.89 || ^1.0"
},
"autoload": {
"psr-4": { "WeChatPay\\" : "src/" }
},
"autoload-dev": {
"psr-4": { "WeChatPay\\Tests\\" : "tests/" }
},
"bin": [
"bin/CertificateDownloader.php"
],
"scripts": {
"v3-certificates": "bin/CertificateDownloader.php"
}
}
includes:
- phpstan.v8.2.neon
parameters:
ignoreErrors:
-
message: "#^(?:Left|Right) side of && is always true#"
path: src/Crypto/Hash.php
<?php declare(strict_types=1);
namespace WeChatPay;
use function preg_replace_callback_array;
use function strtolower;
use function implode;
use function array_filter;
use ArrayIterator;
/**
* Chainable the client for sending HTTP requests.
*/
final class Builder
{
/**
* Building & decorate the chainable `GuzzleHttp\Client`
*
* Minimum mandatory \$config parameters structure
* - mchid: string - The merchant ID
* - serial: string - The serial number of the merchant certificate
* - privateKey: \OpenSSLAsymmetricKey|\OpenSSLCertificate|object|resource|string - The merchant private key.
* - certs: array<string, \OpenSSLAsymmetricKey|\OpenSSLCertificate|object|resource|string> - The wechatpay platform serial and certificate(s), `[$serial => $cert]` pair
* - secret?: string - The secret key string (optional)
* - merchant?: array{key?: string, cert?: string} - The merchant private key and certificate array. (optional)
* - merchant<?key, string|string[]> - The merchant private key(file path string). (optional)
* - merchant<?cert, string|string[]> - The merchant certificate(file path string). (optional)
*
* ```php
* // usage samples
* $instance = Builder::factory([]);
* $res = $instance->chain('v3/merchantService/complaintsV2')->get(['debug' => true]);
* $res = $instance->chain('v3/merchant-service/complaint-notifications')->get(['debug' => true]);
* $instance->v3->merchantService->ComplaintNotifications->postAsync([])->wait();
* $instance->v3->certificates->getAsync()->then(function() {})->otherwise(function() {})->wait();
* ```
*
* @param array<string,string|int|bool|array|mixed> $config - `\GuzzleHttp\Client`, `APIv3` and `APIv2` configuration settings.
*/
public static function factory(array $config = []): BuilderChainable
{
return new class([], new ClientDecorator($config)) extends ArrayIterator implements BuilderChainable
{
use BuilderTrait;
/**
* Compose the chainable `ClientDecorator` instance, most starter with the tree root point
* @param string[] $input
* @param ?ClientDecoratorInterface $instance
*/
public function __construct(array $input = [], ?ClientDecoratorInterface $instance = null) {
parent::__construct($input, self::STD_PROP_LIST | self::ARRAY_AS_PROPS);
$this->setDriver($instance);
}
/**
* @var ClientDecoratorInterface $driver - The `ClientDecorator` instance
*/
protected $driver;
/**
* `$driver` setter
* @param ClientDecoratorInterface $instance - The `ClientDecorator` instance
*/
public function setDriver(ClientDecoratorInterface &$instance): BuilderChainable
{
$this->driver = $instance;
return $this;
}
/**
* @inheritDoc
*/
public function getDriver(): ClientDecoratorInterface
{
return $this->driver;
}
/**
* Normalize the `$thing` by the rules: `PascalCase` -> `camelCase`
* & `camelCase` -> `kebab-case`
* & `_placeholder_` -> `{placeholder}`
*
* @param string $thing - The string waiting for normalization
*
* @return string
*/
protected function normalize(string $thing = ''): string
{
return preg_replace_callback_array([
'#^[A-Z]#' => static function(array $piece): string { return strtolower($piece[0]); },
'#[A-Z]#' => static function(array $piece): string { return '-' . strtolower($piece[0]); },
'#^_(.*)_$#' => static function(array $piece): string { return '{' . $piece[1] . '}'; },
], $thing) ?? $thing;
}
/**
* URI pathname
*
* @param string $seperator - The URI seperator, default is slash(`/`) character
*
* @return string - The URI string
*/
protected function pathname(string $seperator = '/'): string
{
return implode($seperator, $this->simplized());
}
/**
* Only retrieve a copy array of the URI segments
*
* @return string[] - The URI segments array
*/
protected function simplized(): array
{
return array_filter($this->getArrayCopy(), static function($v) { return !($v instanceof BuilderChainable); });
}
/**
* @inheritDoc
*/
public function offsetGet($key): BuilderChainable
{
if (!$this->offsetExists($key)) {
$indices = $this->simplized();
$indices[] = $this->normalize($key);
$this->offsetSet($key, new self($indices, $this->getDriver()));
}
return parent::offsetGet($key);
}
/**
* @inheritDoc
*/
public function chain(string $segment): BuilderChainable
{
return $this->offsetGet($segment);
}
};
}
private function __construct()
{
// cannot be instantiated
}
}
<?php declare(strict_types=1);
namespace WeChatPay;
use GuzzleHttp\Promise\PromiseInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Signature of the Chainable `GuzzleHttp\Client` interface
* @property-read OpenAPI\V2 $v2 - The entrance of the APIv2 endpoint
* @property-read OpenAPI\V3 $v3 - The entrance of the APIv3 endpoint
*/
interface BuilderChainable
{
/**
* `$driver` getter
*/
public function getDriver(): ClientDecoratorInterface;
/**
* Chainable the given `$segment` with the `ClientDecoratorInterface` instance
*
* @param string $segment - The sgement or `URI`
*/
public function chain(string $segment): BuilderChainable;
/**
* Create and send an HTTP GET request.
*
* @param array<string,string|int|bool|array|mixed> $options Request options to apply.
*/
public function get(array $options = []): ResponseInterface;
/**
* Create and send an HTTP PUT request.
*
* @param array<string,string|int|bool|array|mixed> $options Request options to apply.
*/
public function put(array $options = []): ResponseInterface;
/**
* Create and send an HTTP POST request.
*
* @param array<string,string|int|bool|array|mixed> $options Request options to apply.
*/
public function post(array $options = []): ResponseInterface;
/**
* Create and send an HTTP PATCH request.
*
* @param array<string,string|int|bool|array|mixed> $options Request options to apply.
*/
public function patch(array $options = []): ResponseInterface;
/**
* Create and send an HTTP DELETE request.
*
* @param array<string,string|int|bool|array|mixed> $options Request options to apply.
*/
public function delete(array $options = []): ResponseInterface;
/**
* Create and send an asynchronous HTTP GET request.
*
* @param array<string,string|int|bool|array|mixed> $options Request options to apply.
*/
public function getAsync(array $options = []): PromiseInterface;
/**
* Create and send an asynchronous HTTP PUT request.
*
* @param array<string,string|int|bool|array|mixed> $options Request options to apply.
*/
public function putAsync(array $options = []): PromiseInterface;
/**
* Create and send an asynchronous HTTP POST request.
*
* @param array<string,string|int|bool|array|mixed> $options Request options to apply.
*/
public function postAsync(array $options = []): PromiseInterface;
/**
* Create and send an asynchronous HTTP PATCH request.
*
* @param array<string,string|int|bool|array|mixed> $options Request options to apply.
*/
public function patchAsync(array $options = []): PromiseInterface;
/**
* Create and send an asynchronous HTTP DELETE request.
*
* @param array<string,string|int|bool|array|mixed> $options Request options to apply.
*/
public function deleteAsync(array $options = []): PromiseInterface;
}
<?php declare(strict_types=1);
namespace WeChatPay;
use GuzzleHttp\Promise\PromiseInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Chainable points the client interface for sending HTTP requests.
*/
trait BuilderTrait
{
abstract public function getDriver(): ClientDecoratorInterface;
/**
* URI pathname
*
* @param string $seperator - The URI seperator, default is slash(`/`) character
*
* @return string - The URI string
*/
abstract protected function pathname(string $seperator = '/'): string;
/**
* @inheritDoc
*/
public function get(array $options = []): ResponseInterface
{
return $this->getDriver()->request('GET', $this->pathname(), $options);
}
/**
* @inheritDoc
*/
public function put(array $options = []): ResponseInterface
{
return $this->getDriver()->request('PUT', $this->pathname(), $options);
}
/**
* @inheritDoc
*/
public function post(array $options = []): ResponseInterface
{
return $this->getDriver()->request('POST', $this->pathname(), $options);
}
/**
* @inheritDoc
*/
public function patch(array $options = []): ResponseInterface
{
return $this->getDriver()->request('PATCH', $this->pathname(), $options);
}
/**
* @inheritDoc
*/
public function delete(array $options = []): ResponseInterface
{
return $this->getDriver()->request('DELETE', $this->pathname(), $options);
}
/**
* @inheritDoc
*/
public function getAsync(array $options = []): PromiseInterface
{
return $this->getDriver()->requestAsync('GET', $this->pathname(), $options);
}
/**
* @inheritDoc
*/
public function putAsync(array $options = []): PromiseInterface
{
return $this->getDriver()->requestAsync('PUT', $this->pathname(), $options);
}
/**
* @inheritDoc
*/
public function postAsync(array $options = []): PromiseInterface
{
return $this->getDriver()->requestAsync('POST', $this->pathname(), $options);
}
/**
* @inheritDoc
*/
public function patchAsync(array $options = []): PromiseInterface
{
return $this->getDriver()->requestAsync('PATCH', $this->pathname(), $options);
}
/**
* @inheritDoc
*/
public function deleteAsync(array $options = []): PromiseInterface
{
return $this->getDriver()->requestAsync('DELETE', $this->pathname(), $options);
}
}
<?php declare(strict_types=1);
namespace WeChatPay;
use function array_replace_recursive;
use function call_user_func;
use function sprintf;
use function php_uname;
use function implode;
use function strncasecmp;
use function strcasecmp;
use function substr;
use function constant;
use function defined;
use const PHP_OS;
use const PHP_VERSION;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\UriTemplate\UriTemplate;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Decorate the `GuzzleHttp\Client` instance
*/
final class ClientDecorator implements ClientDecoratorInterface
{
use ClientXmlTrait;
use ClientJsonTrait;
/**
* @var ClientInterface - The APIv2's `\GuzzleHttp\Client`
*/
protected $v2;
/**
* @var ClientInterface - The APIv3's `\GuzzleHttp\Client`
*/
protected $v3;
/**
* Deep merge the input with the defaults
*
* @param array<string,string|int|bool|array|mixed> $config - The configuration.
*
* @return array<string, string|mixed> - With the built-in configuration.
*/
protected static function withDefaults(array ...$config): array
{
return array_replace_recursive(static::$defaults, ['headers' => static::userAgent()], ...$config);
}
/**
* Prepare the `User-Agent` value key/value pair
*
* @return array<string, string>
*/
protected static function userAgent(): array
{
return ['User-Agent' => implode(' ', [
sprintf('wechatpay-php/%s', static::VERSION),
sprintf('GuzzleHttp/%s', constant(ClientInterface::class . (defined(ClientInterface::class . '::VERSION') ? '::VERSION' : '::MAJOR_VERSION'))),
sprintf('curl/%s', ((array)call_user_func('\curl_version'))['version'] ?? 'unknown'),
sprintf('(%s/%s)', PHP_OS, php_uname('r')),
sprintf('PHP/%s', PHP_VERSION),
])];
}
/**
* Taken body string
*
* @param MessageInterface $message - The message
*/
protected static function body(MessageInterface $message): string
{
$stream = $message->getBody();
$content = (string) $stream;
$stream->tell() && $stream->rewind();
return $content;
}
/**
* Decorate the `GuzzleHttp\Client` factory
*
* Acceptable \$config parameters stucture
* - mchid: string - The merchant ID
* - serial: string - The serial number of the merchant certificate
* - privateKey: \OpenSSLAsymmetricKey|\OpenSSLCertificate|object|resource|string - The merchant private key.
* - certs: array<string, \OpenSSLAsymmetricKey|\OpenSSLCertificate|object|resource|string> - The wechatpay platform serial and certificate(s), `[$serial => $cert]` pair
* - secret?: string - The secret key string (optional)
* - merchant?: array{key?: string, cert?: string} - The merchant private key and certificate array. (optional)
* - merchant<?key, string|string[]> - The merchant private key(file path string). (optional)
* - merchant<?cert, string|string[]> - The merchant certificate(file path string). (optional)
*
* @param array<string,string|int|bool|array|mixed> $config - `\GuzzleHttp\Client`, `APIv3` and `APIv2` configuration settings.
*/
public function __construct(array $config = [])
{
$this->{static::XML_BASED} = static::xmlBased($config);
$this->{static::JSON_BASED} = static::jsonBased($config);
}
/**
* Identify the `protocol` and `uri`
*
* @param string $uri - The uri string.
*
* @return string[] - the first element is the API version aka `protocol`, the second is the real `uri`
*/
private static function prepare(string $uri): array
{
return $uri && 0 === strncasecmp(static::XML_BASED . '/', $uri, 3)
? [static::XML_BASED, substr($uri, 3)]
: [static::JSON_BASED, $uri];
}
/**
* @inheritDoc
*/
public function select(?string $protocol = null): ClientInterface
{
return $protocol && 0 === strcasecmp(static::XML_BASED, $protocol)
? $this->{static::XML_BASED}
: $this->{static::JSON_BASED};
}
/**
* @inheritDoc
*/
public function request(string $method, string $uri, array $options = []): ResponseInterface
{
[$protocol, $pathname] = self::prepare(UriTemplate::expand($uri, $options));
return $this->select($protocol)->request($method, $pathname, $options);
}
/**
* @inheritDoc
*/
public function requestAsync(string $method, string $uri, array $options = []): PromiseInterface
{
[$protocol, $pathname] = self::prepare(UriTemplate::expand($uri, $options));
return $this->select($protocol)->requestAsync($method, $pathname, $options);
}
}
<?php declare(strict_types=1);
namespace WeChatPay;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Promise\PromiseInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Decorate the `GuzzleHttp\Client` interface
*/
interface ClientDecoratorInterface
{
/**
* @var string - This library version
*/
public const VERSION = '1.4.12';
/**
* @var string - The HTTP transfer `xml` based protocol
* @deprecated 1.0 - @see \WeChatPay\Exception\WeChatPayException::DEP_XML_PROTOCOL_IS_REACHABLE_EOL
*/
public const XML_BASED = 'v2';
/**
* @var string - The HTTP transfer `json` based protocol
*/
public const JSON_BASED = 'v3';
/**
* Protocol selector
*
* @param string|null $protocol - one of the constants of `XML_BASED`, `JSON_BASED`, default is `JSON_BASED`
* @return ClientInterface
*/
public function select(?string $protocol = null): ClientInterface;
/**
* Request the remote `$uri` by a HTTP `$method` verb
*
* @param string $uri - The uri string.
* @param string $method - The method string.
* @param array<string,string|int|bool|array|mixed> $options - The options.
*
* @return ResponseInterface - The `Psr\Http\Message\ResponseInterface` instance
*/
public function request(string $method, string $uri, array $options = []): ResponseInterface;
/**
* Async request the remote `$uri` by a HTTP `$method` verb
*
* @param string $uri - The uri string.
* @param string $method - The method string.
* @param array<string,string|int|bool|array|mixed> $options - The options.
*
* @return PromiseInterface - The `GuzzleHttp\Promise\PromiseInterface` instance
*/
public function requestAsync(string $method, string $uri, array $options = []): PromiseInterface;
}
This diff is collapsed.
<?php declare(strict_types=1);
namespace WeChatPay;
use function strlen;
use function sprintf;
use function in_array;
use function array_key_exists;
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Query;
use GuzzleHttp\Psr7\Utils;
use GuzzleHttp\Promise\Create;
use GuzzleHttp\Promise\PromiseInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\MessageInterface;
/**
* XML based Client interface for sending HTTP requests.
*/
trait ClientXmlTrait
{
/**
* @var array<string, string> - The default headers whose passed in `GuzzleHttp\Client`.
*/
protected static $headers = [
'Accept' => 'text/xml, text/plain, application/x-gzip',
'Content-Type' => 'text/xml; charset=utf-8',
];
/**
* @var string[] - Special URLs whose were designed that none signature respond.
*/
protected static $noneSignatureRespond = [
'/mchrisk/querymchrisk',
'/mchrisk/setmchriskcallback',
'/mchrisk/syncmchriskresult',
'/mmpaymkttransfers/gethbinfo',
'/mmpaymkttransfers/gettransferinfo',
'/mmpaymkttransfers/pay_bank',
'/mmpaymkttransfers/promotion/paywwsptrans2pocket',
'/mmpaymkttransfers/promotion/querywwsptrans2pocket',
'/mmpaymkttransfers/promotion/transfers',
'/mmpaymkttransfers/query_bank',
'/mmpaymkttransfers/sendgroupredpack',
'/mmpaymkttransfers/sendminiprogramhb',
'/mmpaymkttransfers/sendredpack',
'/papay/entrustweb',
'/papay/h5entrustweb',
'/papay/partner/entrustweb',
'/papay/partner/h5entrustweb',
'/pay/downloadbill',
'/pay/downloadfundflow',
'/payitil/report',
'/risk/getpublickey',
'/risk/getviolation',
'/secapi/mch/submchmanage',
'/xdc/apiv2getsignkey/sign/getsignkey',
];
abstract protected static function body(MessageInterface $message): string;
abstract protected static function withDefaults(array ...$config): array;
/**
* APIv2's transformRequest, did the `datasign` and `array2xml` together
*
* @param ?string $mchid - The merchant ID
* @param string $secret - The secret key string (optional)
* @param array{cert?: ?string, key?: ?string} $merchant - The merchant private key and certificate array. (optional)
*
* @return callable(callable(RequestInterface, array))
* @throws \WeChatPay\Exception\InvalidArgumentException
*/
public static function transformRequest(
?string $mchid = null,
#[\SensitiveParameter]
string $secret = '',
?array $merchant = null
): callable
{
return static function (callable $handler) use ($mchid, $secret, $merchant): callable {
return static function (RequestInterface $request, array $options = []) use ($handler, $mchid, $secret, $merchant): PromiseInterface {
$methodIsGet = $request->getMethod() === 'GET';
if ($methodIsGet) {
$queryParams = Query::parse($request->getUri()->getQuery());
}
$data = $options['xml'] ?? ($queryParams ?? []);
if ($mchid && $mchid !== ($inputMchId = $data['mch_id'] ?? $data['mchid'] ?? $data['combine_mch_id'] ?? null)) {
throw new Exception\InvalidArgumentException(sprintf(Exception\EV2_REQ_XML_NOTMATCHED_MCHID, $inputMchId ?? '', $mchid));
}
$type = $data['sign_type'] ?? Crypto\Hash::ALGO_MD5;
isset($options['nonceless']) || $data['nonce_str'] = $data['nonce_str'] ?? Formatter::nonce();
$data['sign'] = Crypto\Hash::sign($type, Formatter::queryStringLike(Formatter::ksort($data)), $secret);
$modify = $methodIsGet ? ['query' => Query::build($data)] : ['body' => Transformer::toXml($data)];
// for security request, it was required the merchant's private_key and certificate
if (isset($options['security']) && true === $options['security']) {
$options['ssl_key'] = $merchant['key'] ?? null;
$options['cert'] = $merchant['cert'] ?? null;
}
unset($options['xml'], $options['nonceless'], $options['security']);
return $handler(Utils::modifyRequest($request, $modify), $options);
};
};
}
/**
* APIv2's transformResponse, doing the `xml2array` then `verify` the signature job only
*
* @param string $secret - The secret key string (optional)
*
* @return callable(callable(RequestInterface, array))
*/
public static function transformResponse(
#[\SensitiveParameter]
string $secret = ''
): callable
{
return static function (callable $handler) use ($secret): callable {
return static function (RequestInterface $request, array $options = []) use ($secret, $handler): PromiseInterface {
if (in_array($request->getUri()->getPath(), static::$noneSignatureRespond)) {
return $handler($request, $options);
}
return $handler($request, $options)->then(static function(ResponseInterface $response) use ($secret) {
$result = Transformer::toArray(static::body($response));
if (!(array_key_exists('return_code', $result) && Crypto\Hash::equals('SUCCESS', $result['return_code']))) {
return Create::rejectionFor($response);
}
if (array_key_exists('result_code', $result) && !Crypto\Hash::equals('SUCCESS', $result['result_code'])) {
return Create::rejectionFor($response);
}
/** @var ?string $sign */
$sign = $result['sign'] ?? null;
$type = $sign && strlen($sign) === 64 ? Crypto\Hash::ALGO_HMAC_SHA256 : Crypto\Hash::ALGO_MD5;
/** @var string $calc - calculated digest string, it's naver `null` here because of \$type known. */
$calc = Crypto\Hash::sign($type, Formatter::queryStringLike(Formatter::ksort($result)), $secret);
return Crypto\Hash::equals($calc, $sign) ? $response : Create::rejectionFor($response);
});
};
};
}
/**
* Create an APIv2's client
*
* @deprecated 1.0 - @see \WeChatPay\Exception\WeChatPayException::DEP_XML_PROTOCOL_IS_REACHABLE_EOL
*
* Optional acceptable \$config parameters
* - mchid?: ?string - The merchant ID
* - secret?: ?string - The secret key string
* - merchant?: array{key?: string, cert?: string} - The merchant private key and certificate array. (optional)
* - merchant<?key, string|string[]> - The merchant private key(file path string). (optional)
* - merchant<?cert, string|string[]> - The merchant certificate(file path string). (optional)
*
* @param array<string,string|int|bool|array|mixed> $config - The configuration
*/
public static function xmlBased(array $config = []): Client
{
/** @var HandlerStack $stack */
$stack = isset($config['handler']) && ($config['handler'] instanceof HandlerStack) ? (clone $config['handler']) : HandlerStack::create();
$stack->before('prepare_body', static::transformRequest($config['mchid'] ?? null, $config['secret'] ?? '', $config['merchant'] ?? []), 'transform_request');
$stack->before('http_errors', static::transformResponse($config['secret'] ?? ''), 'transform_response');
$config['handler'] = $stack;
unset($config['mchid'], $config['serial'], $config['privateKey'], $config['certs'], $config['secret'], $config['merchant']);
return new Client(static::withDefaults(['headers' => static::$headers], $config));
}
}
<?php declare(strict_types=1);
namespace WeChatPay\Crypto;
use function openssl_encrypt;
use function base64_encode;
use function openssl_decrypt;
use function base64_decode;
use const OPENSSL_RAW_DATA;
use UnexpectedValueException;
/**
* Aes encrypt/decrypt using `aes-256-ecb` algorithm with pkcs7padding.
*/
class AesEcb implements AesInterface
{
/**
* @inheritDoc
*/
public static function encrypt(
#[\SensitiveParameter]
string $plaintext,
#[\SensitiveParameter]
string $key,
string $iv = ''
): string
{
$ciphertext = openssl_encrypt($plaintext, static::ALGO_AES_256_ECB, $key, OPENSSL_RAW_DATA, $iv = '');
if (false === $ciphertext) {
throw new UnexpectedValueException('Encrypting the input $plaintext failed, please checking your $key and $iv whether or nor correct.');
}
return base64_encode($ciphertext);
}
/**
* @inheritDoc
*/
public static function decrypt(
#[\SensitiveParameter]
string $ciphertext,
#[\SensitiveParameter]
string $key,
string $iv = ''
): string
{
$plaintext = openssl_decrypt(base64_decode($ciphertext), static::ALGO_AES_256_ECB, $key, OPENSSL_RAW_DATA, $iv = '');
if (false === $plaintext) {
throw new UnexpectedValueException('Decrypting the input $ciphertext failed, please checking your $key and $iv whether or nor correct.');
}
return $plaintext;
}
}
<?php declare(strict_types=1);
namespace WeChatPay\Crypto;
use function in_array;
use function openssl_get_cipher_methods;
use function openssl_encrypt;
use function base64_encode;
use function base64_decode;
use function substr;
use function strlen;
use function openssl_decrypt;
use const OPENSSL_RAW_DATA;
use RuntimeException;
use UnexpectedValueException;
/**
* Aes encrypt/decrypt using `aes-256-gcm` algorithm with additional authenticated data(`aad`).
*/
class AesGcm implements AesInterface
{
/**
* Detect the ext-openssl whether or nor including the `aes-256-gcm` algorithm
*
* @throws RuntimeException
*/
private static function preCondition(): void
{
if (!in_array(static::ALGO_AES_256_GCM, openssl_get_cipher_methods())) {
throw new RuntimeException('It looks like the ext-openssl extension missing the `aes-256-gcm` cipher method.');
}
}
/**
* Encrypts given data with given key, iv and aad, returns a base64 encoded string.
*
* @param string $plaintext - Text to encode.
* @param string $key - The secret key, 32 bytes string.
* @param string $iv - The initialization vector, 16 bytes string.
* @param string $aad - The additional authenticated data, maybe empty string.
*
* @return string - The base64-encoded ciphertext.
*/
public static function encrypt(
#[\SensitiveParameter]
string $plaintext,
#[\SensitiveParameter]
string $key,
string $iv = '',
string $aad = ''
): string
{
self::preCondition();
$ciphertext = openssl_encrypt($plaintext, static::ALGO_AES_256_GCM, $key, OPENSSL_RAW_DATA, $iv, $tag, $aad, static::BLOCK_SIZE);
if (false === $ciphertext) {
throw new UnexpectedValueException('Encrypting the input $plaintext failed, please checking your $key and $iv whether or nor correct.');
}
return base64_encode($ciphertext . $tag);
}
/**
* Takes a base64 encoded string and decrypts it using a given key, iv and aad.
*
* @param string $ciphertext - The base64-encoded ciphertext.
* @param string $key - The secret key, 32 bytes string.
* @param string $iv - The initialization vector, 16 bytes string.
* @param string $aad - The additional authenticated data, maybe empty string.
*
* @return string - The utf-8 plaintext.
*/
public static function decrypt(
#[\SensitiveParameter]
string $ciphertext,
#[\SensitiveParameter]
string $key,
string $iv = '',
string $aad = ''
): string
{
self::preCondition();
$ciphertext = base64_decode($ciphertext);
$authTag = substr($ciphertext, $tailLength = 0 - static::BLOCK_SIZE);
$tagLength = strlen($authTag);
/* Manually checking the length of the tag, because the `openssl_decrypt` was mentioned there, it's the caller's responsibility. */
if ($tagLength > static::BLOCK_SIZE || ($tagLength < 12 && $tagLength !== 8 && $tagLength !== 4)) {
throw new RuntimeException('The inputs `$ciphertext` incomplete, the bytes length must be one of 16, 15, 14, 13, 12, 8 or 4.');
}
$plaintext = openssl_decrypt(substr($ciphertext, 0, $tailLength), static::ALGO_AES_256_GCM, $key, OPENSSL_RAW_DATA, $iv, $authTag, $aad);
if (false === $plaintext) {
throw new UnexpectedValueException('Decrypting the input $ciphertext failed, please checking your $key and $iv whether or nor correct.');
}
return $plaintext;
}
}
<?php declare(strict_types=1);
namespace WeChatPay\Crypto;
/**
* Advanced Encryption Standard Interface
*/
interface AesInterface
{
/**
* Bytes Length of the AES block
*/
public const BLOCK_SIZE = 16;
/**
* Bytes length of the AES secret key.
*/
public const KEY_LENGTH_BYTE = 32;
/**
* Bytes Length of the authentication tag in AEAD cipher mode
* @deprecated 1.0 - As of the OpenSSL described, the `auth_tag` length may be one of 16, 15, 14, 13, 12, 8 or 4.
* Keep it only compatible for the samples on the official documentation.
*/
public const AUTH_TAG_LENGTH_BYTE = 16;
/**
* The `aes-256-gcm` algorithm string
*/
public const ALGO_AES_256_GCM = 'aes-256-gcm';
/**
* The `aes-256-ecb` algorithm string
*/
public const ALGO_AES_256_ECB = 'aes-256-ecb';
/**
* Encrypts given data with given key and iv, returns a base64 encoded string.
*
* @param string $plaintext - Text to encode.
* @param string $key - The secret key, 32 bytes string.
* @param string $iv - The initialization vector, 16 bytes string.
*
* @return string - The base64-encoded ciphertext.
*/
public static function encrypt(
#[\SensitiveParameter]
string $plaintext,
#[\SensitiveParameter]
string $key,
string $iv = ''
): string;
/**
* Takes a base64 encoded string and decrypts it using a given key and iv.
*
* @param string $ciphertext - The base64-encoded ciphertext.
* @param string $key - The secret key, 32 bytes string.
* @param string $iv - The initialization vector, 16 bytes string.
*
* @return string - The utf-8 plaintext.
*/
public static function decrypt(
#[\SensitiveParameter]
string $ciphertext,
#[\SensitiveParameter]
string $key,
string $iv = ''
): string;
}
<?php declare(strict_types=1);
namespace WeChatPay\Crypto;
use function is_null;
use function hash_equals;
use function hash_init;
use function hash_update;
use function hash_final;
use function array_key_exists;
use function strtoupper;
use const HASH_HMAC;
const ALGO_MD5 = 'MD5';
const ALGO_HMAC_SHA256 = 'HMAC-SHA256';
const ALGO_DICTONARIES = [ALGO_HMAC_SHA256 => 'hmac', ALGO_MD5 => 'md5'];
/**
* Crypto hash functions utils.
* [Specification]{@link https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3}
*/
class Hash
{
/** @var string - hashing `MD5` algorithm */
public const ALGO_MD5 = ALGO_MD5;
/** @var string - hashing `HMAC-SHA256` algorithm */
public const ALGO_HMAC_SHA256 = ALGO_HMAC_SHA256;
/**
* Calculate the input string with an optional secret `key` in MD5,
* when the `key` is Falsey, this method works as normal `MD5`.
*
* @param string $thing - The input string.
* @param string $key - The secret key string.
* @param boolean|int|string $agency - The secret **key** is from work.weixin.qq.com, default is `false`,
* placed with `true` or better of the `AgentId` value.
* [spec]{@link https://work.weixin.qq.com/api/doc/90000/90135/90281}
*
* @return string - The data signature
*/
public static function md5(
string $thing,
#[\SensitiveParameter]
string $key = '',
$agency = false
): string
{
$ctx = hash_init(ALGO_MD5);
hash_update($ctx, $thing) && $key && hash_update($ctx, $agency ? '&secret=' : '&key=') && hash_update($ctx, $key);
return hash_final($ctx);
}
/**
* Calculate the input string with a secret `key` as of `algorithm` string which is one of the 'sha256', 'sha512' etc.
*
* @param string $thing - The input string.
* @param string $key - The secret key string.
* @param string $algorithm - The algorithm string, default is `sha256`.
*
* @return string - The data signature
*/
public static function hmac(
string $thing,
#[\SensitiveParameter]
string $key,
string $algorithm = 'sha256'
): string
{
$ctx = hash_init($algorithm, HASH_HMAC, $key);
hash_update($ctx, $thing) && hash_update($ctx, '&key=') && hash_update($ctx, $key);
return hash_final($ctx);
}
/**
* Wrapping the builtins `hash_equals` function.
*
* @param string $known_string - The string of known length to compare against.
* @param ?string $user_string - The user-supplied string.
*
* @return bool - Returns true when the two are equal, false otherwise.
*/
public static function equals(
#[\SensitiveParameter]
string $known_string,
#[\SensitiveParameter]
?string $user_string = null
): bool
{
return is_null($user_string) ? false : hash_equals($known_string, $user_string);
}
/**
* Utils of the data signature calculation.
*
* @param string $type - The sign type, one of the `MD5` or `HMAC-SHA256`.
* @param string $data - The input data.
* @param string $key - The secret key string.
*
* @return ?string - The data signature in UPPERCASE.
*/
public static function sign(
string $type,
string $data,
#[\SensitiveParameter]
string $key
): ?string
{
return array_key_exists($type, ALGO_DICTONARIES) ? strtoupper(static::{ALGO_DICTONARIES[$type]}($data, $key)) : null;
}
}
This diff is collapsed.
<?php declare(strict_types=1);
namespace WeChatPay\Exception;
use GuzzleHttp\Exception\GuzzleException;
class InvalidArgumentException extends \InvalidArgumentException implements WeChatPayException, GuzzleException
{
}
<?php declare(strict_types=1);
namespace WeChatPay\Exception;
const DEP_XML_PROTOCOL_IS_REACHABLE_EOL = 'New features are all in `APIv3`, there\'s no reason to continue use this kind client since v2.0.';
const ERR_INIT_MCHID_IS_MANDATORY = 'The merchant\' ID aka `mchid` is required, usually numerical.';
const ERR_INIT_SERIAL_IS_MANDATORY = 'The serial number of the merchant\'s certificate aka `serial` is required, usually hexadecial.';
const ERR_INIT_PRIVATEKEY_IS_MANDATORY = 'The merchant\'s private key aka `privateKey` is required, usual as pem format.';
const ERR_INIT_CERTS_IS_MANDATORY = 'The platform certificate(s) aka `certs` is required, paired as of `[$serial => $certificate]`.';
const ERR_INIT_CERTS_EXCLUDE_MCHSERIAL = 'The `certs(%1$s)` contains the merchant\'s certificate serial number(%2$s) which is not allowed here.';
const EV2_REQ_XML_NOTMATCHED_MCHID = 'The xml\'s structure[mch_id(%1$s)] doesn\'t matched the init one mchid(%2$s).';
const EV3_RES_HEADERS_INCOMPLETE = 'The response\'s Headers incomplete, must have(`%1$s`, `%2$s`, `%3$s` and `%4$s`).';
const EV3_RES_HEADER_TIMESTAMP_OFFSET = 'It\'s allowed time offset in ± %1$s seconds, the response was on %2$s, your\'s localtime on %3$s.';
const EV3_RES_HEADER_PLATFORM_SERIAL = 'Cannot found the serial(`%1$s`)\'s configuration, which\'s from the response(header:%2$s), your\'s %3$s.';
const EV3_RES_HEADER_SIGNATURE_DIGEST = 'Verify the response\'s data with: timestamp=%1$s, nonce=%2$s, signature=%3$s, cert=[%4$s => ...] failed.';
interface WeChatPayException
{
const DEP_XML_PROTOCOL_IS_REACHABLE_EOL = DEP_XML_PROTOCOL_IS_REACHABLE_EOL;
const EV3_RES_HEADERS_INCOMPLETE = EV3_RES_HEADERS_INCOMPLETE;
const EV3_RES_HEADER_TIMESTAMP_OFFSET = EV3_RES_HEADER_TIMESTAMP_OFFSET;
const EV3_RES_HEADER_PLATFORM_SERIAL = EV3_RES_HEADER_PLATFORM_SERIAL;
const EV3_RES_HEADER_SIGNATURE_DIGEST = EV3_RES_HEADER_SIGNATURE_DIGEST;
}
<?php declare(strict_types=1);
namespace WeChatPay;
use function str_split;
use function array_map;
use function ord;
use function random_bytes;
use function time;
use function sprintf;
use function implode;
use function array_merge;
use function ksort;
use function is_null;
use const SORT_STRING;
use InvalidArgumentException;
/**
* Provides easy used methods using in this project.
*/
class Formatter
{
/**
* Generate a random BASE62 string aka `nonce`, similar as `random_bytes`.
*
* @param int $size - Nonce string length, default is 32.
*
* @return string - base62 random string.
*/
public static function nonce(int $size = 32): string
{
if ($size < 1) {
throw new InvalidArgumentException('Size must be a positive integer.');
}
return implode('', array_map(static function(string $c): string {
return '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'[ord($c) % 62];
}, str_split(random_bytes($size))));
}
/**
* Retrieve the current `Unix` timestamp.
*
* @return int - Epoch timestamp.
*/
public static function timestamp(): int
{
return time();
}
/**
* Formatting for the heading `Authorization` value.
*
* @param string $mchid - The merchant ID.
* @param string $nonce - The Nonce string.
* @param string $signature - The base64-encoded `Rsa::sign` ciphertext.
* @param string $timestamp - The `Unix` timestamp.
* @param string $serial - The serial number of the merchant public certification.
*
* @return string - The APIv3 Authorization `header` value
*/
public static function authorization(string $mchid, string $nonce, string $signature, string $timestamp, string $serial): string
{
return sprintf(
'WECHATPAY2-SHA256-RSA2048 mchid="%s",serial_no="%s",timestamp="%s",nonce_str="%s",signature="%s"',
$mchid, $serial, $timestamp, $nonce, $signature
);
}
/**
* Formatting this `HTTP::request` for `Rsa::sign` input.
*
* @param string $method - The HTTP verb, must be the uppercase sting.
* @param string $uri - Combined string with `URL::pathname` and `URL::search`.
* @param string $timestamp - The `Unix` timestamp, should be the one used in `authorization`.
* @param string $nonce - The `Nonce` string, should be the one used in `authorization`.
* @param string $body - The playload string, HTTP `GET` should be an empty string.
*
* @return string - The content for `Rsa::sign`
*/
public static function request(string $method, string $uri, string $timestamp, string $nonce, string $body = ''): string
{
return static::joinedByLineFeed($method, $uri, $timestamp, $nonce, $body);
}
/**
* Formatting this `HTTP::response` for `Rsa::verify` input.
*
* @param string $timestamp - The `Unix` timestamp, should be the one from `response::headers[Wechatpay-Timestamp]`.
* @param string $nonce - The `Nonce` string, should be the one from `response::headers[Wechatpay-Nonce]`.
* @param string $body - The response payload string, HTTP status(`201`, `204`) should be an empty string.
*
* @return string - The content for `Rsa::verify`
*/
public static function response(string $timestamp, string $nonce, string $body = ''): string
{
return static::joinedByLineFeed($timestamp, $nonce, $body);
}
/**
* Joined this inputs by for `Line Feed`(LF) char.
*
* @param string|float|int|bool $pieces - The scalar variable(s).
*
* @return string - The joined string.
*/
public static function joinedByLineFeed(...$pieces): string
{
return implode("\n", array_merge($pieces, ['']));
}
/**
* Sort an array by key with `SORT_STRING` flag.
*
* @param array<string, string|int> $thing - The input array.
*
* @return array<string, string|int> - The sorted array.
*/
public static function ksort(array $thing = []): array
{
ksort($thing, SORT_STRING);
return $thing;
}
/**
* Like `queryString` does but without the `sign` and `empty value` entities.
*
* @param array<string, string|int|null> $thing - The input array.
*
* @return string - The `key=value` pair string whose joined by `&` char.
*/
public static function queryStringLike(array $thing = []): string
{
$data = [];
foreach ($thing as $key => $value) {
if ($key === 'sign' || is_null($value) || $value === '') {
continue;
}
$data[] = implode('=', [$key, $value]);
}
return implode('&', $data);
}
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment