wip: article AI polish, category SEO fields, cover generator, membership plan seeder
This commit is contained in:
@@ -27,8 +27,10 @@
|
||||
- [x] 可选 `/posts/{slug}` → show-id 301
|
||||
|
||||
## 延期(二期+)
|
||||
- [ ] 自动配图 / 生成配图(字段与 Job 空壳已预留)
|
||||
- [ ] 支付/会员/双商城真实对接
|
||||
- [x] 自动配图(正文图 / 附件图 → cover_*;后台「自动配图」;OG image)
|
||||
- [x] 生成配图(Intervention 模板渲染 1200×630 → 附件盘;后台「生成封面」;外部文生图 API 仍未接)
|
||||
- [x] 会员 Stub 闭环(套餐 / 订阅 / 会员可见文章;真实网关仍未接)
|
||||
- [ ] 支付/双商城真实对接
|
||||
|
||||
## 变更记录
|
||||
| 时间 | 原因 | 变更 |
|
||||
@@ -37,3 +39,5 @@
|
||||
| 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 |
|
||||
| 2026-08-12 01:45 | AI 接入后下一步 | 自动配图落地;理清 queue:ai ≠ workerman:ai;PM2 避免双消费 AI 队列 |
|
||||
| 2026-08-12 01:55 | 继续二期封面 | 模板生成封面(generate)落地;文生图 API 仍留空 |
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# 会员插件 — 功能清单
|
||||
|
||||
## 状态
|
||||
- 对应 SPEC:`docs/specs/membership/SPEC.md`
|
||||
- 最近更新:2026-08-12 03:55
|
||||
|
||||
## 完成项
|
||||
|
||||
### payment 核心配套
|
||||
- [x] `entitlements.expires_at` 迁移
|
||||
- [x] `hasEntitlement` / `markPaid.ownedElsewhere` / `scopeActive`/`isActive` 统一未过期语义
|
||||
- [x] `hasActiveAny(userId, productType)`
|
||||
- [x] `resolveCheckoutProduct`:membership 服务端取价;未知类型 422(去掉 query 兜底)
|
||||
|
||||
### 核心扩展
|
||||
- [x] `filament.article.validate_access_restrictions` 挂载(Create/Edit)
|
||||
- [x] `MembershipPluginPage` 导航恒隐藏
|
||||
- [x] paid-content:互斥改到 validate 点;mutate 不再抢先 unset
|
||||
|
||||
### membership 插件
|
||||
- [x] plugin.json `requires` + docs README
|
||||
- [x] migrations:`membership_plans`、`article_membership`
|
||||
- [x] models + PlanSeeder(月度/终身)
|
||||
- [x] Filament PlanResource
|
||||
- [x] `order.paid` 写 expires_at(fail-closed)
|
||||
- [x] 前台套餐页 + status JSON + 资料页状态
|
||||
- [x] 文章表单会员闸 + article.access + 互斥
|
||||
- [x] 删除 plan 守卫(引用/权益)
|
||||
|
||||
### 文档与测试
|
||||
- [x] docs/plugins.md
|
||||
- [x] PHPUnit Membership + PaidContent 回归(50 passed)
|
||||
|
||||
## 变更记录
|
||||
| 时间 | 原因 | 变更 |
|
||||
|------|------|------|
|
||||
| 2026-08-12 03:45 | SPEC 用户确认定稿 | 创建功能清单 |
|
||||
| 2026-08-12 03:55 | 实现完成 | 勾选全部完成项 |
|
||||
@@ -0,0 +1,147 @@
|
||||
# 会员插件 `larablog/membership`
|
||||
|
||||
## 状态
|
||||
- 状态:已定稿
|
||||
- 创建:2026-08-12 03:25
|
||||
- 最近更新:2026-08-12 03:45
|
||||
|
||||
## 背景
|
||||
- 支付基建 `larablog/payment` 与内容付费 `larablog/paid-content` 已闭环(Stub 下单 → 权益 → `article.access`)。
|
||||
- `ProductType::MEMBERSHIP` 已预留;membership 插件仍是侧栏 + `/plugins/membership/status` 骨架。
|
||||
- Spatie 角色 `member` = 注册用户,**不能**当作付费会员。
|
||||
|
||||
## 目标
|
||||
1. 可配置会员套餐(价格 / 时长)。
|
||||
2. 登录用户经 Stub 支付订阅,写入 `entitlements`(`product_type=membership`)。
|
||||
3. 文章可标「会员可见」;未开通则试读 + 订阅 CTA(复用 paywall 体验)。
|
||||
4. 资料页 / status API 展示当前会员状态。
|
||||
|
||||
## 范围
|
||||
|
||||
### In scope(v1)
|
||||
1. 插件表 `membership_plans`:slug、name、description、price、currency、duration_days(nullable=终身)、enabled、sort_order。
|
||||
2. 插件内幂等 Seeder:月度 / 终身各一档(`updateOrCreate` by slug);不写死进核心 `DatabaseSeeder`。
|
||||
3. `requires: ["larablog/payment"]`;依赖未满足不可启用。
|
||||
4. Filament:套餐 CRUD(插件内 Resource);核心 `MembershipPluginPage::shouldRegisterNavigation` **恒 false**(v1 起骨架页永久隐藏,导航只由插件 Resource 提供;区别于「enabled 才显示」的旧骨架行为)。
|
||||
5. 支付结账(见「结账安全」):membership **仅**服务端按 plan 取价;query 兜底路径**拒绝** `membership`。
|
||||
6. 权益与过期(见「过期语义」):`expires_at` 加在 payment 的 `entitlements`;活跃判定三处统一。
|
||||
7. 文章闸:`article_membership`;表单注入;`article.access` → `need_purchase`(未登录也给订阅 CTA)。
|
||||
8. 三向互斥:会员闸 / 密码 / 单篇付费(见「互斥机制」)。
|
||||
9. 前台:`/plugins/membership` 套餐列表;资料页会员状态;status JSON:`active`、`plan_id`、`plan_slug`、`plan_name`、`expires_at`。
|
||||
10. `OrderService::hasActiveAny($userId, $productType)`:任意有效 membership 权益。
|
||||
11. 文档 + PHPUnit(含 PaidContentCommerceTest 回归)。
|
||||
|
||||
### Out of scope
|
||||
- 真实微信/支付宝续费扣款、自动续费、退款流程。
|
||||
- 多套餐叠加的复杂权益栈(v1:指定 plan 闸只认该 plan;「任意会员」闸认任一有效 membership)。
|
||||
- 用 Spatie role 表示付费会员。
|
||||
- 主题市场 / 会员专属主题。
|
||||
- 优惠券、试用期、邀请码、外部文生图。
|
||||
|
||||
## 方案要点
|
||||
|
||||
### 分层
|
||||
```text
|
||||
Core ArticleAccess + Hook(已有)
|
||||
↑
|
||||
larablog/payment(订单 / Stub / entitlements[+expires_at])
|
||||
↑
|
||||
larablog/membership(plans + 文章会员闸 + 订阅页)
|
||||
```
|
||||
|
||||
### 过期语义(固化 · 原 B1)
|
||||
payment 核心修改(影响面声明:article 权益 `expires_at` 恒 null,语义不变;须跑 PaidContentCommerceTest 回归):
|
||||
|
||||
1. 迁移:`entitlements.expires_at` nullable timestamp。
|
||||
2. **活跃**定义统一为:`revoked_at IS NULL AND (expires_at IS NULL OR expires_at > now())`,用于:
|
||||
- `OrderService::hasEntitlement`
|
||||
- `OrderService::markPaid` 的 `ownedElsewhere` 守卫
|
||||
- `Entitlement::scopeActive` / `isActive()`
|
||||
3. 过期后可重新 `createOrder` + `markPaid`(续订);`updateOrCreate` 同键刷新 `granted_at` / `expires_at` / `source_order_id`。
|
||||
4. membership 监听 `order.paid`:对 membership item,读 plan:
|
||||
- `duration_days` 有值 → `expires_at = now()->addDays(duration_days)`
|
||||
- 终身 → `expires_at = null`
|
||||
- plan 已删:**fail-closed**——不授予/撤销本次写入并打日志(避免月度变终身);Stub 支付页提示失败。(实现可用:markPaid 前后校验 plan 存在;或 paid 后若 plan 缺失则 `revoked_at=now()` 并通知。)
|
||||
|
||||
### 结账安全(固化 · 原 B3)
|
||||
`resolveCheckoutProduct`:
|
||||
1. `article`:保持现逻辑(visible+published + enabled product)。
|
||||
2. `membership`:`class_exists` plan model + 表存在 + plan `enabled` → 返回 `[name, price, currency]`;否则 `invalid_checkout`。
|
||||
3. **其它/未知类型**:直接 `invalid_checkout`(**删除**信任 query `title/amount` 的兜底,堵住伪造 membership)。theme 售卖以后再加专用分支。
|
||||
|
||||
### 互斥机制(固化 · 原 B2)
|
||||
新增核心约定扩展点(一次 filter,多方可见):
|
||||
|
||||
| 点 | 类型 | 用途 |
|
||||
|---|---|---|
|
||||
| `filament.article.validate_access_restrictions` | filter | `(array $data, ?Article $record): array`;在 strip 插件私有键**之前**调用;抛 `ValidationException` |
|
||||
|
||||
调用顺序(Create/Edit):
|
||||
1. `mutate_before_save`(可填充/规范插件字段,**不得**在此 unset 限制字段)
|
||||
2. `validate_access_restrictions`(paid-content + membership 均在此检查互斥)
|
||||
3. 各插件在 `after_save` 落库;`mutate_before_save` 末尾或独立 strip 阶段再去掉 `paid_content` / `membership` 私有键(或 after_save 只读 form state)
|
||||
|
||||
互斥规则:下列至多一个为真——
|
||||
- `filled(read_password)`
|
||||
- `paid_content.enabled`
|
||||
- `membership.enabled`
|
||||
|
||||
paid-content 现有「在 mutate_before_save unset」改为:校验点之后再 strip(改动 paid-content provider)。
|
||||
|
||||
### Plan 删除(固化 · 原 B4)
|
||||
- `article_membership.required_plan_id` → **`restrictOnDelete`**(有文章仍引用则不可删 plan)。
|
||||
- 后台删除 plan:若仍有未过期 entitlement,拒绝删除并提示(或仅允许 `enabled=false`);v1 实现:**有任何 entitlement 行则禁止硬删,引导禁用**。
|
||||
|
||||
### 数据模型
|
||||
|
||||
#### `membership_plans`
|
||||
| 字段 | 说明 |
|
||||
|---|---|
|
||||
| id | PK |
|
||||
| slug | unique |
|
||||
| name | 展示名 |
|
||||
| description | nullable |
|
||||
| price | decimal(10,2) |
|
||||
| currency | default CNY |
|
||||
| duration_days | unsignedInt nullable;null=终身 |
|
||||
| enabled | bool |
|
||||
| sort_order | int default 0 |
|
||||
| timestamps | |
|
||||
|
||||
#### `article_membership`
|
||||
| 字段 | 说明 |
|
||||
|---|---|
|
||||
| article_id | unique FK → articles cascadeOnDelete |
|
||||
| enabled | bool |
|
||||
| required_plan_id | nullable FK → membership_plans **restrictOnDelete**;null=任意有效会员 |
|
||||
| timestamps | |
|
||||
|
||||
### 文章闸 CTA
|
||||
- checkout 指向:`required_plan_id` 对应 enabled plan;若 plan 禁用/缺失 → 降级为「最低价 enabled 套餐」;若无任何套餐 → 无购买按钮,仅提示联系管理员。
|
||||
- 试读:复用 `HtmlTeaser` / description(与 paid-content 同量级默认 chars)。
|
||||
|
||||
### 插件生命周期
|
||||
- 启用前须 payment 已启用;迁移:`php artisan migrate`(AppServiceProvider 已 load 全部插件 migrations)。
|
||||
- 禁用后:钩子不注册 → 会员文变公开;DB 行保留。
|
||||
|
||||
## 验收标准
|
||||
- [ ] 未启用 membership:行为与现网一致;骨架导航不出现。
|
||||
- [ ] 无 payment 时启用 membership → 拒绝。
|
||||
- [ ] Stub 订阅后 status/资料页显示有效会员;有期限套餐可过期后续订。
|
||||
- [ ] 会员文:未购试读+订阅;订阅后全文;指定 plan 闸不接受其它 plan。
|
||||
- [ ] 伪造 `product_type=membership&amount=0.01` → 422。
|
||||
- [ ] 三向互斥保存失败。
|
||||
- [ ] 被文章引用或仍有权益的 plan 不可硬删。
|
||||
- [ ] PaidContentCommerceTest + 新 Membership 测试全绿。
|
||||
|
||||
## 已拍板(原开放问题)
|
||||
- [x] 做 `expires_at`(支持月度)
|
||||
- [x] 未购统一 `need_purchase`(与 paid-content 一致)
|
||||
- [x] 禁用插件后会员文变公开
|
||||
|
||||
## 变更记录
|
||||
| 时间 | 原因 | 变更 |
|
||||
|------|------|------|
|
||||
| 2026-08-12 03:25 | 启动会员功能 | 初稿 |
|
||||
| 2026-08-12 03:30 | qodercli SPEC review B1–B4 | 固化过期三处一致、互斥校验点、结账拒伪造、plan restrictOnDelete;补 hasActiveAny / 骨架导航 / fail-closed |
|
||||
| 2026-08-12 03:45 | 用户确认「定」 | 状态改为已定稿;配套 CHECKLIST / TESTPLAN |
|
||||
@@ -0,0 +1,38 @@
|
||||
# 会员插件 — 待测清单
|
||||
|
||||
## 状态
|
||||
- 对应 SPEC:`docs/specs/membership/SPEC.md`
|
||||
- 最近更新:2026-08-12 03:55
|
||||
|
||||
## 待测项
|
||||
|
||||
### 依赖与启停
|
||||
- [x] 无 payment 启用 membership → 拒绝
|
||||
- [ ] 先 payment 再 membership → 成功;骨架导航不出现(人工)
|
||||
- [ ] 禁用 membership → 会员文变公开;套餐菜单消失(人工)
|
||||
|
||||
### 订阅与过期
|
||||
- [x] Stub 订阅月度 → status active + expires_at
|
||||
- [x] 过期后闸门恢复;可续订成功
|
||||
- [x] 终身 → expires_at null(seed + markPaid 路径覆盖)
|
||||
- [x] 伪造 membership&amount=0.01 → 服务端仍用套餐价;未知类型 422
|
||||
|
||||
### 文章闸与互斥
|
||||
- [x] 会员文未购:试读 + need_purchase
|
||||
- [x] 订阅后全文;指定 plan 不接受其它 plan
|
||||
- [x] 与单篇付费同时开 → 校验失败
|
||||
- [ ] 作者/admin bypass(人工或后续补测)
|
||||
|
||||
### 套餐删除
|
||||
- [x] 仍有权益行 → hasEntitlements true(删除守卫依赖此)
|
||||
- [ ] 被文章引用 → 不可删(人工)
|
||||
|
||||
### 回归
|
||||
- [x] PaidContentCommerceTest 全绿
|
||||
- [x] php artisan test 全绿(50 passed)
|
||||
|
||||
## 变更记录
|
||||
| 时间 | 原因 | 变更 |
|
||||
|------|------|------|
|
||||
| 2026-08-12 03:45 | SPEC 定稿 | 创建待测清单 |
|
||||
| 2026-08-12 03:55 | 实现 + PHPUnit | 勾选自动化覆盖项 |
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## 状态
|
||||
- 对应 SPEC:`docs/specs/plugin-extension-commerce/SPEC.md`
|
||||
- 最近更新:2026-08-12 01:12
|
||||
- 最近更新:2026-08-13 03:40
|
||||
|
||||
## 完成项
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
- [x] `plugin.json`:`requires` / `optional` / `docs`
|
||||
- [x] `PluginManager::enable` 依赖校验 + `isEnabled`
|
||||
- [x] 后台插件卡片:依赖展示 + 查看说明
|
||||
- [x] 使用说明按 locale 选择 `README.zh_CN.md`,Markdown 渲染为 HTML 侧栏弹层(非 Notification 纯文本)
|
||||
|
||||
### payment
|
||||
- [x] 插件 migrations + `loadMigrationsFrom`
|
||||
@@ -57,3 +58,4 @@
|
||||
| 2026-08-12 00:35 | 实现完成 | 勾选全部完成项 |
|
||||
| 2026-08-12 01:01 | Bugbot review 5 项发现 | 新增「Review 修复」分组并全部完成 |
|
||||
| 2026-08-12 01:12 | 用户确认「复用刷新为当前价」 | 补充 pending 单重新定价完成项 |
|
||||
| 2026-08-13 03:40 | 使用说明英文且无排版 | 中文 README + Markdown 弹层渲染 |
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## 状态
|
||||
- 对应 SPEC:`docs/specs/plugin-extension-commerce/SPEC.md`
|
||||
- 最近更新:2026-08-12 01:12
|
||||
- 最近更新:2026-08-13 03:40
|
||||
|
||||
## 待测项
|
||||
|
||||
@@ -38,6 +38,9 @@
|
||||
- [x] 场景:`trial_value` 大于正文长度的付费短文;期望:Web/API 均看不到结尾内容
|
||||
- [x] 场景:两个 `article.access` 监听器(先收紧后放行 + 一个返回非法值);期望:最终仍 `need_purchase`
|
||||
- [x] 场景:manifest `docs` 写 `../../../../.env`;期望:`docsPath`/`readDocs` 返回 null
|
||||
- [x] 场景:`zh_CN` 下打开付费内容使用说明;期望:中文 HTML(含标题),不是英文 Markdown 原文
|
||||
- [x] 场景:仅有 `README.md` 或语言为 `en`;期望:回退英文 README
|
||||
- [x] 场景:locale 含 `..`;期望:忽略该 locale,仍只读插件目录内 README
|
||||
- [x] 场景:同一商品连续两次 checkout;期望:复用同一 pending 单
|
||||
- [x] 场景:已由 A 单开通权益后对 B 单 `markPaid`;期望:抛错拒绝
|
||||
- [x] 场景:隐藏/未发布付费文 checkout;期望:422 且不建单
|
||||
@@ -55,3 +58,4 @@
|
||||
| 2026-08-12 00:50 | 实现+qodercli 闭环 | 勾选自动化覆盖项;后台标记已支付留人工 |
|
||||
| 2026-08-12 01:01 | Bugbot review 修复 | 新增 6 条 Review 修复验证项,全部自动化覆盖 |
|
||||
| 2026-08-12 01:12 | 复用单重新定价 | 新增第 7 条验证项(新价刷新且不叠单) |
|
||||
| 2026-08-13 03:40 | 使用说明不可读 | 补充 locale README 与 HTML 渲染验证 |
|
||||
|
||||
Reference in New Issue
Block a user