ufutx.love.util/app/Services/WechatPayService.php
2026-04-27 09:27:17 +08:00

312 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Services;
use App\Http\Response\ResponseJson;
use Exception;
use Illuminate\Support\Facades\Log;
use WeChatPay\Builder;
use WeChatPay\BuilderChainable;
use WeChatPay\Crypto\AesGcm;
use WeChatPay\Crypto\Rsa;
class WechatPayService
{
use ResponseJson;
/**
* 初始化一个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 让微信重试
// }
$decryptData = $this->decryptToString($rawBody);
// 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'];
$nonce = $resource['nonce'];
$associatedData = $resource['associated_data'] ?? '';
// 1. 检查原始 ciphertext 的类型和长度(它应该是 Base64 字符串)
$originalCiphertext = $resource['ciphertext'];
Log::info('原始 ciphertext 信息', [
'type' => gettype($originalCiphertext), // 应该是 string
'length' => strlen($originalCiphertext), // 通常是一个正整数,如 344
'is_base64' => base64_encode(base64_decode($originalCiphertext, true)) === $originalCiphertext, // 应该是 true
]);
// 2. 检查一次解码后的二进制数据长度
$binaryCiphertext = base64_decode($originalCiphertext);
Log::info('解码后二进制密文长度', [
'binary_length' => strlen($binaryCiphertext), // 通常会是 16 的倍数
]);
// 3. 再次确认你的 APIv3 密钥是否是 32 字节
$apiV3Key = config("wechatpay.payment.api3_key");
Log::info('APIv3 密钥长度', [
'key_length' => strlen($apiV3Key), // 必须输出 32
]);
Log::info("解密数据", ["ciphertext" => $binaryCiphertext, "apiV3Key" => $apiV3Key, "nonce" => $nonce, "associatedData" => $associatedData]);
// PHP 7.1+ 原生支持 AES-256-GCM
$decrypted = AesGcm::decrypt(
$binaryCiphertext, // Base64解码后的密文包含tag
$apiV3Key,
$nonce,
$associatedData
);
if ($decrypted === false) {
return null;
}
return json_decode($decrypted, true);
}
public function decryptToString($data)
{
try {
$resource = $data['resource'];
$nonceStr = $resource['nonce'];
$ciphertext = $resource['ciphertext'];
$associatedData = $resource['associated_data'] ?? '';
$apiV3Key = config("wechatpay.payment.api3_key");
$ciphertext = base64_decode($ciphertext);
if (strlen($ciphertext) <= 16) {
return false;
}
if (in_array('aes-256-gcm', openssl_get_cipher_methods())) {
$ctext = substr($ciphertext, 0, -16);
$authTag = substr($ciphertext, -16);
$res = openssl_decrypt(
$ctext,
'aes-256-gcm',
$apiV3Key,
OPENSSL_RAW_DATA,
$nonceStr,
$authTag,
$associatedData
);
return json_decode($res, true);
}
return null;
} catch (Exception $e) {
$this->getError($e);
return null;
}
}
/**
* 商家批量转账到零钱
*/
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()
{
}
}