文件还在测试中
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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, '.', '');
|
||||
}
|
||||
}
|
||||
@@ -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', ''),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user