Files
SoonDesign/backend-web/src/Services/AlipayClient.php
T
24kycj 88c6ce8ccc 重构 monorepo 并完善网页端订阅与首页体验
- 迁移为 frontend-web、frontend-electron、backend-web 与 docker 部署结构
- 网页端:订阅门禁二次弹窗、套餐/支付组件化、顶栏分组对齐
- 首页:最近文件与模板库布局优化,缩略图对齐,下载与删除操作
- 新增管理后台、支付与云端文件 API,更新 README 与项目规范

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 18:17:39 +08:00

155 lines
5.5 KiB
PHP

<?php
declare(strict_types=1);
namespace Soon\Api\Services;
use Soon\Api\Core\Config;
/**
* 支付宝 PC 网站支付(页面跳转)。
*/
final class AlipayClient
{
/** @return array{gateway: string, params: array<string, string>} */
public static function pagePayForm(string $orderNo, int $amountCents, string $subject): array
{
$cfg = Config::get('alipay', []);
$params = [
'app_id' => (string)($cfg['app_id'] ?? ''),
'method' => 'alipay.trade.page.pay',
'charset' => 'utf-8',
'sign_type' => 'RSA2',
'timestamp' => date('Y-m-d H:i:s'),
'version' => '1.0',
'notify_url' => (string)Config::get('site.base_url', '') . '/api/v1/pay/alipay/notify',
'return_url' => (string)Config::get('site.front_base_url', Config::get('site.base_url', ''))
. '/pages/member.web.html?paid=' . rawurlencode($orderNo),
'biz_content' => json_encode([
'out_trade_no' => $orderNo,
'product_code' => 'FAST_INSTANT_TRADE_PAY',
'total_amount' => number_format($amountCents / 100, 2, '.', ''),
'subject' => $subject,
], JSON_UNESCAPED_UNICODE),
];
$params['sign'] = self::sign($params, (string)($cfg['private_key'] ?? ''));
$gateway = ($cfg['sandbox'] ?? false)
? 'https://openapi.alipaydev.com/gateway.do'
: 'https://openapi.alipay.com/gateway.do';
return ['gateway' => $gateway, 'params' => $params];
}
public static function pagePay(string $orderNo, int $amountCents, string $subject): string
{
$form = self::pagePayForm($orderNo, $amountCents, $subject);
return $form['gateway'] . '?' . http_build_query($form['params']);
}
public static function verifyNotify(array $params): bool
{
$cfg = Config::get('alipay', []);
$sign = (string)($params['sign'] ?? '');
if ($sign === '') {
return false;
}
unset($params['sign'], $params['sign_type']);
ksort($params);
$query = '';
foreach ($params as $k => $v) {
if ($v === '' || $v === null) {
continue;
}
$query .= $k . '=' . $v . '&';
}
$query = rtrim($query, '&');
return self::verifyNotifySign($query, $sign, (string)($cfg['public_key'] ?? ''));
}
public static function verifyNotifySign(string $content, string $signBase64, string $alipayPublicKey): bool
{
$pub = openssl_get_publickey(self::normalizePem($alipayPublicKey, 'PUBLIC KEY'));
if ($pub === false) {
return false;
}
$sig = base64_decode($signBase64, true);
if ($sig === false) {
openssl_free_key($pub);
return false;
}
$ok = openssl_verify($content, $sig, $pub, OPENSSL_ALGO_SHA256);
openssl_free_key($pub);
return $ok === 1;
}
private static function sign(array $params, string $privateKey): string
{
ksort($params);
$query = '';
foreach ($params as $k => $v) {
if ($v === '' || $v === null) {
continue;
}
$query .= $k . '=' . $v . '&';
}
$query = rtrim($query, '&');
$res = openssl_get_privatekey(self::normalizePem($privateKey, 'PRIVATE KEY'));
if ($res === false) {
return '';
}
$signature = '';
openssl_sign($query, $signature, $res, OPENSSL_ALGO_SHA256);
openssl_free_key($res);
return base64_encode($signature);
}
public static function refund(string $orderNo, int $amountCents): bool
{
$cfg = Config::get('alipay', []);
$appId = (string)($cfg['app_id'] ?? '');
$privateKey = (string)($cfg['private_key'] ?? '');
if ($appId === '' || $privateKey === '') {
return false;
}
$params = [
'app_id' => $appId,
'method' => 'alipay.trade.refund',
'charset' => 'utf-8',
'sign_type' => 'RSA2',
'timestamp' => date('Y-m-d H:i:s'),
'version' => '1.0',
'biz_content' => json_encode([
'out_trade_no' => $orderNo,
'refund_amount' => number_format($amountCents / 100, 2, '.', ''),
], JSON_UNESCAPED_UNICODE),
];
$params['sign'] = self::sign($params, $privateKey);
$gateway = ($cfg['sandbox'] ?? false)
? 'https://openapi.alipaydev.com/gateway.do'
: 'https://openapi.alipay.com/gateway.do';
$ch = curl_init($gateway);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($params),
CURLOPT_TIMEOUT => 20,
]);
$resp = curl_exec($ch);
curl_close($ch);
if (!is_string($resp) || $resp === '') {
return false;
}
$data = json_decode($resp, true);
$key = 'alipay_trade_refund_response';
return is_array($data) && ($data[$key]['code'] ?? '') === '10000';
}
private static function normalizePem(string $key, string $type): string
{
$key = trim($key);
if (str_contains($key, 'BEGIN')) {
return $key;
}
$body = chunk_split(preg_replace('/\s+/', '', $key), 64, "\n");
return "-----BEGIN {$type}-----\n{$body}-----END {$type}-----\n";
}
}