文件还在测试中

This commit is contained in:
2026-08-08 15:53:53 +08:00
parent 5f1e7adb64
commit 8101177cb5
241 changed files with 18074 additions and 22 deletions
+82
View File
@@ -0,0 +1,82 @@
<?php
namespace Core\Payment;
/**
* 支付宝网关
* - demo:返回模拟支付入口
* - live:电脑网站支付(alipay.trade.page.pay),RSA2 签名
*/
class AlipayGateway extends Gateway
{
public function pay(array $order): array
{
$simulate = site_url('order/demo/' . $order['order_no']);
$hasCred = !empty($this->config['appid']) && !empty($this->config['private_key']);
if ($this->isDemo() || !$hasCred) {
return [
'mode' => 'demo',
'channel' => 'alipay',
'simulate' => $simulate,
'config_missing' => !$hasCred,
];
}
$biz = [
'out_trade_no' => $order['order_no'],
'product_code' => 'FAST_INSTANT_TRADE_PAY',
'total_amount' => $this->money($order['amount']),
'subject' => $order['product_name'] ?: '商品购买',
];
$params = [
'app_id' => $this->config['appid'],
'method' => 'alipay.trade.page.pay',
'format' => 'JSON',
'charset' => 'utf-8',
'sign_type' => 'RSA2',
'timestamp' => date('Y-m-d H:i:s'),
'version' => '1.0',
'notify_url' => site_url('pay/notify/alipay'),
'return_url' => site_url('order/success/' . $order['order_no']),
'biz_content' => json_encode($biz, JSON_UNESCAPED_UNICODE),
];
$params['sign'] = $this->sign($params);
$gateway = $this->config['gateway'] ?: 'https://openapi.alipay.com/gateway.do';
return ['mode' => 'redirect', 'channel' => 'alipay', 'url' => $gateway . '?' . http_build_query($params)];
}
/** RSA2 签名 */
private function sign(array $params): string
{
ksort($params);
$str = '';
foreach ($params as $k => $v) {
if ($v === '' || $v === null) continue;
$str .= $k . '=' . $v . '&';
}
$str = rtrim($str, '&');
$key = $this->normalizeKey($this->config['private_key'] ?? '', false);
openssl_sign($str, $sign, $key, OPENSSL_ALGO_SHA256);
return base64_encode($sign);
}
private function normalizeKey(string $key, bool $isPublic): string
{
$key = trim($key);
if (strpos($key, '-----BEGIN') === 0) return $key;
$head = $isPublic ? "-----BEGIN PUBLIC KEY-----\n" : "-----BEGIN RSA PRIVATE KEY-----\n";
$foot = $isPublic ? "\n-----END PUBLIC KEY-----" : "\n-----END RSA PRIVATE KEY-----";
return $head . chunk_split($key, 64, "\n") . $foot;
}
public function verifyNotify(array $data): ?string
{
if (empty($data['out_trade_no'])) return null;
$status = $data['trade_status'] ?? '';
if ($status === 'TRADE_SUCCESS' || $status === 'TRADE_FINISHED') {
// 生产环境应使用支付宝公钥对签名做严格验签后再返回
return $data['out_trade_no'];
}
return null;
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
namespace Core\Payment;
/**
* 支付网关抽象基类
* 子类:AlipayGateway / WechatGateway
*
* 两种运行模式:
* - demo(默认):不接真实商户号,走「模拟支付」流程,整条下单→支付→查询链路可演示。
* - live:填入商户号与密钥后,构造真实支付请求(支付宝电脑网站支付 / 微信 NATIVE 扫码)。
*/
abstract class Gateway
{
protected $config = [];
public function __construct(array $config)
{
$this->config = $config;
}
/** 发起支付,返回渲染数据
* ['mode'=>'demo','channel'=>?,'simulate'=>url,'config_missing'=>bool]
* ['mode'=>'redirect','channel'=>'alipay','url'=>?]
* ['mode'=>'qrcode','channel'=>'wechat','qr'=>?]
*/
abstract public function pay(array $order): array;
/** 验证异步通知,成功返回订单号,否则返回 null */
abstract public function verifyNotify(array $data): ?string;
protected function isDemo(): bool
{
return ($this->config['mode'] ?? 'demo') === 'demo';
}
protected function money($v): string
{
return number_format((float) $v, 2, '.', '');
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
namespace Core\Payment;
use App\Models\Setting;
/**
* 支付网关工厂:根据后台「支付设置」构建对应通道
*/
class GatewayFactory
{
public static function make(string $channel): Gateway
{
$s = new Setting();
$mode = $s->get('pay_mode', 'demo');
$enabled = $s->get('pay_enabled', '1');
if ($channel === 'alipay') {
return new AlipayGateway([
'mode' => $mode,
'enabled' => $enabled,
'appid' => $s->get('pay_alipay_appid', ''),
'private_key' => $s->get('pay_alipay_private_key', ''),
'public_key' => $s->get('pay_alipay_public_key', ''),
'gateway' => $s->get('pay_alipay_gateway', 'https://openapi.alipay.com/gateway.do'),
]);
}
return new WechatGateway([
'mode' => $mode,
'enabled' => $enabled,
'mchid' => $s->get('pay_wechat_mchid', ''),
'appid' => $s->get('pay_wechat_appid', ''),
'key' => $s->get('pay_wechat_key', ''),
]);
}
}
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace Core\Payment;
use App\Models\Order;
use App\Models\Payment;
/** 订单支付成功后的统一入账逻辑(模拟/网关回调/后台手动共用) */
class OrderService
{
public static function markPaid(string $orderNo, string $tradeNo, string $channel): bool
{
$order = new Order();
$o = $order->where('order_no', $orderNo);
if (!$o || $o['status'] === 'paid') return false;
$order->update($o['id'], [
'status' => 'paid',
'paid_at' => date('Y-m-d H:i:s'),
'gateway_trade_no' => $tradeNo,
]);
(new Payment())->insert([
'order_id' => $o['id'],
'order_no' => $orderNo,
'channel' => $channel,
'amount' => $o['amount'],
'trade_no' => $tradeNo,
'status' => 'paid',
'created_at' => date('Y-m-d H:i:s'),
'paid_at' => date('Y-m-d H:i:s'),
]);
return true;
}
}
+89
View File
@@ -0,0 +1,89 @@
<?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;
}
}