feat: 插件使用说明(usage)+ 页面缓存命中统计
- plugin.json 新增 usage 字段(多行文本),后台插件卡片折叠展示;四个内置插件补齐使用说明 - PageCacheMiddleware 记录命中/未命中计数(按日滚动),仪表盘新增「页面缓存」统计卡(条目数 + 命中率),用于判断前台缓存效果、是否需额外加速 - 文档与测试同步
This commit is contained in:
@@ -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', []);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 强制校验)
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,5 +4,6 @@
|
|||||||
"description": "完善 SEO(结构化数据/OG 标签)+ GEO(llms.txt / Markdown 导出,面向 AI 搜索引擎优化)",
|
"description": "完善 SEO(结构化数据/OG 标签)+ GEO(llms.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-LD(BlogPosting/WebSite)自动输出\n- GEO:/llms.txt 站点摘要、/posts/{slug}.md 文章 Markdown 导出\n- /sitemap.xml、/robots.txt 由核心路由提供\n站点名/描述等在「博客设置 → 站点」配置,社交账号在「博客设置 → 社交账号」配置"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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. 依赖支付插件:先配置支付或开启沙箱"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
Reference in New Issue
Block a user