# 插件开发指南

## 目录结构

```
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` 队列）。
