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,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 条验证项(新价刷新且不叠单) |
|
||||
Reference in New Issue
Block a user