refactor(wechat): 迁移并重命名微信协议 Trait

- 将 InteractsProtocol 移动到 src/Contract/Trait,并更名为 WechatInteractsProtocol。

- 更新公众平台、小程序、第三方平台客户端的 Trait 引用和通用调用入口。

- 保留 access_token 缓存、raw/download/upload、消息加解密伪路径等协议层能力。

- 补充客户端工厂与 token 缓存相关测试夹具,确保迁移后调用路径保持兼容。
This commit is contained in:
Anyon 2026-05-08 11:32:46 +08:00
parent 278abbf3b8
commit 80af84bcb9
8 changed files with 233 additions and 98 deletions

View File

@ -1,16 +1,16 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
/** /**
* SDK 根入口与通道客户端工厂。 * This file is part of HyperfAdmin.
*
* @Link https://thinkadmin.top
* @Author Anyon<zoujingli@qq.com>
*/ */
namespace We; namespace We;
use GuzzleHttp\ClientInterface; use GuzzleHttp\ClientInterface;
use ReflectionClass;
use ReflectionException;
use We\Config\AlipayPaymentConfig; use We\Config\AlipayPaymentConfig;
use We\Config\AlipayPlatformConfig; use We\Config\AlipayPlatformConfig;
use We\Config\WechatPaymentConfig; use We\Config\WechatPaymentConfig;
@ -27,20 +27,21 @@ use We\Platform\Wechat\PaymentClient as WechatPaymentClient;
use We\Platform\Wechat\PlatformClient as WechatPlatformClient; use We\Platform\Wechat\PlatformClient as WechatPlatformClient;
use We\Platform\Wechat\ServiceClient as WechatServiceClient; use We\Platform\Wechat\ServiceClient as WechatServiceClient;
use We\Platform\Wechat\WxappClient as WechatWxappClient; use We\Platform\Wechat\WxappClient as WechatWxappClient;
use We\Support\CacheKey;
use We\Support\FileCacheStore; use We\Support\FileCacheStore;
/** /**
* SDK 根入口:按平台与业务域创建微信、支付宝客户端,工厂方法命名与配置对象语义保持一致。 * SDK 根入口:按平台与业务域创建微信、支付宝客户端,工厂方法命名与配置对象语义保持一致。
* *
* access_token 等运行态数据的缓存键固定为 `{cacheKeyPrefix}:{platformChannel}:{logicalKey}`,见 {@see \We\Support\CacheKey::compose} * access_token 等运行态数据的缓存键固定为 `{cacheKeyPrefix}:{platformChannel}:{logicalKey}`,见 {@see CacheKey::compose}
* 未注入缓存实现时使用 {@see FileCacheStore},默认目录为 {@see self::defaultCacheStoreDirectory()} * 未注入缓存实现时使用 {@see FileCacheStore},默认目录为 {@see self::defaultCacheStoreDirectory()}
* *
* @method WechatPlatformClient wechatPlatform(WechatPlatformConfig $config) * @method WechatPlatformClient wechatPlatform(WechatPlatformConfig $config)
* @method WechatWxappClient wechatWxapp(WechatWxappConfig $config) * @method WechatWxappClient wechatWxapp(WechatWxappConfig $config)
* @method WechatServiceClient wechatService(WechatServiceConfig $config) * @method WechatServiceClient wechatService(WechatServiceConfig $config)
* @method WechatPaymentClient wechatPayment(WechatPaymentConfig $config) * @method WechatPaymentClient wechatPayment(WechatPaymentConfig $config)
* @method AlipayPlatformClient alipayPlatform(AlipayPlatformConfig $config) * @method AlipayPlatformClient alipayPlatform(AlipayPlatformConfig $config)
* @method AlipayPaymentClient alipayPayment(AlipayPaymentConfig $config) * @method AlipayPaymentClient alipayPayment(AlipayPaymentConfig $config)
*/ */
final class Client final class Client
{ {
@ -78,30 +79,6 @@ final class Client
$this->http = $http; $this->http = $http;
} }
/** 默认缓存落盘目录:`sys_get_temp_dir()` + 包级子目录名,供未显式传入 `cache` 时构造 {@see FileCacheStore}。 */
public static function defaultCacheStoreDirectory(): string
{
return rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR . '/') . DIRECTORY_SEPARATOR . self::DEFAULT_CACHE_STORE_DIR_NAME;
}
/**
* 按字符串通道创建客户端;通道名称与配置对象一一对应。
*/
public function get(string $channel, ConfigInterface $config): object
{
[$factory, $expected] = match ($channel) {
'wechat.platform' => ['wechatPlatform', WechatPlatformConfig::class],
'wechat.wxapp' => ['wechatWxapp', WechatWxappConfig::class],
'wechat.service' => ['wechatService', WechatServiceConfig::class],
'wechat.payment' => ['wechatPayment', WechatPaymentConfig::class],
'alipay.platform' => ['alipayPlatform', AlipayPlatformConfig::class],
'alipay.payment' => ['alipayPayment', AlipayPaymentConfig::class],
default => throw new WechatException('不支持的通道标识: ' . $channel),
};
return $this->__call($factory, [$this->ensureConfig($config, $expected, $factory)]);
}
/** /**
* 魔术工厂:只暴露平台前缀明确的客户端创建方法。 * 魔术工厂:只暴露平台前缀明确的客户端创建方法。
* *
@ -150,6 +127,30 @@ final class Client
}; };
} }
/** 默认缓存落盘目录:`sys_get_temp_dir()` + 包级子目录名,供未显式传入 `cache` 时构造 {@see FileCacheStore}。 */
public static function defaultCacheStoreDirectory(): string
{
return rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR . '/') . DIRECTORY_SEPARATOR . self::DEFAULT_CACHE_STORE_DIR_NAME;
}
/**
* 按字符串通道创建客户端;通道名称与配置对象一一对应。
*/
public function get(string $channel, ConfigInterface $config): object
{
[$factory, $expected] = match ($channel) {
'wechat.platform' => ['wechatPlatform', WechatPlatformConfig::class],
'wechat.wxapp' => ['wechatWxapp', WechatWxappConfig::class],
'wechat.service' => ['wechatService', WechatServiceConfig::class],
'wechat.payment' => ['wechatPayment', WechatPaymentConfig::class],
'alipay.platform' => ['alipayPlatform', AlipayPlatformConfig::class],
'alipay.payment' => ['alipayPayment', AlipayPaymentConfig::class],
default => throw new WechatException('不支持的通道标识: ' . $channel),
};
return $this->__call($factory, [$this->ensureConfig($config, $expected, $factory)]);
}
/** /**
* 使用反射创建具体平台客户端实例。 * 使用反射创建具体平台客户端实例。
* *
@ -159,8 +160,8 @@ final class Client
private function instantiate(string $class, array $args): object private function instantiate(string $class, array $args): object
{ {
try { try {
return (new ReflectionClass($class))->newInstanceArgs($args); return (new \ReflectionClass($class))->newInstanceArgs($args);
} catch (ReflectionException $e) { } catch (\ReflectionException $e) {
throw new WechatException('通道客户端实例化失败: ' . $e->getMessage(), 0, $e); throw new WechatException('通道客户端实例化失败: ' . $e->getMessage(), 0, $e);
} }
} }

View File

@ -1,21 +1,24 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
/** /**
* 微信协议层客户端通用能力。 * This file is part of HyperfAdmin.
*
* @Link https://thinkadmin.top
* @Author Anyon<zoujingli@qq.com>
*/ */
namespace We\Platform\Wechat\Concerns; namespace We\Contract\Trait;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use We\Exception\ApiException;
/** /**
* 微信协议层客户端通用能力。 * 微信协议层客户端通用能力。
* *
* 复用 client_credential access_token 缓存、原始响应、下载、上传和 JSON 请求选项构造逻辑。 * 复用 client_credential access_token 缓存、原始响应、下载、上传和 JSON 请求选项构造逻辑。
*/ */
trait InteractsProtocol trait WechatInteractsProtocol
{ {
/** /**
* 获取并缓存 client_credential access_token。 * 获取并缓存 client_credential access_token。
@ -36,13 +39,43 @@ trait InteractsProtocol
'appid' => $appid, 'appid' => $appid,
'secret' => $appSecret, 'secret' => $appSecret,
]); ]);
$token = (string)($data['access_token'] ?? ''); $token = $this->wechatTokenValue($data, 'access_token', '微信 access_token');
$this->cache->set($key, $token, max(1, (int)($data['expires_in'] ?? 7200) - 300)); $this->cache->set($key, $token, $this->wechatTokenTtl($data, '微信 access_token'));
return $token; return $token;
}); });
} }
/**
* 从微信 Token 响应中提取非空字符串 Token。
*
* @param array<string,mixed> $data
*/
private function wechatTokenValue(array $data, string $field, string $label): string
{
$token = $data[$field] ?? null;
if (!is_string($token) || trim($token) === '') {
throw new ApiException($label . ' 响应缺少 ' . $field, 0, null, $data);
}
return $token;
}
/**
* 从微信 Token 响应中计算缓存 TTL。
*
* @param array<string,mixed> $data
*/
private function wechatTokenTtl(array $data, string $label): int
{
$expiresIn = $data['expires_in'] ?? 7200;
if (!is_numeric($expiresIn) || (int)$expiresIn <= 0) {
throw new ApiException($label . ' 响应 expires_in 无效', 0, null, $data);
}
return max(1, (int)$expiresIn - 300);
}
/** /**
* 按需在 query 中附加 access_token。 * 按需在 query 中附加 access_token。
* *
@ -52,6 +85,9 @@ trait InteractsProtocol
private function withAccessToken(array $query, bool $withToken): array private function withAccessToken(array $query, bool $withToken): array
{ {
if ($withToken) { if ($withToken) {
/**
* @phpstan-ignore-next-line 宿主为公众平台或小程序客户端时提供 accessToken()
*/
$query['access_token'] = $query['access_token'] ?? $this->accessToken(); $query['access_token'] = $query['access_token'] ?? $this->accessToken();
} }
@ -118,6 +154,9 @@ trait InteractsProtocol
private function handleWechatMessageCryptoCall(string $uri, array $params): ?array private function handleWechatMessageCryptoCall(string $uri, array $params): ?array
{ {
if ($uri === 'decrypt_message') { if ($uri === 'decrypt_message') {
/**
* @phpstan-ignore-next-line 只有提供 messageCrypto() 的宿主会调用消息加解密伪路径。
*/
return $this->messageCrypto()->decryptMessage( return $this->messageCrypto()->decryptMessage(
(string)($params['body'] ?? ''), (string)($params['body'] ?? ''),
(string)($params['msg_signature'] ?? ''), (string)($params['msg_signature'] ?? ''),
@ -126,12 +165,17 @@ trait InteractsProtocol
); );
} }
if ($uri === 'encrypt_message') { if ($uri === 'encrypt_message') {
/**
* @phpstan-ignore-next-line 只有提供 messageCrypto() 的宿主会调用消息加解密伪路径。
*/
$xml = $this->messageCrypto()->encryptMessage(
(string)($params['body'] ?? ''),
(string)($params['timestamp'] ?? time()),
(string)($params['nonce'] ?? ''),
);
return [ return [
'xml' => $this->messageCrypto()->encryptMessage( 'xml' => $xml,
(string)($params['body'] ?? ''),
(string)($params['timestamp'] ?? time()),
(string)($params['nonce'] ?? ''),
),
]; ];
} }
@ -161,6 +205,7 @@ trait InteractsProtocol
$query = $this->wechatCallQuery($method, $params, $options); $query = $this->wechatCallQuery($method, $params, $options);
$requestOptions = $this->buildWechatJsonOptions($method, $params, $options, $internalKeys); $requestOptions = $this->buildWechatJsonOptions($method, $params, $options, $internalKeys);
if ($tokenAware) { if ($tokenAware) {
/** @phpstan-ignore-next-line tokenAware 仅由支持 withToken 参数的公众平台和小程序客户端启用。 */
return $this->request($method, $uri, $query, $requestOptions, (bool)($options['with_token'] ?? true)); return $this->request($method, $uri, $query, $requestOptions, (bool)($options['with_token'] ?? true));
} }

View File

@ -1,9 +1,11 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
/** /**
* 微信公众平台客户端。 * This file is part of HyperfAdmin.
*
* @Link https://thinkadmin.top
* @Author Anyon<zoujingli@qq.com>
*/ */
namespace We\Platform\Wechat; namespace We\Platform\Wechat;
@ -13,7 +15,7 @@ use Psr\Http\Message\ResponseInterface;
use We\Client; use We\Client;
use We\Config\WechatPlatformConfig; use We\Config\WechatPlatformConfig;
use We\Contract\StoreCacheInterface; use We\Contract\StoreCacheInterface;
use We\Platform\Wechat\Concerns\InteractsProtocol; use We\Contract\Trait\WechatInteractsProtocol;
use We\Support\CacheKey; use We\Support\CacheKey;
use We\Support\JsonClient; use We\Support\JsonClient;
use We\Support\MessageCrypto; use We\Support\MessageCrypto;
@ -27,9 +29,9 @@ use We\Support\TokenCacheKey;
*/ */
final class PlatformClient final class PlatformClient
{ {
use InteractsProtocol; use WechatInteractsProtocol;
/** 与 {@see \We\Client::get} 通道标识一致,用于 Token 键平台段 */ /** 与 {@see Client::get} 通道标识一致,用于 Token 键平台段 */
private const TOKEN_PLATFORM_CHANNEL = 'wechat.platform'; private const TOKEN_PLATFORM_CHANNEL = 'wechat.platform';
private const API = 'https://api.weixin.qq.com/'; private const API = 'https://api.weixin.qq.com/';
@ -110,14 +112,6 @@ final class PlatformClient
return $this->uploadWechatResource($uri, $multipart, $this->withAccessToken($query, $withToken), $options); return $this->uploadWechatResource($uri, $multipart, $this->withAccessToken($query, $withToken), $options);
} }
/**
* 创建微信公众平台消息安全模式加解密工具。
*/
private function messageCrypto(): MessageCrypto
{
return new MessageCrypto($this->config->token, $this->config->encodingAesKey, $this->config->appid);
}
/** /**
* 通用 API 调用入口:按官方接口 path 与参数发起请求。 * 通用 API 调用入口:按官方接口 path 与参数发起请求。
* *
@ -166,6 +160,14 @@ final class PlatformClient
return $this->call($uriOrPath, $params, 'GET', $options); return $this->call($uriOrPath, $params, 'GET', $options);
} }
/**
* 创建微信公众平台消息安全模式加解密工具。
*/
private function messageCrypto(): MessageCrypto
{
return new MessageCrypto($this->config->token, $this->config->encodingAesKey, $this->config->appid);
}
/** /**
* 生成微信网页授权地址open.weixin.qq.com/connect/oauth2/authorize * 生成微信网页授权地址open.weixin.qq.com/connect/oauth2/authorize
* *

View File

@ -1,9 +1,11 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
/** /**
* 微信服务平台(第三方平台)客户端。 * This file is part of HyperfAdmin.
*
* @Link https://thinkadmin.top
* @Author Anyon<zoujingli@qq.com>
*/ */
namespace We\Platform\Wechat; namespace We\Platform\Wechat;
@ -14,8 +16,8 @@ use We\Client;
use We\Config\WechatServiceConfig; use We\Config\WechatServiceConfig;
use We\Contract\StoreCacheInterface; use We\Contract\StoreCacheInterface;
use We\Contract\StoreTokenInterface; use We\Contract\StoreTokenInterface;
use We\Contract\Trait\WechatInteractsProtocol;
use We\Exception\WechatException; use We\Exception\WechatException;
use We\Platform\Wechat\Concerns\InteractsProtocol;
use We\Support\CacheKey; use We\Support\CacheKey;
use We\Support\JsonClient; use We\Support\JsonClient;
use We\Support\MessageCrypto; use We\Support\MessageCrypto;
@ -29,12 +31,15 @@ use We\Support\TokenCacheKey;
*/ */
final class ServiceClient final class ServiceClient
{ {
use InteractsProtocol; use WechatInteractsProtocol;
/** 与 {@see \We\Client::get} 通道标识一致 */ /** 与 {@see Client::get} 通道标识一致 */
private const TOKEN_PLATFORM_CHANNEL = 'wechat.service'; private const TOKEN_PLATFORM_CHANNEL = 'wechat.service';
private JsonClient $http; private JsonClient $http;
private readonly ?StoreTokenInterface $authorizers;
/** /**
* 创建微信服务平台(第三方平台)客户端并初始化官方 API HTTP 客户端。 * 创建微信服务平台(第三方平台)客户端并初始化官方 API HTTP 客户端。
*/ */
@ -49,8 +54,6 @@ final class ServiceClient
$this->authorizers = $authorizers; $this->authorizers = $authorizers;
} }
private readonly ?StoreTokenInterface $authorizers;
/** /**
* 获取第三方平台接口调用凭据 component_access_token缓存未命中或强制刷新时调用官方 component_token 接口。 * 获取第三方平台接口调用凭据 component_access_token缓存未命中或强制刷新时调用官方 component_token 接口。
*/ */
@ -72,8 +75,8 @@ final class ServiceClient
'component_verify_ticket' => $componentVerifyTicket, 'component_verify_ticket' => $componentVerifyTicket,
], ],
]); ]);
$token = (string)$data['component_access_token']; $token = $this->wechatTokenValue($data, 'component_access_token', '微信 component_access_token');
$this->cache->set($key, $token, max(1, (int)($data['expires_in'] ?? 7200) - 300)); $this->cache->set($key, $token, $this->wechatTokenTtl($data, '微信 component_access_token'));
return $token; return $token;
}); });
@ -193,14 +196,6 @@ final class ServiceClient
return $this->jsonWechatRequest($method, $uri, $query, $options); return $this->jsonWechatRequest($method, $uri, $query, $options);
} }
/**
* 创建第三方平台授权事件消息加解密工具。
*/
private function messageCrypto(): MessageCrypto
{
return new MessageCrypto($this->config->componentToken, $this->config->componentEncodingAesKey, $this->config->componentAppid);
}
/** /**
* 通用 API 调用入口:按官方接口 path、授权方上下文和参数发起请求。 * 通用 API 调用入口:按官方接口 path、授权方上下文和参数发起请求。
* *
@ -254,12 +249,21 @@ final class ServiceClient
return $this->call($uriOrPath, $params, 'GET', $options); return $this->call($uriOrPath, $params, 'GET', $options);
} }
/**
* 创建第三方平台授权事件消息加解密工具。
*/
private function messageCrypto(): MessageCrypto
{
return new MessageCrypto($this->config->componentToken, $this->config->componentEncodingAesKey, $this->config->componentAppid);
}
/** /**
* 获取授权方接口调用凭据 authorizer_access_token缓存未命中时使用 authorizer_refresh_token 刷新。 * 获取授权方接口调用凭据 authorizer_access_token缓存未命中时使用 authorizer_refresh_token 刷新。
*/ */
private function authorizerAccessToken(string $componentAccessToken, string $authorizerAppid): string private function authorizerAccessToken(string $componentAccessToken, string $authorizerAppid): string
{ {
if (!$this->authorizers) { $authorizers = $this->authorizers;
if (!$authorizers) {
throw new WechatException('未配置授权账号 Token 仓库'); throw new WechatException('未配置授权账号 Token 仓库');
} }
$key = $this->cacheKey(TokenCacheKey::wechatServiceAuthorizerAccessToken( $key = $this->cacheKey(TokenCacheKey::wechatServiceAuthorizerAccessToken(
@ -271,14 +275,18 @@ final class ServiceClient
return $token; return $token;
} }
return $this->cache->lock('lock:' . $key, 30, function () use ($key, $componentAccessToken, $authorizerAppid): string { return $this->cache->lock('lock:' . $key, 30, function () use ($key, $componentAccessToken, $authorizerAppid, $authorizers): string {
if (is_string($token = $this->cache->get($key, '')) && $token !== '') { if (is_string($token = $this->cache->get($key, '')) && $token !== '') {
return $token; return $token;
} }
$data = $this->refreshAuthorizerToken($componentAccessToken, $authorizerAppid, $this->authorizers->refreshToken($authorizerAppid)); $refreshToken = $authorizers->refreshToken($authorizerAppid);
$this->authorizers->saveAuthorizerToken($authorizerAppid, $data); if (trim($refreshToken) === '') {
$token = (string)$data['authorizer_access_token']; throw new WechatException('授权方 authorizer_refresh_token 不能为空');
$this->cache->set($key, $token, max(1, (int)($data['expires_in'] ?? 7200) - 300)); }
$data = $this->refreshAuthorizerToken($componentAccessToken, $authorizerAppid, $refreshToken);
$authorizers->saveAuthorizerToken($authorizerAppid, $data);
$token = $this->wechatTokenValue($data, 'authorizer_access_token', '微信 authorizer_access_token');
$this->cache->set($key, $token, $this->wechatTokenTtl($data, '微信 authorizer_access_token'));
return $token; return $token;
}); });

View File

@ -1,9 +1,11 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
/** /**
* 微信小程序客户端。 * This file is part of HyperfAdmin.
*
* @Link https://thinkadmin.top
* @Author Anyon<zoujingli@qq.com>
*/ */
namespace We\Platform\Wechat; namespace We\Platform\Wechat;
@ -13,7 +15,7 @@ use Psr\Http\Message\ResponseInterface;
use We\Client; use We\Client;
use We\Config\WechatWxappConfig; use We\Config\WechatWxappConfig;
use We\Contract\StoreCacheInterface; use We\Contract\StoreCacheInterface;
use We\Platform\Wechat\Concerns\InteractsProtocol; use We\Contract\Trait\WechatInteractsProtocol;
use We\Support\CacheKey; use We\Support\CacheKey;
use We\Support\JsonClient; use We\Support\JsonClient;
use We\Support\NullCacheStore; use We\Support\NullCacheStore;
@ -26,10 +28,11 @@ use We\Support\TokenCacheKey;
*/ */
final class WxappClient final class WxappClient
{ {
use InteractsProtocol; use WechatInteractsProtocol;
/** 与 {@see \We\Client::get} 通道标识一致 */ /** 与 {@see Client::get} 通道标识一致 */
private const TOKEN_PLATFORM_CHANNEL = 'wechat.wxapp'; private const TOKEN_PLATFORM_CHANNEL = 'wechat.wxapp';
private JsonClient $http; private JsonClient $http;
/** /**

View File

@ -1,9 +1,11 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
/** /**
* 微信公众平台 access_token 缓存行为测试。 * This file is part of HyperfAdmin.
*
* @Link https://thinkadmin.top
* @Author Anyon<zoujingli@qq.com>
*/ */
namespace We\Tests; namespace We\Tests;
@ -25,6 +27,7 @@ use We\Support\TokenCacheKey;
/** /**
* 微信公众平台 access_token 缓存行为测试用例。 * 微信公众平台 access_token 缓存行为测试用例。
* @internal
*/ */
#[CoversClass(WechatPlatformClient::class)] #[CoversClass(WechatPlatformClient::class)]
final class AccessTokenCacheTest extends TestCase final class AccessTokenCacheTest extends TestCase
@ -75,11 +78,11 @@ final class AccessTokenCacheTest extends TestCase
*/ */
final class ArrayCacheStore implements StoreCacheInterface final class ArrayCacheStore implements StoreCacheInterface
{ {
public int $lockCalls = 0;
/** @var array<string,mixed> */ /** @var array<string,mixed> */
private array $values = []; private array $values = [];
public int $lockCalls = 0;
/** /**
* 读取测试缓存值。 * 读取测试缓存值。
*/ */
@ -147,6 +150,7 @@ final class FakeHttpClient implements ClientInterface
/** /**
* 实现测试 HTTP 客户端请求接口或记录请求。 * 实现测试 HTTP 客户端请求接口或记录请求。
* @param mixed $uri
*/ */
public function request(string $method, $uri = '', array $options = []): ResponseInterface public function request(string $method, $uri = '', array $options = []): ResponseInterface
{ {
@ -157,6 +161,7 @@ final class FakeHttpClient implements ClientInterface
/** /**
* 实现测试 HTTP 客户端异步请求接口。 * 实现测试 HTTP 客户端异步请求接口。
* @param mixed $uri
*/ */
public function requestAsync(string $method, $uri = '', array $options = []): PromiseInterface public function requestAsync(string $method, $uri = '', array $options = []): PromiseInterface
{ {

View File

@ -1,9 +1,11 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
/** /**
* SDK 根入口通道工厂测试。 * This file is part of HyperfAdmin.
*
* @Link https://thinkadmin.top
* @Author Anyon<zoujingli@qq.com>
*/ */
namespace We\Tests; namespace We\Tests;
@ -21,6 +23,7 @@ use We\Platform\Wechat\ServiceClient as WechatServiceClient;
/** /**
* SDK 根入口通道工厂测试用例。 * SDK 根入口通道工厂测试用例。
* @internal
*/ */
#[CoversClass(Client::class)] #[CoversClass(Client::class)]
final class ClientTest extends TestCase final class ClientTest extends TestCase
@ -66,7 +69,7 @@ final class ClientTest extends TestCase
$this->expectException(WechatException::class); $this->expectException(WechatException::class);
$this->expectExceptionMessage('WechatPlatformConfig'); $this->expectExceptionMessage('WechatPlatformConfig');
$client->get('wechat.platform', new WechatServiceConfig('app', 'sec', 'token', 'encoding')); $client->get('wechat.platform', new WechatServiceConfig('app', 'sec', 'token', TestKeys::encodingAesKey()));
} }
/** /**
@ -109,8 +112,8 @@ final class ClientTest extends TestCase
$service = $client->wechatService(new WechatServiceConfig( $service = $client->wechatService(new WechatServiceConfig(
'wx_component', 'wx_component',
'component_secret', 'component_secret',
'component_token', 'componentToken123',
'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG' TestKeys::encodingAesKey()
)); ));
$url = $service->authorizationUrl('preauthcode', 'https://example.com/callback', 3, 'STATE_TEST'); $url = $service->authorizationUrl('preauthcode', 'https://example.com/callback', 3, 'STATE_TEST');
@ -126,7 +129,7 @@ final class ClientTest extends TestCase
public function testAlipayPlatformCallReturnsAuthorizationUrl(): void public function testAlipayPlatformCallReturnsAuthorizationUrl(): void
{ {
$client = new Client(); $client = new Client();
$alipay = $client->alipayPlatform(new AlipayPlatformConfig('202605010001', str_repeat('a', 64))); $alipay = $client->alipayPlatform(new AlipayPlatformConfig('202605010001', TestKeys::privateKey()));
$result = $alipay->get('auth', [ $result = $alipay->get('auth', [
'redirect_uri' => 'https://example.com/alipay/callback', 'redirect_uri' => 'https://example.com/alipay/callback',
'scope' => 'auth_user', 'scope' => 'auth_user',
@ -143,7 +146,7 @@ final class ClientTest extends TestCase
public function testGetCanReturnSpecificChannelClient(): void public function testGetCanReturnSpecificChannelClient(): void
{ {
$client = new Client(); $client = new Client();
$channelClient = $client->get('alipay.platform', new AlipayPlatformConfig('202605010001', str_repeat('a', 64))); $channelClient = $client->get('alipay.platform', new AlipayPlatformConfig('202605010001', TestKeys::privateKey()));
$this->assertInstanceOf(AlipayPlatformClient::class, $channelClient); $this->assertInstanceOf(AlipayPlatformClient::class, $channelClient);
$this->assertNotInstanceOf(WechatServiceClient::class, $channelClient); $this->assertNotInstanceOf(WechatServiceClient::class, $channelClient);

68
tests/TestKeys.php Normal file
View File

@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
/**
* This file is part of HyperfAdmin.
*
* @Link https://thinkadmin.top
* @Author Anyon<zoujingli@qq.com>
*/
namespace We\Tests;
/**
* 测试密钥夹具。
*/
final class TestKeys
{
/** @var null|array{0:string,1:string} */
private static ?array $keyPair = null;
/**
* 返回测试用 EncodingAESKey。
*/
public static function encodingAesKey(): string
{
return 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG';
}
/**
* 返回测试用 RSA 私钥。
*/
public static function privateKey(): string
{
return self::keyPair()[0];
}
/**
* 返回测试用 RSA 公钥。
*/
public static function publicKey(): string
{
return self::keyPair()[1];
}
/**
* 生成并缓存测试使用的 RSA 密钥对。
*
* @return array{0:string,1:string}
*/
public static function keyPair(): array
{
if (self::$keyPair !== null) {
return self::$keyPair;
}
$resource = openssl_pkey_new(['private_key_bits' => 2048, 'private_key_type' => OPENSSL_KEYTYPE_RSA]);
if ($resource === false) {
throw new \RuntimeException('Unable to create test RSA key pair.');
}
openssl_pkey_export($resource, $privateKey);
$details = openssl_pkey_get_details($resource);
if (!is_array($details)) {
throw new \RuntimeException('Unable to read test RSA public key.');
}
return self::$keyPair = [$privateKey, (string)$details['key']];
}
}