256 lines
9.0 KiB
PHP
256 lines
9.0 KiB
PHP
<?php
|
||
|
||
namespace App\Services;
|
||
use Exception;
|
||
use Illuminate\Support\Facades\Log;
|
||
use WeChatPay\Builder;
|
||
use WeChatPay\BuilderChainable;
|
||
use WeChatPay\Crypto\AesGcm;
|
||
use WeChatPay\Crypto\Rsa;
|
||
|
||
class WechatPayService
|
||
{
|
||
/**
|
||
* 初始化一个APIv3客户端
|
||
* @return void
|
||
*/
|
||
private function newClient(): BuilderChainable
|
||
{
|
||
// 商户号
|
||
$merchantId = config("wechatpay.payment.mch_id");
|
||
|
||
// 从本地文件中加载「商户API私钥」,「商户API私钥」会用来生成请求的签名
|
||
$merchantPrivateKeyFilePath = 'file://' . config("wechatpay.payment.cert_key_path");
|
||
$merchantPrivateKeyInstance = Rsa::from($merchantPrivateKeyFilePath, Rsa::KEY_TYPE_PRIVATE);
|
||
|
||
// 「商户API证书」的「证书序列号」
|
||
$merchantCertificateSerial = config("wechatpay.payment.serial");
|
||
|
||
// 从本地文件中加载「微信支付平台证书」,可由内置CLI工具下载到,用来验证微信支付应答的签名
|
||
$platformCertificateFilePath = 'file://' . config("wechatpay.payment.platform_cert_path");
|
||
$onePlatformPublicKeyInstance = Rsa::from($platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);
|
||
|
||
// 「微信支付平台证书」的「平台证书序列号」
|
||
// 可以从「微信支付平台证书」文件解析,也可以在 商户平台 -> 账户中心 -> API安全 查询到
|
||
$platformCertificateSerial = config("wechatpay.payment.platform_cert_serial");
|
||
|
||
// // 从本地文件中加载「微信支付公钥」,用来验证微信支付应答的签名
|
||
// $platformPublicKeyFilePath = 'file://' . config("wechatpay.payment.public_key_path");
|
||
// $twoPlatformPublicKeyInstance = Rsa::from($platformPublicKeyFilePath, Rsa::KEY_TYPE_PUBLIC);
|
||
|
||
// // 「微信支付公钥」的「微信支付公钥ID」
|
||
// // 需要在 商户平台 -> 账户中心 -> API安全 查询
|
||
// $platformPublicKeyId = config("wechatpay.payment.public_key_id");
|
||
|
||
// 构造一个 APIv3 客户端实例
|
||
$instance = Builder::factory([
|
||
'mchid' => $merchantId,
|
||
'serial' => $merchantCertificateSerial,
|
||
'privateKey' => $merchantPrivateKeyInstance,
|
||
'certs' => [
|
||
$platformCertificateSerial => $onePlatformPublicKeyInstance,
|
||
// $platformPublicKeyId => $twoPlatformPublicKeyInstance,
|
||
],
|
||
"secret" => config("wechatpay.payment.api3_key")
|
||
]);
|
||
return $instance;
|
||
}
|
||
|
||
/**
|
||
* 商家转账
|
||
* @return void
|
||
*/
|
||
public function mchTransfer(string $trade_no, string $scene_id, string $openid, int $amount, string $remark, string $notify_url, array $transfer_scene_report_infos = []): array
|
||
{
|
||
// 发送请求
|
||
if (empty(count($transfer_scene_report_infos))) {
|
||
$transfer_scene_report_infos = [
|
||
[
|
||
'info_type' => '岗位类型', // 固定值
|
||
'info_content' => '商家/商户', // 示例值
|
||
],
|
||
[
|
||
'info_type' => '报酬说明', // 固定值
|
||
'info_content' => '商家/商户提现', // 示例值
|
||
],
|
||
];
|
||
}
|
||
// try {
|
||
$appid = config("wechat.official_account.default.app_id");
|
||
$data = [
|
||
"appid" => $appid,
|
||
"out_bill_no" => $trade_no,
|
||
"transfer_scene_id" => (string) $scene_id,
|
||
"openid" => $openid,
|
||
"transfer_amount" => $amount,
|
||
"transfer_remark" => $remark,
|
||
"notify_url" => $notify_url,
|
||
"transfer_scene_report_infos" => $transfer_scene_report_infos
|
||
];
|
||
Log::info("转账数据", $data);
|
||
$instance = $this->newClient();
|
||
$resp = $instance->chain('v3/fund-app/mch-transfer/transfer-bills')->post([
|
||
"json" => $data
|
||
]);
|
||
$res = json_decode($resp->getBody(), true);
|
||
return ["code" => 0, "err_msg" => "", "data" => $res];
|
||
// } catch (\Exception $e) {
|
||
// // 进行异常捕获并进行错误判断处理
|
||
// Log::info($e->getMessage());
|
||
// if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
|
||
// $r = $e->getResponse();
|
||
// $res = json_decode($r->getBody());
|
||
// return ["code" => 1, "err_msg" => $r->getBody(), "data" => null];
|
||
// }
|
||
|
||
// return ["code" => 1, "err_msg" => $e->getMessage(), "data" => null];
|
||
// }
|
||
}
|
||
|
||
/**
|
||
* 商家转账回调
|
||
*/
|
||
public function mchTransferCallback($headers, $rawBody)
|
||
{
|
||
// if (!$this->verifySignature($headers, $rawBody)) {
|
||
// Log::error('微信回调验签失败', ['headers' => $headers]);
|
||
// return null; // 返回 500 让微信重试
|
||
// }
|
||
|
||
// 4. 解密回调数据
|
||
$decryptData = $this->decryptNotifyData($rawBody);
|
||
if (!$decryptData) {
|
||
Log::error('数据解密失败');
|
||
return null;
|
||
}
|
||
|
||
return $decryptData;
|
||
}
|
||
|
||
/**
|
||
* 验证签名(RSA-SHA256)
|
||
*/
|
||
private function verifySignature(array $headers, array $body): bool
|
||
{
|
||
// 检查必要头是否存在
|
||
if (
|
||
empty($headers['wechatpay-timestamp']) || empty($headers['wechatpay-nonce']) ||
|
||
empty($headers['wechatpay-signature']) || empty($headers['wechatpay-serial'])
|
||
) {
|
||
Log::error('请求头参数缺失');
|
||
return false;
|
||
}
|
||
|
||
// 检查序列号是否匹配(可选的额外校验)
|
||
// $expectedSerial = $this->getPlatformCertSerial();
|
||
// if ($headers['serial'] !== $expectedSerial) {
|
||
// return false;
|
||
// }
|
||
|
||
// 构造验签名串:timestamp + \n + nonce + \n + body + \n
|
||
$signStr = $headers['wechatpay-timestamp'] . "\n"
|
||
. $headers['wechatpay-nonce'] . "\n"
|
||
. json_encode($body) . "\n";
|
||
|
||
// 加载微信支付平台公钥
|
||
$publicKey = openssl_pkey_get_public('file://' . config("wechatpay.payment.platform_cert_path"));
|
||
if (!$publicKey) {
|
||
Log::error('加载平台证书失败');
|
||
return false;
|
||
}
|
||
|
||
// Base64 解码签名
|
||
$signature = base64_decode($headers['wechatpay-signature']);
|
||
|
||
// 验证签名
|
||
$result = openssl_verify($signStr, $signature, $publicKey, OPENSSL_ALGO_SHA256);
|
||
Log::info("验签结果", ["res" => $result]);
|
||
return $result === 1;
|
||
}
|
||
|
||
/**
|
||
* AES-256-GCM 解密回调数据
|
||
*/
|
||
private function decryptNotifyData(array $data): ?array
|
||
{
|
||
// $data = json_decode($rawBody, true);
|
||
|
||
if (!isset($data['resource'])) {
|
||
Log::info("缺少参数 resource");
|
||
return null;
|
||
}
|
||
|
||
$resource = $data['resource'];
|
||
$ciphertext = base64_decode($resource['ciphertext']);
|
||
$nonce = $resource['nonce'];
|
||
$associatedData = $resource['associated_data'] ?? '';
|
||
|
||
// PHP 7.1+ 原生支持 AES-256-GCM
|
||
$decrypted = AesGcm::decrypt(
|
||
$ciphertext, // Base64解码后的密文(包含tag)
|
||
config("wechatpay.payment.api3_key"),
|
||
$nonce,
|
||
$associatedData
|
||
);
|
||
if ($decrypted === false) {
|
||
return null;
|
||
}
|
||
|
||
return json_decode($decrypted, true);
|
||
}
|
||
|
||
/**
|
||
* 商家批量转账到零钱
|
||
*/
|
||
|
||
public function transferBatches(string $trade_no, string $trade_no2, string $openid, int $amount, string $remark): array
|
||
{
|
||
// try {
|
||
$appid = config("wechat.official_account.default.app_id");
|
||
$data = [
|
||
"appid" => $appid,
|
||
"out_batch_no" => $trade_no,
|
||
"batch_name" => $remark,
|
||
"batch_remark" => $remark,
|
||
"total_amount" => $amount,
|
||
"total_num" => 1,
|
||
"transfer_detail_list" => [
|
||
[
|
||
"out_detail_no" => $trade_no2,
|
||
"transfer_amount" => $amount,
|
||
"transfer_remark" => $remark,
|
||
"openid" => $openid,
|
||
]
|
||
]
|
||
];
|
||
Log::info("转账到零钱数据", $data);
|
||
$instance = $this->newClient();
|
||
$resp = $instance->chain('v3/transfer/batches')->post([
|
||
"json" => $data
|
||
]);
|
||
$res = json_decode($resp->getBody(), true);
|
||
dd($res);
|
||
// } catch (\Exception $e) {
|
||
// // 进行异常捕获并进行错误判断处理
|
||
// Log::info($e->getMessage());
|
||
// if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
|
||
// $r = $e->getResponse();
|
||
// $res = json_decode($r->getBody());
|
||
// return ["code" => 1, "err_msg" => $r->getBody()];
|
||
// }
|
||
|
||
// return ["code" => 1, "err_msg" => $e->getMessage()];
|
||
// }
|
||
}
|
||
|
||
|
||
/**
|
||
* 转账银行卡
|
||
* @return void
|
||
*/
|
||
public function bankTransfer()
|
||
{
|
||
|
||
}
|
||
}
|