Files
2026-08-08 15:53:53 +08:00

90 lines
3.2 KiB
PHP

<?php
namespace Core\Payment;
/**
* 微信支付网关(NATIVE 扫码支付)
* - demo:返回模拟支付入口
* - live:调用统一下单接口获取 code_url,前端展示二维码
*/
class WechatGateway extends Gateway
{
public function pay(array $order): array
{
$simulate = site_url('order/demo/' . $order['order_no']);
$hasCred = !empty($this->config['mchid']) && !empty($this->config['appid']) && !empty($this->config['key']);
if ($this->isDemo() || !$hasCred) {
return [
'mode' => 'demo',
'channel' => 'wechat',
'simulate' => $simulate,
'config_missing' => !$hasCred,
];
}
$params = [
'appid' => $this->config['appid'],
'mch_id' => $this->config['mchid'],
'nonce_str' => bin2hex(random_bytes(16)),
'body' => $order['product_name'] ?: '商品购买',
'out_trade_no' => $order['order_no'],
'total_fee' => (int) round((float) $order['amount'] * 100), // 分
'spbill_create_ip' => $_SERVER['SERVER_ADDR'] ?? '127.0.0.1',
'notify_url' => site_url('pay/notify/wechat'),
'trade_type' => 'NATIVE',
];
$params['sign'] = $this->sign($params);
$xml = $this->toXml($params);
$resp = @file_get_contents('https://api.mch.weixin.qq.com/pay/unifiedorder', false, stream_context_create([
'http' => ['method' => 'POST', 'header' => 'Content-Type: text/xml', 'content' => $xml, 'timeout' => 8],
]));
$res = $resp ? $this->fromXml($resp) : [];
if (!empty($res['code_url'])) {
return ['mode' => 'qrcode', 'channel' => 'wechat', 'qr' => $res['code_url']];
}
// 调用失败则回退演示,避免卡死
return ['mode' => 'demo', 'channel' => 'wechat', 'simulate' => $simulate, 'config_missing' => false, 'api_error' => true];
}
/** HMAC-SHA256 签名 */
private function sign(array $params): string
{
ksort($params);
$str = '';
foreach ($params as $k => $v) {
if ($v === '' || $v === null) continue;
$str .= $k . '=' . $v . '&';
}
$str .= 'key=' . ($this->config['key'] ?? '');
return strtoupper(hash_hmac('sha256', $str, $this->config['key'] ?? ''));
}
private function toXml(array $params): string
{
$xml = '<xml>';
foreach ($params as $k => $v) {
$xml .= "<{$k}>" . htmlspecialchars($v, ENT_XML1) . "</{$k}>";
}
$xml .= '</xml>';
return $xml;
}
private function fromXml(string $xml): array
{
$r = @simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA);
return $r ? json_decode(json_encode($r), true) : [];
}
public function verifyNotify(array $data): ?string
{
if (empty($data['out_trade_no'])) return null;
if (($data['result_code'] ?? '') === 'SUCCESS' && ($data['return_code'] ?? '') === 'SUCCESS') {
// 生产环境应重新按 key 验签后返回
return $data['out_trade_no'];
}
return null;
}
}