feat: 前台 .shtml 后缀 canonical + strict_types 全量启用 + 对外 API(Sanctum+Scramble 文档)+ pm2 ecosystem + GitHub Actions CI + 插件/主题/架构/API 文档 + REDIS_PREFIX 说明
This commit is contained in:
+86
@@ -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 Token(Laravel 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`(可扩展)
|
||||
@@ -0,0 +1,79 @@
|
||||
# 架构与开发模式
|
||||
|
||||
## 技术栈
|
||||
|
||||
| 层 | 技术 | 说明 |
|
||||
|----|------|------|
|
||||
| 框架 | Laravel 12.65 | PHP ^8.2,`declare(strict_types=1)` 全量启用 |
|
||||
| 后台 | Filament 5.7 | admin panel,zh_CN,资源/页面/组件 |
|
||||
| 前台交互 | Livewire 4 | Filament 依赖 |
|
||||
| 数据库 | MySQL 8(生产)/ SQLite(测试 :memory:) | |
|
||||
| 缓存/队列 | database / redis | 队列默认 database,可切 redis |
|
||||
| Markdown | league/commonmark(GFM)+ 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 附件**:上传磁盘 = s3(R2/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
|
||||
- **测试**:PHPUnit,SQLite `:memory:`(`phpunit.xml`),`RefreshDatabase` + `seed()`;MySQL 专属 SQL(syncCounters)在 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
@@ -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
@@ -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),后台「主题管理」上传安装,或远程市场安装
|
||||
- 不能删除当前激活的主题
|
||||
Reference in New Issue
Block a user