90 lines
3.2 KiB
PHP
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;
|
|
}
|
|
}
|