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 */ 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 */ 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 $payload 请求体 * @return array */ 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 */ 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 */ 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), '/'); } }