- plugin.json 新增 usage 字段(多行文本),后台插件卡片折叠展示;四个内置插件补齐使用说明 - PageCacheMiddleware 记录命中/未命中计数(按日滚动),仪表盘新增「页面缓存」统计卡(条目数 + 命中率),用于判断前台缓存效果、是否需额外加速 - 文档与测试同步
211 lines
8.5 KiB
Markdown
211 lines
8.5 KiB
Markdown
# 插件开发指南
|
||
|
||
## 目录结构
|
||
|
||
```
|
||
plugins/{vendor}.{name}/
|
||
├── plugin.json # 插件清单(必填)
|
||
├── src/
|
||
│ ├── ServiceProvider.php # 插件入口(继承基类)
|
||
│ ├── ... # 业务代码
|
||
│ └── Filament/ # 可选:后台页面/资源
|
||
├── routes/web.php # 可选:前台路由(boot 时自动加载)
|
||
├── database/migrations/ # 可选:迁移(migrate 时自动加载)
|
||
└── views/ # 可选:视图(命名空间 plugin.{vendor}.{name})
|
||
```
|
||
|
||
## plugin.json
|
||
|
||
```json
|
||
{
|
||
"title": "插件标题",
|
||
"version": "1.0.0",
|
||
"description": "插件描述",
|
||
"usage": "使用说明(多行文本,后台插件卡片「使用说明」折叠展示)",
|
||
"author": "作者",
|
||
"type": "core",
|
||
"provider": "Plugins\\Vendor\\Name\\ServiceProvider",
|
||
"requires": ["neatstudio.payment"],
|
||
"filament_pages": ["Plugins\\Vendor\\Name\\Filament\\Pages\\SettingsPage"],
|
||
"filament_resources": ["Plugins\\Vendor\\Name\\Filament\\Resources\\OrderResource"]
|
||
}
|
||
```
|
||
|
||
| 字段 | 说明 |
|
||
|------|------|
|
||
| `provider` | 入口类 FQCN;缺省时自动推断 `src/ServiceProvider.php` |
|
||
| `requires` | 依赖的其他插件(如会员依赖支付),按 `vendor.name` 引用 |
|
||
| `usage` | 使用说明(换行文本),后台插件卡片可折叠查看 |
|
||
| `filament_pages` / `filament_resources` | 注册到后台的页面/资源(由 `PluginPages` 汇总,统一归入「插件」导航分组) |
|
||
|
||
## 依赖管理(requires 强制校验)
|
||
|
||
- `enable()`:依赖未安装或未启用 → 拒绝启用并提示(后台弹错误通知)
|
||
- `disable()` / `uninstall()`:有已启用的插件依赖它 → 拒绝操作,需先停用上层插件
|
||
- 应用启动时依赖未满足的插件**跳过 boot**,避免运行时缺底层能力
|
||
- 后台插件卡片展示依赖链状态(徽章:✓ 已启用 / ✗ 未安装 / ✗ 未启用),依赖不满足的插件无法启用
|
||
|
||
插件分层示例:`payment`(支付)→ `membership`(会员,requires payment)→ 你的业务插件(requires membership 或 payment)。上层插件可放心假设底层能力存在。
|
||
|
||
## ServiceProvider
|
||
|
||
```php
|
||
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace Plugins\Vendor\Name;
|
||
|
||
use App\Blog\Support\PluginManager;
|
||
use App\Blog\Support\PluginServiceProvider;
|
||
|
||
class ServiceProvider extends PluginServiceProvider
|
||
{
|
||
protected function boot(PluginManager $manager): void
|
||
{
|
||
// 加载前台路由(可选)
|
||
$this->loadRoutes(__DIR__.'/../routes/web.php');
|
||
|
||
// 注册视图命名空间(可选)
|
||
$this->loadViews(__DIR__.'/../views', 'plugin.vendor.name');
|
||
|
||
// 注册动作/过滤器
|
||
$manager->addAction('comment.created', function ($comment) { ... }, 10);
|
||
$manager->addFilter('post.rendered', fn (string $html, $post) => $html, 10);
|
||
}
|
||
}
|
||
```
|
||
|
||
> 注意:插件入口类**不要**命名为 `PluginServiceProvider`(与基类短名冲突会导致 PHP 声明错误),统一用 `ServiceProvider`。
|
||
|
||
## 钩子系统
|
||
|
||
### addAction(hook, callback, priority) — 无返回值
|
||
|
||
| Hook | 参数 | 用途 |
|
||
|------|------|------|
|
||
| `comment.created` | `Comment` | 新评论创建(AI 审核在此监听) |
|
||
| `payment.paid` | `Payment` | 支付成功(按 `$payment->payable` 分发:订阅激活/解锁) |
|
||
| `filament.post_form` | `Schema` | 文章编辑表单追加字段(会员付费设置在此注入) |
|
||
| `filament.post_table` | `Table` | 文章列表追加列/操作/过滤 |
|
||
|
||
### addFilter(hook, callback, priority) — 返回值传给下一个过滤器
|
||
|
||
| Hook | 签名 | 用途 |
|
||
|------|------|------|
|
||
| `post.rendered` | `(string $html, Post $post): string` | 文章渲染后处理(付费内容过滤、paywall) |
|
||
| `seo.structured_data` | `(array $data): array` | 扩展 JSON-LD 结构化数据 |
|
||
| `payment.gateway` | `(array $gateways): array` | 注册支付渠道 |
|
||
|
||
## 后台注入(Filament 表单 / 表格)
|
||
|
||
核心资源的表单/表格会触发 `filament.post_form` / `filament.post_table` 两个钩子,插件可追加自己的字段与列,停用插件后字段自动消失,核心零侵入。
|
||
|
||
> 注意:`Schema::components()` 与 `Table::columns()` 都是**整体替换**语义,追加要这样写:
|
||
> - 表单:`$schema->components([...$schema->getComponents(), Section::make(...)])`
|
||
> - 表格:`$table->pushColumns([...])`
|
||
|
||
示例(给文章加「付费设置」):
|
||
|
||
```php
|
||
$manager->addAction('filament.post_form', function (Schema $schema) {
|
||
$schema->components([
|
||
...$schema->getComponents(),
|
||
Section::make('付费设置')->columns(3)->collapsible()->schema([
|
||
TextInput::make('meta.price')->label('单篇价格(分)')->numeric()->default(0),
|
||
Toggle::make('meta.members_only')->label('会员专享'),
|
||
]),
|
||
]);
|
||
});
|
||
|
||
$manager->addAction('filament.post_table', function (Table $table) {
|
||
$table->pushColumns([
|
||
TextColumn::make('meta.price')->label('单篇价格'),
|
||
IconColumn::make('meta.members_only')->label('会员')->boolean(),
|
||
]);
|
||
});
|
||
```
|
||
|
||
字段用点号路径(`meta.price`)直接映射到 `Post` 的 `meta` JSON。核心保存时会把表单里的 meta 键与已有 meta **合并**,不会覆盖其他插件写入的键(如解锁用户)。
|
||
|
||
## 订单商品化(可购买实体)
|
||
|
||
`Payment` 用多态关联指向被购买的实体(`payable_type` / `payable_id`)。支付成功后的 `payment.paid` 钩子按 `$payment->payable` 分发(`instanceof` 判断),**不要**再用字符串解析业务类型。
|
||
|
||
订单后台的「商品」列与跳转链接由实体上的两个约定方法生成:
|
||
|
||
```php
|
||
public function payableLabel(): string { return '文章:'.$this->title; }
|
||
public function payableUrl(): ?string { return route('filament.admin.resources.posts.edit', $this); }
|
||
```
|
||
|
||
已实现:`App\Models\Post`(单篇解锁)、`MembershipPlan`(会员套餐)。新实体(皮肤授权、插件授权、打赏)实现同样两个方法即自动接入订单展示,支付插件零改动。
|
||
|
||
## 付费内容前台展示(paywall)
|
||
|
||
会员插件在 `post.rendered` 过滤器里处理两种付费形态:
|
||
|
||
- 整篇锁定(`meta.members_only`)→ 渲染 `membership.paywall`(封面 + 标题 + 价格 + 单篇解锁/开通会员按钮)
|
||
- `[paid]` 付费块 → 替换为 `membership.paid-teaser`(内嵌解锁按钮的提示块)
|
||
|
||
缺省视图在 `resources/views/membership/`,主题可覆盖:`themes/{name}/views/membership/paywall.blade.php`。视图内可用 `$post`,按钮沿用 `.btn` 类,`.paywall` / `.paid-teaser` 样式两套内置主题已带。
|
||
|
||
## 迁移
|
||
|
||
插件迁移放在 `database/migrations/`,`app/Providers/AppServiceProvider` 启动时自动注册,`php artisan migrate` 会一并执行(无需手动 --path)。
|
||
|
||
## 打包与安装
|
||
|
||
- 将插件目录打成 ZIP(根目录含 plugin.json)
|
||
- 后台「插件管理」→ 上传 ZIP 安装,或配置远程市场 `MARKET_URL`
|
||
- 内置插件(config/plugins.php enabled 列表)不可卸载,只能停用
|
||
|
||
## 远程市场契约
|
||
|
||
配置 `MARKET_URL` 后,后台「插件管理 / 主题管理」会请求 `{MARKET_URL}/items`(`market.token` 作为 Bearer Token),响应格式:
|
||
|
||
```json
|
||
{
|
||
"items": [
|
||
{
|
||
"name": "vendor.demo",
|
||
"type": "plugin",
|
||
"title": "演示插件",
|
||
"description": "描述",
|
||
"version": "1.0.0",
|
||
"author": "作者",
|
||
"price": 9900,
|
||
"download_url": "https://example.com/vendor.demo.zip"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
- `name`:包键(插件为 `vendor.name`,主题为主题名),**购买授权按此键记录**
|
||
- `type`:`plugin` / `theme`——插件市场 Tab 只显示 plugin,主题市场 Tab 只显示 theme
|
||
- `price`:价格(分),`0` = 免费;`> 0` 显示「购买」按钮,走 `Payment` 订单(可 `payable` 为 `MarketPackage`),支付成功后标记已购,未购买无法安装
|
||
|
||
## 异步任务(配合 Workerman)
|
||
|
||
实现 `App\Blog\Jobs\AiJob` 接口并在 Workerman 常驻进程执行:
|
||
|
||
```php
|
||
use App\Blog\Jobs\AiJob;
|
||
use Illuminate\Bus\Queueable;
|
||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||
|
||
class MyAiJob implements AiJob, ShouldQueue
|
||
{
|
||
use Queueable;
|
||
|
||
public function __construct(public int $id) {}
|
||
|
||
public function handle(\App\Blog\Services\LlmClient $llm): void
|
||
{
|
||
// LlmClient 由容器自动注入
|
||
}
|
||
}
|
||
```
|
||
|
||
入队:`dispatch(new MyAiJob($id))->onQueue('ai')`(Workerman 默认消费 `default,ai` 队列)。
|