mirror of
https://gitee.com/zoujingli/WeChatDeveloper.git
synced 2026-09-04 23:12:09 +08:00
- 微信支付 APIv3 call/raw 统一按实际 query/body 生成签名,并继续透传 Guzzle options。 - 微信支付默认关闭 http_errors,解析非 2xx JSON 错误并统一抛出 ApiException。 - 移除默认写入错误的 Wechatpay-Serial,请求敏感信息加密时由业务传平台序列号。 - 支付宝网关请求捕获 Guzzle 异常并转换为 SDK 异常,保留验签和支付快捷调用。 - 补充支付下载、通知解密、错误响应、支付宝验签和 options 透传测试。
101 lines
3.3 KiB
PHP
101 lines
3.3 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
/**
|
||
* This file is part of HyperfAdmin.
|
||
*
|
||
* @Link https://thinkadmin.top
|
||
* @Author Anyon<zoujingli@qq.com>
|
||
*/
|
||
|
||
namespace We\Support;
|
||
|
||
use GuzzleHttp\Client;
|
||
use GuzzleHttp\ClientInterface;
|
||
use GuzzleHttp\Exception\GuzzleException;
|
||
use Psr\Http\Message\ResponseInterface;
|
||
use We\Exception\ApiException;
|
||
|
||
/**
|
||
* JSON HTTP 客户端封装。
|
||
*
|
||
* 统一发送 JSON 风格平台接口请求,只允许相对路径,并将平台错误响应转换为 {@see ApiException}。
|
||
*/
|
||
final class JsonClient
|
||
{
|
||
/**
|
||
* 创建 JSON HTTP 客户端封装。
|
||
*/
|
||
public function __construct(
|
||
private ClientInterface $http = new Client(['timeout' => 20.0]),
|
||
) {}
|
||
|
||
/**
|
||
* 发送 HTTP 请求并将 JSON 响应解析为数组。
|
||
*
|
||
* @param array<string,mixed> $query
|
||
* @param array<string,mixed> $options
|
||
* @return array<string,mixed>
|
||
*/
|
||
public function request(string $method, string $uri, array $query = [], array $options = []): array
|
||
{
|
||
$response = $this->raw($method, $uri, $query, $options);
|
||
$statusCode = (int)$response->getStatusCode();
|
||
$body = (string)$response->getBody();
|
||
$data = $body === '' ? [] : json_decode($body, true);
|
||
if (!is_array($data)) {
|
||
throw new ApiException('微信接口响应不是有效 JSON', $statusCode, null, ['body' => $body]);
|
||
}
|
||
$errcode = (int)($data['errcode'] ?? 0);
|
||
if ($errcode !== 0) {
|
||
throw new ApiException((string)($data['errmsg'] ?? '微信接口请求失败'), $errcode, null, $data);
|
||
}
|
||
if ($statusCode >= 400) {
|
||
throw new ApiException((string)($data['message'] ?? '微信接口请求失败'), $statusCode, null, $data);
|
||
}
|
||
|
||
return $data;
|
||
}
|
||
|
||
/**
|
||
* 发送 HTTP 请求并返回原始 PSR-7 响应,不执行 JSON 解析。
|
||
*
|
||
* @param array<string,mixed> $query
|
||
* @param array<string,mixed> $options
|
||
*/
|
||
public function raw(string $method, string $uri, array $query = [], array $options = []): ResponseInterface
|
||
{
|
||
$options['query'] = array_merge($query, is_array($options['query'] ?? null) ? $options['query'] : []);
|
||
|
||
return $this->send($method, $uri, $options);
|
||
}
|
||
|
||
/**
|
||
* 发送底层 Guzzle 请求,并禁止绝对 URL。
|
||
*
|
||
* @param array<string,mixed> $options
|
||
*/
|
||
public function send(string $method, string $uri, array $options = []): ResponseInterface
|
||
{
|
||
$this->assertRelativeUri($uri);
|
||
$options['http_errors'] = $options['http_errors'] ?? false;
|
||
try {
|
||
return $this->http->request($method, $uri, $options);
|
||
} catch (GuzzleException $exception) {
|
||
throw new ApiException($exception->getMessage(), (int)$exception->getCode(), $exception);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 校验接口 path 必须为相对路径,避免调用方传入绝对 URL。
|
||
*/
|
||
private function assertRelativeUri(string $uri): void
|
||
{
|
||
$uri = trim($uri);
|
||
// SDK 的微信类客户端都绑定了官方 base_uri,禁止传入绝对 URL,避免网关代调用场景被放大成 SSRF。
|
||
if (str_starts_with($uri, '//') || preg_match('#^[a-z][a-z0-9+.-]*:#i', $uri) === 1) {
|
||
throw new ApiException('接口路径必须是相对路径');
|
||
}
|
||
}
|
||
}
|