mirror of
https://gitee.com/zoujingli/WeChatDeveloper.git
synced 2026-09-04 15:04:32 +08:00
refactor(wechat): 迁移并重命名微信协议 Trait
- 将 InteractsProtocol 移动到 src/Contract/Trait,并更名为 WechatInteractsProtocol。 - 更新公众平台、小程序、第三方平台客户端的 Trait 引用和通用调用入口。 - 保留 access_token 缓存、raw/download/upload、消息加解密伪路径等协议层能力。 - 补充客户端工厂与 token 缓存相关测试夹具,确保迁移后调用路径保持兼容。
This commit is contained in:
parent
278abbf3b8
commit
80af84bcb9
@ -1,16 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SDK 根入口与通道客户端工厂。
|
||||
* This file is part of HyperfAdmin.
|
||||
*
|
||||
* @Link https://thinkadmin.top
|
||||
* @Author Anyon<zoujingli@qq.com>
|
||||
*/
|
||||
|
||||
namespace We;
|
||||
|
||||
use GuzzleHttp\ClientInterface;
|
||||
use ReflectionClass;
|
||||
use ReflectionException;
|
||||
use We\Config\AlipayPaymentConfig;
|
||||
use We\Config\AlipayPlatformConfig;
|
||||
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\ServiceClient as WechatServiceClient;
|
||||
use We\Platform\Wechat\WxappClient as WechatWxappClient;
|
||||
use We\Support\CacheKey;
|
||||
use We\Support\FileCacheStore;
|
||||
|
||||
/**
|
||||
* SDK 根入口:按平台与业务域创建微信、支付宝客户端,工厂方法命名与配置对象语义保持一致。
|
||||
*
|
||||
* access_token 等运行态数据的缓存键固定为 `{cacheKeyPrefix}:{platformChannel}:{logicalKey}`,见 {@see \We\Support\CacheKey::compose}。
|
||||
* access_token 等运行态数据的缓存键固定为 `{cacheKeyPrefix}:{platformChannel}:{logicalKey}`,见 {@see CacheKey::compose}。
|
||||
* 未注入缓存实现时使用 {@see FileCacheStore},默认目录为 {@see self::defaultCacheStoreDirectory()}。
|
||||
*
|
||||
* @method WechatPlatformClient wechatPlatform(WechatPlatformConfig $config)
|
||||
* @method WechatWxappClient wechatWxapp(WechatWxappConfig $config)
|
||||
* @method WechatServiceClient wechatService(WechatServiceConfig $config)
|
||||
* @method WechatPaymentClient wechatPayment(WechatPaymentConfig $config)
|
||||
* @method WechatWxappClient wechatWxapp(WechatWxappConfig $config)
|
||||
* @method WechatServiceClient wechatService(WechatServiceConfig $config)
|
||||
* @method WechatPaymentClient wechatPayment(WechatPaymentConfig $config)
|
||||
* @method AlipayPlatformClient alipayPlatform(AlipayPlatformConfig $config)
|
||||
* @method AlipayPaymentClient alipayPayment(AlipayPaymentConfig $config)
|
||||
* @method AlipayPaymentClient alipayPayment(AlipayPaymentConfig $config)
|
||||
*/
|
||||
final class Client
|
||||
{
|
||||
@ -78,30 +79,6 @@ final class Client
|
||||
$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
|
||||
{
|
||||
try {
|
||||
return (new ReflectionClass($class))->newInstanceArgs($args);
|
||||
} catch (ReflectionException $e) {
|
||||
return (new \ReflectionClass($class))->newInstanceArgs($args);
|
||||
} catch (\ReflectionException $e) {
|
||||
throw new WechatException('通道客户端实例化失败: ' . $e->getMessage(), 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,21 +1,24 @@
|
||||
<?php
|
||||
|
||||
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 We\Exception\ApiException;
|
||||
|
||||
/**
|
||||
* 微信协议层客户端通用能力。
|
||||
*
|
||||
* 复用 client_credential access_token 缓存、原始响应、下载、上传和 JSON 请求选项构造逻辑。
|
||||
*/
|
||||
trait InteractsProtocol
|
||||
trait WechatInteractsProtocol
|
||||
{
|
||||
/**
|
||||
* 获取并缓存 client_credential access_token。
|
||||
@ -36,13 +39,43 @@ trait InteractsProtocol
|
||||
'appid' => $appid,
|
||||
'secret' => $appSecret,
|
||||
]);
|
||||
$token = (string)($data['access_token'] ?? '');
|
||||
$this->cache->set($key, $token, max(1, (int)($data['expires_in'] ?? 7200) - 300));
|
||||
$token = $this->wechatTokenValue($data, 'access_token', '微信 access_token');
|
||||
$this->cache->set($key, $token, $this->wechatTokenTtl($data, '微信 access_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。
|
||||
*
|
||||
@ -52,6 +85,9 @@ trait InteractsProtocol
|
||||
private function withAccessToken(array $query, bool $withToken): array
|
||||
{
|
||||
if ($withToken) {
|
||||
/**
|
||||
* @phpstan-ignore-next-line 宿主为公众平台或小程序客户端时提供 accessToken()。
|
||||
*/
|
||||
$query['access_token'] = $query['access_token'] ?? $this->accessToken();
|
||||
}
|
||||
|
||||
@ -118,6 +154,9 @@ trait InteractsProtocol
|
||||
private function handleWechatMessageCryptoCall(string $uri, array $params): ?array
|
||||
{
|
||||
if ($uri === 'decrypt_message') {
|
||||
/**
|
||||
* @phpstan-ignore-next-line 只有提供 messageCrypto() 的宿主会调用消息加解密伪路径。
|
||||
*/
|
||||
return $this->messageCrypto()->decryptMessage(
|
||||
(string)($params['body'] ?? ''),
|
||||
(string)($params['msg_signature'] ?? ''),
|
||||
@ -126,12 +165,17 @@ trait InteractsProtocol
|
||||
);
|
||||
}
|
||||
if ($uri === 'encrypt_message') {
|
||||
/**
|
||||
* @phpstan-ignore-next-line 只有提供 messageCrypto() 的宿主会调用消息加解密伪路径。
|
||||
*/
|
||||
$xml = $this->messageCrypto()->encryptMessage(
|
||||
(string)($params['body'] ?? ''),
|
||||
(string)($params['timestamp'] ?? time()),
|
||||
(string)($params['nonce'] ?? ''),
|
||||
);
|
||||
|
||||
return [
|
||||
'xml' => $this->messageCrypto()->encryptMessage(
|
||||
(string)($params['body'] ?? ''),
|
||||
(string)($params['timestamp'] ?? time()),
|
||||
(string)($params['nonce'] ?? ''),
|
||||
),
|
||||
'xml' => $xml,
|
||||
];
|
||||
}
|
||||
|
||||
@ -161,6 +205,7 @@ trait InteractsProtocol
|
||||
$query = $this->wechatCallQuery($method, $params, $options);
|
||||
$requestOptions = $this->buildWechatJsonOptions($method, $params, $options, $internalKeys);
|
||||
if ($tokenAware) {
|
||||
/** @phpstan-ignore-next-line tokenAware 仅由支持 withToken 参数的公众平台和小程序客户端启用。 */
|
||||
return $this->request($method, $uri, $query, $requestOptions, (bool)($options['with_token'] ?? true));
|
||||
}
|
||||
|
||||
@ -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\Wechat;
|
||||
@ -13,7 +15,7 @@ use Psr\Http\Message\ResponseInterface;
|
||||
use We\Client;
|
||||
use We\Config\WechatPlatformConfig;
|
||||
use We\Contract\StoreCacheInterface;
|
||||
use We\Platform\Wechat\Concerns\InteractsProtocol;
|
||||
use We\Contract\Trait\WechatInteractsProtocol;
|
||||
use We\Support\CacheKey;
|
||||
use We\Support\JsonClient;
|
||||
use We\Support\MessageCrypto;
|
||||
@ -27,9 +29,9 @@ use We\Support\TokenCacheKey;
|
||||
*/
|
||||
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 API = 'https://api.weixin.qq.com/';
|
||||
@ -110,14 +112,6 @@ final class PlatformClient
|
||||
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 与参数发起请求。
|
||||
*
|
||||
@ -166,6 +160,14 @@ final class PlatformClient
|
||||
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)。
|
||||
*
|
||||
|
||||
@ -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\Wechat;
|
||||
@ -14,8 +16,8 @@ use We\Client;
|
||||
use We\Config\WechatServiceConfig;
|
||||
use We\Contract\StoreCacheInterface;
|
||||
use We\Contract\StoreTokenInterface;
|
||||
use We\Contract\Trait\WechatInteractsProtocol;
|
||||
use We\Exception\WechatException;
|
||||
use We\Platform\Wechat\Concerns\InteractsProtocol;
|
||||
use We\Support\CacheKey;
|
||||
use We\Support\JsonClient;
|
||||
use We\Support\MessageCrypto;
|
||||
@ -29,12 +31,15 @@ use We\Support\TokenCacheKey;
|
||||
*/
|
||||
final class ServiceClient
|
||||
{
|
||||
use InteractsProtocol;
|
||||
use WechatInteractsProtocol;
|
||||
|
||||
/** 与 {@see \We\Client::get} 通道标识一致 */
|
||||
/** 与 {@see Client::get} 通道标识一致 */
|
||||
private const TOKEN_PLATFORM_CHANNEL = 'wechat.service';
|
||||
|
||||
private JsonClient $http;
|
||||
|
||||
private readonly ?StoreTokenInterface $authorizers;
|
||||
|
||||
/**
|
||||
* 创建微信服务平台(第三方平台)客户端并初始化官方 API HTTP 客户端。
|
||||
*/
|
||||
@ -49,8 +54,6 @@ final class ServiceClient
|
||||
$this->authorizers = $authorizers;
|
||||
}
|
||||
|
||||
private readonly ?StoreTokenInterface $authorizers;
|
||||
|
||||
/**
|
||||
* 获取第三方平台接口调用凭据 component_access_token;缓存未命中或强制刷新时调用官方 component_token 接口。
|
||||
*/
|
||||
@ -72,8 +75,8 @@ final class ServiceClient
|
||||
'component_verify_ticket' => $componentVerifyTicket,
|
||||
],
|
||||
]);
|
||||
$token = (string)$data['component_access_token'];
|
||||
$this->cache->set($key, $token, max(1, (int)($data['expires_in'] ?? 7200) - 300));
|
||||
$token = $this->wechatTokenValue($data, 'component_access_token', '微信 component_access_token');
|
||||
$this->cache->set($key, $token, $this->wechatTokenTtl($data, '微信 component_access_token'));
|
||||
|
||||
return $token;
|
||||
});
|
||||
@ -193,14 +196,6 @@ final class ServiceClient
|
||||
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、授权方上下文和参数发起请求。
|
||||
*
|
||||
@ -254,12 +249,21 @@ final class ServiceClient
|
||||
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 刷新。
|
||||
*/
|
||||
private function authorizerAccessToken(string $componentAccessToken, string $authorizerAppid): string
|
||||
{
|
||||
if (!$this->authorizers) {
|
||||
$authorizers = $this->authorizers;
|
||||
if (!$authorizers) {
|
||||
throw new WechatException('未配置授权账号 Token 仓库');
|
||||
}
|
||||
$key = $this->cacheKey(TokenCacheKey::wechatServiceAuthorizerAccessToken(
|
||||
@ -271,14 +275,18 @@ final class ServiceClient
|
||||
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 !== '') {
|
||||
return $token;
|
||||
}
|
||||
$data = $this->refreshAuthorizerToken($componentAccessToken, $authorizerAppid, $this->authorizers->refreshToken($authorizerAppid));
|
||||
$this->authorizers->saveAuthorizerToken($authorizerAppid, $data);
|
||||
$token = (string)$data['authorizer_access_token'];
|
||||
$this->cache->set($key, $token, max(1, (int)($data['expires_in'] ?? 7200) - 300));
|
||||
$refreshToken = $authorizers->refreshToken($authorizerAppid);
|
||||
if (trim($refreshToken) === '') {
|
||||
throw new WechatException('授权方 authorizer_refresh_token 不能为空');
|
||||
}
|
||||
$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;
|
||||
});
|
||||
|
||||
@ -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\Wechat;
|
||||
@ -13,7 +15,7 @@ use Psr\Http\Message\ResponseInterface;
|
||||
use We\Client;
|
||||
use We\Config\WechatWxappConfig;
|
||||
use We\Contract\StoreCacheInterface;
|
||||
use We\Platform\Wechat\Concerns\InteractsProtocol;
|
||||
use We\Contract\Trait\WechatInteractsProtocol;
|
||||
use We\Support\CacheKey;
|
||||
use We\Support\JsonClient;
|
||||
use We\Support\NullCacheStore;
|
||||
@ -26,10 +28,11 @@ use We\Support\TokenCacheKey;
|
||||
*/
|
||||
final class WxappClient
|
||||
{
|
||||
use InteractsProtocol;
|
||||
use WechatInteractsProtocol;
|
||||
|
||||
/** 与 {@see \We\Client::get} 通道标识一致 */
|
||||
/** 与 {@see Client::get} 通道标识一致 */
|
||||
private const TOKEN_PLATFORM_CHANNEL = 'wechat.wxapp';
|
||||
|
||||
private JsonClient $http;
|
||||
|
||||
/**
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* 微信公众平台 access_token 缓存行为测试。
|
||||
* This file is part of HyperfAdmin.
|
||||
*
|
||||
* @Link https://thinkadmin.top
|
||||
* @Author Anyon<zoujingli@qq.com>
|
||||
*/
|
||||
|
||||
namespace We\Tests;
|
||||
@ -25,6 +27,7 @@ use We\Support\TokenCacheKey;
|
||||
|
||||
/**
|
||||
* 微信公众平台 access_token 缓存行为测试用例。
|
||||
* @internal
|
||||
*/
|
||||
#[CoversClass(WechatPlatformClient::class)]
|
||||
final class AccessTokenCacheTest extends TestCase
|
||||
@ -75,11 +78,11 @@ final class AccessTokenCacheTest extends TestCase
|
||||
*/
|
||||
final class ArrayCacheStore implements StoreCacheInterface
|
||||
{
|
||||
public int $lockCalls = 0;
|
||||
|
||||
/** @var array<string,mixed> */
|
||||
private array $values = [];
|
||||
|
||||
public int $lockCalls = 0;
|
||||
|
||||
/**
|
||||
* 读取测试缓存值。
|
||||
*/
|
||||
@ -147,6 +150,7 @@ final class FakeHttpClient implements ClientInterface
|
||||
|
||||
/**
|
||||
* 实现测试 HTTP 客户端请求接口或记录请求。
|
||||
* @param mixed $uri
|
||||
*/
|
||||
public function request(string $method, $uri = '', array $options = []): ResponseInterface
|
||||
{
|
||||
@ -157,6 +161,7 @@ final class FakeHttpClient implements ClientInterface
|
||||
|
||||
/**
|
||||
* 实现测试 HTTP 客户端异步请求接口。
|
||||
* @param mixed $uri
|
||||
*/
|
||||
public function requestAsync(string $method, $uri = '', array $options = []): PromiseInterface
|
||||
{
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SDK 根入口通道工厂测试。
|
||||
* 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\Wechat\ServiceClient as WechatServiceClient;
|
||||
|
||||
/**
|
||||
* SDK 根入口通道工厂测试用例。
|
||||
* @internal
|
||||
*/
|
||||
#[CoversClass(Client::class)]
|
||||
final class ClientTest extends TestCase
|
||||
@ -66,7 +69,7 @@ final class ClientTest extends TestCase
|
||||
|
||||
$this->expectException(WechatException::class);
|
||||
$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(
|
||||
'wx_component',
|
||||
'component_secret',
|
||||
'component_token',
|
||||
'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG'
|
||||
'componentToken123',
|
||||
TestKeys::encodingAesKey()
|
||||
));
|
||||
$url = $service->authorizationUrl('preauthcode', 'https://example.com/callback', 3, 'STATE_TEST');
|
||||
|
||||
@ -126,7 +129,7 @@ final class ClientTest extends TestCase
|
||||
public function testAlipayPlatformCallReturnsAuthorizationUrl(): void
|
||||
{
|
||||
$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', [
|
||||
'redirect_uri' => 'https://example.com/alipay/callback',
|
||||
'scope' => 'auth_user',
|
||||
@ -143,7 +146,7 @@ final class ClientTest extends TestCase
|
||||
public function testGetCanReturnSpecificChannelClient(): void
|
||||
{
|
||||
$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->assertNotInstanceOf(WechatServiceClient::class, $channelClient);
|
||||
|
||||
68
tests/TestKeys.php
Normal file
68
tests/TestKeys.php
Normal 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']];
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user