refactor(core): 统一核心配置缓存命名与注释

- 统一根 Client 工厂为 wechatXXX/alipayXXX,保持与配置语义一致。

- 规范微信 platform、wxapp、service、payment 等缓存逻辑键命名。

- 补齐 Config、Contract、Exception 与缓存实现的中文 PHPDoc,提升开源项目可读性。
This commit is contained in:
Anyon 2026-05-08 00:29:04 +08:00
parent 41e5265e08
commit 05cb47d3fd
18 changed files with 266 additions and 44 deletions

View File

@ -2,6 +2,10 @@
declare(strict_types=1);
/**
* SDK 根入口与通道客户端工厂。
*/
namespace We;
use GuzzleHttp\ClientInterface;
@ -26,10 +30,10 @@ use We\Platform\Wechat\WxappClient as WechatWxappClient;
use We\Support\FileCacheStore;
/**
* 根入口:通过 `__call` 以通道工厂方法名创建各平台客户端(内部 `ReflectionClass::newInstanceArgs`),便于集中维护构造参数
* SDK 根入口:按平台与业务域创建微信、支付宝客户端,工厂方法命名与配置对象语义保持一致
*
* 缓存键固定为 `{cacheKeyPrefix}:{platformChannel}:{logicalKey}`,见 {@see \We\Support\CacheKey::compose};未传 `cacheKeyPrefix` 时使用 {@see self::DEFAULT_CACHE_KEY_PREFIX},不得传空白字符串
* 传入 `cache` 时默认使用 {@see FileCacheStore}目录为 {@see self::defaultCacheStoreDirectory()}(位于 PHP `sys_get_temp_dir()` 下)
* access_token 等运行态数据的缓存键固定为 `{cacheKeyPrefix}:{platformChannel}:{logicalKey}`,见 {@see \We\Support\CacheKey::compose}
* 注入缓存实现时使用 {@see FileCacheStore}默认目录为 {@see self::defaultCacheStoreDirectory()}
*
* @method WechatPlatformClient wechatPlatform(WechatPlatformConfig $config)
* @method WechatWxappClient wechatWxapp(WechatWxappConfig $config)
@ -54,6 +58,9 @@ final class Client
private readonly string $cacheKeyPrefix;
/**
* 初始化 SDK 根入口,注入运行态缓存、授权方 Token 仓库和 HTTP 客户端。
*/
public function __construct(
?StoreCacheInterface $cache = null,
?StoreTokenInterface $authorizers = null,
@ -78,27 +85,27 @@ final class Client
}
/**
* 通道标识取实例:`new Client()->get('wechat.platform', new WechatPlatformConfig(...))`
* 字符串通道创建客户端;通道名称与配置对象一一对应。
*/
public function get(string $channel, ConfigInterface $config): object
{
$factory = match ($channel) {
'wechat.platform' => 'wechatPlatform',
'wechat.wxapp' => 'wechatWxapp',
'wechat.service' => 'wechatService',
'wechat.payment' => 'wechatPayment',
'alipay.platform' => 'alipayPlatform',
'alipay.payment' => 'alipayPayment',
[$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, [$config]);
return $this->__call($factory, [$this->ensureConfig($config, $expected, $factory)]);
}
/**
* 魔术工厂:`$client->wechatPlatform($config)` 等价于反射 `new WechatPlatformClient(...)`
* 魔术工厂:只暴露平台前缀明确的客户端创建方法
*
* @param array<int, mixed> $arguments
* @param array<int,mixed> $arguments
*/
public function __call(string $name, array $arguments): object
{
@ -144,6 +151,8 @@ final class Client
}
/**
* 使用反射创建具体平台客户端实例。
*
* @param class-string $class
* @param array<int, mixed> $args
*/
@ -157,6 +166,8 @@ final class Client
}
/**
* 校验工厂方法收到的配置对象类型。
*
* @template T of object
* @param class-string<T> $expected
* @return T
@ -170,7 +181,11 @@ final class Client
return $config;
}
/** @param class-string $fqcn */
/**
* 获取类短名,用于生成清晰的配置类型错误信息。
*
* @param class-string $fqcn
*/
private function shortClass(string $fqcn): string
{
$pos = strrpos($fqcn, '\\');

View File

@ -2,8 +2,15 @@
declare(strict_types=1);
/**
* 支付宝支付配置对象。
*/
namespace We\Config;
/**
* 支付宝支付配置,继承支付宝开放平台网关签名与验签参数。
*/
final class AlipayPaymentConfig extends AlipayPlatformConfig
{
}

View File

@ -2,16 +2,27 @@
declare(strict_types=1);
/**
* 支付宝开放平台配置对象。
*/
namespace We\Config;
use We\Contract\ConfigInterface;
use We\Exception\WechatException;
/**
* 支付宝开放平台基础配置。
*
* 应用私钥用于支付宝开放平台网关请求签名;支付宝公钥用于同步响应和异步通知验签。
*
* @phpstan-consistent-constructor
*/
class AlipayPlatformConfig implements ConfigInterface
{
/**
* 创建支付宝开放平台配置并执行必填项校验。
*/
public function __construct(
public string $appid,
public string $privateKey,
@ -25,6 +36,9 @@ class AlipayPlatformConfig implements ConfigInterface
$this->validate();
}
/**
* 校验支付宝 app_id 与应用私钥。
*/
public function validate(): void
{
if (trim($this->appid) === '' || trim($this->privateKey) === '') {
@ -33,6 +47,8 @@ class AlipayPlatformConfig implements ConfigInterface
}
/**
* 从数组创建支付宝开放平台配置。
*
* @param array<string,mixed> $data
*/
public static function fromArray(array $data): static

View File

@ -2,26 +2,44 @@
declare(strict_types=1);
/**
* 微信支付 APIv3 商户配置对象。
*/
namespace We\Config;
use We\Contract\ConfigInterface;
use We\Exception\WechatException;
/**
* 微信支付 APIv3 商户配置。
*
* 商户号、商户 API 证书序列号和商户私钥用于生成 APIv3 请求签名APIv3 密钥用于回调资源解密;微信支付平台证书或平台公钥用于通知验签。
*/
final class WechatPaymentConfig implements ConfigInterface
{
/**
* 创建微信支付 APIv3 配置并执行必填项校验。
*/
public function __construct(
public string $appid,
public string $mchId,
public string $apiV3Key,
public string $merchantSerial,
public string $merchantPrivateKey,
/** 微信支付平台证书 PEM未配置 platformPublicKey 时用于回调验签。 */
public string $platformCertificate = '',
/** 微信支付平台公钥 PEM优先用于回调验签。 */
public string $platformPublicKey = '',
/** 微信支付平台证书/公钥序列号;非空时会校验回调头 Wechatpay-Serial。 */
public string $platformSerial = '',
) {
$this->validate();
}
/**
* 校验微信支付 appid、商户号、APIv3 密钥、商户证书序列号和商户私钥。
*/
public function validate(): void
{
foreach ([
@ -38,6 +56,8 @@ final class WechatPaymentConfig implements ConfigInterface
}
/**
* 从数组创建微信支付 APIv3 商户配置。
*
* @param array<string,mixed> $data
*/
public static function fromArray(array $data): static

View File

@ -2,20 +2,32 @@
declare(strict_types=1);
/**
* 微信公众平台配置对象。
*/
namespace We\Config;
use We\Contract\ConfigInterface;
use We\Exception\WechatException;
/**
* 微信公众平台基础配置。
*
* appId/appSecret 用于获取微信公众平台接口调用凭据 access_tokenToken EncodingAESKey 用于服务器配置、消息签名校验和安全模式消息加解密。
*/
final class WechatPlatformConfig implements ConfigInterface
{
/**
* 创建微信公众平台配置并执行必填项校验。
*/
public function __construct(
public string $appid,
public string $appSecret,
public string $token = '',
public string $encodingAesKey = '',
/**
* 业务维度的缓存分桶Token 键形如 `wechat:app:{appid}:official:access_token:scope:{storageScope}`
* 业务维度的缓存分桶Token 键形如 `wechat:app:{appid}:platform:access_token:scope:{storageScope}`
* 空字符串则仅按 appid 分组。多租户下同一 appid 若需隔离(极少见)可传租户/账号标识。
*/
public string $storageScope = '',
@ -24,6 +36,8 @@ final class WechatPlatformConfig implements ConfigInterface
}
/**
* 从数组创建微信公众平台配置,兼容常见下划线字段名。
*
* @param array<string,mixed> $data
*/
public static function fromArray(array $data): static
@ -37,6 +51,9 @@ final class WechatPlatformConfig implements ConfigInterface
);
}
/**
* 校验微信公众平台 appid appSecret。
*/
public function validate(): void
{
foreach ([

View File

@ -2,13 +2,25 @@
declare(strict_types=1);
/**
* 微信服务平台(第三方平台)配置对象。
*/
namespace We\Config;
use We\Contract\ConfigInterface;
use We\Exception\WechatException;
/**
* 微信服务平台(第三方平台)配置。
*
* componentAppid/componentAppSecret 用于获取第三方平台 component_access_tokencomponentToken/componentEncodingAesKey 用于授权事件接收 URL 的签名校验与安全模式消息解密。
*/
final class WechatServiceConfig implements ConfigInterface
{
/**
* 创建微信服务平台(第三方平台)配置并执行必填项校验。
*/
public function __construct(
public string $componentAppid,
public string $componentAppSecret,
@ -20,6 +32,9 @@ final class WechatServiceConfig implements ConfigInterface
$this->validate();
}
/**
* 校验第三方平台 component_appid、secret、Token EncodingAESKey。
*/
public function validate(): void
{
foreach ([
@ -35,6 +50,8 @@ final class WechatServiceConfig implements ConfigInterface
}
/**
* 从数组创建微信服务平台(第三方平台)配置。
*
* @param array<string,mixed> $data
*/
public static function fromArray(array $data): static

View File

@ -2,13 +2,25 @@
declare(strict_types=1);
/**
* 微信小程序配置对象。
*/
namespace We\Config;
use We\Contract\ConfigInterface;
use We\Exception\WechatException;
/**
* 微信小程序基础配置。
*
* appid/appSecret 用于获取小程序接口调用凭据 access_tokenstorageScope 用于隔离不同业务上下文下的凭据缓存。
*/
final class WechatWxappConfig implements ConfigInterface
{
/**
* 创建微信小程序配置并执行必填项校验。
*/
public function __construct(
public string $appid,
public string $appSecret,
@ -19,6 +31,8 @@ final class WechatWxappConfig implements ConfigInterface
}
/**
* 从数组创建微信小程序配置,兼容常见下划线字段名。
*
* @param array<string,mixed> $data
*/
public static function fromArray(array $data): static
@ -30,6 +44,9 @@ final class WechatWxappConfig implements ConfigInterface
);
}
/**
* 校验小程序 appid appSecret。
*/
public function validate(): void
{
if (trim($this->appid) === '' || trim($this->appSecret) === '') {

View File

@ -2,20 +2,26 @@
declare(strict_types=1);
/**
* 平台配置契约。
*/
namespace We\Contract;
/**
* 平台配置统一契约:所有通道配置必须能从数组构造,并在构造阶段完成自身业务校验。
* 平台配置统一契约:所有配置对象必须支持数组构造,并在构造阶段完成平台必填项校验。
*/
interface ConfigInterface
{
/**
* 通过数组创建配置对象。
*
* @param array<string,mixed> $data
*/
public static function fromArray(array $data): static;
/**
* 校验配置字段完整性、密钥格式等通道级约束;失败时抛出 SDK 异常。
* 校验平台配置的必填字段、密钥格式等约束;校验失败时抛出 SDK 异常。
*/
public function validate(): void;
}

View File

@ -2,20 +2,24 @@
declare(strict_types=1);
/**
* SDK 运行态缓存契约。
*/
namespace We\Contract;
/**
* 通用缓存契约:用于 access_token SDK 运行态数据,必须显式支持 TTL 与刷新锁。
* 运行态缓存契约:用于保存 access_token、component_access_token 等平台接口调用凭据,必须支持 TTL 与刷新锁。
*/
interface StoreCacheInterface
{
/**
* 读取缓存值缓存不存在、已过期或无法解析时返回默认值。
* 读取缓存值缓存不存在、已过期或无法解析时返回默认值。
*/
public function get(string $key, mixed $default = null): mixed;
/**
* 写入缓存值;$ttl 为剩余秒数,必须至少按 1 秒处理。
* 写入缓存值;TTL 单位为秒,实现侧至少按 1 秒处理。
*/
public function set(string $key, mixed $value, int $ttl): void;
@ -25,7 +29,7 @@ interface StoreCacheInterface
public function del(string $key): void;
/**
* 锁内执行回调,避免集群或多进程下重复刷新 access_token
* 互斥锁内执行回调,避免多进程或集群环境重复刷新平台接口调用凭据
*
* @template T
* @param callable():T $callback

View File

@ -2,20 +2,24 @@
declare(strict_types=1);
/**
* 微信服务平台授权方 Token 存储契约。
*/
namespace We\Contract;
/**
* 开放平台授权方 Token 存储契约SDK 只读取 refresh token 并回写刷新结果。
* 微信服务平台授权方 Token 存储契约SDK 读取 authorizer_refresh_token并在刷新后回写授权方 Token 数据
*/
interface StoreTokenInterface
{
/**
* 获取授权账号 refresh token。
* 获取授权方账号的 authorizer_refresh_token。
*/
public function refreshToken(string $authorizerAppid): string;
/**
* 授权账号 token 刷新后回写业务存储,避免 SDK 持有请求态数据
* 授权authorizer_access_token 刷新后回写业务存储
*
* @param array<string,mixed> $payload
*/

View File

@ -2,6 +2,13 @@
declare(strict_types=1);
/**
* 平台接口请求异常。
*/
namespace We\Exception;
/**
* HTTP 请求失败或平台接口返回错误时抛出的异常。
*/
final class ApiException extends WechatException {}

View File

@ -2,6 +2,13 @@
declare(strict_types=1);
/**
* 签名与验签异常。
*/
namespace We\Exception;
/**
* 微信回调、微信支付通知或支付宝通知签名验证失败时抛出的异常。
*/
final class SignatureException extends WechatException {}

View File

@ -2,14 +2,25 @@
declare(strict_types=1);
/**
* SDK 基础异常。
*/
namespace We\Exception;
use RuntimeException;
use Throwable;
/**
* SDK 基础异常。
*
* 通过 context() 暴露平台响应、验签明细等上下文,便于业务侧记录日志和排查问题。
*/
class WechatException extends RuntimeException
{
/**
* 创建 SDK 异常并保存可选上下文。
*
* @param array<string,mixed> $context
*/
public function __construct(
@ -22,6 +33,8 @@ class WechatException extends RuntimeException
}
/**
* 获取异常附带的平台响应或验签上下文。
*
* @return array<string,mixed>
*/
public function context(): array

View File

@ -2,16 +2,22 @@
declare(strict_types=1);
/**
* 缓存键生成工具。
*/
namespace We\Support;
use We\Exception\WechatException;
/**
* SDK 缓存完整键生成器,固定为 `{cacheKeyPrefix}:{platformChannel}:{logicalKey}` 三段
* SDK 缓存完整键生成器,固定为 `{cacheKeyPrefix}:{platformChannel}:{logicalKey}` 三段,便于按项目、通道和逻辑键隔离
*/
final class CacheKey
{
/**
* 组合缓存完整键。
*
* @param string $prefix Client 通用命名段,不得为空,用于区分部署、租户或应用。
* @param string $channel 通道段,如 `wechat.platform`,必须与 Client 通道标识一致。
* @param string $logicalKey 业务逻辑键,如 TokenCacheKey 生成的 access_token 键。
@ -34,6 +40,9 @@ final class CacheKey
return $prefix . ':' . $channel . ':' . $logical;
}
/**
* 规范化缓存键命名段并去除两侧多余冒号。
*/
private static function normalizeSegment(string $segment): string
{
$segment = trim($segment);

View File

@ -2,16 +2,23 @@
declare(strict_types=1);
/**
* 本地文件缓存实现。
*/
namespace We\Support;
use We\Contract\StoreCacheInterface;
use We\Exception\WechatException;
/**
* 单机文件缓存:缓存值以 JSON 保存,使用 flock 提供单机多进程刷新锁。
* 本地文件缓存实现:缓存值以 JSON 保存,并使用 flock 提供单机多进程刷新锁。
*/
final class FileCacheStore implements StoreCacheInterface
{
/**
* 创建文件缓存目录并校验可写权限。
*/
public function __construct(private readonly string $directory)
{
if ($this->directory === '') {
@ -25,6 +32,9 @@ final class FileCacheStore implements StoreCacheInterface
}
}
/**
* 读取文件缓存值;文件不存在、过期或 JSON 无效时返回默认值。
*/
public function get(string $key, mixed $default = null): mixed
{
$path = $this->pathFor($key);
@ -55,7 +65,7 @@ final class FileCacheStore implements StoreCacheInterface
if (!is_array($payload) || !array_key_exists('expires_at', $payload) || !array_key_exists('value', $payload)) {
return $default;
}
if ((int)$payload['expires_at'] < time()) {
if ((int)$payload['expires_at'] <= time()) {
$this->del($key);
return $default;
@ -64,6 +74,9 @@ final class FileCacheStore implements StoreCacheInterface
return $payload['value'];
}
/**
* 以原子写入方式保存缓存值与过期时间。
*/
public function set(string $key, mixed $value, int $ttl): void
{
$path = $this->pathFor($key);
@ -90,6 +103,9 @@ final class FileCacheStore implements StoreCacheInterface
}
}
/**
* 删除指定缓存键对应的文件。
*/
public function del(string $key): void
{
$path = $this->pathFor($key);
@ -98,6 +114,9 @@ final class FileCacheStore implements StoreCacheInterface
}
}
/**
* 基于 flock 在互斥锁内执行回调。
*/
public function lock(string $key, int $ttl, callable $callback): mixed
{
$path = $this->pathFor('lock:' . $key) . '.lock';
@ -123,6 +142,9 @@ final class FileCacheStore implements StoreCacheInterface
}
}
/**
* 根据缓存键生成哈希分片后的文件路径。
*/
private function pathFor(string $key): string
{
$hash = hash('sha256', $key);

View File

@ -2,24 +2,40 @@
declare(strict_types=1);
/**
* 空缓存实现。
*/
namespace We\Support;
use We\Contract\StoreCacheInterface;
/**
* 空缓存实现:用于测试或显式禁用缓存,锁回调直接执行。
* 空缓存实现:用于测试或显式禁用缓存;读取永远返回默认值,锁回调直接执行。
*/
final class NullCacheStore implements StoreCacheInterface
{
/**
* 返回默认值,不保存任何缓存数据。
*/
public function get(string $key, mixed $default = null): mixed
{
return $default;
}
/**
* 忽略写入请求。
*/
public function set(string $key, mixed $value, int $ttl): void {}
/**
* 忽略删除请求。
*/
public function del(string $key): void {}
/**
* 不加锁,直接执行回调。
*/
public function lock(string $key, int $ttl, callable $callback): mixed
{
return $callback();

View File

@ -2,6 +2,10 @@
declare(strict_types=1);
/**
* PSR-16 缓存适配器。
*/
namespace We\Support;
use Psr\SimpleCache\CacheInterface;
@ -9,11 +13,13 @@ use We\Contract\StoreCacheInterface;
use We\Exception\WechatException;
/**
* PSR-16 缓存适配器;集群锁需由业务注入原子锁回调
* PSR-16 缓存适配器;缓存能力委托给 PSR Simple Cache实现侧需注入原子锁回调以支持刷新锁
*/
final class PsrSimpleCacheStore implements StoreCacheInterface
{
/**
* 创建 PSR-16 缓存适配器,并可选注入锁回调。
*
* @param null|callable(string,int,callable):mixed $locker
*/
public function __construct(
@ -21,21 +27,33 @@ final class PsrSimpleCacheStore implements StoreCacheInterface
private readonly mixed $locker = null,
) {}
/**
* PSR-16 缓存读取值。
*/
public function get(string $key, mixed $default = null): mixed
{
return $this->cache->get($key, $default);
}
/**
* 写入 PSR-16 缓存并设置 TTL。
*/
public function set(string $key, mixed $value, int $ttl): void
{
$this->cache->set($key, $value, max(1, $ttl));
}
/**
* PSR-16 缓存删除值。
*/
public function del(string $key): void
{
$this->cache->delete($key);
}
/**
* 通过业务注入的锁回调执行互斥逻辑。
*/
public function lock(string $key, int $ttl, callable $callback): mixed
{
if (!is_callable($this->locker)) {

View File

@ -2,10 +2,14 @@
declare(strict_types=1);
/**
* 平台接口调用凭据缓存逻辑键生成工具。
*/
namespace We\Support;
/**
* Token 缓存键统一按「微信侧 appid 优先」分段,便于 Redis 等按前缀扫描、按应用清理。
* 微信接口调用凭据缓存逻辑键统一按「微信侧 appid 优先」分段,便于 Redis 等存储按应用前缀扫描和清理。
*
* 约定前缀:`wechat:app:{appid}:...`;若 Config `storageScope`(业务隔离),追加 `:scope:{值}`
* 经根 {@see \We\Client} 与各通道写入存储时,作为第三段与通用前缀、平台段经 {@see CacheKey::compose} 拼成固定三段键。
@ -13,41 +17,44 @@ namespace We\Support;
final class TokenCacheKey
{
/**
* 公众号 client_credential access_token
* 例:`wechat:app:wxabcd:official:access_token` 或带 scope 后缀。
* 微信公众平台 client_credential access_token。
* 例:`wechat:app:wxabcd:platform:access_token` 或带 scope 后缀。
*/
public static function wechatOfficialAccessToken(string $appid, string $storageScope = ''): string
public static function wechatPlatformAccessToken(string $appid, string $storageScope = ''): string
{
return self::appendScope('wechat:app:' . $appid . ':official:access_token', $storageScope);
return self::appendScope('wechat:app:' . $appid . ':platform:access_token', $storageScope);
}
/**
* 小程序 client_credential access_token
* 小程序 client_credential access_token
*/
public static function wechatMiniAccessToken(string $appid, string $storageScope = ''): string
public static function wechatWxappAccessToken(string $appid, string $storageScope = ''): string
{
return self::appendScope('wechat:app:' . $appid . ':mini:access_token', $storageScope);
return self::appendScope('wechat:app:' . $appid . ':wxapp:access_token', $storageScope);
}
/**
* 开放平台第三方 component_access_token component_appid 分组)
* 微信服务平台(第三方平台) component_access_token component_appid 分组)
*/
public static function wechatOpenComponentAccessToken(string $componentAppid, string $storageScope = ''): string
public static function wechatServiceComponentAccessToken(string $componentAppid, string $storageScope = ''): string
{
return self::appendScope('wechat:app:' . $componentAppid . ':open:component_access_token', $storageScope);
return self::appendScope('wechat:app:' . $componentAppid . ':service:component_access_token', $storageScope);
}
/**
* 开放平台代授权方 authorizer_access_token挂在 component_appid 下,避免键平面冲突)
* 微信服务平台代授权方 authorizer_access_token挂在 component_appid 下,避免键平面冲突)
*/
public static function wechatOpenAuthorizerAccessToken(string $componentAppid, string $authorizerAppid, string $storageScope = ''): string
public static function wechatServiceAuthorizerAccessToken(string $componentAppid, string $authorizerAppid, string $storageScope = ''): string
{
return self::appendScope(
'wechat:app:' . $componentAppid . ':open:authorizer:' . $authorizerAppid . ':access_token',
'wechat:app:' . $componentAppid . ':service:authorizer:' . $authorizerAppid . ':access_token',
$storageScope,
);
}
/**
* 按需追加 storageScope隔离同一 appid 在不同业务上下文中的凭据缓存。
*/
private static function appendScope(string $base, string $storageScope): string
{
$s = trim($storageScope);