Files
2026-08-14 10:55:06 +08:00

162 lines
5.5 KiB
PHP

<?php
/**
* 微信支付网关(Native 扫码支付 / H5 支付)
*
* 配置项(settings 表):
* pay_wechat_enabled - 是否启用 (1/0)
* pay_wechat_mch_id - 商户号
* pay_wechat_api_key - API 密钥
* pay_wechat_appid - 公众号/小程序 APPID(可选)
* pay_wechat_cert_path - 证书路径(退款等需要)
*
* 使用 V3 接口规范,支持 Native 扫码支付。
*/
if (!defined('IN_APP')) exit('Forbidden');
class WechatGateway implements PaymentGatewayInterface
{
private $mchId;
private $apiKey;
private $appId;
private $apiUrl = 'https://api.mch.weixin.qq.com/v3';
public function __construct()
{
$this->mchId = getSetting('pay_wechat_mch_id', '');
$this->apiKey = getSetting('pay_wechat_api_key', '');
$this->appId = getSetting('pay_wechat_appid', '');
}
public function getName()
{
return '微信支付';
}
/**
* 创建微信 Native 支付订单(返回二维码链接)。
*/
public function createOrder(array $order)
{
// 构建 V3 Native 下单请求
$url = $this->apiUrl . '/pay/transactions/native';
$body = [
'mchid' => $this->mchId,
'out_trade_no' => $order['order_no'],
'appid' => $this->appId ?: '',
'description' => $order['subject'],
'notify_url' => $order['notify_url'],
'amount' => [
'total' => (int)round($order['amount'] * 100), // 单位:分
'currency' => 'CNY',
],
];
$headers = [
'Content-Type: application/json',
'Accept: application/json',
'Authorization: WECHATPAY2-SHA256-RSA2048 ' . $this->buildAuth($url, 'POST', json_encode($body)),
];
$result = $this->httpPost($url, json_encode($body), $headers);
$data = json_decode($result, true);
if (isset($data['code_url'])) {
return [
'url' => '',
'form' => '<div class="wechat-qr-wrap"><p>请使用微信扫码支付</p>' .
'<img src="https://api.qrserver.com/v1/create-qr-code/?size=200x&data=' . urlencode($data['code_url']) . '" alt="微信支付二维码" width="200"></div>',
'qr_code' => $data['code_url'],
];
}
// 错误时返回错误信息
Logger::error('微信支付下单失败', ['response' => $data]);
return ['url' => '', 'form' => '<p style="color:red">支付创建失败: ' . h($data['message'] ?? '未知错误') . '</p>'];
}
public function verifyNotify(array $params)
{
// V3 回调验签
if (empty($_SERVER['HTTP_WECHATPAY_SIGNATURE'])) return false;
$serial = $_SERVER['HTTP_WECHATPAY_SERIAL'] ?? '';
$signature = $_SERVER['HTTP_WECHATPAY_SIGNATURE'] ?? '';
$timestamp = $_SERVER['HTTP_WECHATPAY_TIMESTAMP'] ?? '';
$nonce = $_SERVER['HTTP_WECHATPAY_NONCE'] ?? '';
// TODO: 完整的 V3 验签实现需加载微信平台证书
// 这里做基础验证
return !empty($params['resource']['ciphertext']);
}
public function getTradeNo(array $params)
{
// V3 回调解密后获取 transaction_id
$resource = $params['resource'] ?? [];
return $resource['ciphertext'] ? $this->decryptResource($resource) : '';
}
public function isPaid($params)
{
$resource = $params['resource'] ?? [];
$decrypted = $resource['ciphertext'] ? json_decode($this->decryptResource($resource), true) : [];
return ($decrypted['trade_state'] ?? '') === 'SUCCESS';
}
public function respondSuccess()
{
header('Content-Type: application/json');
echo json_encode(['code' => 'SUCCESS', 'message' => '成功']);
}
// ─── V3 签名与请求 ───
private function buildAuth($url, $method, $body)
{
$timestamp = (string)time();
$nonce = bin2hex(random_bytes(16));
$signStr = "$method\n$url\n$timestamp\n$nonce\n$body\n";
// 使用 API Key 做 HMAC-SHA256 签名(简化版)
$signature = hash_hmac('SHA256', $signStr, $this->apiKey);
// 实际 V3 需要商户私钥签名,这里简化处理
return "mchid=\"{$this->mchId}\",nonce_str=\"{$nonce}\",timestamp=\"{$timestamp}\",serial_no=\"\",signature=\"$signature\"";
}
private function httpPost($url, $body, $headers = [])
{
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_SSL_VERIFYPEER => false,
]);
$result = curl_exec($ch);
curl_close($ch);
return $result;
}
/**
* 解密 V3 回调资源。
*/
private function decryptResource($resource)
{
$ciphertext = base64_decode($resource['ciphertext']);
$nonce = $resource['nonce'];
$associatedData = $resource['associated_data'];
// AES-256-GCM 解密
$key = hash('sha256', $this->apiKey, true); // 用 API Key 派生密钥(简化)
if (function_exists('openssl_decrypt') && defined('AES_256_GCM')) {
$plain = openssl_decrypt($ciphertext, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $nonce, $associatedData);
return $plain !== false ? $plain : '';
}
return '';
}
}