feat: 前台 .shtml 后缀 canonical + strict_types 全量启用 + 对外 API(Sanctum+Scramble 文档)+ pm2 ecosystem + GitHub Actions CI + 插件/主题/架构/API 文档 + REDIS_PREFIX 说明

This commit is contained in:
ak
2026-08-11 19:37:33 +08:00
parent 1c8a801238
commit fc6624cf84
244 changed files with 1902 additions and 97 deletions
+86
View File
@@ -0,0 +1,86 @@
# API 文档
对外 HTTP API,供小程序 / 第三方平台接入。Base URL:`https://your-domain.com/api`
- **交互式文档**OpenAPI/Swagger 风格,Scramble 自动生成):`GET /docs/api`
- **OpenAPI JSON**`GET /docs/api.json`
- 认证:Bearer TokenLaravel Sanctum),仅 `/api/me` 需要
## 认证(可选)
```bash
# 生成 API Token
php artisan tinker --execute='$u = App\Models\User::where("email","you@example.com")->first(); dump($u->createToken("mini-program")->plainTextToken);'
```
请求头:`Authorization: Bearer <token>`
## 端点总览
| 方法 | 路径 | 说明 | 认证 |
|------|------|------|------|
| GET | `/api/site` | 站点信息 | 否 |
| GET | `/api/posts` | 文章列表(分页/筛选/搜索) | 否 |
| GET | `/api/posts/{slug}` | 文章详情(含渲染后 HTML) | 否 |
| GET | `/api/categories` | 分类列表 | 否 |
| GET | `/api/tags` | 标签列表(含文章数) | 否 |
| POST | `/api/comments` | 提交评论 | 否 |
| GET | `/api/me` | 当前用户 | 是 |
## 示例
### 站点信息
```bash
curl http://laralog.test/api/site
```
```json
{"name":"旧博客的名字","description":"老博客描述","icp":"京ICP备12345678号","url":"http://laralog.test","rss":"http://laralog.test/rss.xml","api_version":"1.0"}
```
### 文章列表
```bash
curl "http://laralog.test/api/posts?page=1&per_page=10&category=tech&tag=laravel&q=关键词"
```
```json
{
"data": [
{
"id": 1, "title": "你好,世界", "slug": "post-1",
"excerpt": "第一篇博客文章", "category": "生活随笔",
"tags": ["随笔"], "views": 100, "comment_count": 2,
"published_at": "2020-09-13T12:26:40+00:00",
"url": "http://laralog.test/posts/post-1.shtml"
}
],
"meta": { "current_page": 1, "last_page": 1, "per_page": 10, "total": 1 }
}
```
### 文章详情
```bash
curl http://laralog.test/api/posts/post-1
```
返回 `content_html`(与前台一致:Markdown 渲染 / [attach] 短代码解析 / 付费内容过滤)。
### 提交评论
```bash
curl -X POST http://laralog.test/api/comments \
-H "Content-Type: application/json" \
-d '{"post_id":1,"author_name":"访客","content":"写得很好","website":""}'
```
- `website` 为蜜罐字段,必须留空
- 评论审核开关(comment_audit)开启时返回 `pending`,关闭直接发布
### 小程序接入建议
- 启动时请求 `/api/site` + `/api/posts` 缓存首页
- 文章详情拉取 `content_html` 直接渲染(富文本/图片已含 S3 URL)
- 评论提交带 `post_id`;如需"我的评论/会员"能力,用 Bearer Token 调 `/api/me`(可扩展)
+79
View File
@@ -0,0 +1,79 @@
# 架构与开发模式
## 技术栈
| 层 | 技术 | 说明 |
|----|------|------|
| 框架 | Laravel 12.65 | PHP ^8.2`declare(strict_types=1)` 全量启用 |
| 后台 | Filament 5.7 | admin panelzh_CN,资源/页面/组件 |
| 前台交互 | Livewire 4 | Filament 依赖 |
| 数据库 | MySQL 8(生产)/ SQLite(测试 :memory: | |
| 缓存/队列 | database / redis | 队列默认 database,可切 redis |
| Markdown | league/commonmarkGFM+ html-to-markdown | 内容双格式 |
| 附件 | S3 兼容(R2/COS/OSS+ spatie/laravel-medialibrary | 零落盘 |
| 异步 | Workerman 5 常驻(队列消费者 + WebSocket | 复用 Laravel Queue Worker |
| 支付 | yansongda/pay(支付宝/微信) | 沙箱模式内置 |
| 权限 | spatie/laravel-permission | admin/editor/member |
## 目录分层
```
app/
Blog/ # 前台域模块(与 Laravel 默认 app/ 分离)
Controllers/ # 前台控制器(theme_view() 渲染)
Jobs/ # AI 异步任务(AiJob 接口)
Services/ # 领域服务:渲染、支付、导入、S3 同步、LLM 客户端
Support/ # 基础设施:ThemeManager、PluginManager、SlugGenerator、MediaDisk、WorkermanBroadcaster
Providers/ # ThemeServiceProvider、PluginManagerServiceProvider
View/Composers/ # 侧边栏数据共享
Filament/ # 后台(Resources/Pages/Widgets
Console/Commands/ # sablog:import / attachments:sync-s3 / workerman:serve / content:convert / theme:publish
Models/ # Post/Category/Comment/Link/Setting/User...
themes/ # 皮肤(theme.json + views + assets
plugins/ # 插件(plugin.json + ServiceProvider + 迁移/路由/视图)
```
## 核心设计决策
1. **内容双格式**`posts.content_format`markdown/html)。新文章默认 Markdown;老数据导入标记 html 原样保留,`content:convert` 可批量转 Markdown。渲染管线 `PostContentRenderer::render()` 统一输出 HTML,再经 `post.rendered` 过滤器(付费内容等)。
2. **附件短代码**HTML 内容 `[attach=1]`/`[img=1]`Markdown 内容用 `{{attach:1}}`/`{{img:1}}` 令牌(`[]` 是 Markdown 保留字符)。渲染时按**全局** legacy_attachmentid 解析,找不到媒体自动清理残留。
3. **主题系统**:激活主题的 views 目录前置到全局视图路径(View Finder),主题覆盖同名视图、缺失回退默认视图。header 打开的容器由页面视图自闭合(避免 footer 错位)。资产开发期流式返回、生产 `theme:publish`
4. **插件系统**:目录即插件(`plugins/{vendor}.{name}/plugin.json`),钩子(action/filter)解耦。后台页面/资源通过 manifest 的 `filament_pages`/`filament_resources``PluginPages` 汇总注册。插件迁移自动加载。
5. **S3 附件**:上传磁盘 = s3R2/COS/OSS endpoint),新上传零落盘;未配置 S3 时回退本地 public(开发友好)。`attachments:sync-s3` 幂等同步(含大文件 Multipart),损坏文件降级 pending 不中断。
6. **异步 LLM**:任务实现 `AiJob` 接口,Workerman 常驻进程复用 Laravel `Queue\Worker` 消费(database/redis 统一),`sleep=0` + 1s Timer 不阻塞 event loop;失败重试 `failed_jobs` 表。
7. **URL 兼容**canonical 前台 URL 统一 `.shtml` 后缀(`/posts/{slug}.shtml`);无后缀版本 301;sablog 老 URL(伪静态/查询串/PHP 入口)全部 301;trackback 类垃圾功能直接 410 废弃。
8. **迁移保 ID**`sablog:import``DB::table()->updateOrInsert` 保留老主键(Eloquent insertGetId 会忽略显式自增 id),老 MD5 密码登录时自动升级 bcrypt。
## 开发模式
- **本地**Herd 托管 `laralog.test`PHP 8.2 + MySQL 8);无 S3 时附件落本地 public
- **测试**PHPUnitSQLite `:memory:``phpunit.xml`),`RefreshDatabase` + `seed()`MySQL 专属 SQLsyncCounters)在 sqlite 兼容
- **常用命令**
- `php artisan sablog:import --fresh --convert-markdown`(老库迁移)
- `php artisan workerman:serve start|stop`
- `php artisan test`
- `php artisan theme:publish` / `content:convert --all` / `attachments:sync-s3`
## 配置速查
| 配置 | 说明 |
|------|------|
| `config/blog.php` | 站点/每页数/附件磁盘 |
| `config/themes.php` | 主题目录/默认主题 |
| `config/plugins.php` | 插件目录/内置启用列表 |
| `config/workerman.php` | 消费者数/队列连接/队列名/重试 |
| `config/media.php` | S3 配置(R2/COS/OSS)与 Multipart 阈值 |
| `config/market.php` | 插件/主题市场远程源 |
## 已知边界
- AI 审核/润色需要真实 LLM Key(OpenAI 兼容);支付需真实网关密钥(沙箱已验证)
- 插件/主题市场客户端已就绪,市场服务端需另行部署(接口约定见 `App\Blog\Services\MarketplaceClient`
- 自动配图(封面生成)为后续迭代项:封面媒体集合与主题展示已就位,生成器走队列/脚本
+119
View File
@@ -0,0 +1,119 @@
# 插件开发指南
## 目录结构
```
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": "插件描述",
"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` 引用 |
| `filament_pages` / `filament_resources` | 注册到后台的页面/资源(由 `PluginPages` 汇总) |
## 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` | 支付成功(订阅激活/解锁在此监听) |
### addFilter(hook, callback, priority) — 返回值传给下一个过滤器
| Hook | 签名 | 用途 |
|------|------|------|
| `post.rendered` | `(string $html, Post $post): string` | 文章渲染后处理(付费内容过滤) |
| `seo.structured_data` | `(array $data): array` | 扩展 JSON-LD 结构化数据 |
| `payment.gateway` | `(array $gateways): array` | 注册支付渠道 |
## 迁移
插件迁移放在 `database/migrations/``app/Providers/AppServiceProvider` 启动时自动注册,`php artisan migrate` 会一并执行(无需手动 --path)。
## 打包与安装
- 将插件目录打成 ZIP(根目录含 plugin.json
- 后台「插件管理」→ 上传 ZIP 安装,或配置远程市场 `MARKET_URL`
- 内置插件(config/plugins.php enabled 列表)不可卸载,只能停用
## 异步任务(配合 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` 队列)。
+106
View File
@@ -0,0 +1,106 @@
# 主题(皮肤)开发指南
## 目录结构
```
themes/{name}/
├── theme.json # 主题清单(必填)
├── views/ # Blade 视图(激活后前置到全局视图路径)
│ ├── partials/ # head / header / sidebar / footer 等公共片段
│ ├── index.blade.php
│ ├── show.blade.php
│ ├── list.blade.php
│ └── ... # 缺省视图自动回退默认主题(resources/views/
└── assets/ # CSS / JS / 图片(/themes/{name}/assets/... 访问)
```
## theme.json
```json
{
"title": "主题标题",
"version": "1.0.0",
"description": "主题描述",
"author": "作者",
"screenshot": null
}
```
## 视图解析规则
1. 激活主题的 `views/` 目录被**前置到全局视图路径**
2. 页面视图用 `@include('partials.header')` 等**按名字解析** → 主题有同名 partial 就用主题的,否则用默认主题的
3. 页面视图同理:主题提供 `index.blade.php` 则覆盖默认首页,否则用 `resources/views/index.blade.php`
所以做一个新皮肤通常只需要:`theme.json` + `assets/style.css` + 覆盖 `partials/`head/header/sidebar/footer+ 需要特别定制的页面视图。
## 布局结构(约定)
### 默认主题(resources/views/
```blade
@include('partials.head') {{-- <head> 含 SEO meta --}}
<body>
@include('partials.header') {{-- 站点头 --}}
<div class="container main-layout"> {{-- 两栏容器 --}}
<main class="content">...页面内容...</main>
@include('partials.sidebar') {{-- 侧边栏(自动获得共享数据) --}}
</div>
@include('partials.footer') {{-- 页脚(含备案号) --}}
</body></html>
```
### sablog 主题(两栏经典风)
```blade
@include('partials.head')
<body>
@include('partials.header') {{-- 打开 <div id="outmain"> + header,自闭合 --}}
<div id="page"> {{-- 页面视图自己开闭 #page --}}
<div id="content">...内容...</div>
@include('partials.sidebar')
</div>{{-- #page --}}
@include('partials.footer') {{-- <div id="footer"> + 关闭 #outmain --}}
</body></html>
```
> 关键约定:**header 打开的容器由页面视图自闭合,footer 只负责自己打开的内容**。避免把 footer 渲染进 flex 容器导致错位(历史教训)。
## 页面视图清单(控制器会传的数据)
| 视图 | 控制器 | 变量 |
|------|--------|------|
| `index` | HomeController | `$posts`(分页) |
| `show` | PostController | `$post, $comments, $contentHtml` |
| `list` | Category/Archive | `$posts, $category? / $archiveTitle?` |
| `archives` | ArchiveController | `$archives` |
| `tags` | TagController | `$tags` |
| `tag` | TagController | `$tag, $posts` |
| `search` | SearchController | `$posts, $keyword` |
| `links` | LinkController | `$links` |
| `comments` | CommentController | `$comments` |
| `login` / `register` / `profile` | AuthController | — |
| `membership.index` / `membership.mine` | 会员插件 | `$plans / $subscriptions` |
| `payments.sandbox` / `payments.result` | 支付插件 | `$payment` |
## 侧边栏共享数据(SidebarComposer 自动注入所有前台视图)
`$categories``$recentPosts``$recentComments``$hotTags``$links``$blogStats``$siteName``$siteDescription``$siteIcp`
## 主题辅助函数
```blade
{{ theme('asset', 'style.css') }} {{-- 主题资产 URL --}}
{{ theme('var', 'key', '默认值') }} {{-- 主题变量(后台可编辑) --}}
{{ theme('name') }} {{-- 当前主题名 --}}
```
## 资产
- 开发期:`GET /themes/{name}/assets/{path}` 流式返回(带缓存头)
- 生产:`php artisan theme:publish` 复制到 `public/themes/`,由 Web 服务器托管
## 打包与安装
- 目录打成 ZIP(根目录含 theme.json),后台「主题管理」上传安装,或远程市场安装
- 不能删除当前激活的主题