feat: 插件使用说明(usage)+ 页面缓存命中统计

- plugin.json 新增 usage 字段(多行文本),后台插件卡片折叠展示;四个内置插件补齐使用说明
- PageCacheMiddleware 记录命中/未命中计数(按日滚动),仪表盘新增「页面缓存」统计卡(条目数 + 命中率),用于判断前台缓存效果、是否需额外加速
- 文档与测试同步
This commit is contained in:
ak
2026-08-12 03:57:22 +08:00
parent 22df17f02a
commit 01861f8809
10 changed files with 60 additions and 5 deletions
+33
View File
@@ -28,12 +28,16 @@ class PageCacheMiddleware
$ttl = (int) config('blog.page_cache_ttl', 300); $ttl = (int) config('blog.page_cache_ttl', 300);
if ($cached = Cache::get($key)) { if ($cached = Cache::get($key)) {
self::increment('hits');
return response($cached['body'], 200, [ return response($cached['body'], 200, [
'Content-Type' => $cached['type'], 'Content-Type' => $cached['type'],
'Cache-Control' => 'public, max-age='.$ttl, 'Cache-Control' => 'public, max-age='.$ttl,
]); ]);
} }
self::increment('misses');
$response = $next($request); $response = $next($request);
if ($response->getStatusCode() === 200) { if ($response->getStatusCode() === 200) {
@@ -61,6 +65,35 @@ class PageCacheMiddleware
return count((array) Cache::get(self::TAG.'_keys', [])); return count((array) Cache::get(self::TAG.'_keys', []));
} }
/**
* 命中/未命中统计(按日滚动),用于判断前台缓存效果。
*/
public static function stats(): array
{
$hits = (int) Cache::get(self::TAG.'_hits', 0);
$misses = (int) Cache::get(self::TAG.'_misses', 0);
$total = $hits + $misses;
return [
'hits' => $hits,
'misses' => $misses,
'count' => self::count(),
'rate' => $total > 0 ? round($hits / $total * 100, 1) : 0.0,
];
}
public static function resetStats(): void
{
Cache::forget(self::TAG.'_hits');
Cache::forget(self::TAG.'_misses');
}
private static function increment(string $metric): void
{
$key = self::TAG.'_'.$metric;
Cache::put($key, (int) Cache::get($key, 0) + 1, now()->addDay());
}
private static function rememberKey(string $key): void private static function rememberKey(string $key): void
{ {
$keys = (array) Cache::get(self::TAG.'_keys', []); $keys = (array) Cache::get(self::TAG.'_keys', []);
+1
View File
@@ -81,6 +81,7 @@ class PluginManager
'title' => $manifest['title'] ?? $name, 'title' => $manifest['title'] ?? $name,
'version' => $manifest['version'] ?? '0.0.0', 'version' => $manifest['version'] ?? '0.0.0',
'description' => $manifest['description'] ?? '', 'description' => $manifest['description'] ?? '',
'usage' => $manifest['usage'] ?? '',
'author' => $manifest['author'] ?? '', 'author' => $manifest['author'] ?? '',
'enabled' => $this->isEnabled($basename), 'enabled' => $this->isEnabled($basename),
'requires' => $requires, 'requires' => $requires,
+5
View File
@@ -17,6 +17,8 @@ class BlogStats extends BaseWidget
protected function getStats(): array protected function getStats(): array
{ {
$cache = \App\Blog\Support\PageCacheMiddleware::stats();
return [ return [
Stat::make('文章', Post::published()->count()) Stat::make('文章', Post::published()->count())
->description('总浏览量 '.number_format((float) Post::sum('views'))) ->description('总浏览量 '.number_format((float) Post::sum('views')))
@@ -26,6 +28,9 @@ class BlogStats extends BaseWidget
->icon('heroicon-o-chat-bubble-left-right'), ->icon('heroicon-o-chat-bubble-left-right'),
Stat::make('用户', User::count()) Stat::make('用户', User::count())
->icon('heroicon-o-users'), ->icon('heroicon-o-users'),
Stat::make('页面缓存', $cache['count'].' 条')
->description('命中率 '.$cache['rate'].'%(命中 '.$cache['hits'].' / 未命中 '.$cache['misses'].',按日滚动)')
->icon('heroicon-o-bolt'),
]; ];
} }
} }
+2
View File
@@ -21,6 +21,7 @@ plugins/{vendor}.{name}/
"title": "插件标题", "title": "插件标题",
"version": "1.0.0", "version": "1.0.0",
"description": "插件描述", "description": "插件描述",
"usage": "使用说明(多行文本,后台插件卡片「使用说明」折叠展示)",
"author": "作者", "author": "作者",
"type": "core", "type": "core",
"provider": "Plugins\\Vendor\\Name\\ServiceProvider", "provider": "Plugins\\Vendor\\Name\\ServiceProvider",
@@ -34,6 +35,7 @@ plugins/{vendor}.{name}/
|------|------| |------|------|
| `provider` | 入口类 FQCN;缺省时自动推断 `src/ServiceProvider.php` | | `provider` | 入口类 FQCN;缺省时自动推断 `src/ServiceProvider.php` |
| `requires` | 依赖的其他插件(如会员依赖支付),按 `vendor.name` 引用 | | `requires` | 依赖的其他插件(如会员依赖支付),按 `vendor.name` 引用 |
| `usage` | 使用说明(换行文本),后台插件卡片可折叠查看 |
| `filament_pages` / `filament_resources` | 注册到后台的页面/资源(由 `PluginPages` 汇总,统一归入「插件」导航分组) | | `filament_pages` / `filament_resources` | 注册到后台的页面/资源(由 `PluginPages` 汇总,统一归入「插件」导航分组) |
## 依赖管理(requires 强制校验) ## 依赖管理(requires 强制校验)
+2 -1
View File
@@ -7,5 +7,6 @@
"provider": "Plugins\\Neatstudio\\AiModeration\\ServiceProvider", "provider": "Plugins\\Neatstudio\\AiModeration\\ServiceProvider",
"filament_pages": [ "filament_pages": [
"Plugins\\Neatstudio\\AiModeration\\Filament\\Pages\\AiSettings" "Plugins\\Neatstudio\\AiModeration\\Filament\\Pages\\AiSettings"
] ],
"usage": "1. 后台「AI 设置」配置 Base URL / API Key / 模型(OpenAI 兼容,支持 DeepSeek/通义等)\n2. 新评论会自动进入 AI 审核队列(可在「AI 设置」页实时查看进度)\n3. 文章列表点「AI 润色」生成润色稿,确认后采纳\n4. 需要消费者运行:php artisan workerman:serve 或 php artisan queue:work --queue=default,ai"
} }
+2 -1
View File
@@ -4,5 +4,6 @@
"description": "完善 SEO(结构化数据/OG 标签)+ GEOllms.txt / Markdown 导出,面向 AI 搜索引擎优化)", "description": "完善 SEO(结构化数据/OG 标签)+ GEOllms.txt / Markdown 导出,面向 AI 搜索引擎优化)",
"author": "LaraLog", "author": "LaraLog",
"type": "core", "type": "core",
"provider": "Plugins\\Neatstudio\\BlogSeo\\ServiceProvider" "provider": "Plugins\\Neatstudio\\BlogSeo\\ServiceProvider",
"usage": "无需额外配置,启用即生效:\n- SEO:页面 title/description/keywords、OG/Twitter 标签、JSON-LDBlogPosting/WebSite)自动输出\n- GEO/llms.txt 站点摘要、/posts/{slug}.md 文章 Markdown 导出\n- /sitemap.xml、/robots.txt 由核心路由提供\n站点名/描述等在「博客设置 → 站点」配置,社交账号在「博客设置 → 社交账号」配置"
} }
+5 -2
View File
@@ -5,9 +5,12 @@
"author": "LaraLog", "author": "LaraLog",
"type": "core", "type": "core",
"provider": "Plugins\\Neatstudio\\Membership\\ServiceProvider", "provider": "Plugins\\Neatstudio\\Membership\\ServiceProvider",
"requires": ["neatstudio.payment"], "requires": [
"neatstudio.payment"
],
"filament_resources": [ "filament_resources": [
"Plugins\\Neatstudio\\Membership\\Filament\\Resources\\MembershipPlanResource", "Plugins\\Neatstudio\\Membership\\Filament\\Resources\\MembershipPlanResource",
"Plugins\\Neatstudio\\Membership\\Filament\\Resources\\SubscriptionResource" "Plugins\\Neatstudio\\Membership\\Filament\\Resources\\SubscriptionResource"
] ],
"usage": "1. 后台创建会员套餐(价格/时长/权限)\n2. 文章编辑页「付费设置」可设单篇价格或会员专享;正文用 [paid]...[/paid] 标记付费块\n3. 前台 /membership 展示套餐,游客解锁或开通会员后可见付费内容\n4. 依赖支付插件:先配置支付或开启沙箱"
} }
+2 -1
View File
@@ -10,5 +10,6 @@
], ],
"filament_resources": [ "filament_resources": [
"Plugins\\Neatstudio\\Payment\\Filament\\Resources\\PaymentResource" "Plugins\\Neatstudio\\Payment\\Filament\\Resources\\PaymentResource"
] ],
"usage": "1. 后台「支付设置」配置支付宝/微信凭据,或开启沙箱模式(模拟支付,无需密钥)\n2. 会员套餐、单篇付费文章、市场商品购买都会创建订单并跳转支付\n3. 支付结果在「订单」页查看;沙箱模式走模拟收银台\n4. 真实支付需公网可访问的回调地址(部署后或内网穿透)"
} }
@@ -17,6 +17,13 @@
<p class="mt-1 flex-1 text-sm text-gray-500 dark:text-gray-400">{{ $plugin['description'] }}</p> <p class="mt-1 flex-1 text-sm text-gray-500 dark:text-gray-400">{{ $plugin['description'] }}</p>
<p class="mt-2 text-xs text-gray-400">v{{ $plugin['version'] }} · {{ $plugin['vendor'] }}.{{ $plugin['name'] }}</p> <p class="mt-2 text-xs text-gray-400">v{{ $plugin['version'] }} · {{ $plugin['vendor'] }}.{{ $plugin['name'] }}</p>
@if(! empty($plugin['usage']))
<details class="mt-2">
<summary class="cursor-pointer text-xs text-primary-600">使用说明</summary>
<div class="mt-1 whitespace-pre-wrap text-xs text-gray-500 dark:text-gray-400">{{ $plugin['usage'] }}</div>
</details>
@endif
@if(! empty($plugin['dependencies'])) @if(! empty($plugin['dependencies']))
<div class="mt-2 flex flex-wrap items-center gap-1"> <div class="mt-2 flex flex-wrap items-center gap-1">
<span class="text-xs text-gray-400">依赖:</span> <span class="text-xs text-gray-400">依赖:</span>
+1
View File
@@ -73,6 +73,7 @@ class PluginDependencyTest extends TestCase
$this->assertSame(['neatstudio.payment'], $member['requires']); $this->assertSame(['neatstudio.payment'], $member['requires']);
$this->assertTrue($member['dependencies'][0]['ok']); $this->assertTrue($member['dependencies'][0]['ok']);
$this->assertEmpty($member['dependency_errors']); $this->assertEmpty($member['dependency_errors']);
$this->assertNotEmpty($member['usage']);
// 停用依赖后,依赖状态应变为失败 // 停用依赖后,依赖状态应变为失败
$manager->disable('neatstudio.membership'); $manager->disable('neatstudio.membership');