wiseadmin / wise-support
WiseAdmin 基础设施层 — ExtPoint 扩展注册中心 / Auth+JWT 统一认证 / ServiceBus 服务总线 / Connector 连接器 / CircuitBreaker 断路器 / Backoff 退避策略 / DistributedLock 分布式锁 / RateLimiter 限流 / ConfigCheck 配置检查 / Context 请求上下文 / Crypto AES-GCM / WebSocket 基础设施 / Pipeline / Exception 错误码体系
Requires
- php: >=8.1
- ext-openssl: *
- topthink/framework: ^8.0
Requires (Dev)
- mockery/mockery: >=1.0
- phpunit/phpunit: >=9.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
为 WiseAdmin 生态提供统一的「扩展注册中心 / 统一认证 / 服务总线 / 分布式锁 / 断路器 / 限流 / 配置检查 / 请求上下文 / 加解密 / WebSocket / 错误码体系 / 门面」基础设施 是全体
wise-*业务包的公共基座,与具体业务解耦、可独立复用 版本:v1.0 | PHP >= 8.1 | ThinkPHP ^6.0 || ^8.0
目录
- 概述
- 核心能力 / 特性
- 设计动机与概念
- 架构设计
- 安装
- 快速上手
- 统一认证(AuthManager + Guard)
- JWT 统一门面
- 扩展点注册中心(ExtPoint)
- 服务总线(ServiceBus)
- 分布式锁(DistributedLock)
- 断路器(CircuitBreaker)
- 限流(RateLimiter / SlidingWindow / TokenBucket)
- 重试与退避(retry / BackoffStrategy)
- 管道(Pipeline)与请求级缓存(Once)
- 请求上下文与链路日志(RequestContext / ContextualLogger)
- WebSocket 基础设施
- 连接器(Connector)
- 配置检查(ConfigCheck)
- 异常与错误码(WiseException)
- .env 密钥播种(EnvSeedService)
- 辅助函数
- API / 方法参考
- 事件 / 扩展点
- 与其他 wise-* 包协作
- 目录结构
- 依赖关系
- 开发与测试
- 常见问题
- 许可证
概述
Wise-Support 是 WiseAdmin 生态中不依赖任何业务包的基础设施库,沉淀了跨业务通用的能力,供全体 wise-* 业务包复用。它通过 ExtPoint 扩展注册中心 实现「能力注册 — 发现 — 调度」的松耦合机制,业务包只需把自己实现的能力注册进来,wise-support 与业务包之间互不直接依赖。
设计理念
| 设计原则 | 说明 |
|---|---|
| 零业务依赖 | 仅依赖 topthink/framework,可被任何 wise-* 包或独立项目复用 |
| 可替换实现 | 通过 ExtPoint 定义单一契约,业务包注册具体实现(如 JWT 引擎、WebSocket 服务端) |
| 生产就绪 | 提供分布式锁、断路器、限流、重试退避等高可用组件,且均支持 Redis/文件/数据库多驱动与降级 |
| 可观测 | 请求上下文 + 链路日志 + 配置检查,贯穿请求生命周期 |
| 统一错误 | WiseException + GeneralErrorCode 为全体包提供一致的错误码与 JSON 渲染规范 |
核心能力 / 特性
- ExtPoint 扩展注册中心(SPI Registry):
ExtPoint::definePoint / register / resolve / active / all,支持POLICY_SINGLE(独占)、POLICY_MULTI(共享)、POLICY_ROUTED(按名路由)三种调度策略,是各包解耦的核心机制。 - 统一认证(AuthManager + Guard):经
ExtPoint('auth.guard')发现并管理多个 Guard(admin / api / member / 自定义),user()/id()/check()/guest()/authenticate()统一入口;内置SessionGuard(后台 Session)与TokenGuard(JWT Bearer 解析)。 - JWT 引擎代理(JwtManager):统一代理
ExtPoint('jwt.provider')发现的当前活跃 JWT 引擎(默认由wise-jwt包注册),通过JWT门面一行签发/解析/续签/吊销。 - ServiceBus 服务总线:跨插件统一通信,支持请求-响应(
ask)、发布-订阅(publish)、广播(broadcast)三种模式,可经 think-queue 异步分发。 - Connector 连接器:统一抽象 OAuth / Webhook / API / 消息推送 / 支付 / 云存储 / AI / IoT 等所有外部系统交互,支持按名查找、按类型选活跃、健康检查与配置 Schema。
- 高可用组件:
DistributedLock分布式锁 —— Redis / File / Database 三驱动,Token 所有权 + Lua 原子释放 + TTL 兜底 + 阻塞/非阻塞双模式;CircuitBreaker断路器 —— CLOSED → OPEN → HALF_OPEN 三态,状态存 Redis 支持分布式;BackoffStrategy退避策略 —— 线性 / 指数 / 固定 / 自定义四种延迟计算;RateLimiter+SlidingWindow+TokenBucket—— 通用限流、滑动窗口、令牌桶三种算法,Redis 原子 / 文件降级 / fail-open。
- 重试与缓存原语:
retry()失败重试(指数退避 + 条件过滤)、once()请求级闭包缓存(按 call-site memoize)。 - 可观测与上下文:
RequestContext请求级上下文容器(request_id / user / guard / tenant / locale)、ContextualLogger自动注入链路上下文日志、ConfigCheckCollector配置检查(冷却防刷屏)。 - 安全与通信:
AesGcmAES-256-GCM 加解密、WebSocket 基础设施(连接管理 / 消息路由 / 频道 / 心跳)、Pipeline洋葱模型管道。 - 错误码体系:
WiseException统一异常基类(链式构建 + 上下文)、GeneralErrorCode框架级通用错误码、DefaultExceptionRenderer默认渲染器、JsonResponse/ErrorHandlerTrait。 - 配置驱动路由:
RouteRegistrar抽象基类,将包路由前缀、中间件、限流收敛到config/<pkg>.php的web节统一管理。 - .env 密钥播种:
EnvSeedService+EnvValueGenerator+EnvSeedProvider契约,安装期幂等合并密钥到.env,杜绝洗掉用户已生效配置。 - 通用 Trait:
ServiceTrait(配置合并)、RegistersMigrations(迁移路径注册)、EventTrait(事件调度)、Conditionable/Tappable(链式编程)。 - Facade 门面:
Auth、Context、JWT提供静态快捷访问。
设计动机与概念
为什么要统一到 ExtPoint?
在引入 ExtPoint 之前,WiseAdmin 生态中多种扩展机制各自为政(payment driver、oauth driver、notification channel 等散落各处)。ExtPoint 将所有可替换实现的扩展点收敛到单一注册中心:
| 概念 | 说明 |
|---|---|
| 扩展点(Point) | 一个能力类别,如 'jwt.provider'、'auth.guard'、'connector' |
| 实现(Implementation) | 扩展点下的一个具体注册项(类名或工厂闭包 + 优先级) |
| 调度策略(Policy) | 多个实现时的冲突处理规则:single / multi / routed |
| 点元数据(Meta) | 展示名 / 描述 / 归属模块等,由点的归属方自声明(definePoint(..., $meta) 或 describePoint()),供能力市场自动发现,新增包无需改动框架 |
ExtPoints 类集中定义了所有框架级扩展点的常量,避免字符串硬编码。
认证的多 Guard 模型
AuthManager 通过 ExtPoint('auth.guard', POLICY_ROUTED) 按 Guard 名称精确发现实现。默认回退逻辑:
admin→SessionGuard(从 Session 读取admin_id)api/member→TokenGuard(从Authorization: Bearer解析 JWT)
业务包可注册自定义 Guard 以覆盖默认行为。
ServiceBus 与 Event 的分工
| 机制 | 用途 |
|---|---|
ServiceBus::publish() |
有名称的分发,处理器通过 bus.handler.{action} 明确注册 |
ServiceBus::broadcast() |
无回复的分发 |
Event::trigger() |
框架内部生命周期事件 |
架构设计
分层结构
┌───────────────────────────────────────────────────────┐
│ 业务层(wise-* 业务包) │
│ wise-game-* / wise-api / wise-jwt / wise-auth ... │
│ 通过 ExtPoint 注册 Guard / JWT 引擎 / WS 服务端 │
├───────────────────────────────────────────────────────┤
│ 基础设施层(wise-support) │
│ ExtPoint 注册中心 │ Auth+JWT │ ServiceBus │ Connector│
│ CircuitBreaker │ DistributedLock │ RateLimiter │
│ ConfigCheck │ RequestContext │ Pipeline │ WebSocket │
│ WiseException │ 门面 │ traits │ .env 播种 │
└───────────────────────────────────────────────────────┘
请求生命周期中的上下文流
Request
→ 全局中间件 RequestContext::setRequestId() # 注入 request_id(同时开启新请求世代)
→ AuthManager::user() 解析用户后 setUser()/setGuard()
→ 各 wise-* 包通过 Context 门面 / ContextualLogger 读取
→ 请求结束 RequestContext::clear() # 防常驻进程泄漏
setRequestId()同时充当请求世代标记:非协程常驻进程共用同一个上下文桶,世代变化时 自动丢弃上一个请求写入的user/guard/tenant_id等数据;AuthManager的 Guard 实例 缓存也按同一世代失效,因此常驻运行时不会复用上一个请求的身份。
目录结构
vendor/wiseadmin/wise-support/
├── composer.json # 包定义(PSR-4: wise\support\ → src/)
├── src/
│ ├── SupportServiceProvider.php # 服务提供者(容器绑定 + 扩展点声明 + 内置实现注册)
│ ├── ExtPoint.php # 扩展点注册中心(SPI Registry)
│ ├── ExtPoints.php # 框架标准扩展点常量
│ ├── Pipeline.php # 洋葱模型管道
│ ├── Once.php # 请求级闭包缓存(call-site memoize)
│ ├── helper.php # 全局辅助函数(str_pipeline / rate_limiter / retry 等)
│ ├── auth/
│ │ ├── AuthManager.php # 统一认证管理器
│ │ ├── GuardInterface.php # 认证守卫契约
│ │ ├── SessionGuard.php # Session 守卫
│ │ └── TokenGuard.php # JWT Token 守卫
│ ├── jwt/
│ │ └── JwtManager.php # JWT 引擎代理(经 ExtPoint 发现引擎)
│ ├── bus/
│ │ ├── ServiceBus.php # 插件服务总线
│ │ ├── Message.php # 总线消息(request/event/reply)
│ │ └── BusHandlerJob.php # think-queue 异步作业
│ ├── connector/
│ │ ├── ConnectorManager.php # 连接器管理器(含 payment()/sms()/storage() 强类型入口)
│ │ ├── ConnectorInterface.php # 连接器通用契约
│ │ ├── AbstractConnector.php # 连接器抽象基类(通用默认实现)
│ │ ├── ConnectorResult.php # 连接器执行结果
│ │ ├── ConnectorType.php # 连接器类型枚举
│ │ ├── PaymentConnectorInterface.php # 支付领域子接口
│ │ ├── SmsConnectorInterface.php # 短信领域子接口
│ │ ├── StorageConnectorInterface.php # 存储领域子接口
│ │ └── StorageConnectorTrait.php # 存储连接器默认实现 Trait
│ ├── circuit/
│ │ ├── CircuitBreaker.php # 断路器
│ │ └── CircuitBreakerExceptionInterface.php
│ ├── lock/
│ │ ├── DistributedLock.php # 分布式锁(Redis/File/DB)
│ │ └── LockInterface.php # 锁契约
│ ├── rate/
│ │ ├── RateLimiter.php # 通用限流器
│ │ ├── RateLimitInterface.php # 限流接口
│ │ ├── SlidingWindow.php # 滑动窗口算法
│ │ ├── TokenBucket.php # 令牌桶算法
│ │ ├── AtomicCounter.php # 原子递增计数器
│ │ └── traits/RedisHelper.php # Redis 连接获取 trait
│ ├── backoff/
│ │ └── BackoffStrategy.php # 退避策略
│ ├── crypto/
│ │ └── AesGcm.php # AES-256-GCM 加解密
│ ├── config/
│ │ ├── ConfigCheckCollector.php # 配置检查收集器
│ │ ├── ConfigCheckInterface.php # 配置检查契约
│ │ ├── CheckLevel.php # 检查严重级别枚举
│ │ └── CheckResult.php # 检查结果值对象
│ ├── context/
│ │ ├── RequestContext.php # 请求级上下文容器
│ │ └── ContextualLogger.php # 上下文感知日志记录器
│ ├── env/
│ │ ├── EnvSeedService.php # .env 密钥播种引擎
│ │ └── EnvValueGenerator.php # .env 值生成器
│ ├── contracts/
│ │ └── EnvSeedProvider.php # 包级 .env 播种契约
│ ├── event/
│ │ └── WiseEvents.php # 事件常量(预留)
│ ├── exception/
│ │ ├── WiseException.php # 统一异常基类
│ │ ├── GeneralErrorCode.php # 框架级通用错误码
│ │ ├── ErrorCodeInterface.php # 错误码枚举契约
│ │ ├── ExceptionRendererInterface.php # 异常渲染器接口
│ │ └── DefaultExceptionRenderer.php # 默认 JSON 渲染器
│ ├── router/
│ │ └── RouteRegistrar.php # 配置驱动路由注册基类
│ ├── ws/
│ │ ├── ConnectionManager.php # WebSocket 连接管理器
│ │ ├── MessageRouter.php # WebSocket 消息路由器
│ │ ├── Connection.php # 连接实体
│ │ ├── WsServerInterface.php # WS 服务端接口
│ │ ├── ChannelInterface.php # 频道接口
│ │ └── handler/PingPongHeartbeat.php # PING/PONG 心跳
│ ├── facade/
│ │ ├── Auth.php # Auth 门面
│ │ ├── Context.php # Context 门面
│ │ └── JWT.php # JWT 门面
│ └── traits/
│ ├── ServiceTrait.php # 包 Service 配置合并
│ ├── RegistersMigrations.php # 迁移路径注册
│ ├── EventTrait.php # 事件调度
│ ├── JsonResponse.php # 统一 JSON 响应
│ ├── ErrorHandler.php # 错误处理
│ ├── Conditionable.php # 条件链式调用
│ └── Tappable.php # tap() 旁观链式调用
└── tests/ # 单元测试(PHPUnit 9)
安装
前置条件
- PHP >= 8.1
- ThinkPHP ^6.0 || ^8.0
topthink/framework
通过 Composer 安装
composer require wiseadmin/wise-support
wise-support 通过 Composer 的 extra.think.services 自动注册 SupportServiceProvider,无需手动配置。
本包不包含
config/目录、无migrate-path,因此 无需发布配置、无需执行迁移,安装后即开箱即用。SupportServiceProvider自动完成以下绑定:
ConfigCheckCollector单例AuthManager单例 +'auth'容器别名'jwt'→JwtManager代理ConnectionManager单例MessageRouter单例(注入 ConnectionManager)- 声明标准扩展点(
auth.guard/jwt.provider/ws.server/exception.renderer/log.driver/ws.handler/ws.heartbeat)- 注册内置实现(
PingPongHeartbeat、DefaultExceptionRenderer)
快速上手
统一认证(AuthManager + Guard)
use wise\support\auth\AuthManager; $auth = app(AuthManager::class); $admin = $auth->user(); // 默认 guard(admin)→ SessionGuard $api = $auth->user('api'); // api guard → TokenGuard(JWT) $userId = $auth->id(); if ($auth->check('admin')) { /* 已登录 */ } if ($auth->guest('api')) { /* 未登录 */ } // 强制认证:未登录抛出 WiseException(401) $user = $auth->authenticate('admin'); // 用户信息变更后刷新 Guard 用户缓存 $auth->refresh('admin');
注册自定义 Guard:
use wise\support\ExtPoint; use wise\support\ExtPoints; use wise\support\auth\GuardInterface; // 实现 GuardInterface class WechatGuard implements GuardInterface { /* ... */ } ExtPoint::register(ExtPoints::AUTH_GUARD, 'wechat', WechatGuard::class); $wechatUser = $auth->user('wechat');
JWT 统一门面
use wise\support\facade\JWT; // 签发 Token(透明代理到 wise-jwt 引擎) $token = JWT::issue(42, ['role' => 'admin']); // subject + claims $pair = JWT::issuePair(42); // Access + Refresh 对 // 解析与校验 $payload = JWT::parse($token); // 解析失败抛异常 $payload = JWT::tryParse($token); // 失败返回 null // 续签 / 吊销 $newToken = JWT::refresh($token); JWT::revoke($token);
JWT门面并不实现 JWT 逻辑,而是经container('jwt') → JwtManager → ExtPoint('jwt.provider')发现当前活跃引擎(默认wise-jwt包注册)。引擎未就绪时JwtManager::isReady()返回false并记录告警日志,TokenGuard会宽容地将认证降级为「未登录」。
扩展点注册中心(ExtPoint)
use wise\support\ExtPoint; // 1. 定义扩展点(通常在 ServiceProvider::register() 中) // 第 4 个参数为「市场展示元数据」:由点的归属方自声明,能力市场自动发现, // 无需框架层硬编码各业务包的点(新增包零改框架)。 ExtPoint::definePoint('example.single', ExtPoint::POLICY_SINGLE, SomeDriverInterface::class); ExtPoint::definePoint('example.routed', ExtPoint::POLICY_ROUTED, SomeRoutedInterface::class); ExtPoint::definePoint('example.multi', ExtPoint::POLICY_MULTI, null, [ 'name' => '示例共享扩展点', 'description' => '……', 'module' => 'wise-example', // 归属包,用于按来源分组 'category' => '示例', ]); // 1b. 点已定义、但描述归其归属包(或运行时动态点):可独立声明 / 更新元数据 ExtPoint::describePoint('example.multi', ['name' => '示例共享扩展点']); // 读取(能力市场等消费方使用) $meta = ExtPoint::getPointMeta('example.multi'); // 单点元数据 $allMeta = ExtPoint::describedPoints(); // 所有自声明元数据的点([point => meta]) // 2. 注册实现 ExtPoint::register('example.single', 'alpha', AlphaDriver::class); ExtPoint::register('example.single', 'beta', BetaDriver::class, 20); // priority 越小越优先 ExtPoint::register('example.routed', 'github', GithubDriver::class); // 3. 消费方使用 $active = ExtPoint::active('example.single'); // 当前活跃实现 $all = ExtPoint::all('example.multi'); // 所有实现 $github = ExtPoint::get('example.routed', 'github'); // 指定平台(routed) // 其他 ExtPoint::has('example.single', 'alpha'); // 是否存在 ExtPoint::count('example.multi'); // 实现数量 ExtPoint::points(); // 列出所有已注册扩展点
动态扩展点(
bus.handler.*/ws.handler.*/slot.*前缀)允许在未调用definePoint()时直接register(),便于按需注册。
能力市场解耦(两条通道)
消费方(如能力市场)需要感知「实现状态」时不内联判断业务包,而由业务包自注册给 ExtPoint:
use think\facade\Event; use wise\support\event\CapabilityToggled; // ① 能力启停事件(写侧):宿主绑定派发器(app\AppService 已默认桥接到 think Event) ExtPoint::bindEventDispatcher(static fn(object $event) => Event::trigger($event)); // 业务包订阅(关注方自行响应,如审计留痕)——事件对象携带强类型字段 Event::listen(CapabilityToggled::class, function (CapabilityToggled $e) { // $e->point / $e->name / $e->before / $e->after / $e->source }); // ② 实现级展示元数据(读侧):业务包提供单个实现的展示标记 ExtPoint::registerImplementationDescriber(static function (string $point, string $name, mixed $concrete): array { return $point === 'member.feature' ? ['pending' => isPending($name)] : []; }); // 消费方汇总(提供者异常被吞,fail-safe) $meta = ExtPoint::describeImplementation('member.feature', 'demo:achievements', DemoClass::class);
设计要点:
ExtPoint保持零依赖(派发器 / 提供者均为回调注入,未绑定时静默); 监听方与提供者异常一律被吞掉并记录,绝不影响扩展点自身的注册与调度。 事件统一采用事件对象范式(与生态一致:Event::listen(XxxEvent::class, ...)), 不再使用字符串事件名常量。
服务总线(ServiceBus)
use wise\support\bus\ServiceBus; $bus = app(ServiceBus::class); // 请求-响应:同步调用一个处理器并获取回复 $result = $bus->ask('payment.charge', ['amount' => 100]); if ($result->isError()) { throw new \Exception($result->getError()); } // 发布-订阅:异步发布给所有注册处理器 $bus->async()->publish('order.created', ['order_id' => 42]); // 指定队列 + 广播 $bus->async()->onQueue('high_priority')->broadcast('cache.invalidate', ['tag' => 'product_list']); // 检查是否有处理器 if ($bus->canHandle('order.created')) { /* ... */ }
注册处理器(类名或闭包,经 bus.handler.{action} 注册):
use wise\support\ExtPoint; ExtPoint::register('bus.handler.order.created', 'notifier', OrderNotifierHandler::class); // 处理器类:优先调用 handle(Message),否则 __invoke class OrderNotifierHandler { public function handle(Message $message): void { /* 消费 $message->getPayload() */ } }
异步分发依赖
topthink/think-queue。闭包处理器无法序列化,异步模式下会被跳过并记录告警,应注入类名。
分布式锁(DistributedLock)
use wise\support\lock\DistributedLock; // 默认非阻塞(Redis 驱动) $lock = new DistributedLock('redis'); if ($lock->acquire('task:send_sms', 60)) { try { doSomething(); } finally { $lock->release('task:send_sms'); } } // 阻塞模式:等最多 10 秒(指数退避轮询) $lock = new DistributedLock('redis', ['blocking' => true, 'blocking_timeout' => 10]); // 文件驱动(单机零依赖) $lock = new DistributedLock('file', ['lock_dir' => runtime_path() . 'lock']); // 数据库驱动(多机无 Redis) $lock = new DistributedLock('database', ['db_table' => 'system_lock']); // 语法糖:自动 acquire + 执行 + release $lock->withLock('task:send_sms', 60, function () { doSomething(); });
锁表结构由
ensureDbTable()自动创建(字段见 数据库表结构)。Redis 驱动使用SET NX EX+ Lua 原子释放,Token 所有权防止误删他进程的锁。
语义与文件锁注意事项:
isAcquired($key)是占用语义:只要锁被任意进程持有就返回true(redis / file / database 三驱动一致)。 需要判断「本实例是否持有」(例如释放前自检)请用DistributedLock::isHeld($key);接口LockInterface的方法集保持acquire / release / isAcquired三项不变。- file 驱动不会删除锁文件:flock 的互斥性绑定在 inode,删除目录项会让阻塞等待者锁住已删除的 inode、 而新进程
fopen('c+')锁住新 inode,二者同时视为持有锁(互斥失效)。因此lock_dir下会保留.lock文件(陈旧文件在 flock 语义下无害),请按需为lock_dir配置轮转/清理策略。- file 驱动仅适用于单机多进程互斥;多机共享目录(NFS/SMB)下 flock 语义不可靠,请改用 redis / database 驱动。
断路器(CircuitBreaker)
use wise\support\circuit\CircuitBreaker; $cb = new CircuitBreaker('payment_service', cache('redis'), [ 'failure_threshold' => 5, // 连续失败 5 次熔断 'timeout' => 30, // 冷却 30 秒 'key_prefix' => 'wise_api:default:cb', ]); if (!$cb->isAvailable()) { return ['error' => 'Service unavailable']; // 熔断中,直接拒绝 } try { $result = callExternalService(); $cb->reportSuccess(); return $result; } catch (\Throwable $e) { $cb->reportFailure(); throw $e; } // 查询状态 $state = $cb->getState(); // closed | open | half_open $fails = $cb->getFailureCount(); $cb->reset(); // 手动恢复
状态机:CLOSED →(连续失败 N 次)→ OPEN →(冷却 T 秒)→ HALF_OPEN →(探测成功)→ CLOSED。状态存于 Redis,支持分布式部署。
限流(RateLimiter / SlidingWindow / TokenBucket)
use wise\support\rate\RateLimiter; // 生产环境:使用 Redis 驱动(原子操作,避免 TOCTOU) $limiter = RateLimiter::perMinute(60)->via('redis')->by("api:user:{$userId}"); if ($limiter->tooManyAttempts()) { throw new RuntimeException('请求过于频繁'); } $remaining = $limiter->hit(); // 记录访问并返回剩余次数 $limiter->hit(10); // 批量操作加权计数 $headers = $limiter->toHeaders(); // X-RateLimit-* + Retry-After $limiter->onLimitReached(function ($key, $max) { Log::warning("限流触发: {$key}, 上限: {$max}"); });
工厂方法:perMinute / perMinutes / perHour / perHours / perDay / for(max, seconds) / forRequest。
滑动窗口 / 令牌桶(Redis Lua 原子 + 降级):
use wise\support\rate\SlidingWindow; use wise\support\rate\TokenBucket; // 滑动窗口:精确时间窗口,支持 member 去重 $sw = new SlidingWindow(); $res = $sw->attempt('api:login', ['limit' => 10, 'window' => 60]); // ['allowed' => bool, 'current' => int, 'remaining' => int] // 令牌桶:平滑突发 + 多令牌消费 $tb = new TokenBucket(); $res = $tb->attempt('api:search', ['capacity' => 60, 'rate' => 1.0, 'consumed' => 1]); // ['allowed' => bool, 'remaining' => float, 'retry_after_ms' => int]
Redis 不可用时的策略(默认拒绝):
- 优先走
setFallbackLimiter()注册的降级限流器;没有降级器时默认 fail-closed(拒绝放行) —— 「判定不了配额」不应被默认解释为「允许」。可用性优先的入口显式选择放行:(new SlidingWindow())->failOpen()->attempt(...)、(new TokenBucket())->failOpen()->attempt(...)。- 兼容旧写法:单次调用仍可传
['fail_closed' => true]强制拒绝。RateLimiter::hit()的窗口语义:原子inc()分支只在首次创建计数键时写入窗口 TTL(固定窗口); 当前缓存驱动没有inc()时走非原子回退分支,该分支每次写入都携带窗口 TTL(否则窗口永不重置), 因此该分支表现为滑动窗口,且并发下仍可能丢计数 —— 生产请via('redis')。
重试与退避(retry / BackoffStrategy)
use wise\support\backoff\BackoffStrategy; // 失败重试,指数退避(500ms 起步,封顶 30s),仅重试可恢复的异常 $result = retry(5, fn() => callApi(), BackoffStrategy::exponential(500, 30000)); // 动态退避闭包 + 条件过滤 + 失败回调 $result = retry( 3, fn($attempt) => callApi($attempt), fn($attempt) => $attempt * 1000, // 动态 sleep 毫秒数 fn(\Throwable $e) => $e instanceof TransientException, // 仅重试瞬时异常 fn(\Throwable $e, $attempt) => Log::warning("第{$attempt}次失败: {$e->getMessage()}"), ); // 队列 Job 的 backoff() 方法 class MyJob { public function backoff(): array { return BackoffStrategy::linear(1000)->toArray(5); // [1000,2000,3000,4000,5000] } }
管道(Pipeline)与请求级缓存(Once)
use wise\support\Pipeline; $result = (new Pipeline()) ->send($data) ->through([ TrimStringsPipe::class, // 类名 → 调用 handle() fn($data, $next) => $next(array_map('strtolower', $data)), // 闭包 ]) ->then(fn($data) => DataModel::create($data)); // 最终回调 // 辅助函数 $result = str_pipeline($data)->through([...])->then(fn($d) => $d);
use wise\support\Once; // 同一 call-site 的同一闭包在同一请求内只执行一次 function getConfig(): array { return Once::memoize(fn() => include config_path('complex.php')); } // 或使用 once() 辅助函数 $cfg = once(fn() => include config_path('complex.php')); Once::forget($callback); // 强制重算 Once::flush(); // 清空所有缓存
请求上下文与链路日志(RequestContext / ContextualLogger)
use wise\support\context\RequestContext; use wise\support\context\ContextualLogger; use wise\support\facade\Context; // 全局中间件入口 RequestContext::setRequestId(bin2hex(random_bytes(8))); RequestContext::set('locale', 'zh-CN'); // 认证后自动注入 user/guard(AuthManager::user() 已完成) $userId = RequestContext::getUserId(); $requestId = RequestContext::getRequestId(); $tenantId = RequestContext::getTenantId(); // 请求结束清理(防 Swoole/Workerman 常驻进程泄漏) RequestContext::clear(); // 上下文感知日志:自动注入 request_id / user_id / guard ContextualLogger::info('支付成功', ['order_id' => 42]); ContextualLogger::channel('backup')->error('备份失败', ['task_id' => 1]);
WebSocket 基础设施
wise-support 不负责建立底层 Socket 连接(那是 Swoole/Workerman 的事),而是提供连接元数据管理与消息路由:
use wise\support\ws\ConnectionManager; use wise\support\ws\MessageRouter; $manager = app(ConnectionManager::class); $router = app(MessageRouter::class); // 连接建立 / 关闭 $manager->onOpen($fd, ['user_id' => 42, 'ip' => '127.0.0.1']); $manager->onClose($fd); // 消息发送 $manager->sendToUser(42, json_encode(['action' => 'notification', 'msg' => 'hi'])); $manager->sendToRoom('game_room_123', json_encode(['action' => 'player_joined']), $excludeFd); $manager->broadcast($message); // 房间管理 $manager->joinRoom($fd, 'game_room_123'); $manager->leaveRoom($fd, 'game_room_123'); // 心跳 $manager->heartbeat($fd); $timeoutFds = $manager->checkHeartbeat(60); // 60 秒无心跳的连接 // 统计 $stats = $manager->stats(); // connections / users / rooms / channels
消息路由(JSON 格式 {"action": "...", "room": "...", "data": {...}}):
$router->dispatch($fd, $rawJsonMessage); // 注册自定义处理器 ExtPoint::register('ws.handler.game.frame_sync', 'frame_sync', FrameSyncHandler::class);
频道接口(ChannelInterface)实现:
use wise\support\ws\ChannelInterface; class FrameSyncChannel implements ChannelInterface { public function getName(): string { return 'game.frame_sync'; } public function onMessage(int $fd, array $data): void { /* 业务处理 */ } public function onJoin(int $fd): void {} public function onLeave(int $fd): void {} }
通过
ExtPoint('ws.server', SINGLE)注册WsServerInterface实现(Swoole/Workerman),PingPongHeartbeat内置处理器处理{"type":"ping"}→ 回复{"type":"pong","time":...}。
鉴权预检与心跳的执行顺序:
- 所有最终进入业务分发的帧都会经过
MessageRouter::authorize()注册的预检回调 —— 包括action为ping/pong、但心跳层未接管的帧(它们实际会调用ws.handler.ping|pong或bus.handler.ping|pong,属业务分发)。回调入参含权威action键,判断依据不再是客户端自选的 body。- 心跳在预检之前执行(控制消息不能被业务鉴权拦住):未绑定用户(未登录)的连接只放行内置
PingPongHeartbeat,其余ws.heartbeat实现(含闭包)会被跳过并记一条 debug 日志。- 仅做心跳、不把
ping/pong当业务 action 的部署无需改动;若确实暴露了ping/pong业务处理器, 需在预检回调里显式放行。- 业务
data只取帧内data字段:缺省即为空数组,帧顶层字段(action/room/ 客户端自选的user_id)不会并入业务 body;权威user_id始终由服务端连接状态推导。
连接器(Connector)
use wise\support\connector\ConnectorManager; use wise\support\connector\ConnectorType; $manager = app(ConnectorManager::class); // 按名称获取 $wechat = $manager->driver('wechat_pay'); $result = $wechat->execute('charge', ['amount' => 100, 'currency' => 'CNY']); if ($result->isSuccess()) { $data = $result->getData(); } // 获取活跃的支付连接器(按类型) $payment = $manager->activeFor(ConnectorType::PAYMENT); // 健康检查 $health = $manager->healthCheck('wechat_pay'); $allHealth = $manager->healthCheckAll(); // 配置管理 $schema = $manager->getConfigSchema('wechat_pay'); // 管理后台渲染配置表单 $manager->setConfig('wechat_pay', ['api_key' => 'xxx']);
注册连接器:
use wise\support\ExtPoint; use wise\support\ExtPoints; use wise\support\connector\ConnectorInterface; use wise\support\connector\ConnectorType; class WechatPayConnector implements ConnectorInterface { public function getId(): string { return 'wechat_pay'; } public function getName(): string { return '微信支付'; } public function getType(): ConnectorType { return ConnectorType::PAYMENT; } public function execute(string $action, array $input, array $context = []): \wise\support\connector\ConnectorResult { /* ... */ } // ... healthCheck / getConfigSchema / setConfig / getConfig } ExtPoint::register(ExtPoints::EXTERNAL_CONNECTOR, 'wechat_pay', WechatPayConnector::class);
领域子接口(推荐):外部系统能力建议实现领域子接口并继承 AbstractConnector,
业务代码经 ConnectorManager 的强类型入口消费(instanceof 断言收口在 Manager 内):
| 领域 | 子接口 | 强类型入口 |
|---|---|---|
| 支付 | PaymentConnectorInterface(charge / queryOrder / refund) |
$manager->payment() |
| 短信 | SmsConnectorInterface(send / sendBatch) |
$manager->sms() |
| 存储 | StorageConnectorInterface(组合 ConnectorInterface + StorageDriverInterface) |
$manager->storage($name = null) |
// 业务侧:强类型调用(推荐) app(ConnectorManager::class)->payment()->charge(['out_trade_no' => 'T1', 'amount' => 100]); // 管理后台 / AI:只依赖通用契约,用 getActions()/getConfigSchema() 自动渲染 foreach (app(ConnectorManager::class)->all() as $type => $connectors) { /* ... */ }
存储驱动可
use StorageConnectorTrait补齐通用方法,仅需继续实现存储业务方法。payment.driver/sms.provider/storage.driver/oauth.driver/notification.channel/anti_addiction.identity等外部系统扩展点已废除, 能力统一由连接器体系承接;captcha.driver/exception.renderer/workflow.pipe及框架基础设施驱动点(cache/queue/log/ws.server)等空转扩展点亦已清理。
配置检查(ConfigCheck)
use wise\support\config\ConfigCheckInterface; use wise\support\config\ConfigCheckCollector; use wise\support\config\CheckLevel; use wise\support\config\CheckResult; class JwtSecretCheck implements ConfigCheckInterface { public function id(): string { return 'wise-jwt:secret'; } public function package(): string { return 'wise-jwt'; } public function level(): CheckLevel { return CheckLevel::ERROR; } public function check(): CheckResult { return env('JWT_SECRET') ? CheckResult::ok('JWT 密钥已配置') : CheckResult::fail('缺少 JWT_SECRET', '请在 .env 中设置 JWT_SECRET'); } } // 在 ServiceProvider 中注册 app(ConfigCheckCollector::class)->register(new JwtSecretCheck()); // 应用启动时执行检查(失败日志按 5 分钟冷却防刷屏) $failed = app(ConfigCheckCollector::class)->report();
异常与错误码(WiseException)
use wise\support\exception\WiseException; use wise\support\exception\GeneralErrorCode; use wise\support\exception\ErrorCodeInterface; // 直接 new throw new WiseException(1001, '参数错误', 400); // 静态工厂(推荐):自动从枚举取码与默认说明 throw WiseException::of(GeneralErrorCode::NOT_FOUND) ->withContext('resource', 'order'); // 链式构建 throw WiseException::of(GeneralErrorCode::BAD_REQUEST) ->withContextMany(['field' => 'amount', 'raw' => $raw]); // 公开数据 vs 诊断数据:withContext() 会进响应体,withDiagnosticContext() 只进日志 throw WiseException::of(GeneralErrorCode::SERVICE_UNAVAILABLE) ->withContext('service', 'pay') // 客户端可见 ->withDiagnosticContext('upstream_body', $raw) // 仅日志(SQL/路径/上游报文放这里) ->withDiagnosticContextMany(['trace_id' => $traceId]); // 各包定义自有错误码枚举(号段互不重叠) enum ErrorCode: int implements ErrorCodeInterface { case PAY_SIGN_INVALID = 2201; public function defaultMessage(): string { return match ($this) { self::PAY_SIGN_INVALID => '支付签名验证失败', }; } } throw WiseException::of(ErrorCode::PAY_SIGN_INVALID, '自定义提示', 400);
GeneralErrorCode 框架级通用码(100~999):
| 分类 | 号段 | 枚举 |
|---|---|---|
| HTTP 语义 | 100~199 | BAD_REQUEST=100 UNAUTHORIZED=101 FORBIDDEN=102 NOT_FOUND=103 METHOD_NOT_ALLOWED=104 TOO_MANY_REQUESTS=105 SERVER_ERROR=150 SERVICE_UNAVAILABLE=151 |
| 校验错误 | 200~299 | VALIDATION_FAILED=200 REQUIRED_FIELD=201 INVALID_FORMAT=202 |
| 基础设施 | 300~399 | CONFIG_ERROR=300 DB_ERROR=301 CACHE_ERROR=302 IO_ERROR=303 NETWORK_ERROR=304 THIRD_PARTY_ERROR=305 EXT_POINT_NOT_DEFINED=306 |
控制器统一响应(Trait):
use wise\support\traits\JsonResponse; class MyController { use JsonResponse; public function index() { return $this->success('操作成功', $data); return $this->error('参数错误', $errors, 1001); return $this->exceptionResponse($e); // WiseException → 统一 JSON return $this->circuitResponse($e); // 熔断 → HTTP 503 + Retry-After } }
.env 密钥播种(EnvSeedService)
use wise\support\env\EnvSeedService; // 一键播种:发现 → 收集 → 幂等合并进 .env $service = new EnvSeedService(); $report = $service->seed($envPath, $vendorDir); // ['written' => [...], 'skipped' => [...], 'warnings' => [...]]
并发安全与落盘方式:
- 整段「读 → 改 → 写」在独占锁内完成;锁文件位于系统临时目录 (
wise-env-seed-<md5(.env 绝对路径)>.lock),不锁.env本身。- 提交使用同目录临时文件 + 原子 rename,不再就地
ftruncate: 写入失败不会把既有.env截断,只会留下一条 warning(原文件保持不动)。- 拿不到锁时跳过播种并写入 warning(绝不在无锁状态下改写
.env)。 并发安装请让两个入口(Web 安装器 /env:seed命令)依赖该锁串行执行。EnvValueGenerator生成 RSA 私钥时,在写入内容前就把文件设为 0600 (独占创建 → 设权限 → 写内容 → 原子改名),并在 POSIX 上校验实际模式; 无法限制为 0600 时删除该文件并抛RuntimeException(不再「静默 0644」)。 Windows 的chmod无法限制读权限,请用 ACL 保证密钥目录仅服务账户可读。
各包声明播种契约(实现 EnvSeedProvider):
use wise\support\contracts\EnvSeedProvider; class WiseJwtServiceProvider implements EnvSeedProvider { public static function envSeeds(): array { return [ 'JWT_SECRET' => [ 'type' => 'secret', 'generator' => 'random_bytes_32', 'comment' => 'JWT 签名密钥(64 位十六进制随机串)', ], 'OPENAI_API_KEY' => [ 'type' => 'external', // 绝不写假值,仅留空占位 'comment' => 'OpenAI 密钥,请在下方手动填写', ], ]; } }
幂等约束:任意键在
.env中已存在时绝不覆盖,保证重装/二次安装不会洗掉用户已生效的密钥。生成策略为封闭集合:random_bytes_32/hmac_salt/uuid/rsa_2048_pem。
辅助函数
src/helper.php 提供的全局函数:
| 函数 | 说明 |
|---|---|
str_pipeline($passable) |
创建 Pipeline 实例 |
rate_limiter($max, $decaySeconds) |
创建 RateLimiter 实例 |
array_only_dot($array, $keys) |
按 . 多级路径提取数组键 |
retry($times, $callback, $sleepMs, $when, $onFailure) |
失败重试原语 |
once($callback) |
请求级闭包缓存 |
request_id() |
获取当前请求 ID |
context($key, $default) |
获取/设置请求上下文 |
wex($errorCode, $message, $httpCode, $context) |
创建 WiseException 快捷方式 |
auth_user($guard) |
获取当前登录用户 |
auth_id($guard) |
获取当前登录用户 ID |
API / 方法参考
ExtPoint(静态注册中心)
| 方法 | 返回 | 说明 |
|---|---|---|
definePoint(string $point, string $policy = POLICY_SINGLE, ?string $contract = null) |
void |
定义扩展点元信息(策略 + 契约) |
defined(string $point) |
bool |
扩展点是否已定义 |
getDefinition(string $point) |
?array |
获取扩展点定义信息 |
getPolicy(string $point) |
string |
获取扩展点调度策略 |
register(string $point, string $name, string|callable $concrete, int $priority = 100) |
void |
注册一个实现(契约校验 + 策略激活) |
all(string $point) |
array |
获取所有实现(按优先级排序) |
get(string $point, string $name) |
string|callable|null |
获取指定名称的实现 |
active(string $point) |
?array |
获取 SINGLE 策略下的活跃实现 |
setActive(string $point, string $name, bool $active) |
bool |
设置实现活跃状态;安全相关扩展点(auth.guard / jwt.provider / connector / exception.renderer)需经 bindOverrideAuthorizer() 授权,未授权返回 false 且不改动状态 |
bindOverrideStore(?callable $loader, ?callable $saver = null) |
void |
注入活跃覆盖表的持久化存储(能力市场) |
bindOverrideAuthorizer(?callable $authorizer) |
void |
注入活跃变更授权回调 fn(string $point, string $name, bool $active): bool;未绑定时安全相关扩展点的活跃变更一律拒绝(fail-closed) |
SECURITY_RELEVANT_POINTS |
string[] |
安全相关扩展点清单 |
resolve(string $point, ?string $routeKey = null, ?string $strategy = null) |
array |
按策略解析扩展点 |
unregister(string $point, string $name) |
void |
注销实现 |
has(string $point, string $name) |
bool |
是否存在指定实现 |
isEmpty(string $point) |
bool |
扩展点下是否有实现 |
count(string $point) |
int |
实现数量 |
points() |
array |
列出所有已注册扩展点名称 |
getUndefinedRegistrations() |
array |
未定义即注册的诊断记录 |
reset() |
void |
重置所有注册数据(测试用) |
AuthManager / GuardInterface
AuthManager:
| 方法 | 返回 | 说明 |
|---|---|---|
user(?string $guard = null) |
?object |
获取当前登录用户(同步注入 Context) |
id(?string $guard = null) |
string|int|null |
获取用户 ID |
check(?string $guard = null) |
bool |
是否已登录 |
guest(?string $guard = null) |
bool |
是否未登录 |
authenticate(?string $guard = null) |
object |
强制认证,未登录抛 WiseException(401) |
guard(string $name) |
GuardInterface |
获取 Guard 实例(ExtPoint 发现 / 回退内置) |
registerGuard(string $name, GuardInterface $guard) |
self |
手动注册 Guard |
setDefaultGuard(string $guard) / getDefaultGuard() |
— | 设置 / 获取默认 Guard |
getRegisteredGuards() |
array |
列出所有已注册 Guard |
refresh(?string $guard = null) |
self |
刷新 Guard 用户缓存 |
clearCache() |
void |
清除 Guard 实例缓存 |
GuardInterface:user(): ?object、id(): string|int|null、check(): bool、guest(): bool、setUser(object $user): void、getName(): string、resetUser(): void。
SessionGuard 额外:setSessionKey(string)、getSessionKey(): string、setUserResolver(callable)。
AuthManager 的 Guard 实例缓存按请求世代失效(以 RequestContext 的 request_id 为标记):同一请求内保留记忆化,世代变化时丢弃,因此常驻运行时不会复用上一个请求的 Guard 及其 principal 缓存。
TokenGuard 额外:setUserResolver(callable)、allowParamToken(bool)(默认关闭 query/cookie 通道)、failClosedOnEngineUnavailable(bool)(引擎不可用时抛 503 而不是降级为访客,默认关闭)。引擎不可用(未注册 / 初始化失败)与凭据非法被区分处理:前者不锁存为「确定未登录」,同一请求内可重试。
TokenGuard 额外:setUserResolver(callable)、autoSetContext(bool)、getPayload(): ?array。
JwtManager(代理)
| 方法 | 返回 | 说明 |
|---|---|---|
getEngine(?string $guard = null) |
object |
获取 JWT 引擎(经 ExtPoint('jwt.provider'));传守卫名时按 guard 解析并缓存,provider 可实现为 fn(?string $guard = null) 以返回守卫专属引擎 |
isReady(?string $guard = null) |
bool |
引擎是否就绪 |
clearCache() |
void |
清除引擎缓存 |
__call($method, $args) |
mixed |
透明转发到真实引擎 |
JWT 门面(由 wise-jwt 引擎提供,示意):issue($subject, $claims, $ttl)、issuePair($subject)、parse($token)、tryParse($token)、refresh($token)、revoke($token)、shouldRefresh($payload)、ttlOf($payload)。
ServiceBus / Message
ServiceBus:
| 方法 | 返回 | 说明 |
|---|---|---|
ask(string $action, array $payload, array $context) |
Message |
请求-响应(始终同步) |
publish(string $action, array $payload, ?callable $onReply) |
void |
发布-订阅 |
broadcast(string $action, array $payload) |
void |
广播(单处理器异常不中断) |
canHandle(string $action) |
bool |
是否有处理器 |
listHandlers(string $action) |
array |
列出处理器 |
async() / sync() |
self |
切换异步/同步分发 |
onQueue(string $queueName) |
self |
指定异步队列 |
setLogger(LoggerInterface) |
self |
设置日志记录器 |
Message(静态工厂):request($action, $payload, $context)、event($action, $payload, $context)、reply($original, $payload, $context)、error($original, $error, $code);读取:getId()/getType()/getAction()/getPayload()/get($key, $default)/getContext()/getCorrelationId()/isError()/getError()/getErrorCode()/toArray()、Message::fromArray()。
ConnectorManager / ConnectorInterface
ConnectorManager:driver(string $name)、activeFor(ConnectorType $type)、all()、healthCheck(string $name)、healthCheckAll()、getConfigSchema(string $name)、setConfig(string $name, array)、clearCache();领域便捷入口:payment(): PaymentConnectorInterface、sms(): SmsConnectorInterface、storage(?string $name = null): StorageConnectorInterface(内部完成契约断言收口)。
AbstractConnector:连接器抽象基类,为 ConnectorInterface 提供通用默认实现(getId/getName/getDescription/getActions/execute/healthCheck/getConfigSchema/setConfig/getConfig),子类只需实现 getType() 与领域方法。
ConnectorInterface:getId(): string、getName(): string、getType(): ConnectorType、getDescription(): string、getActions(): array、execute(string $action, array $input, array $context): ConnectorResult、healthCheck(): bool、getConfigSchema(): array、setConfig(array)、getConfig(): array。
ConnectorType 枚举:OAUTH / WEBHOOK / API / MESSAGE / PAYMENT / STORAGE / AI / IOT(含 label() 中文标签)。
CircuitBreaker
| 方法 | 返回 | 说明 |
|---|---|---|
isAvailable() |
bool |
服务是否可用(含冷却→半开自动迁移) |
reportSuccess() |
void |
上报成功 |
reportFailure() |
void |
上报失败(达阈值熔断 / 半开失败重新打开) |
getState() |
string |
当前状态(closed/open/half_open) |
getFailureCount() |
int |
失败次数 |
reset() |
void |
手动恢复 |
setKeyPrefix(string $prefix) |
void |
设置 Redis key 前缀 |
构造参数:$serviceName, $cache, $options = ['failure_threshold'=>5, 'timeout'=>30, 'half_open_max_requests'=>3, 'success_threshold'=>2, 'key_prefix'=>'cb']。
DistributedLock / LockInterface
LockInterface:acquire(string $key, int $ttl): bool、release(string $key): bool、isAcquired(string $key): bool。
DistributedLock 额外:isHeld(string $key): bool(本实例是否持有,与 isAcquired 的占用语义区分)、withLock(string $key, int $ttl, callable $callback, int $waitMs = 0): array(返回 ['acquired'=>bool, 'result'=>mixed])。构造:new DistributedLock('redis'|'file'|'database', $options)。
RateLimiter / RateLimitInterface
RateLimiter 静态工厂:for($max, $decay)、perMinute($max)、perMinutes($max, $minutes)、perHour($max)、perHours($max, $hours)、perDay($max)、forRequest($max, $decay, ?$key)。
流式配置:by($key)、withPrefix($prefix)、via($store)、setCache($cache)、onLimitReached($closure)。
核心操作:tooManyAttempts()、hit($weight=1, $limit=null, $window=null): int、attempts()、remaining()、getLimit()、cooldown()、availableAt()、toHeaders()、clear(),以及 RateLimitInterface 的 attempt($key, $options): array、status($key, $options): array、reset($key)。
SlidingWindow / TokenBucket(实现 RateLimitInterface):attempt($key, $options)、status($key, $options)、reset($key)、setFallbackLimiter(RateLimiter $limiter)。
AtomicCounter::increment($key, $expire, $context): int — 三层降级(Redis Lua → Cache::inc → 非原子兜底)。
BackoffStrategy
| 方法 | 返回 | 说明 |
|---|---|---|
linear(int $stepMs = 1000) |
self |
线性退避:step, step*2, ... |
exponential(int $baseMs = 1000, ?int $maxMs = 60000) |
self |
指数退避(封顶) |
fixed(int $delayMs = 5000) |
self |
固定延迟 |
custom(array $delaysMs) |
self |
自定义延迟数组 |
delayForAttempt(int $attempt) |
int |
计算第 N 次延迟 |
toArray(?int $maxAttempts) |
array |
序列化策略 / 生成延迟数组 |
__invoke(int $attempt) |
int |
可直接作为 retry() 的 $sleepMs |
fromArray(array $data) |
self |
队列反序列化 |
serialize() |
array |
队列传输序列化 |
RequestContext / ContextualLogger
RequestContext(静态):set/get/has/remove/setMany/all/clear、setRequestId/getRequestId、setUser/getUser/getUserId、setTenantId/getTenantId、setGuard/getGuard、setLocale/getLocale、logContext()。
ContextualLogger:channel(string $name): self、静态或实例调用 info/error/warning/debug/notice/alert/critical/emergency/sql($message, $context)。
RouteRegistrar
| 方法 | 说明 |
|---|---|
register() |
静态入口(由 ServiceProvider::boot() 调用) |
group(string $groupKey, callable $definer) |
创建配置驱动的路由分组(enable/prefix/middleware 合并) |
define(array $config) |
抽象方法,子类定义具体路由 |
子类需定义 $configKey(如 'wise-wx'),路由配置统一放在 config/<pkg>.php 的 web 节。
EnvSeedService
discoverProviderClasses(string $vendorDir, &$warnings): array、collectSpecs(array $providerClasses): array、seed(string $envPath, ?string $vendorDir): array、mergeIntoEnv(array $specs, string $envPath): array。
事件 / 扩展点
标准扩展点(ExtPoints)
| 扩展点 | 策略 | 契约 | 说明 |
|---|---|---|---|
auth.guard |
routed | GuardInterface |
认证守卫(admin/api/member) |
jwt.provider |
single | — | JWT 引擎(wise-jwt 注册) |
image.driver |
single | — | 图片处理驱动 |
image.processor |
multi | — | 图片处理器 |
connector |
routed | ConnectorInterface |
外部系统连接器(支付/短信/存储/OAuth/通知/身份等统一入口) |
login.user_resolver |
routed | — | 登录安全用户解析器 |
dashboard.widget |
multi | — | 仪表盘挂件 |
menu.item |
multi | — | 菜单项 |
search.provider |
multi | — | 全局搜索后端(第三方 ES/Meilisearch,内置 provider 兜底) |
bus.handler |
multi | — | 服务总线消息处理器(动态前缀) |
ws.handler / ws.heartbeat |
multi | — | WebSocket 消息/心跳处理器 |
slot.content |
multi | — | 前端插槽内容 |
route.api / route.admin / route.member / route.plugin |
multi | — | 应用路由 |
permission.sync_provider / ai.tool.permission_provider |
multi | — | 权限同步 / AI 工具权限 |
所有标准扩展点元数据可经
ExtPoints::getPointMeta()获取(供 Capability 等管理界面展示)。
事件
本包主要为其他包提供「扩展点」而非直接触发业务事件。src/event/WiseEvents.php 为事件常量预留。内置实现:
ws.heartbeat→PingPongHeartbeat({"type":"ping"}→{"type":"pong","time":...})exception.renderer→DefaultExceptionRenderer(WiseException → toArray();其他 Throwable 仅在app.debug且请求 IP 命中app.debug_allow_ips(或子类覆写isDebugAuthorized())时输出异常 message/file/line,默认隐藏)
ServiceBus::publish/broadcast与本包的事件触发接口(EventTrait::dispatch/dispatchMany/dispatchUntil)共同构成消息/事件分发能力,但ServiceBus是「有名称、显式注册处理器」的机制,与 ThinkPHPEvent生命周期事件互补。
与其他 wise-* 包协作
wise-support 是整个 WiseAdmin 生态的基座,几乎被所有业务包依赖。典型协作关系:
| 场景 | 协作方式 |
|---|---|
| JWT 认证 | wise-jwt 包在 ServiceProvider::boot() 调用 ExtPoint::register(ExtPoints::JWT_PROVIDER, 'wise-jwt', fn() => new Jwt(...));wise-support 的 JWT 门面 / TokenGuard 透明代理到该引擎 |
| RBAC 鉴权 | wise-auth 包实现 GuardInterface 并通过 ExtPoint('auth.guard') 注册;AuthManager 统一读取用户 |
| 外部服务调用 | 业务包(如支付、短信)实现 ConnectorInterface 并注册到 ExtPoint('connector'),经 ConnectorManager 统一调度、熔断、健康检查 |
| 跨插件通信 | 插件经 ExtPoint::register('bus.handler.{action}', ...) 注册处理器,通过 ServiceBus 的 ask/publish/broadcast 交互(可异步进 think-queue) |
| WebSocket 推送 | 游戏/IM 类包实现 WsServerInterface(Swoole/Workerman)与 ChannelInterface,经 ConnectionManager 做按用户/房间推送 |
| 配置检查 | 各包实现 ConfigCheckInterface 注册到 ConfigCheckCollector,report() 统一汇总失败项 |
| .env 播种 | 各包在 ServiceProvider 上实现 EnvSeedProvider::envSeeds(),安装命令经 EnvSeedService 幂等合并密钥 |
| 统一路由 | 各包继承 RouteRegistrar 定义路由,前缀/中间件/限流收敛到各自 config/<pkg>.php 的 web 节 |
| 统一响应与异常 | 各包控制器 use JsonResponse / ErrorHandler,抛 WiseException(ErrorCode 枚举实现 ErrorCodeInterface),经 DefaultExceptionRenderer 输出统一 JSON |
| 配置与静态资源发布 | 包配置由 ThinkPHP vendor:publish(extra.think.config)发布;包内 public/ 静态资源由 wise:vendor:publish(extra.think.publish)发布 |
数据库表结构
本包不包含任何迁移文件(composer.json 无 migrate-path,无 database/ 目录),开箱即用。
但 DistributedLock 的 database 驱动会自动创建锁表(ensureDbTable(),默认逻辑表名 system_lock)。
db_table 传不含前缀的逻辑表名,物理表名由连接配置的 database.prefix 拼出(当前为 ws_system_lock):
CREATE TABLE `{prefix}system_lock` ( `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, `lock_key` VARCHAR(64) NOT NULL COMMENT '锁键(MD5)', `task_name` VARCHAR(255) NOT NULL COMMENT '任务名称', `create_time` DATETIME NOT NULL COMMENT '创建时间', `expires_at` INT UNSIGNED NOT NULL COMMENT '过期时间戳(Unix 秒)', UNIQUE KEY `uk_lock_key` (`lock_key`), KEY `idx_expires_at` (`expires_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分布式锁表';
若使用
DistributedLock('database')且表不存在,会在首次acquire()时自动创建。Redis / File 驱动不依赖此表。 时间列约定:expires_at保持无符号INT(Unix 秒,参与expires_at < time()比较,无符号可避免 2038 溢出);create_time为datetime,与框架时间字段约定一致。wise-crontab的crontab_lock迁移与此结构保持同步。
依赖关系
| 依赖 | 版本 | 说明 |
|---|---|---|
| PHP | >= 8.1 | 核心语言 |
| topthink/framework | ^6.0 || ^8.0 | ThinkPHP 框架(唯一必需依赖) |
开发依赖
| 依赖 | 版本 | 说明 |
|---|---|---|
| phpunit/phpunit | >= 9.0 | 单元测试 |
| mockery/mockery | >= 1.0 | Mock 框架 |
可选协作包(被依赖侧)
- 被依赖:几乎所有
wise-*业务包(wise-jwt、wise-auth、wise-api等)均依赖本包。 - 可选:
topthink/think-queue(ServiceBus 异步分发、BusHandlerJob)、Redis扩展(分布式锁/限流/断路器生产环境)、Swoole/Workerman(WebSocket 服务端)。
开发与测试
cd vendor/wiseadmin/wise-support
composer install --dev
./vendor/bin/phpunit
常见问题
Q: JWT 门面报「JWT 引擎未注册」怎么办?
JWT 门面是代理,必须由 wise-jwt 包(或其他实现)注册 ExtPoint('jwt.provider', 'wise-jwt', ...)。安装 wise-jwt 后即可。也可在任意 ServiceProvider 中注册自定义引擎。
Q: 为什么 Auth::user('api') 返回 null?
可能原因:
- 未安装
wise-jwt(引擎未就绪,TokenGuard宽容降级为未登录); - 请求头缺少
Authorization: Bearer <token>; TokenGuard未设置userResolver(默认只返回{id, jti}基础对象)。
Q: RateLimiter 生产环境用哪个驱动?
必须使用 Redis(->via('redis')),file 驱动存在 TOCTOU 竞态条件(高并发计数丢失),代码中会在降级路径触发 E_USER_WARNING 告警。
Q: 分布式锁的 blocking 模式与阻塞获取有什么区别?
blocking => true 时 acquire() 会按指数退避轮询直到成功或 blocking_timeout 超时;blocking_timeout = 0 时仅 file 驱动支持内核级 flock(LOCK_EX) 原生挂起(需调用方保证锁必然释放)。
Q: 常驻进程(Swoole/Workerman)下上下文为什么必须 clear?
RequestContext 使用静态属性存储。非协程常驻进程(Swoole 常驻 / Workerman / 队列 worker)里所有请求共用一个桶:setRequestId() 检测到 request_id 变化时会先丢弃上一世代的数据,因此身份不会跨请求泄漏;仍建议在请求结束时调用 RequestContext::clear() 显式兜底(协程环境按协程分桶,且协程结束有 defer 回收)。
Q: 如何让配置检查日志不刷屏?
ConfigCheckCollector 默认按检查项 id 使用 shared_file 缓存做 5 分钟冷却,同一失败项在冷却期内只打印一次。可调整 $cooldownSeconds。
许可证
Apache-2.0 License
更多信息请参考 WiseAdmin 框架文档及各 wise-* 业务包 README。