Files
chatroom/app/Services/VipPaymentCenterClient.php
2026-04-13 17:25:33 +08:00

213 lines
6.9 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 文件功能NovaLink 支付中心客户端
* 负责读取聊天室后台配置,按支付中心要求生成签名并发起下单、查单等请求
*/
namespace App\Services;
use App\Models\Sysparam;
use App\Models\VipPaymentOrder;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
use RuntimeException;
/**
* NovaLink 支付中心客户端
* 负责构建开放接口签名请求,并与远端支付中心完成下单和查单通信。
*/
class VipPaymentCenterClient
{
/**
* 判断 VIP 在线支付是否已启用
*/
public function isEnabled(): bool
{
return Sysparam::getValue('vip_payment_enabled', '0') === '1';
}
/**
* 判断支付中心接入配置是否完整
*/
public function hasValidConfig(): bool
{
$config = $this->getConfig();
return $config['base_url'] !== ''
&& $config['app_key'] !== ''
&& $config['app_secret'] !== '';
}
/**
* 获取当前支付中心配置
*
* @return array{base_url:string,app_key:string,app_secret:string,timeout:int}
*/
public function getConfig(): array
{
return [
'base_url' => $this->normalizeBaseUrl(Sysparam::getValue('vip_payment_base_url', '')),
'app_key' => Sysparam::getValue('vip_payment_app_key', ''),
'app_secret' => Sysparam::getValue('vip_payment_app_secret', ''),
'timeout' => (int) Sysparam::getValue('vip_payment_timeout', '10'),
];
}
/**
* 为本地 VIP 订单在支付中心创建远端支付单
*
* @param VipPaymentOrder $vipPaymentOrder 本地 VIP 支付订单
* @param string $syncReturnUrl 同步回调地址
* @param string $asyncNotifyUrl 异步回调地址
* @return array<string, mixed>
*/
public function createOrder(VipPaymentOrder $vipPaymentOrder, string $syncReturnUrl, string $asyncNotifyUrl): array
{
$payload = [
'merchant_order_no' => $vipPaymentOrder->merchant_order_no,
'subject' => $vipPaymentOrder->subject,
'body' => '聊天室 VIP 会员购买:'.$vipPaymentOrder->vip_name,
'amount' => number_format((float) $vipPaymentOrder->amount, 2, '.', ''),
// 支付中心要求商户显式指定 provider不能依赖平台自动推断。
'provider' => $vipPaymentOrder->provider,
'business_type' => 'chatroom_vip',
'sync_return_url' => $syncReturnUrl,
'async_notify_url' => $asyncNotifyUrl,
'meta' => [
'user_id' => $vipPaymentOrder->user_id,
'vip_level_id' => $vipPaymentOrder->vip_level_id,
'local_order_no' => $vipPaymentOrder->order_no,
],
];
return $this->request('POST', '/api/open/v1/pay/orders', $payload);
}
/**
* 查询支付中心订单状态
*
* @param string $paymentOrderNo 远端平台支付单号
* @return array<string, mixed>
*/
public function queryOrder(string $paymentOrderNo): array
{
return $this->request('GET', '/api/open/v1/pay/orders/'.$paymentOrderNo);
}
/**
* 校验支付中心异步回调签名
*
* @param string $signature 请求头携带的签名
* @param string $rawBody 原始请求体 JSON
*/
public function isValidWebhookSignature(string $signature, string $rawBody): bool
{
$config = $this->getConfig();
if ($signature === '' || $config['app_secret'] === '') {
return false;
}
// 支付中心回调直接使用 JSON 原文做 HMAC避免数组重组后签名不一致。
$expectedSignature = hash_hmac('sha256', $rawBody, $config['app_secret']);
return hash_equals($expectedSignature, $signature);
}
/**
* 调用支付中心开放接口
*
* @param string $method 请求方法
* @param string $path 接口路径
* @param array<string, mixed> $payload 请求体
* @return array<string, mixed>
*/
private function request(string $method, string $path, array $payload = []): array
{
if (! $this->isEnabled() || ! $this->hasValidConfig()) {
throw new RuntimeException('VIP 支付中心尚未完成配置。');
}
$config = $this->getConfig();
$body = strtoupper($method) === 'GET'
? ''
: json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
// 先按 NovaLink 约定计算签名头,再发起请求。
$headers = $this->buildHeaders($method, $path, $body, $config['app_key'], $config['app_secret']);
$request = Http::timeout(max($config['timeout'], 3))
->acceptJson()
->withHeaders($headers);
$response = strtoupper($method) === 'GET'
? $request->get($config['base_url'].$path)
: $request->withBody($body, 'application/json')->send($method, $config['base_url'].$path);
return $this->parseResponse($response);
}
/**
* 构建开放 API 认证请求头
*
* @param string $method 请求方法
* @param string $path 接口路径
* @param string $body JSON 请求体
* @param string $appKey 接入应用 Key
* @param string $appSecret 接入应用 Secret
* @return array<string, string>
*/
private function buildHeaders(string $method, string $path, string $body, string $appKey, string $appSecret): array
{
$timestamp = (string) now()->timestamp;
$nonce = (string) str()->uuid();
// 保持与 NovaLink 中间件完全一致的签名原文拼接顺序。
$signString = implode("\n", [
strtoupper($method),
$path,
$timestamp,
$nonce,
hash('sha256', $body),
]);
return [
'X-App-Key' => $appKey,
'X-Timestamp' => $timestamp,
'X-Nonce' => $nonce,
'X-Signature' => hash_hmac('sha256', $signString, $appSecret),
];
}
/**
* 解析支付中心接口响应
*
* @param Response $response HTTP 响应对象
* @return array<string, mixed>
*/
private function parseResponse(Response $response): array
{
if (! $response->successful()) {
throw new RuntimeException('支付中心请求失败:'.$response->body());
}
$json = $response->json();
if (! is_array($json) || ! ($json['success'] ?? false)) {
throw new RuntimeException((string) ($json['message'] ?? '支付中心返回了无效响应。'));
}
return (array) ($json['data'] ?? []);
}
/**
* 规范化支付中心地址
*
* @param string $baseUrl 后台录入的地址
*/
private function normalizeBaseUrl(string $baseUrl): string
{
return rtrim(trim($baseUrl), '/');
}
}