Initial baseline: LaraBlog core with plugin commerce surface.
Captures the current working tree after theme slots, ArticleAccess, and the payment / paid-content plugins so subsequent work has a reviewable git history.
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
# API 文档
|
||||
|
||||
- **OpenAPI 3**:[`openapi.yaml`](./openapi.yaml)
|
||||
- **实现**:`routes/api.php` → `/api/v1/*`
|
||||
- **阶段**:一期以**只读内容接口**为主,供小程序/H5/其他端拉取文章、分类、标签与站点信息
|
||||
|
||||
## 快速试
|
||||
|
||||
```bash
|
||||
curl -s http://larablog.test/api/v1/meta | jq
|
||||
curl -s http://larablog.test/api/v1/articles | jq
|
||||
curl -s http://larablog.test/api/v1/articles/1 | jq
|
||||
```
|
||||
|
||||
## Swagger UI
|
||||
仓库不强制绑定某一 UI 实现。可任选:
|
||||
1. 把 `openapi.yaml` 导入 [Swagger Editor](https://editor.swagger.io/)
|
||||
2. 自建静态页引用 swagger-ui(二期可加 `l5-swagger` / `scramble`)
|
||||
|
||||
## 鉴权(规划)
|
||||
写接口(发评论、会员、支付回调)将走 Bearer Token / 小程序 session;一期未开放写操作。
|
||||
@@ -0,0 +1,83 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: LaraBlog Public API
|
||||
version: 1.0.0
|
||||
description: |
|
||||
面向小程序 / 第三方客户端的只读 JSON API(一期)。
|
||||
Base path: `/api/v1`
|
||||
servers:
|
||||
- url: /api/v1
|
||||
description: Relative to APP_URL
|
||||
paths:
|
||||
/meta:
|
||||
get:
|
||||
summary: 站点元信息
|
||||
operationId: getMeta
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
/articles:
|
||||
get:
|
||||
summary: 文章列表
|
||||
operationId: listArticles
|
||||
parameters:
|
||||
- in: query
|
||||
name: per_page
|
||||
schema: { type: integer, minimum: 1, maximum: 50 }
|
||||
- in: query
|
||||
name: page
|
||||
schema: { type: integer, minimum: 1 }
|
||||
responses:
|
||||
'200':
|
||||
description: Paginated articles
|
||||
/articles/{id}:
|
||||
get:
|
||||
summary: 文章详情(匿名;受限文仅试读)
|
||||
description: |
|
||||
匿名 API。免费文返回 `content` / `content_html`。
|
||||
密码文与付费文不返回全文:`content` 为 null,`content_html`/`teaser_html` 为试读或摘要,
|
||||
并带 `access.status`(`need_password` / `need_purchase` 等)。
|
||||
已购/管理员全文仅 Web 登录会话可用(本期)。
|
||||
operationId: getArticle
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema: { type: integer }
|
||||
responses:
|
||||
'200': { description: OK }
|
||||
'404': { description: Not found }
|
||||
/categories:
|
||||
get:
|
||||
summary: 分类列表
|
||||
operationId: listCategories
|
||||
responses:
|
||||
'200': { description: OK }
|
||||
/categories/{id}/articles:
|
||||
get:
|
||||
summary: 分类下文章
|
||||
operationId: listCategoryArticles
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema: { type: integer }
|
||||
responses:
|
||||
'200': { description: OK }
|
||||
/tags:
|
||||
get:
|
||||
summary: 标签列表
|
||||
operationId: listTags
|
||||
responses:
|
||||
'200': { description: OK }
|
||||
/tags/{name}/articles:
|
||||
get:
|
||||
summary: 标签下文章
|
||||
operationId: listTagArticles
|
||||
parameters:
|
||||
- in: path
|
||||
name: name
|
||||
required: true
|
||||
schema: { type: string }
|
||||
responses:
|
||||
'200': { description: OK }
|
||||
@@ -0,0 +1,43 @@
|
||||
# LaraBlog 架构与开发思想
|
||||
|
||||
## 目标
|
||||
把 sablog 内容与 SEO 权重迁到现代 Laravel 栈:**不克隆过时能力**(Trackback/WAP/旧 Admin),用主题/插件/对象存储/AI 队列替换。
|
||||
|
||||
## 分层
|
||||
```
|
||||
HTTP (legacy .shtml + /admin Filament + /api/v1)
|
||||
→ Controllers / Filament Pages
|
||||
→ Domain (Blog / Media / Theme / Plugin / Seo / Ai)
|
||||
→ Eloquent Models + Spatie Settings
|
||||
→ S3 disk / Redis queues / Workerman
|
||||
```
|
||||
|
||||
## 关键约定
|
||||
| 区域 | 路径 |
|
||||
|---|---|
|
||||
| Legacy 前台 URL | `routes/legacy.php` |
|
||||
| Web 辅助 | `routes/web.php` |
|
||||
| JSON API(小程序/其他端) | `routes/api.php` → `/api/v1/*` |
|
||||
| 领域逻辑 | `app/Domain/*` |
|
||||
| 主题 | `themes/{slug}/` |
|
||||
| 插件 | `plugins/{vendor}/{name}/` |
|
||||
| 规格 | `docs/specs/larablog-platform/` |
|
||||
| OpenAPI | `docs/api/openapi.yaml` |
|
||||
|
||||
## 开发模式
|
||||
1. **规格先行**:非琐碎功能写 SPEC / CHECKLIST / TESTPLAN
|
||||
2. **兼容优先**:文章/分类 ID、`.shtml` 内容 URL 保持可达
|
||||
3. **引用不写死 CDN**:正文存 `[attach=id]` / `attach:id`,渲染期转 `/attachment.php?id=`
|
||||
4. **异步 AI**:Web 不阻塞 LLM;队列 / Workerman 消费
|
||||
5. **默认中文**:`APP_LOCALE=zh_CN`,文案走 `lang/zh_CN/*`
|
||||
6. **测试**:PHPUnit Feature sqlite `:memory:`(见 `phpunit.xml`);CI 同配置
|
||||
|
||||
## PHP / 代码风格
|
||||
- 目标运行时:**PHP 8.2+**(`composer.json`)
|
||||
- 应用代码逐步统一 `declare(strict_types=1);`
|
||||
- 类型提示、返回类型、枚举优先;避免软依赖全局状态
|
||||
|
||||
## 一期 vs 二期
|
||||
- **一期已做**:核心博客、主题引擎、插件框架、SEO/GEO、导入、AI stub 管道、前台皮肤、后台 Filament
|
||||
- **一期骨架**:支付 / 会员 / 双商城(非真实结算)
|
||||
- **二期**:自动配图、真实支付会员、小程序完整写接口与鉴权深化
|
||||
@@ -0,0 +1,45 @@
|
||||
# 部署与进程管理
|
||||
|
||||
## Web
|
||||
- nginx + php-fpm(或 Laravel Herd)
|
||||
- 参考 `deploy/nginx.conf`
|
||||
- 附件:生产 `ATTACHMENTS_DRIVER=s3`;开发可用 `local`
|
||||
|
||||
## 长驻进程(PM2)
|
||||
配置文件:仓库根目录 `ecosystem.config.cjs`
|
||||
|
||||
```bash
|
||||
# 建议 QUEUE_CONNECTION=redis,并先起 Redis
|
||||
composer install --no-dev --optimize-autoloader
|
||||
php artisan migrate --force
|
||||
php artisan config:cache
|
||||
php artisan route:cache
|
||||
php artisan themes:publish
|
||||
php artisan plugins:sync
|
||||
|
||||
pm2 start ecosystem.config.cjs
|
||||
pm2 save
|
||||
```
|
||||
|
||||
进程:
|
||||
| name | 作用 |
|
||||
|---|---|
|
||||
| `larablog-queue` | `queue:work`(default / ai-content / ai-moderation) |
|
||||
| `larablog-schedule` | `schedule:work` |
|
||||
| `larablog-ai-workerman` | Workerman AI 运行时(可选,若只用 queue:work 可删) |
|
||||
|
||||
> HTTP **不要**交给 pm2。
|
||||
|
||||
## GitHub Actions
|
||||
- `.github/workflows/ci.yml`:PHP 8.2/8.3 跑 PHPUnit(sqlite memory)
|
||||
- `deploy` job 仅作占位,需绑定 `production` environment 与自有发布脚本
|
||||
|
||||
## Redis 键前缀
|
||||
`.env`:
|
||||
|
||||
```env
|
||||
REDIS_PREFIX=larablog_
|
||||
CACHE_PREFIX=larablog_cache_
|
||||
```
|
||||
|
||||
Laravel 会在 `config/database.php` → `redis.options.prefix` 与 cache prefix 生效。
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
# 插件开发指南
|
||||
|
||||
## 目录格式
|
||||
```
|
||||
plugins/{vendor}/{name}/
|
||||
plugin.json
|
||||
README.md
|
||||
database/migrations/ # 可选
|
||||
resources/views/ # 可选
|
||||
src/PluginServiceProvider.php
|
||||
```
|
||||
|
||||
### plugin.json
|
||||
```json
|
||||
{
|
||||
"name": "vendor/my-plugin",
|
||||
"title": "显示名",
|
||||
"version": "1.0.0",
|
||||
"description": "说明",
|
||||
"provider": "Plugins\\Vendor\\MyPlugin\\PluginServiceProvider",
|
||||
"requires": ["larablog/payment"],
|
||||
"optional": [],
|
||||
"docs": "README.md"
|
||||
}
|
||||
```
|
||||
|
||||
- `requires`:硬依赖,未启用时 `PluginManager::enable` 会拒绝
|
||||
- `docs`:相对插件根目录的说明文件,后台卡片可查看;只允许指向插件目录内(`..`、绝对路径、软链越界会被拒绝)
|
||||
|
||||
在 `composer.json` → `autoload.psr-4` 注册命名空间,然后:
|
||||
|
||||
```bash
|
||||
composer dump-autoload
|
||||
php artisan plugins:sync
|
||||
# 启用后如有迁移:
|
||||
php artisan migrate
|
||||
```
|
||||
|
||||
## 生命周期
|
||||
1. `PluginManager::discover()` 扫描磁盘
|
||||
2. `plugins:sync` / 后台同步写入 `plugins` 表
|
||||
3. 启用后 `registerEnabledProviders()` 注册 Provider(Filament panel 构建前也会注册,以便 `Panel::configureUsing`)
|
||||
4. Provider `register()` / `boot()` 内挂 Hook、路由、Filament Resource、迁移
|
||||
|
||||
## Hook 类型
|
||||
| 方法 | 用途 |
|
||||
|---|---|
|
||||
| `listen` + `dispatch` | 事件通知(如 `comment.created`、`order.paid`) |
|
||||
| `gather` | 字符串管道(主题槽 `theme.*`) |
|
||||
| `collect` | 数组合并(Filament 组件/列/动作) |
|
||||
| `filter` | 值管道(表单 mutate 等;每个监听器可替换值) |
|
||||
| `listeners` | 取回监听器自行折叠(`article.access` 用它保证只许变严) |
|
||||
|
||||
```php
|
||||
use App\Domain\Plugin\Hook;
|
||||
|
||||
Hook::listen('comment.created', function (Comment $comment): void { /* ... */ });
|
||||
Hook::listen('theme.sidebar', function (string $html): string {
|
||||
return $html.'<p>侧栏注入</p>';
|
||||
});
|
||||
Hook::listen('filament.article.form', function (array $components): array {
|
||||
return [/* Section / Field */];
|
||||
});
|
||||
Hook::listen('article.access', function (AccessDecision $decision, array $ctx): AccessDecision {
|
||||
return $decision->tightenWith(/* 更严决策 */);
|
||||
});
|
||||
```
|
||||
|
||||
## 约定扩展点(首批)
|
||||
| 点 | 类型 | 用途 |
|
||||
|---|---|---|
|
||||
| `theme.*` | gather | 前台主题槽 |
|
||||
| `article.access` | fold | 收紧阅读权限;核心逐个 `tightenWith` 折叠,放宽无效、非 `AccessDecision` 返回值忽略 |
|
||||
| `filament.article.form` | collect | 文章表单追加组件 |
|
||||
| `filament.article.table.columns` | collect | 文章表追加列 |
|
||||
| `filament.article.actions` | collect | 文章动作 |
|
||||
| `filament.article.mutate_before_fill` | filter | 编辑回填 |
|
||||
| `filament.article.mutate_before_save` | filter | 保存前改/剥数据 |
|
||||
| `filament.article.after_save` | dispatch | 保存后写插件表 |
|
||||
| `order.paid` / `order.refunded` | dispatch | 支付插件发出 |
|
||||
|
||||
## 三类注入
|
||||
1. **UI**:前台主题槽 + 后台 Filament collect
|
||||
2. **功能**:access / lifecycle Hook
|
||||
3. **数据**:插件自有 migration/model,用外键或 `product_type`+`product_id` 关联
|
||||
|
||||
## Filament 页面/资源
|
||||
放在插件命名空间,于 `register()` 中:
|
||||
|
||||
```php
|
||||
Panel::configureUsing(function (Panel $panel): void {
|
||||
if ($panel->getId() !== 'admin') return;
|
||||
$panel->resources([OrderResource::class])->pages([SettingsPage::class]);
|
||||
});
|
||||
```
|
||||
|
||||
Resource / Page 需用 `canAccess` / `shouldRegisterNavigation` 检查插件已启用。
|
||||
|
||||
## 后台
|
||||
- 「插件」页:卡片启停、依赖展示、使用说明
|
||||
- 可选:继续用核心 `PluginSkeletonPage` 做极简占位
|
||||
|
||||
## 内置插件
|
||||
| 插件 | 状态 |
|
||||
|---|---|
|
||||
| `larablog/ai-comment-moderation` | 可运行 |
|
||||
| `larablog/payment` | Stub 订单/权益可跑通 |
|
||||
| `larablog/paid-content` | 文章付费(依赖 payment) |
|
||||
| `larablog/membership` | 骨架 |
|
||||
| `larablog/plugin-marketplace` | 骨架 |
|
||||
| `larablog/theme-marketplace` | 骨架 |
|
||||
@@ -0,0 +1,21 @@
|
||||
# 路由与 `.shtml` 后缀
|
||||
|
||||
Laravel **没有**全局 `route suffix` 配置(不像某些框架的 `url_suffix`)。LaraBlog 用**显式路由**兼容 sablog:
|
||||
|
||||
```php
|
||||
// routes/legacy.php
|
||||
Route::get('/show-{id}.shtml', ...);
|
||||
Route::get('/category-{cid}.shtml', ...);
|
||||
Route::get('/archives.shtml', ...);
|
||||
Route::get('/login.shtml', ...);
|
||||
// ...
|
||||
```
|
||||
|
||||
## 规则
|
||||
1. **内容向 GET** 优先提供 `.shtml`(及必要的 `index.php?action=` 兼容)
|
||||
2. 新 canonical 可并存(如 `/tag/{name}`、`/posts/{slug}` → 301 到 show-id)
|
||||
3. 不要指望「所有 GET 自动加 `.shtml`」;新增页面请在 `legacy.php` **写明**后缀
|
||||
4. API 走 `/api/v1`,**不加** `.shtml`
|
||||
|
||||
## 验收
|
||||
`php artisan route:list` 应能看到主要 `.shtml` 路由;`tests/Feature/BlogFrontendTest` 覆盖核心路径。
|
||||
@@ -0,0 +1,39 @@
|
||||
# LaraBlog 平台一期 — 功能清单
|
||||
|
||||
## 状态
|
||||
- 对应 SPEC:`docs/specs/larablog-platform/SPEC.md`
|
||||
- 最近更新:2026-08-11 18:35
|
||||
|
||||
## 完成项
|
||||
- [x] Laravel 12 脚手架 + Filament 5 + Spatie + Workerman + S3 + purifier + commonmark/html-to-markdown
|
||||
- [x] 领域模型与 migrations(无 trackbacks;含 slug、content_format、cover_* 预留)
|
||||
- [x] Spatie Settings(站点/SEO/主题/LLM/正文格式;无 trackback)
|
||||
- [x] ContentRenderer + AttachEmbed(HTML/Markdown + attach 引用)
|
||||
- [x] `attachments` S3 disk(`ATTACHMENTS_DRIVER=local|s3`);legacy 附件入口 302
|
||||
- [x] Legacy 内容 URL + tburl/trackback 410
|
||||
- [x] 前台:首页/文章/分类/归档/标签/评论/搜索/链接
|
||||
- [x] 前台登录/注册/资料(`/login.shtml` `/reg.shtml` `/post.php` + legacy 密码升级)
|
||||
- [x] 前台评论 + rate limit + purifier;AI 审核插件可启用
|
||||
- [x] Filament:文章/分类/评论/标签/链接/附件上传/站点片段/用户角色 + 主题/插件/设置/清缓存
|
||||
- [x] 插件框架(发现/启停/Hook)+ AI 审核可运行;支付/会员/双商城为骨架(侧栏+桩接口,非真实结算)
|
||||
- [x] 主题引擎 + default 完整皮肤 UI + example 可切换第二套皮肤
|
||||
- [x] SEO/GEO:canonical/OG/Twitter/JSON-LD/sitemap/rss/robots/llms.txt
|
||||
- [x] 插件框架 + 5 插件同步;ai-comment-moderation 可启用
|
||||
- [x] SEO/GEO:meta/OG/JSON-LD/sitemap/rss/llms.txt
|
||||
- [x] Workerman AI + `queue:ai`;stub Provider;文章「AI 优化」投递
|
||||
- [x] sablog:import `--mode=raw|markdown`
|
||||
- [x] tests/fixtures 最小 sablog 样例包 + 导入回归测试
|
||||
- [x] deploy/nginx.conf + README
|
||||
- [x] 可选 `/posts/{slug}` → show-id 301
|
||||
|
||||
## 延期(二期+)
|
||||
- [ ] 自动配图 / 生成配图(字段与 Job 空壳已预留)
|
||||
- [ ] 支付/会员/双商城真实对接
|
||||
|
||||
## 变更记录
|
||||
| 时间 | 原因 | 变更 |
|
||||
|------|------|------|
|
||||
| 2026-08-11 17:26 | SPEC 定稿后初版 | 创建功能清单 |
|
||||
| 2026-08-11 18:00 | 继续实现后台/插件/AI | 大批量勾选已完成项;登录注册与 fixture 留待补 |
|
||||
| 2026-08-11 18:10 | qodercli 提测修 BUG | 修 attach 属性/MD 导入占位、index.php tags、tb CSRF、导入 withoutEvents、附件路径、密码门、API Key |
|
||||
| 2026-08-11 18:35 | 补齐一期缺口 | 前台 auth、Stylevar/User、附件上传、插件骨架页、fixture、清缓存、slug 301 |
|
||||
@@ -0,0 +1,156 @@
|
||||
# LaraBlog 平台一期
|
||||
|
||||
## 状态
|
||||
- 状态:已定稿
|
||||
- 创建:2026-08-11 17:14
|
||||
- 最近更新:2026-08-11 17:53
|
||||
|
||||
## 背景
|
||||
SaBlog-X 1.6(约二十年前设计)需迁移到现代 Laravel 栈。迁移目标不是「功能克隆」,而是:**保留有价值的内容、SEO 权重与核心写作/阅读体验**,用当代博客实践替换过时、产垃圾或维护成本不合理的设计。
|
||||
|
||||
参考源:https://github.com/neatstudio/sablog-archive
|
||||
|
||||
## 现代化取舍原则
|
||||
1. **内容与 SEO 优先**:文章/分类/标签 ID 与关键旧 URL 保留,避免权重归零
|
||||
2. **不迁垃圾协议与死技术**:Trackback/Pingback、WAP、旧 searchindex、旧 PHP Admin、Feedsky 等一律不实现
|
||||
3. **用当代能力替换**:AI 评论审核代替巨型敏感词表为主力;对象存储代替本地附件目录;Workerman 承载 LLM;Filament 后台
|
||||
4. **导入可跳过废弃表**:`sablog_trackbacks` / `sablog_trackbacklog` / `sablog_searchindex` / `sablog_sessions` 等只记录「已跳过」,不入新库
|
||||
5. **旧入口若被爬虫打到**:对明确废弃路径返回 **410 Gone**(不实现业务)
|
||||
|
||||
## 目标
|
||||
- 可运行的现代博客:前台阅读/评论/搜索/归档 + Filament 5 后台
|
||||
- 严格兼容 sablog **内容向**伪静态与关键 query URL;导入后文章/分类 ID 一致
|
||||
- 多主题、插件框架、SEO/GEO、S3 兼容附件、Workerman AI 运行时
|
||||
- `sablog:import`:内容与附件上云;跳过过时模块并报告
|
||||
|
||||
## 范围
|
||||
|
||||
### In scope
|
||||
1. Laravel 12 + Livewire 4 + Filament 5 + Spatie(permission、settings、feed、sitemap、activitylog)
|
||||
2. 博客核心:文章、分类、标签、评论、附件、友情链接、用户/角色、站点片段(stylevars)
|
||||
3. Legacy **内容 URL** + 新 canonical(见 URL 矩阵)
|
||||
4. 主题引擎 + `default` + 示例主题(主题 zip 存本地 `themes/`)
|
||||
5. 插件框架 + payment / membership / plugin-marketplace / theme-marketplace(骨架)+ ai-comment-moderation(可运行管道)
|
||||
6. SEO/GEO:meta、OG、JSON-LD、sitemap、rss(含 ?cid)、`llms.txt`
|
||||
7. 对象存储附件全链路 + legacy `/attachment.php` 与 `/attachments/{path}`
|
||||
8. Workerman:`ai-content`、`ai-moderation`
|
||||
9. `sablog:import` 幂等 upsert + 跳过废弃表报告
|
||||
10. 防刷:rate limit;XSS purifier;附件 MIME 白名单
|
||||
11. 可选现代增强:文章 `slug`(不取代 `/show-{id}.shtml`)
|
||||
12. **正文格式双轨**:`content_format = html|markdown`;站点默认新作为 Markdown;导入默认保留 HTML;可选 HTML→Markdown 转换开关
|
||||
|
||||
### Out of scope(明确不迁移 / 不复刻)
|
||||
- **Trackback / Pingback**(含表、后台、前台、开启开关);`/tburl.php`、`/trackback.php` 仅 410
|
||||
- WAP、旧 `searchindex` 搜索缓存表、自建 sessions 表、Feedsky/旧 JS 挂件
|
||||
- 旧 `/admin/*` PHP 后台原样兼容(新后台 Filament `/admin`;旧路径 410)
|
||||
- 旧主题静态 `/templates/default/*` 原样托管
|
||||
- 真实支付/会员计费/远程商城结算;多站点 SaaS
|
||||
- Workerman 替代主站 HTTP;绑定单一 LLM 厂商;应用侧长期存附件
|
||||
- 以巨型政治/广告敏感词表为唯一防垃圾手段(可保留极简词表作预过滤,主力为 AI + rate limit)
|
||||
|
||||
### 延期(二期+,本期只预留)
|
||||
**自动配图 / 由正文生成封面**(老 sablog 无现代「特色图」概念):
|
||||
- 能力意向:
|
||||
1. **自动配图**:从正文首图 / 附件图 / OG 候选中选封面
|
||||
2. **生成配图**:用标题+摘要(或 LLM 提示)调用文生图 / 模板渲染,产出封面,写入对象存储
|
||||
- 执行方式:Workerman / 队列 Job + 可重跑 Artisan 脚本,**批量慢生成**,不堵发布主路径
|
||||
- 本期只做:表字段与状态枚举预留、`GenerateArticleCoverJob` 空壳、SPEC/清单占位;**不做**真实选图/生图对接
|
||||
- 封面文件同样只存对象存储,规则同附件
|
||||
|
||||
## 方案要点
|
||||
|
||||
### 技术栈
|
||||
| 层 | 选型 |
|
||||
|---|---|
|
||||
| 框架 | Laravel 12、PHP 8.2+、Livewire 4 |
|
||||
| 后台 | Filament 5 |
|
||||
| Spatie | permission、settings、feed、sitemap、activitylog |
|
||||
| 附件 | 自建 `attachments` + S3 disk(不用 medialibrary) |
|
||||
| AI | Workerman + Redis;`LlmProvider`(OpenAI 兼容 + Stub) |
|
||||
| 净化 / 图 | mews/purifier;Intervention Image(临时处理后退对象存储) |
|
||||
|
||||
### 数据模型
|
||||
- `articles`:id、category_id、user_id、title、content、**content_format(`html`|`markdown`)**、description、keywords、slug(nullable)、published_at、views、comments_count、stick、visible、close_comment、read_password(nullable,兼容旧私密文)、ai_summary、ai_suggestions、legacy_attachments、**cover_***(二期配图预留,见下)
|
||||
- 封面预留字段:`cover_disk`、`cover_path`、`cover_source`(`none`|`manual`|`attachment`|`content_image`|`generated`)、`cover_status`(`none`|`pending`|`ready`|`failed`)、`cover_generated_at`
|
||||
- **无** `trackbacks` 表;**无** `close_trackback` / `trackbacks_count`
|
||||
|
||||
### 正文格式(HTML / Markdown)
|
||||
- **原则**:旧 sablog 正文是 HTML;**新写作默认 Markdown**(未来主路径)
|
||||
- **文章级开关**:`articles.content_format`;后台编辑器按格式切换(Markdown 文本域 / HTML 富文本)
|
||||
- **导入双模式**(两份能力,同一命令开关):
|
||||
1. **`raw`(默认)**:正文保持 HTML,`content_format=html`,保留 sablog `[attach=id]`
|
||||
2. **`markdown`**:HTML→Markdown,`content_format=markdown`;因 `[]` 是 Markdown 保留字符,**导入时把 `[attach=id]` 改写成 Markdown 安全引用**,不把对象存储 URL 写进库
|
||||
- **附件引用策略(已定)**:
|
||||
| 场景 | 数据库保存 | 预览/前台输出 |
|
||||
|---|---|---|
|
||||
| raw/HTML | `[attach=123]`(原样) | 渲染期解析为 `<img>`/`<a>`,href/src 用稳定入口 `/attachment.php?id=123`(再 302 到 S3) |
|
||||
| markdown | 图片 ``;文件 `[name](attach:123)` | CommonMark 后把 `attach:123` 换成 `/attachment.php?id=123` |
|
||||
- **不在导入时写死 S3/CDN 路径**:避免换桶/换域名后正文失效;**平时 DB 只存引用,输出时转换**
|
||||
- **站点设置**:`default_content_format=markdown`;导入模式由 CLI `--mode=raw|markdown` 决定(可覆盖 settings)
|
||||
- **渲染**:`ContentRenderer::toHtml()` = 格式转换 + attach 水合 + Purifier
|
||||
- `categories`、`comments`(moderation_status:`pending`|`pending_ai`|`approved`|`rejected`|`needs_human`)、`tags` + `article_tag`、`attachments`、`links`、`stylevars`、`plugins`、`users`(password + password_legacy)
|
||||
- Settings:站点、SEO、active_theme、LLM、存储、正文格式默认/导入策略(**无** trackback_enabled)
|
||||
|
||||
#### 导入映射(摘要)
|
||||
| 源 | 目标 | 规则 |
|
||||
|---|---|---|
|
||||
| articles 核心字段 | articles.* | 保留 id/cid/uid/views/close_comment/read_password 等 |
|
||||
| articles.content | content + content_format | 默认 `html`;若开启转换则转为 Markdown 且 format=`markdown` |
|
||||
| articles.trackbacks / closetrackback | — | **忽略** |
|
||||
| articles.attachments serialize | legacy_attachments + attachments 表 | 文件上云 |
|
||||
| comments.visible | moderation_status | 1→approved,0→pending |
|
||||
| tags.aids | article_tag | 拆 pivot |
|
||||
| users.password | password_legacy | 无盐 MD5 → 登录升级 bcrypt |
|
||||
| attachments.* | attachments.* | 逐条上云,保留 downloads |
|
||||
| links / stylevars / settings | 对应表/Settings | 直通或键映射 |
|
||||
| sablog_trackbacks / trackbacklog / searchindex / sessions | — | **跳过并计入报告** |
|
||||
|
||||
### 附件与对象存储
|
||||
- 一律 S3 兼容;临时文件用完即删
|
||||
- `/attachment.php?id=`、`/attachments/{path}` → 去重计数后 302
|
||||
- 正文 `[attach=id]` 解析为对象存储 URL
|
||||
|
||||
### URL 矩阵
|
||||
|
||||
#### Legacy 内容向(必须业务可达)
|
||||
`/`、`/index.php?action=…`、`/show-{id}[-{page}].shtml`、`/category-{cid}[-{page}].shtml`、`/archives…`、`/tagslist…`、`/index.php?action=tags&item=`、`/comments…`、`/search…`、`/links.shtml`、`/reg.shtml`、`/login.shtml`、`/rss.xml[?cid=]`、`/sitemap.xml`、`/attachment.php`、`/attachments/{path}`、`/post.php`(登录/注册/评论/搜索)
|
||||
|
||||
#### 新 canonical
|
||||
- `/tag/{name}`(自旧 tags query 301)
|
||||
- `/llms.txt`
|
||||
- 可选 `/posts/{slug}`(不取代 show-id)
|
||||
|
||||
#### 明确废弃(410,无业务)
|
||||
- `/tburl.php`、`/trackback.php`
|
||||
- 旧 `/admin/*.php`(非 Filament 面板)
|
||||
- WAP 相关路径(若出现)
|
||||
|
||||
### 主题 / 插件 / Workerman / 导入
|
||||
同前:主题回退、插件生命周期、AI 双队列、`sablog:import` 幂等与「跳过废弃表」报告;不删源数据。
|
||||
|
||||
## 验收标准
|
||||
- [x] 内容向 Legacy URL 可达;文章/分类 ID 与源一致(`upsertWithId` + fixture 回归)
|
||||
- [x] **无** Trackback 功能/表/设置;tb 入口 410
|
||||
- [x] 导入报告含「跳过 trackbacks/searchindex/sessions」计数
|
||||
- [x] 正文:新建默认 Markdown;导入默认可为 HTML;格式开关可切换;Markdown/HTML 渲染正确;`--mode=markdown`
|
||||
- [x] 前台评论闭环(含 AI 管道 stub);仅 approved 展示
|
||||
- [x] Filament 管理内容与配置(含 content_format;无 trackback 菜单)
|
||||
- [x] 附件对象存储(生产 S3;开发 local);双入口 302;主题可切换;插件可启停
|
||||
- [x] SEO/GEO 正常;Web 无同步 LLM;legacy MD5 可升级登录
|
||||
- [x] 导入幂等且不改源
|
||||
|
||||
## 开放问题
|
||||
- [x] 现代化:剔除 Trackback 等过时能力 — 已定
|
||||
- [ ] 部署环境对象存储凭据;开发可用 MinIO / fake
|
||||
|
||||
## 变更记录
|
||||
| 时间 | 原因 | 变更 |
|
||||
|------|------|------|
|
||||
| 2026-08-11 17:14 | 初稿 | 创建 SPEC |
|
||||
| 2026-08-11 17:20 | qodercli A1–A8 | URL/附件/映射等 |
|
||||
| 2026-08-11 17:25 | qodercli 映射缺口 | 补字段 |
|
||||
| 2026-08-11 17:26 | 三轮无阻塞;用户确认 | 定稿 |
|
||||
| 2026-08-11 17:35 | 用户:勿迁 trackback 等过时设计 | 增加现代化取舍;删除 Trackback 全链路;废弃入口 410;导入跳过垃圾表 |
|
||||
| 2026-08-11 17:42 | 用户:HTML 旧文 / Markdown 未来 | 增加 content_format 双轨、站点/导入开关、可选 HTML→MD 转换 |
|
||||
| 2026-08-11 17:50 | 用户:导入双份 + attach 与 MD 保留字 | 明确 DB 存引用、渲染期解析;`--mode=raw|markdown`;attach: 协议 |
|
||||
| 2026-08-11 17:53 | 用户:自动配图/生成配图可后推 | 延期章节 + cover 字段/Job 空壳预留 |
|
||||
@@ -0,0 +1,33 @@
|
||||
# LaraBlog 平台一期 — 待测清单
|
||||
|
||||
## 状态
|
||||
- 对应 SPEC:`docs/specs/larablog-platform/SPEC.md`
|
||||
- 最近更新:2026-08-11 19:05
|
||||
|
||||
## 自动化覆盖(`php artisan test`)
|
||||
- [x] AttachEmbed / ContentRenderer(HTML/Markdown + attach)
|
||||
- [x] 前台 legacy URL / 410 / rss / sitemap / llms
|
||||
- [x] AI stub 管道与队列投递
|
||||
- [x] 登录/注册/资料 + legacy MD5 升级
|
||||
- [x] sablog fixture 导入 raw/markdown + ID 保留
|
||||
- [x] local 附件直出 `/attachments-local` + 删除清磁盘
|
||||
|
||||
## 待测项(手工 / 环境相关)
|
||||
- [ ] 场景:Markdown 新文;步骤:后台以 markdown 发布含标题/链接;期望:前台渲染为 HTML 且 XSS 被滤
|
||||
- [ ] 场景:格式开关;步骤:同文在 html/markdown 间切换并保存;期望:按当前 format 渲染
|
||||
- [ ] 场景:分类/归档/标签云/评论列表/搜索/链接抽样
|
||||
- [ ] 场景:评论闭环(含启用 AI 插件)
|
||||
- [ ] 场景:生产 S3/R2/MinIO 附件 302
|
||||
- [ ] 场景:主题切换 example
|
||||
- [ ] 场景:插件启停后骨架导航显隐
|
||||
- [ ] 场景:Workerman 长驻消费 `ai-content` / `ai-moderation`
|
||||
- [ ] 场景:XSS/限流手工压测
|
||||
|
||||
## 变更记录
|
||||
| 时间 | 原因 | 变更 |
|
||||
|------|------|------|
|
||||
| 2026-08-11 17:26 | SPEC 定稿后初版 | 创建待测清单 |
|
||||
| 2026-08-11 17:35 | 剔除 trackback | 410 废弃入口;导入跳过报告 |
|
||||
| 2026-08-11 17:42 | HTML/Markdown 双轨 | 增加渲染与导入转换场景 |
|
||||
| 2026-08-11 17:53 | 自动配图后推 | 配图场景列入二期,本期不测真实生成 |
|
||||
| 2026-08-11 19:05 | 一期收尾 | 勾选自动化覆盖;手工项收窄为环境相关 |
|
||||
@@ -0,0 +1,59 @@
|
||||
# 插件扩展面 + 支付 / 内容付费 — 功能清单
|
||||
|
||||
## 状态
|
||||
- 对应 SPEC:`docs/specs/plugin-extension-commerce/SPEC.md`
|
||||
- 最近更新:2026-08-12 01:12
|
||||
|
||||
## 完成项
|
||||
|
||||
### 核心扩展面
|
||||
- [x] `Hook::collect` / `Hook::filter`
|
||||
- [x] `ArticleAccess` 决策(作者/admin、密码、filter 只许变严)
|
||||
- [x] `BlogController::show` 改用 ArticleAccess;密码 POST 解锁后再 resolve
|
||||
- [x] 试读+购买主题模板(default;example fallback)
|
||||
- [x] API show 经 ArticleAccess;响应含 `access`;受限无全文
|
||||
- [x] RSS 受限文无全文
|
||||
- [x] `ArticleForm` / Table / Create+Edit 挂载 filament.article.* 扩展点
|
||||
- [x] `plugin.json`:`requires` / `optional` / `docs`
|
||||
- [x] `PluginManager::enable` 依赖校验 + `isEnabled`
|
||||
- [x] 后台插件卡片:依赖展示 + 查看说明
|
||||
|
||||
### payment
|
||||
- [x] 插件 migrations + `loadMigrationsFrom`
|
||||
- [x] models:Order / OrderItem / Entitlement / PaymentTransaction
|
||||
- [x] StubGateway 建单 → 确认页 → 入账 → entitlement → `order.paid`
|
||||
- [x] 重复有效权益拒绝建单
|
||||
- [x] Filament OrderResource + 标记已支付 + 设置/说明页(插件内注册)
|
||||
- [x] 迁出/替换核心 `PaymentPluginPage` 骨架(导航隐藏)
|
||||
- [x] README + lang
|
||||
|
||||
### paid-content
|
||||
- [x] 插件骨架 + PSR-4 + `requires: larablog/payment`
|
||||
- [x] `article_products` migration/model
|
||||
- [x] 表单 Section 注入 + after_save 落库
|
||||
- [x] 与 `read_password` 互斥校验
|
||||
- [x] `article.access` 收紧 + 试读截断(渲染后 chars)
|
||||
- [x] 表列「付费」标识
|
||||
- [x] README + lang
|
||||
|
||||
### Review 修复(Bugbot 2026-08-12)
|
||||
- [x] `HtmlTeaser` 恒截断,永不返回全文(含 trial ≥ 正文长度)
|
||||
- [x] `article.access` 用 `tightenWith` 逐个折叠,只许变严
|
||||
- [x] `PluginManager::docsPath` 限制在插件目录内(防路径穿越)
|
||||
- [x] 建单/`markPaid` 事务内复核权益 + pending 单复用 + 跨单重复支付拦截
|
||||
- [x] checkout 仅接受 `visible`+`published` 文章
|
||||
- [x] 复用 pending 单时按当前价格重新定价(避免旧价成交)
|
||||
|
||||
### 文档与测试
|
||||
- [x] `docs/plugins.md` 扩展点 / requires
|
||||
- [x] OpenAPI + ApiV1Test 同步(破坏性说明写入 openapi)
|
||||
- [x] PHPUnit:依赖、access 矩阵、Stub、重复下单、API/RSS 防泄露
|
||||
- [x] 破坏性变更说明(匿名 API/RSS 不再泄密码/付费全文)
|
||||
|
||||
## 变更记录
|
||||
| 时间 | 原因 | 变更 |
|
||||
|------|------|------|
|
||||
| 2026-08-12 00:17 | SPEC 定稿后初版 | 创建功能清单 |
|
||||
| 2026-08-12 00:35 | 实现完成 | 勾选全部完成项 |
|
||||
| 2026-08-12 01:01 | Bugbot review 5 项发现 | 新增「Review 修复」分组并全部完成 |
|
||||
| 2026-08-12 01:12 | 用户确认「复用刷新为当前价」 | 补充 pending 单重新定价完成项 |
|
||||
@@ -0,0 +1,176 @@
|
||||
# 插件扩展面 + 支付 / 内容付费
|
||||
|
||||
## 状态
|
||||
- 状态:已定稿
|
||||
- 创建:2026-08-12 00:08
|
||||
- 最近更新:2026-08-12 01:12
|
||||
|
||||
## 背景
|
||||
一期已具备:主题槽(`@themeslot` / `theme.*`)、轻量 `Hook`、插件启停与骨架(`larablog/payment`、`membership`、双商城)。
|
||||
缺口:后台表单/表格无插件注入;无插件依赖声明;无订单/权益模型;文章仅有 `visible` + `read_password`,无法做付费阅读。
|
||||
全文出口今天不止 Web:`/api/v1/articles/{id}` 与 `/rss.xml` 会输出全文(密码文亦然),付费能力必须一并收口。
|
||||
|
||||
## 目标
|
||||
1. **插件扩展平台**:前台 + 后台均支持约定式注入(UI / 功能 / 数据关联)。
|
||||
2. **支付基建** `larablog/payment`:订单、Stub 网关、回调日志、权益记账;后台订单可查。
|
||||
3. **内容付费** `larablog/paid-content`:依赖 payment;文章价格/试读(表单注入);Web 访问闸 + 全出口防泄露。
|
||||
4. **插件元数据**:`requires`、README 使用说明;启用时校验依赖。
|
||||
|
||||
## 范围
|
||||
|
||||
### In scope
|
||||
1. 增强 `Hook`:在现有 `listen` / `dispatch` / `gather` 之上增加:
|
||||
- `collect(string $event, array $initial = [], mixed ...$payload): array`(数组合并,供 Filament 组件)
|
||||
- `filter(string $event, mixed $value, mixed ...$payload): mixed`(值管道)
|
||||
2. **首批约定扩展点**:
|
||||
| 点 | 类型 | 用途 |
|
||||
|---|---|---|
|
||||
| `theme.*` | gather | 已有前台槽 |
|
||||
| `article.access` | filter | 收紧访问决策(只许变严) |
|
||||
| `filament.article.form` | collect | 文章表单追加 Schema 组件 |
|
||||
| `filament.article.table.columns` | collect | 文章表追加列 |
|
||||
| `filament.article.actions` | collect | 文章表/页动作 |
|
||||
| `filament.article.mutate_before_save` | filter | `(array $data, ?Article $record)` |
|
||||
| `filament.article.after_save` | dispatch | `(Article $record, array $data)` |
|
||||
| `order.paid` / `order.refunded` | dispatch | payment 发出 |
|
||||
3. `plugin.json`:`requires`、`optional`、`docs`(相对 README,默认 `README.md`)。本期**不**引入无消费方的 `provides`。
|
||||
4. `PluginManager::enable`:硬依赖未启用则拒绝;后台卡片展示依赖与「查看说明」。
|
||||
5. **插件 Filament 注册机制(已定)**:订单 Resource / 插件设置页放在**插件命名空间**内;通过 `Filament\Panel::configureUsing`(或 `Filament::serving`)在 panel 配置阶段按「插件已启用」注册 `pages`/`resources`。不再把商务 UI 永久写死在核心 `app/Filament`(现有 skeleton 页可改为薄代理或迁入插件后删除)。
|
||||
6. **payment(最小可跑闭环)**:
|
||||
- 迁移:插件目录 `database/migrations`,Provider `loadMigrationsFrom`(`plugins:sync --enable` / 启用时确保可 migrate;文档说明需 `php artisan migrate`)。
|
||||
- 表:`orders`、`order_items`、`entitlements`、`payment_transactions`。
|
||||
- StubGateway:登录用户建单 → 确认页「模拟支付成功」→ paid → 写 entitlement → `order.paid`。
|
||||
- Filament:订单列表/详情;详情「标记已支付」运维按钮;插件设置说明页。
|
||||
7. **paid-content**:
|
||||
- 表:`article_products`;`composer.json` PSR-4 + sync。
|
||||
- 注入文章表单付费 Section;`after_save` 落库。
|
||||
- 实现 `article.access` 收紧;试读在 **HTML 渲染之后**按纯文本长度截断(避免截断 Markdown 语法)。
|
||||
8. **ArticleAccess(核心,纯决策)** + 全部全文出口收口:Web show、API show、RSS。
|
||||
9. 文档:`docs/plugins.md`、两插件 README;OpenAPI / `ApiV1Test` 同步 API 破坏性变更。
|
||||
10. PHPUnit:依赖校验、access 矩阵、Stub 入账、RSS/API 不泄露全文。
|
||||
|
||||
### Out of scope
|
||||
- 真实微信/支付宝/Stripe 商户对接(Stub + Gateway 接口预留)。
|
||||
- API / 小程序登录鉴权与「已购用户经 API 读全文」(见下方 API 策略)。
|
||||
- `membership` 套餐续费、主题市场真实售卖(仅预留 `product_type` 枚举值)。
|
||||
- 优惠券、购物车、游客购买、多币种汇率。
|
||||
- 任意后台 DOM 注入;远程安装插件。
|
||||
- 价格字段写入 `articles` 核心表。
|
||||
|
||||
## 方案要点
|
||||
|
||||
### 分层
|
||||
```text
|
||||
Core: ArticleAccess(决策)+ Hook 增强 + Filament 扩展点挂载 + 全文出口收口
|
||||
↑
|
||||
larablog/payment(订单 / Stub 网关 / entitlements / order.*)
|
||||
↑
|
||||
larablog/paid-content(article_products + 表单注入 + access 收紧 + 试读)
|
||||
```
|
||||
|
||||
### ArticleAccess 契约(已定)
|
||||
- **纯决策**,不含 HTTP 副作用。密码 POST 解锁仍由 `BlogController`(或细小 Action)写入 session 后**再次**调用 `resolve()`。
|
||||
- Decision:`status` ∈ `allow | need_password | need_purchase | need_login`;可选 `teaser_html`、`checkout_url`、`message`。
|
||||
- 解析顺序:
|
||||
1. 若 Web 上下文且用户为文章作者,或具备 spatie 角色 `admin` → `allow`(**仅 Web/session 用户**;API 本期无用户)。
|
||||
2. 若 `read_password` 非空且 session 未解锁 → `need_password`(**到此结束,插件不参与**)。
|
||||
3. 否则 `status=allow`,再跑 `Hook::filter('article.access', $decision, $ctx)`。
|
||||
- **`need_login` vs `need_purchase`(已定)**:本期 `paid-content` 对未购读者统一返回 `need_purchase`(未登录同样给购买 CTA,点购买时再要求登录)。`need_login` 预留给后续 membership 等插件,本期核心/paid-content 不主动产生。
|
||||
- **filter 合并规则(只许变严)**:严重度 `allow < need_login < need_purchase < need_password`;监听器返回的 status 仅当不低于当前严重度时才接受;`allow` 不能覆盖已收紧结果。元数据字段(teaser/checkout_url)可由插件填充。
|
||||
- **密码与付费互斥(已定)**:后台保存时校验——`read_password` 与 `article_products.enabled` 不可同时有效;冲突则校验失败提示。私密分享用密码,售卖用付费。
|
||||
|
||||
### API 策略(已定 · 对应原 B1)
|
||||
- 本期 API **保持匿名**(不加 Sanctum)。
|
||||
- API / RSS 调用 ArticleAccess 时 **user=null**:
|
||||
- 密码文 → 不返回全文(可返回标题/摘要/access 状态;与「今日 API 泄露密码文」相比为**明确破坏性收紧**)。
|
||||
- 付费文未购 → 试读 + `access`,无全文。
|
||||
- **不**在 API 识别管理员/已购(已购全文仅 Web 登录会话)。
|
||||
- OpenAPI 与测试同步;兼容说明写入 CHANGELOG/文档:「受限文不再经匿名 API/RSS 输出全文」。
|
||||
|
||||
### RSS(已定 · 对应原 B5)
|
||||
- 密码文、付费文:RSS `description`/`content` 仅输出试读或站点摘要字段,**禁止** `renderedHtml()` 全文。
|
||||
- 免费可见文保持现行为。
|
||||
|
||||
### 后台 UI 注入与保存(已定)
|
||||
- `ArticleForm`:`components` 合并 `Hook::collect('filament.article.form')`。
|
||||
- `CreateArticle` / `EditArticle`:`mutateFormDataBeforeSave` → filter `filament.article.mutate_before_save`;`afterSave` → dispatch `filament.article.after_save`。
|
||||
- 插件 state 前缀:`paid_content.*`;不得进入 Article `$fillable`;由 after_save 写 `article_products`。
|
||||
|
||||
### 插件页面注册(已定 · 对应原 B2)
|
||||
```php
|
||||
// 插件 ServiceProvider::boot
|
||||
Filament\Panel::configureUsing(function (Panel $panel): void {
|
||||
if (! app(PluginManager::class)->isEnabled('larablog/payment')) {
|
||||
return;
|
||||
}
|
||||
$panel->resources([OrderResource::class])->pages([PaymentSettingsPage::class]);
|
||||
});
|
||||
```
|
||||
(若 `configureUsing` 时机与 discover 冲突,实现时以「启用才注册、禁用不可见」为准,允许微调为 `Filament::serving`;SPEC 验收看行为不绑死 API 名。)
|
||||
|
||||
### 数据模型
|
||||
|
||||
#### payment
|
||||
| 表 | 关键字段 |
|
||||
|---|---|
|
||||
| `orders` | id, user_id(not null 本期), status(`pending`\|`paid`\|`cancelled`\|`refunded`), amount `decimal(10,2)`(= items 之和), currency, gateway, paid_at, meta json |
|
||||
| `order_items` | order_id, product_type, product_id, title, amount `decimal(10,2)` |
|
||||
| `entitlements` | user_id, product_type, product_id, source_order_id, granted_at, revoked_at nullable;**唯一索引** `unique(user_id, product_type, product_id)`(一行表示当前权益;撤销写 `revoked_at`,再次授予清除 `revoked_at` 并更新 source_order_id——**可移植,不用 partial unique**) |
|
||||
| `payment_transactions` | order_id, gateway, external_id nullable, payload json, status, timestamps |
|
||||
|
||||
`product_type` 枚举预留:`article` \| `theme` \| `membership`(后两者本期不实现业务)。
|
||||
|
||||
#### paid-content
|
||||
| 表 | 关键字段 |
|
||||
|---|---|
|
||||
| `article_products` | article_id unique, FK cascade delete → articles, enabled bool, price decimal(10,2), currency default `CNY`, trial_mode `chars`\|`none`, trial_value int default 200, timestamps |
|
||||
|
||||
### Stub 支付 UX(已定)
|
||||
- 独立简页(不依赖主题卡片):展示订单金额 →「模拟支付成功」。
|
||||
- Filament 订单详情额外提供「标记已支付」。
|
||||
- 必须登录下单;已有未撤销 entitlement → 拒绝重复建单并提示。
|
||||
|
||||
### 试读(已定)
|
||||
- 默认 `trial_mode=chars`,`trial_value=200`。
|
||||
- 流程:先 `ContentRenderer` 出 HTML → 再按可见文本截断并净化 → 作 teaser。
|
||||
- **teaser 恒为纯文本摘录,且必须保留部分正文**:当 `trial_value` ≥ 正文可见字数时,仍只输出一半(防止短文/大试读值导致全文泄露)。
|
||||
- 主题:default 提供试读+CTA 模板即可(example fallback 到 default)。
|
||||
|
||||
### 安全
|
||||
- 全文出口:Web show、API show、RSS、**列表摘要**均经 ArticleAccess(或等价 helper)。
|
||||
- 金额以后端 `article_products.price` 为准。
|
||||
- 订单状态机 + entitlement 唯一防重复开通;建单与 `markPaid` 均在事务内复核权益,pending 单按 user+商品复用而非叠加,且复用时按当前价格重新定价。
|
||||
- 只有 `visible` + `published` 的文章可被下单(草稿/隐藏文不可购买)。
|
||||
- 插件 manifest `docs` 只允许指向插件目录内的文件(拒绝 `..`、绝对路径、软链越界)。
|
||||
|
||||
## 验收标准
|
||||
- [ ] 未启用 payment/paid-content 时:Web 免费文/密码文行为与现网一致;匿名 API/RSS **不再**输出密码文全文(文档标明的破坏性收紧)。
|
||||
- [ ] 仅启用 payment:Stub 可完成一笔测试单并产生 entitlement;后台订单可见;「标记已支付」可用;对已有有效 entitlement 的商品重复下单被拒绝并提示。
|
||||
- [ ] 启用 paid-content 前未启用 payment → 拒绝启用并提示。
|
||||
- [ ] 启用后:文章表单有付费 Section;保存写入 `article_products`;与 `read_password` 互斥校验生效。
|
||||
- [ ] 禁用 paid-content / payment 后:其 Filament Resource/设置页不可见;文章表单不再出现付费 Section。
|
||||
- [ ] Web:未购付费文见试读+购买(含未登录,status=`need_purchase`);登录 Stub 支付后见全文;作者与 `admin` 角色见全文。
|
||||
- [ ] 匿名 API:付费/密码文无全文,响应含 `access`;免费文仍有全文。
|
||||
- [ ] RSS:付费/密码文无全文。
|
||||
- [ ] 插件 README 可从后台说明入口查看;`docs/plugins.md` 含扩展点与 requires。
|
||||
- [ ] PHPUnit 覆盖:依赖校验、access 矩阵、Stub 入账、重复下单、API/RSS 防泄露。
|
||||
|
||||
## 开放问题
|
||||
(实现前已拍板,保留备查)
|
||||
- [x] Stub UX:独立简页 + 后台「标记已支付」
|
||||
- [x] 游客购买:否,必须登录
|
||||
- [x] membership/主题市场:仅 `product_type` 预留
|
||||
- [x] 试读默认:chars=200,渲染后截断
|
||||
- [x] API:本期匿名;已购/管理员全文仅 Web
|
||||
- [x] 密码与付费:互斥
|
||||
- [x] 插件 Filament:插件内 Resource + Panel::configureUsing
|
||||
|
||||
## 变更记录
|
||||
| 时间 | 原因 | 变更 |
|
||||
|------|------|------|
|
||||
| 2026-08-12 00:08 | 初稿 | 创建 SPEC |
|
||||
| 2026-08-12 00:12 | qodercli review 阻塞项 B1–B5 | 定 API 匿名策略与破坏性说明;插件 Filament 注册机制;ArticleAccess 纯决策/互斥/只许变严;entitlement 可移植唯一键;RSS 纳入闸门;采纳保存钩子与迁移/试读等建议 |
|
||||
| 2026-08-12 00:14 | 二轮 review 无阻塞;采纳建议 | 明确 need_login 预留;补禁用可见性/重复下单验收;状态改为待用户确认 |
|
||||
| 2026-08-12 00:17 | 用户确认定稿 | 状态改为已定稿;配套 CHECKLIST / TESTPLAN |
|
||||
| 2026-08-12 01:01 | Bugbot review 发现 5 项(3 high) | 明确 teaser 必须保留部分正文;列表摘要纳入闸门;补充事务内复核/pending 复用、仅可售已发布文、docs 路径限制 |
|
||||
| 2026-08-12 01:12 | qodercli 复测提出金额陈旧,用户选「刷新为当前价」 | 复用 pending 单时按服务端当前价重新定价 |
|
||||
@@ -0,0 +1,57 @@
|
||||
# 插件扩展面 + 支付 / 内容付费 — 待测清单
|
||||
|
||||
## 状态
|
||||
- 对应 SPEC:`docs/specs/plugin-extension-commerce/SPEC.md`
|
||||
- 最近更新:2026-08-12 01:12
|
||||
|
||||
## 待测项
|
||||
|
||||
### 未启用商务插件
|
||||
- [x] 场景:免费文 Web;步骤:打开 show;期望:全文可见,行为同现网
|
||||
- [x] 场景:密码文 Web;步骤:未解锁打开 → 输错 → 输对;期望:密码页 / 错误 / 全文
|
||||
- [x] 场景:密码文 API;步骤:GET `/api/v1/articles/{id}`;期望:无 `content`/`content_html` 全文,有 `access.status=need_password`
|
||||
- [x] 场景:密码文 RSS;步骤:打开 `/rss.xml`;期望:该条目无全文 HTML
|
||||
|
||||
### 依赖与插件管理
|
||||
- [x] 场景:仅启用 paid-content;步骤:后台启用;期望:拒绝并提示需要 payment
|
||||
- [x] 场景:先 payment 再 paid-content;步骤:依次启用;期望:成功
|
||||
- [x] 场景:查看说明;步骤:插件卡片打开 docs;期望:可见 README 内容
|
||||
- [x] 场景:禁用后 UI;步骤:禁用两插件;期望:订单菜单/付费表单 Section 消失
|
||||
- [x] 场景:禁用被依赖的 payment;期望:拒绝并提示 dependents
|
||||
|
||||
### payment Stub
|
||||
- [x] 场景:登录建单支付;步骤:对测试商品走 checkout → 模拟成功;期望:order=paid、entitlement 有效、触发逻辑可观测
|
||||
- [ ] 场景:后台标记已支付;步骤:pending 订单点标记;期望:同上入账(人工)
|
||||
- [x] 场景:重复购买;步骤:已有权益再 checkout;期望:拒绝并提示
|
||||
- [x] 场景:未登录 checkout;步骤:访问建单;期望:重定向 `/login.shtml`(非 500)
|
||||
|
||||
### paid-content
|
||||
- [x] 场景:后台定价;步骤:编辑文章启用付费设价格保存;期望:`article_products` 有记录
|
||||
- [x] 场景:互斥;步骤:同时设密码与付费启用;期望:校验失败
|
||||
- [x] 场景:未购 Web(含未登录);步骤:打开付费文;期望:试读+购买,`need_purchase`,无全文
|
||||
- [x] 场景:购买后 Web;步骤:登录 Stub 支付后打开;期望:全文
|
||||
- [x] 场景:作者/admin;步骤:作者或 admin 打开未购付费文;期望:全文
|
||||
- [x] 场景:付费文 API/RSS;步骤:匿名拉取;期望:无全文
|
||||
- [x] 场景:首页列表;期望:不泄露付费/密码全文
|
||||
|
||||
### Review 修复验证(Bugbot 2026-08-12)
|
||||
- [x] 场景:`trial_value` 大于正文长度的付费短文;期望:Web/API 均看不到结尾内容
|
||||
- [x] 场景:两个 `article.access` 监听器(先收紧后放行 + 一个返回非法值);期望:最终仍 `need_purchase`
|
||||
- [x] 场景:manifest `docs` 写 `../../../../.env`;期望:`docsPath`/`readDocs` 返回 null
|
||||
- [x] 场景:同一商品连续两次 checkout;期望:复用同一 pending 单
|
||||
- [x] 场景:已由 A 单开通权益后对 B 单 `markPaid`;期望:抛错拒绝
|
||||
- [x] 场景:隐藏/未发布付费文 checkout;期望:422 且不建单
|
||||
- [x] 场景:复用 pending 单时价格已变;期望:订单与明细按新价刷新,仍只有 1 张单
|
||||
|
||||
### 回归
|
||||
- [x] 场景:免费文 API/RSS;期望:仍有全文(或既有摘要策略不变)
|
||||
- [x] 场景:主题槽/snippet;期望:不受影响
|
||||
- [x] 场景:`php artisan test` 相关用例全绿(40 passed)
|
||||
|
||||
## 变更记录
|
||||
| 时间 | 原因 | 变更 |
|
||||
|------|------|------|
|
||||
| 2026-08-12 00:17 | SPEC 定稿后初版 | 创建待测清单 |
|
||||
| 2026-08-12 00:50 | 实现+qodercli 闭环 | 勾选自动化覆盖项;后台标记已支付留人工 |
|
||||
| 2026-08-12 01:01 | Bugbot review 修复 | 新增 6 条 Review 修复验证项,全部自动化覆盖 |
|
||||
| 2026-08-12 01:12 | 复用单重新定价 | 新增第 7 条验证项(新价刷新且不叠单) |
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
# 皮肤(主题)开发指南
|
||||
|
||||
## 目录格式
|
||||
```
|
||||
themes/{slug}/
|
||||
theme.json # 元数据
|
||||
assets/ # 静态资源(CSS/图片/预览)
|
||||
style.css
|
||||
preview.svg # 后台卡片预览(建议 16:10)
|
||||
views/ # Blade,命名空间 theme::
|
||||
layout.blade.php
|
||||
home.blade.php
|
||||
article.blade.php
|
||||
...
|
||||
```
|
||||
|
||||
### theme.json 示例
|
||||
```json
|
||||
{
|
||||
"name": "my-theme",
|
||||
"title": "我的皮肤",
|
||||
"version": "1.0.0",
|
||||
"description": "说明文字",
|
||||
"slots": "*"
|
||||
}
|
||||
```
|
||||
|
||||
### 必须声明 `slots`
|
||||
皮肤应在 `theme.json` 声明支持的注入槽(后台「皮肤」卡片会校验):
|
||||
|
||||
| 写法 | 含义 |
|
||||
|---|---|
|
||||
| `"slots": "*"` 或 `"all"` | 支持全部标准槽(推荐) |
|
||||
| `"slots": ["head", "sidebar", "my_slot"]` | 显式列表;可混用 `"*"` 再加自定义槽 |
|
||||
| 省略 `slots` | **不合规**:后台标「未声明」;启用时警告 |
|
||||
|
||||
校验还会扫 `views/**/*.blade.php` 里的 `@themeslot` / `ThemeSlot::render`(非 default 主题会把未覆盖的 default 视图算进有效槽)。声明了但视图未调用 →「声明与视图不一致」。
|
||||
|
||||
## 视图约定
|
||||
- 使用 `@extends('theme::layout')`
|
||||
- 控制器传:`$articles` / `$article` / `$categories` / `$settings` / `$seo`
|
||||
- 静态资源 URL:`app(ThemeManager::class)->assetUrl('style.css')` → `/themes/{slug}/style.css`
|
||||
- 文案:`__('frontend.nav.home')` 等,勿写死中英文
|
||||
|
||||
## 内容注入点(ThemeSlot)
|
||||
|
||||
主题应在固定位置调用 `@themeslot('槽名')`(或 `ThemeSlot::render('槽名')`)。
|
||||
插件与后台「注入 / 广告 / 统计」都会写入同一套槽。
|
||||
|
||||
### 同一位置可多条
|
||||
`Hook::gather('theme.{slot}')` 会按注册顺序把**后台片段 + 各插件**拼成一段 HTML。
|
||||
同一槽塞 3 个插件广告 + 后台一段,都会出来(纵向叠放,由主题 CSS 决定外观)。
|
||||
|
||||
### 可控边界(换别人做的 theme)
|
||||
| 可控 | 不可强控 |
|
||||
|---|---|
|
||||
| 标准槽名(API):`head` / `sidebar` / … | 像素级宽高、是否做成卡片 |
|
||||
| 主题是否调用了 `@themeslot`(合规皮肤应全挂) | 侧栏在左还是右、主栏实际像素 |
|
||||
| 建议尺寸写在 `ThemeSlot::catalog()` / 后台 helper | 第三方主题漏掉某个槽 → 该注入静默不显示 |
|
||||
|
||||
结论:**槽名是硬约定,版式是软建议**。别人做 theme 只要挂齐标准槽,内容就能到「大概那个位置」;精确到「300×250 居中」做不到,也不该做进核心。合规检查可后续做:扫描 views 是否包含全部标准 `@themeslot`。
|
||||
|
||||
尺寸/位置说明见 `ThemeSlot::catalog()`(后台「注入」Tab 的 helper 同步显示)。
|
||||
|
||||
| 槽名 | 典型用途 | 建议尺寸(软) |
|
||||
|---|---|---|
|
||||
| `head` | 统计 / 额外 CSS·JS | 无版面 |
|
||||
| `body_start` / `body_end` | body 首尾脚本 | 无版面 |
|
||||
| `header_after` | 页头横幅、公告 | 通栏高约 60–120px |
|
||||
| `nav_after` | 导航后临时入口 | 文字链 |
|
||||
| `content_before` / `content_after` | 主栏前后 | 主栏宽约 640–760px |
|
||||
| `article_top` / `article_bottom` | 文章广告 | 主栏宽 × 90–250px |
|
||||
| `sidebar_before` / `sidebar` / `sidebar_after` | 侧栏广告 / 临时链接 | 侧栏约 280–320px,多条叠放 |
|
||||
| `footer_before` / `footer_after` | 页脚附加 | 通栏 |
|
||||
|
||||
### 后台怎么填
|
||||
`/admin` → **站点设置** → Tab「注入 / 广告 / 统计」:
|
||||
1. **统计代码** → `head`
|
||||
2. **侧栏广告** → `sidebar`
|
||||
3. **临时链接 HTML** → `sidebar_after`(可写 `<a href="...">活动</a>`)
|
||||
4. **文章顶/底广告** → `article_top` / `article_bottom`
|
||||
|
||||
也可继续用 **站点片段(Stylevars)** 做纯文本侧栏块;友情链接仍走「友情链接」资源。
|
||||
|
||||
### 插件怎么注入
|
||||
```php
|
||||
use App\Domain\Plugin\Hook;
|
||||
use App\Domain\Theme\ThemeSlot;
|
||||
|
||||
Hook::listen(ThemeSlot::event(ThemeSlot::SIDEBAR), function (string $html): string {
|
||||
return $html.'<p><a href="/promo">活动页</a></p>';
|
||||
});
|
||||
```
|
||||
|
||||
### 主题里任意位置
|
||||
在任意 Blade 加一行即可,例如正文中间:
|
||||
|
||||
```blade
|
||||
@themeslot('article_top')
|
||||
{{-- 或自定义新槽:插件 listen theme.my_slot,主题写 @themeslot('my_slot') --}}
|
||||
```
|
||||
|
||||
新槽无需改核心:约定 `theme.{name}` 事件即可。
|
||||
|
||||
## 发布
|
||||
```bash
|
||||
php artisan themes:publish
|
||||
# 或后台「皮肤」→ 启用 / 发布静态资源
|
||||
```
|
||||
资源复制到 `public/themes/{slug}/`。
|
||||
|
||||
## 回退
|
||||
`ThemeManager` 先注册 `default` views,再 `prepend` 当前主题;缺视图自动回退 default。
|
||||
|
||||
## 切换
|
||||
后台「皮肤」卡片启用,或改 Settings `general.active_theme`。
|
||||
Reference in New Issue
Block a user