feat(payment): 完善支付调用透传与错误处理

- 微信支付 APIv3 call/raw 统一按实际 query/body 生成签名,并继续透传 Guzzle options。

- 微信支付默认关闭 http_errors,解析非 2xx JSON 错误并统一抛出 ApiException。

- 移除默认写入错误的 Wechatpay-Serial,请求敏感信息加密时由业务传平台序列号。

- 支付宝网关请求捕获 Guzzle 异常并转换为 SDK 异常,保留验签和支付快捷调用。

- 补充支付下载、通知解密、错误响应、支付宝验签和 options 透传测试。
This commit is contained in:
Anyon 2026-05-08 11:33:10 +08:00
parent e8cd998852
commit 09f64b45b1
8 changed files with 380 additions and 65 deletions

View File

@ -1,9 +1,11 @@
<?php
declare(strict_types=1);
/**
* 支付宝支付客户端。
* This file is part of HyperfAdmin.
*
* @Link https://thinkadmin.top
* @Author Anyon<zoujingli@qq.com>
*/
namespace We\Platform\Alipay;

View File

@ -1,17 +1,22 @@
<?php
declare(strict_types=1);
/**
* 支付宝开放平台客户端。
* This file is part of HyperfAdmin.
*
* @Link https://thinkadmin.top
* @Author Anyon<zoujingli@qq.com>
*/
namespace We\Platform\Alipay;
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\GuzzleException;
use We\Config\AlipayPlatformConfig;
use We\Exception\ApiException;
use We\Exception\WechatException;
use We\Support\CredentialValidator;
/**
* 支付宝开放平台客户端。
@ -42,10 +47,14 @@ class PlatformClient
public function request(string $apiMethod, array $bizContent = [], array $extra = []): array
{
$params = $this->buildGatewayParams($apiMethod, $bizContent, $extra);
$response = $this->http->request('POST', $this->config->gateway, [
'form_params' => $params,
'headers' => ['Accept' => 'application/json'],
]);
try {
$response = $this->http->request('POST', $this->config->gateway, [
'form_params' => $params,
'headers' => ['Accept' => 'application/json'],
]);
} catch (GuzzleException $e) {
throw new ApiException('支付宝网关请求失败: ' . $e->getMessage(), (int)$e->getCode(), $e);
}
$body = (string)$response->getBody();
$payload = json_decode($body, true);
if (!is_array($payload)) {
@ -67,7 +76,7 @@ class PlatformClient
/**
* 验证支付宝异步通知签名;业务处理通知前应先完成验签。
*
* @param array<string,mixed> $params 支付宝通知完整参数,包含 sign/sign_type
* @param array<string,mixed> $params 支付宝通知完整参数,包含 sign/sign_type
*/
public function verifyNotify(array $params): bool
{
@ -112,12 +121,18 @@ class PlatformClient
*/
public function decrypt(string $encryptedData, string $sessionKey, string $iv): array
{
$ciphertext = base64_decode($encryptedData, true);
$key = base64_decode($sessionKey, true);
$ivValue = base64_decode($iv, true);
if ($ciphertext === false || $key === false || $ivValue === false) {
throw new WechatException('支付宝数据解密参数 Base64 无效');
}
$plain = openssl_decrypt(
base64_decode($encryptedData, true) ?: '',
$ciphertext,
'AES-128-CBC',
base64_decode($sessionKey, true) ?: '',
$key,
OPENSSL_RAW_DATA,
base64_decode($iv, true) ?: ''
$ivValue
);
if (!is_string($plain) || $plain === '') {
throw new WechatException('支付宝数据解密失败');
@ -221,7 +236,7 @@ class PlatformClient
'sign_type' => $this->config->signType,
'timestamp' => date('Y-m-d H:i:s'),
'version' => $this->config->version,
'biz_content' => json_encode($bizContent, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '{}',
'biz_content' => $this->jsonString($bizContent, '{}'),
];
foreach ($extra as $key => $value) {
$params[(string)$key] = $this->gatewayValue($value);
@ -270,7 +285,17 @@ class PlatformClient
*/
private function gatewayValue(mixed $value): string
{
return is_scalar($value) ? (string)$value : (json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '');
return is_scalar($value) ? (string)$value : $this->jsonString($value, '');
}
/**
* 将支付宝网关数组参数编码为 JSON 字符串。
*/
private function jsonString(mixed $value, string $fallback): string
{
$json = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
return is_string($json) ? $json : $fallback;
}
/**
@ -297,7 +322,7 @@ class PlatformClient
*/
private function normalizePrivateKey(string $privateKey): string
{
return str_contains($privateKey, 'BEGIN') ? $privateKey : "-----BEGIN PRIVATE KEY-----\n" . chunk_split($privateKey, 64, "\n") . "-----END PRIVATE KEY-----";
return CredentialValidator::normalizePrivateKey($privateKey, true);
}
/**
@ -305,7 +330,7 @@ class PlatformClient
*/
private function normalizePublicKey(string $publicKey): string
{
return str_contains($publicKey, 'BEGIN') ? $publicKey : "-----BEGIN PUBLIC KEY-----\n" . chunk_split($publicKey, 64, "\n") . "-----END PUBLIC KEY-----";
return CredentialValidator::normalizePublicKey($publicKey, true);
}
/**

View File

@ -1,13 +1,16 @@
<?php
declare(strict_types=1);
/**
* 微信支付 APIv3 客户端。
* This file is part of HyperfAdmin.
*
* @Link https://thinkadmin.top
* @Author Anyon<zoujingli@qq.com>
*/
namespace We\Platform\Wechat;
use GuzzleHttp\Client;
use GuzzleHttp\ClientInterface;
use Psr\Http\Message\ResponseInterface;
use We\Config\WechatPaymentConfig;
@ -34,22 +37,28 @@ final class PaymentClient
private readonly WechatPaymentConfig $config,
?ClientInterface $http = null,
) {
$this->http = new JsonClient($http ?? new \GuzzleHttp\Client(['base_uri' => 'https://api.mch.weixin.qq.com/', 'timeout' => 20.0]));
$this->http = new JsonClient($http ?? new Client(['base_uri' => 'https://api.mch.weixin.qq.com/', 'timeout' => 20.0]));
}
/**
* 发起微信支付 APIv3 请求并附加商户请求签名。
*
* @param array<string,mixed> $payload
* @param array<string,mixed> $query
* @param array<string,mixed> $options
* @return array<string,mixed>
*/
public function request(string $method, string $uri, array $payload = [], array $query = []): array
public function request(string $method, string $uri, array $payload = [], array $query = [], array $options = []): array
{
$response = $this->raw($method, $uri, $payload, $query);
$response = $this->raw($method, $uri, $payload, $query, $options);
$statusCode = (int)$response->getStatusCode();
$body = (string)$response->getBody();
$data = $body === '' ? [] : json_decode($body, true);
if (!is_array($data)) {
throw new ApiException('微信支付接口响应不是有效 JSON', (int)$response->getStatusCode(), null, ['body' => $body]);
throw new ApiException('微信支付接口响应不是有效 JSON', $statusCode, null, ['body' => $body]);
}
if ($statusCode >= 400) {
throw new ApiException((string)($data['message'] ?? $data['code'] ?? '微信支付接口请求失败'), $statusCode, null, $data);
}
return $data;
@ -66,9 +75,7 @@ final class PaymentClient
{
$uri = '/' . ltrim($uri, '/');
$query = $this->mergeQuery($query, $options);
$body = array_key_exists('body', $options)
? (string)$options['body']
: ($payload === [] ? '' : (json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '{}'));
$body = $this->resolveRequestBody($payload, $options);
$nonce = bin2hex(random_bytes(16));
$timestamp = (string)time();
$authorization = $this->authorization($method, $uri . $this->queryString($query), $timestamp, $nonce, $body);
@ -90,28 +97,6 @@ final class PaymentClient
return $this->raw('GET', $uri, [], $query, $options);
}
/**
* 校验微信支付通知签名并解密通知 resource。
*
* @param array<string,string> $headers
* @param array<string,mixed>|string $body 原始 JSON 字符串优先;传数组仅用于兼容旧调用,验签会退化为本地重编码。
* @return array<string,mixed>
*/
private function decryptNotification(array $headers, array|string $body): array
{
// 微信支付 APIv3 签名串要求使用 HTTP 原始 body不能先 json_decode 再重新编码,否则字段顺序或转义差异会导致验签失败。
$rawBody = is_string($body) ? $body : (json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '{}');
$this->assertNotificationSignature($headers, $rawBody);
$payload = is_string($body) ? json_decode($body, true) : $body;
if (!is_array($payload)) {
throw new WechatException('微信支付回调 JSON 无效');
}
/** @var array{ciphertext:string,nonce:string,associated_data?:string} $resource */
$resource = $payload['resource'] ?? [];
return PaymentCrypto::decryptResource($this->config->apiV3Key, $resource);
}
/**
* 通用 API 调用入口;官方接口使用 API path + 参数数组,特殊路径仅用于支付通知验签与解密。
*
@ -134,8 +119,9 @@ final class PaymentClient
return $this->request(
$method,
$uri,
$method === 'GET' ? (is_array($options['payload'] ?? null) ? $options['payload'] : []) : $params,
$method === 'GET' ? $params : (is_array($options['query'] ?? null) ? $options['query'] : []),
$this->paymentCallPayload($method, $params, $options),
$this->paymentCallQuery($method, $params, $options),
$this->paymentCallOptions($options),
);
}
@ -163,6 +149,30 @@ final class PaymentClient
return $this->call($uriOrPath, $params, 'GET', $options);
}
/**
* 校验微信支付通知签名并解密通知 resource。
*
* @param array<string,string> $headers
* @param array<string,mixed>|string $body 原始 JSON 字符串优先;传数组仅用于兼容旧调用,验签会退化为本地重编码
* @return array<string,mixed>
*/
private function decryptNotification(array $headers, array|string $body): array
{
// 微信支付 APIv3 签名串要求使用 HTTP 原始 body不能先 json_decode 再重新编码,否则字段顺序或转义差异会导致验签失败。
$rawBody = is_string($body) ? $body : $this->encodeJsonBody($body);
$this->assertNotificationSignature($headers, $rawBody);
$payload = is_string($body) ? json_decode($body, true) : $body;
if (!is_array($payload)) {
throw new WechatException('微信支付回调 JSON 无效');
}
$resource = $payload['resource'] ?? null;
if (!is_array($resource)) {
throw new WechatException('微信支付回调 resource 无效');
}
return PaymentCrypto::decryptResource($this->config->apiV3Key, $resource);
}
/**
* 生成微信支付 APIv3 `WECHATPAY2-SHA256-RSA2048` Authorization 请求头。
*/
@ -184,6 +194,8 @@ final class PaymentClient
/**
* 校验微信支付通知头中的平台证书/公钥序列号与 RSA-SHA256 签名。
*
* @param array<string,mixed> $headers
*/
private function assertNotificationSignature(array $headers, string $body): void
{
@ -235,6 +247,83 @@ final class PaymentClient
return array_merge($query, is_array($options['query'] ?? null) ? $options['query'] : []);
}
/**
* 解析微信支付 APIv3 请求体,确保参与签名的 body 与实际发送的 body 完全一致。
*
* @param array<string,mixed> $payload
* @param array<string,mixed> $options
*/
private function resolveRequestBody(array $payload, array &$options): string
{
if (array_key_exists('body', $options)) {
unset($options['json']);
return (string)$options['body'];
}
if (array_key_exists('json', $options)) {
$body = $this->encodeJsonBody($options['json']);
unset($options['json']);
return $body;
}
return $payload === [] ? '' : $this->encodeJsonBody($payload);
}
/**
* 编码微信支付 APIv3 JSON 请求体。
*/
private function encodeJsonBody(mixed $payload): string
{
try {
return json_encode($payload, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
} catch (\JsonException $e) {
throw new WechatException('微信支付请求 JSON 编码失败', 0, $e);
}
}
/**
* 构造通用调用的请求 payloadGET 默认无 body可通过 options.payload 显式传入。
*
* @param array<string,mixed> $params
* @param array<string,mixed> $options
* @return array<string,mixed>
*/
private function paymentCallPayload(string $method, array $params, array $options): array
{
return $method === 'GET' ? (is_array($options['payload'] ?? null) ? $options['payload'] : []) : $params;
}
/**
* 构造通用调用的 queryGET 使用 params其他方法使用 options.query。
*
* @param array<string,mixed> $params
* @param array<string,mixed> $options
* @return array<string,mixed>
*/
private function paymentCallQuery(string $method, array $params, array $options): array
{
$query = $method === 'GET' ? $params : [];
if (is_array($options['query'] ?? null)) {
$query = array_merge($query, $options['query']);
}
return $query;
}
/**
* 移除 SDK 内部控制项,其余 Guzzle options 继续透传到底层 HTTP 客户端。
*
* @param array<string,mixed> $options
* @return array<string,mixed>
*/
private function paymentCallOptions(array $options): array
{
unset($options['payload'], $options['query'], $options['raw_body']);
return $options;
}
/**
* 构造签名所需的 query string。
*
@ -257,7 +346,6 @@ final class PaymentClient
$headers['Accept'] = $headers['Accept'] ?? 'application/json';
$headers['Content-Type'] = $headers['Content-Type'] ?? 'application/json';
$headers['Authorization'] = $authorization;
$headers['Wechatpay-Serial'] = $this->config->merchantSerial;
return $headers;
}

View File

@ -1,9 +1,11 @@
<?php
declare(strict_types=1);
/**
* JSON HTTP 客户端封装。
* This file is part of HyperfAdmin.
*
* @Link https://thinkadmin.top
* @Author Anyon<zoujingli@qq.com>
*/
namespace We\Support;
@ -38,15 +40,19 @@ final class JsonClient
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', (int)$response->getStatusCode(), null, ['body' => $body]);
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;
}
@ -72,6 +78,7 @@ final class JsonClient
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) {

View File

@ -1,9 +1,11 @@
<?php
declare(strict_types=1);
/**
* 支付宝开放平台网关调用与验签测试。
* This file is part of HyperfAdmin.
*
* @Link https://thinkadmin.top
* @Author Anyon<zoujingli@qq.com>
*/
namespace We\Tests;
@ -21,6 +23,7 @@ use We\Platform\Alipay\PlatformClient as AlipayPlatformClient;
/**
* 支付宝开放平台网关调用与验签测试用例。
* @internal
*/
#[CoversClass(AlipayPlatformClient::class)]
final class AlipayPlatformClientTest extends TestCase
@ -118,6 +121,7 @@ final class AlipayFakeHttpClient implements ClientInterface
/**
* 实现测试 HTTP 客户端请求接口或记录请求。
* @param mixed $uri
*/
public function request(string $method, $uri = '', array $options = []): ResponseInterface
{
@ -126,6 +130,7 @@ final class AlipayFakeHttpClient implements ClientInterface
/**
* 实现测试 HTTP 客户端异步请求接口。
* @param mixed $uri
*/
public function requestAsync(string $method, $uri = '', array $options = []): PromiseInterface
{

View File

@ -1,20 +1,29 @@
<?php
declare(strict_types=1);
/**
* JSON HTTP 客户端安全约束测试。
* This file is part of HyperfAdmin.
*
* @Link https://thinkadmin.top
* @Author Anyon<zoujingli@qq.com>
*/
namespace We\Tests;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Promise\Create;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Psr7\Response;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use We\Exception\ApiException;
use We\Support\JsonClient;
/**
* JSON HTTP 客户端安全约束测试用例。
* @internal
*/
#[CoversClass(JsonClient::class)]
final class JsonClientTest extends TestCase
@ -44,4 +53,81 @@ final class JsonClientTest extends TestCase
$client->send('GET', '//example.com/evil');
}
/**
* 测试默认关闭 Guzzle http_errors并解析非 2xx JSON 错误响应。
*/
public function testRequestDisablesHttpErrorsAndParsesErrorPayload(): void
{
$http = new JsonClientFakeHttpClient(new Response(400, [], '{"errcode":40001,"errmsg":"invalid credential"}'));
$client = new JsonClient($http);
try {
$client->request('GET', 'cgi-bin/token');
self::fail('Expected ApiException was not thrown.');
} catch (ApiException $exception) {
self::assertSame(40001, $exception->getCode());
self::assertSame('invalid credential', $exception->getMessage());
self::assertSame(false, $http->requests[0]['options']['http_errors']);
self::assertSame(40001, $exception->context()['errcode']);
}
}
}
/**
* JSON 客户端测试用 HTTP 客户端。
*/
final class JsonClientFakeHttpClient implements ClientInterface
{
/** @var array<int,array{method:string,uri:mixed,options:array<string,mixed>}> */
public array $requests = [];
/**
* 创建固定响应测试客户端。
*/
public function __construct(private readonly ResponseInterface $response) {}
/**
* 实现测试 HTTP 客户端同步发送接口。
*/
public function send(RequestInterface $request, array $options = []): ResponseInterface
{
return $this->response;
}
/**
* 实现测试 HTTP 客户端异步发送接口。
*/
public function sendAsync(RequestInterface $request, array $options = []): PromiseInterface
{
return Create::rejectionFor(new \RuntimeException('sendAsync is not used in this test'));
}
/**
* 实现测试 HTTP 客户端请求接口并记录请求。
* @param mixed $uri
*/
public function request(string $method, $uri = '', array $options = []): ResponseInterface
{
$this->requests[] = ['method' => $method, 'uri' => $uri, 'options' => $options];
return $this->response;
}
/**
* 实现测试 HTTP 客户端异步请求接口。
* @param mixed $uri
*/
public function requestAsync(string $method, $uri = '', array $options = []): PromiseInterface
{
return Create::rejectionFor(new \RuntimeException('requestAsync is not used in this test'));
}
/**
* 返回测试 HTTP 客户端配置。
*/
public function getConfig(?string $option = null): mixed
{
return null;
}
}

View File

@ -1,9 +1,11 @@
<?php
declare(strict_types=1);
/**
* 微信支付 APIv3 通知验签与解密测试。
* This file is part of HyperfAdmin.
*
* @Link https://thinkadmin.top
* @Author Anyon<zoujingli@qq.com>
*/
namespace We\Tests;
@ -12,11 +14,13 @@ use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
use We\Config\WechatPaymentConfig;
use We\Exception\SignatureException;
use We\Exception\WechatException;
use We\Platform\Wechat\PaymentClient as WechatPaymentClient;
use We\Support\Signature;
/**
* 微信支付 APIv3 通知验签与解密测试用例。
* @internal
*/
#[CoversClass(WechatPaymentClient::class)]
final class PaymentClientTest extends TestCase
@ -48,7 +52,7 @@ final class PaymentClientTest extends TestCase
'mch_id',
$apiV3Key,
'merchant-serial',
'merchant-private-key',
TestKeys::privateKey(),
'',
$platformPublicKey,
'platform-serial',
@ -83,7 +87,7 @@ final class PaymentClientTest extends TestCase
'mch_id',
str_repeat('k', 32),
'merchant-serial',
'merchant-private-key',
TestKeys::privateKey(),
'',
$platformPublicKey,
'platform-serial',
@ -98,6 +102,41 @@ final class PaymentClientTest extends TestCase
]);
}
/**
* 测试微信支付回调 resource 结构异常时抛出 SDK 异常而不是 TypeError。
*/
public function testDecryptNotificationRejectsInvalidResourceShape(): void
{
[$platformPrivateKey, $platformPublicKey] = self::keyPair();
$rawBody = '{"resource":"invalid"}';
$timestamp = '1777600000';
$notifyNonce = 'notify-nonce';
$headers = [
'Wechatpay-Timestamp' => $timestamp,
'Wechatpay-Nonce' => $notifyNonce,
'Wechatpay-Serial' => 'platform-serial',
'Wechatpay-Signature' => Signature::paymentV3Sign($platformPrivateKey, "{$timestamp}\n{$notifyNonce}\n{$rawBody}\n"),
];
$client = new WechatPaymentClient(new WechatPaymentConfig(
'wx_app',
'mch_id',
str_repeat('k', 32),
'merchant-serial',
TestKeys::privateKey(),
'',
$platformPublicKey,
'platform-serial',
));
$this->expectException(WechatException::class);
$this->expectExceptionMessage('resource');
$client->post('decrypt_notification', [], [
'headers' => $headers,
'raw_body' => $rawBody,
]);
}
/**
* 生成测试使用的 RSA 密钥对。
*

View File

@ -1,9 +1,11 @@
<?php
declare(strict_types=1);
/**
* 协议层原始响应、下载和上传能力测试。
* This file is part of HyperfAdmin.
*
* @Link https://thinkadmin.top
* @Author Anyon<zoujingli@qq.com>
*/
namespace We\Tests;
@ -22,6 +24,7 @@ use We\Config\WechatPlatformConfig;
use We\Config\WechatServiceConfig;
use We\Contract\StoreCacheInterface;
use We\Contract\StoreTokenInterface;
use We\Exception\ApiException;
use We\Platform\Wechat\PaymentClient as WechatPaymentClient;
use We\Platform\Wechat\PlatformClient as WechatPlatformClient;
use We\Platform\Wechat\ServiceClient as WechatServiceClient;
@ -31,6 +34,7 @@ use We\Support\TokenCacheKey;
/**
* 协议层原始响应、下载和上传能力测试用例。
* @internal
*/
#[CoversClass(JsonClient::class)]
#[CoversClass(WechatPlatformClient::class)]
@ -118,10 +122,67 @@ final class ProtocolClientTest extends TestCase
$this->assertSame('BILL-DATA', (string)$response->getBody());
$headers = $http->requests[0]['options']['headers'];
$this->assertStringStartsWith('WECHATPAY2-SHA256-RSA2048 ', (string)$headers['Authorization']);
$this->assertSame('merchant-serial', $headers['Wechatpay-Serial']);
$this->assertArrayNotHasKey('Wechatpay-Serial', $headers);
$this->assertSame('2026-05-08', $http->requests[0]['options']['query']['bill_date']);
}
/**
* 测试微信支付通用调用会透传 Guzzle options并用 json option 生成参与签名的请求体。
*/
public function testWechatPaymentCallPassesGuzzleOptions(): void
{
[$merchantPrivateKey] = self::keyPair();
$http = new ProtocolHttpClient([new Response(200, [], '{"ok":true}')]);
$payment = new WechatPaymentClient(new WechatPaymentConfig(
'wx_app',
'mch_id',
str_repeat('k', 32),
'merchant-serial',
$merchantPrivateKey,
), $http);
$data = $payment->post('v3/custom/request', ['ignored' => 'payload'], [
'query' => ['debug' => '1'],
'headers' => ['X-Request-Id' => 'RID-20260508', 'Wechatpay-Serial' => 'platform-serial'],
'timeout' => 5.0,
'json' => ['custom' => 'body'],
]);
$this->assertTrue($data['ok']);
$this->assertSame('1', $http->requests[0]['options']['query']['debug']);
$this->assertSame('RID-20260508', $http->requests[0]['options']['headers']['X-Request-Id']);
$this->assertSame('platform-serial', $http->requests[0]['options']['headers']['Wechatpay-Serial']);
$this->assertSame(5.0, $http->requests[0]['options']['timeout']);
$this->assertSame('{"custom":"body"}', $http->requests[0]['options']['body']);
$this->assertArrayNotHasKey('json', $http->requests[0]['options']);
}
/**
* 测试微信支付非 2xx JSON 错误响应会被解析并转换为 ApiException。
*/
public function testWechatPaymentRequestThrowsOnHttpErrorPayload(): void
{
[$merchantPrivateKey] = self::keyPair();
$http = new ProtocolHttpClient([new Response(400, [], '{"code":"PARAM_ERROR","message":"参数错误"}')]);
$payment = new WechatPaymentClient(new WechatPaymentConfig(
'wx_app',
'mch_id',
str_repeat('k', 32),
'merchant-serial',
$merchantPrivateKey,
), $http);
try {
$payment->post('v3/pay/transactions/jsapi', ['appid' => 'wx_app']);
self::fail('Expected ApiException was not thrown.');
} catch (ApiException $exception) {
self::assertSame(400, $exception->getCode());
self::assertSame('参数错误', $exception->getMessage());
self::assertSame('PARAM_ERROR', $exception->context()['code']);
self::assertSame(false, $http->requests[0]['options']['http_errors']);
}
}
/**
* 测试微信服务平台代授权方 GET 调用会把 params 作为 query 并附加授权方 access_token。
*/
@ -135,7 +196,7 @@ final class ProtocolClientTest extends TestCase
), 'authorizer-token', 3600);
$http = new ProtocolHttpClient([new Response(200, [], '{"ok":true}')]);
$service = (new Client(cache: $cache, authorizers: new ProtocolAuthorizerTokenStore(), http: $http))
->wechatService(new WechatServiceConfig('component_app', 'component_secret', 'component_token', 'encoding_key'));
->wechatService(new WechatServiceConfig('component_app', 'component_secret', 'componentToken123', TestKeys::encodingAesKey()));
$data = $service->get('cgi-bin/user/get', ['next_openid' => 'NEXT'], [
'authorizer_appid' => 'authorizer_app',
@ -197,6 +258,7 @@ final class ProtocolHttpClient implements ClientInterface
/**
* 实现测试 HTTP 客户端请求接口并记录请求。
* @param mixed $uri
*/
public function request(string $method, $uri = '', array $options = []): ResponseInterface
{
@ -207,6 +269,7 @@ final class ProtocolHttpClient implements ClientInterface
/**
* 实现测试 HTTP 客户端异步请求接口。
* @param mixed $uri
*/
public function requestAsync(string $method, $uri = '', array $options = []): PromiseInterface
{