13 KiB
余额订阅业务插件 V1 RFC
状态:V1 只读试验实现已落库;余额购买、续费、撤销仍为 V1.1 Draft
本文定义基于 PLUGIN_FRAMEWORK_V1_RFC.md 的第一个业务插件试验,对应实现为 plugins/subscription-admin。插件是独立端口的管理员后台,复用 Sub2API Core 的管理员鉴权,不创建 Core 用户表,也不直接连接 Core 数据库。当前实现已完成框架、管理员会话、只读套餐/余额/订阅/审计联调;余额购买、续费和撤销写操作后置到 Core 原子接口冻结之后。
1. 目标和范围
1.1 V1 目标
- 提供独立的管理员订阅后台,普通账号拒绝登录和访问;
- 插件登录调用 Core 现有
/api/v1/auth/login、/api/v1/auth/login/2fa和/api/v1/auth/me,不维护第二套用户密码; - 展示 Core 中的套餐、用户余额和订阅实例状态;
- 通过现有
custom_menu_items的visibility=admin入口嵌入 Core 页面,也支持新窗口打开; - 插件可独立升级、停用和回滚,不影响 Core 网关、余额账本和已有订阅;
- 记录未来订阅写操作所需的业务语义、幂等和审计契约,但本阶段不执行扣款。
1.2 V1 非目标
- 不接入支付宝、微信、Stripe 或其他外部支付渠道;
- 不允许普通用户登录插件后台或在插件内自助购买;
- 不把余额、套餐、订阅额度复制成插件自己的权威账本;
- 不把每次网关请求改成调用插件 RPC;
- 不通过多个现有 Admin API 调用拼接一次购买;
- 不在本次设计阶段修改 Core 代码、数据库迁移、现有前端路由或
.s2pluginABI。
2. 业务语义(供后续 Core 原子接口使用)
订阅插件以后需要保持当前业务语义,写操作必须由 Core 在单事务内完成:
- 支持多个同档位:同一用户可以拥有多个相同套餐/档位的订阅实例。每个实例有独立的
subscription_id、期限、配额、用量和审计记录;实例数量受套餐的上限字段约束。 - 支持单独续费:续费请求必须指定
subscription_id,只延长目标实例的期限或按 Core 规则生成续费记录,不影响同用户的其他同档位实例。 - 不可由用户取消:插件和用户端不提供“取消订阅”入口。管理员撤销属于单独的受控操作,需 Core 权限、原因、二次确认和审计;它不等同于用户取消。
- 购买时快照:价格、货币、有效期、额度、Group 覆盖和实例规则以购买时版本写入 Core 快照,后续编辑套餐不静默改写已购买实例。
- 余额付款:余额扣减、订阅创建/续费、余额流水、幂等记录和审计必须在 Core 同一个事务边界内完成。
这些语义是插件的业务约束,不代表当前 Core 已经提供了对应的业务插件 API。第一阶段只验证读取现有订阅数据,写入契约单独评审。
3. 推荐拓扑
Core 管理后台
└─ custom_menu_items (visibility=admin)
└─ sandbox iframe / 新窗口
└─ 反向代理 -> Plugin `127.0.0.1:PLUGIN_PORT`
├─ Plugin /login -> Core /api/v1/auth/login (+ /login/2fa)
├─ Plugin /auth/me -> 只允许 role=admin
├─ HttpOnly 插件会话(不建 Core 用户表)
└─ 服务端 Bearer Core JWT -> 现有 Core Admin API(V1 只读)
插件 UI 只访问自己的 BFF;Core JWT、refresh token 和 Admin API Key 只在插件服务端会话或 secret 中出现。iframe 不会自动继承 Core localStorage 登录态,因此 V1 首屏显示插件登录页属于预期行为。sandbox 或第三方 Cookie 策略可能导致嵌入会话刷新后失效,插件必须提供新窗口登录路径;登录密码只在一次转发请求中经过插件后端,不落库、不写日志。
4. 权威边界
4.1 必须留在 Core
- 用户身份、管理员权限和资源授权;
- 用户余额和余额流水;
- 套餐价格、货币、有效期、额度、包含的 Group、实例模式和上限;
- 购买时的套餐快照;
- 订阅实例状态、期限、配额、reserved 和用量;
- 余额扣减、订阅创建/续费、撤销、退款/补偿;
- 额度预留、实际结算、幂等、防超卖、缓存失效和网关放行判定。
4.2 插件可以负责
- 管理员登录代理、插件会话和插件内角色;
- 套餐、用户和订阅的分页筛选与展示;
- 只读缓存、操作结果页和管理员审计视图;
- 未来写操作的确认表单,但提交必须调用 Core 原子命令;
- 插件 UI 的版本和发布。
插件的本地副本可删除、可重建、带来源版本和更新时间,不能作为网关授权或扣费依据。
5. V1 现有 Core API 适配
V1 不调用尚不存在的 /api/v1/plugin-host/*。插件后端通过严格 allowlist 调用 Core 已有接口,具体路径按部署的 Core 版本冻结:
POST /api/v1/auth/login
POST /api/v1/auth/login/2fa
POST /api/v1/auth/refresh
POST /api/v1/auth/logout
GET /api/v1/auth/me
GET /api/v1/settings/public
GET /api/v1/admin/payment/plans
GET /api/v1/admin/subscriptions
GET /api/v1/admin/subscriptions/{id}
GET /api/v1/admin/users/{id}
GET /api/v1/admin/users/{id}/subscriptions
请求头只由插件后端添加:
Authorization: Bearer CORE_ACCESS_TOKEN
X-Request-Id: UNIQUE_REQUEST_ID
列表接口必须设置分页、字段最小化、超时和审计。插件配置列出具体方法和路径,不能使用任意 URL 或宽泛通配符。Core 返回 401 时,插件先尝试一次 refresh;refresh 失败就销毁插件会话并要求重新登录。
6. 未来写操作契约(V1.1,当前不实现)
现有 Core Admin API 的 assign、extend、revoke 等操作属于 Core 管理面,当前不把它们组合成余额购买流程。要在插件内支持余额购买,Core 需要增加版本化原子命令或等价 Host Adapter,并在 Core 内完成余额、订单/购买记录、订阅和审计的一致性事务。
6.1 余额购买请求示例
POST /api/v1/plugin-host/v1/subscription-purchases/balance
Authorization: Bearer PLUGIN_ACCESS_TOKEN
Idempotency-Key: subscription-purchase-UNIQUE_ID
Content-Type: application/json
{
"user_id": 123,
"plan_id": 12,
"expected_plan_version": 4,
"expected_price": "19.00",
"currency": "USD",
"request_id": "UNIQUE_REQUEST_ID"
}
该路径是未来接口示例,当前 Core 没有实现。Core 必须重新读取当前套餐和余额,expected_* 只用于发现界面陈旧;价格以字符串 decimal 处理,不使用二进制浮点数。
6.2 Core 事务要求
未来 Core 原子命令至少需要:
- 校验操作者为管理员并通过资源级授权;
- 锁定用户余额、套餐购买上限和目标订阅资源;
- 校验套餐可售、Group 覆盖、余额和实例上限;
- 写入购买快照;
- 扣余额并写余额流水;
- 创建新实例或仅续费指定
subscription_id; - 写唯一 operation、幂等记录和管理员审计;
- 事务提交后失效相关缓存。
任一步失败都回滚整笔命令。插件不接触 SQL transaction,也不执行失败后的自行补偿。
6.3 操作状态和重试
accepted -> processing -> completed
└── failed
同一 Idempotency-Key 重试返回第一次操作结果,不重复扣款。网络超时后插件查询 operation 状态,不再次创建购买。401/403 不重试;429 按 Retry-After 有上限退避;只有明确幂等命令才允许对 5xx 重试。
7. 同档位实例和续费规则
未来实现必须覆盖以下测试矩阵:
| 场景 | 预期结果 |
|---|---|
| 同一用户购买两个相同档位 | 产生两个独立实例,各自有期限、配额和 subscription_id。 |
| 续费实例 A | 只改变实例 A;实例 B 的期限和配额保持不变。 |
| 达到实例上限 | 返回稳定业务错误,不扣余额、不创建半成品订阅。 |
| 用户尝试取消 | 插件没有取消入口;Core 用户接口也不提供用户自助取消语义。 |
| 管理员撤销 | 走单独的受控 Core 操作,要求原因、审计和明确的退款/补偿规则。 |
| 两个管理员并发购买 | 由 Core 锁和幂等保证不超卖;失败方查询最终状态。 |
8. 套餐快照和变更
推荐后续 Core 设计提供不可变套餐版本或购买快照,至少包含价格、货币、有效期、三类额度、覆盖 Group、实例模式和上限。当前 schema 的运行时 Group 覆盖和套餐字段存在可变性,不能在本 RFC 中宣称已经提供完整快照保证。
套餐下架只影响新购买;已有订阅如何处理由 Core 现有授权和计费规则决定。若要迁移已有实例,需要单独的预览、影响数量、确认 token、幂等键和审计命令。
9. 管理员 UI V1
首版只读页面建议顺序:
- 概览:Core 连接状态、插件版本、最后同步时间和健康状态;
- 套餐:价格、货币、有效期、额度、覆盖 Group 和可售状态;
- 用户订阅:按用户 ID、脱敏名称、订阅状态、实例档位和到期时间查询;
- 订阅详情:展示单个
subscription_id的期限、窗口额度、用量和来源版本; - 操作记录:展示操作者、Core request ID、结果和时间;
- 设置:Core 地址、allowlist、会话/凭据状态和健康检查;secret 只允许轮换,不允许回显。
余额购买、单独续费和管理员撤销在 V1 只显示“未启用”状态,不渲染可提交按钮,避免误触发现有非原子接口。
10. 安全验收
- 页面源码、网络请求、下载文件和浏览器存储中没有 Admin Key 或 Core JWT;
- 普通用户使用 Core JWT 登录插件返回
403,直接访问插件 API 也返回403; - Core access/refresh token 不进入插件响应、页面、日志和异常堆栈;
- 插件后端只访问配置中的 Core API 路径,禁止 SSRF 和任意 URL 代理;
- 插件服务停用后,已有订阅仍按 Core 原有网关鉴权和计费逻辑运行;
- 清空插件本地缓存后,可从 Core 重建只读列表,不影响核心余额和订阅;
- 用户端没有取消订阅入口,管理员撤销(未来启用时)必须有原因、审计和幂等键。
11. 测试计划
11.1 插件后端
- 管理员登录、2FA、普通用户拒绝、会话过期、登出和 CSRF;
- Core token refresh、撤销和 Core
401/403/429/5xx处理; - 套餐/余额/订阅查询的分页、字段脱敏、超时和缓存重建;
- allowlist 拒绝未声明路径、任意 URL 和跨插件会话;
- 多实例会话存储和服务重启后的会话策略。
11.2 未来 Core 写操作
- 两个同档位实例、目标实例单独续费和实例上限;
- 用户取消入口不存在,管理员撤销的审计和补偿规则;
- 余额不足、套餐下架、价格版本冲突、并发购买和幂等重试;
- 事务失败时余额、订单、订阅和审计均保持原状;
- 购买快照内容不可变、缓存只在提交后失效。
11.3 浏览器和部署
- 管理员可在 iframe 和新窗口完成登录;普通用户菜单不可见且 API 拒绝;
- 425px、900px、1440px 下无横向溢出、遮挡或敏感字段泄露;
- DevTools 请求、下载、页面源和日志中没有 secret;
- 反向代理、HTTPS、低权限运行、停用、升级和回滚流程可恢复。
12. 分阶段实施计划
Phase 0:框架契约
- 冻结插件端口、反向代理路径、
custom_menu_items字段和健康检查; - 冻结 Core 登录/2FA、管理员角色判断和现有只读 API allowlist;
- 确认会话存储、Core token 生命周期、日志脱敏和 secret 轮换;
- 不改 Core 代码、迁移、现有插件 ABI 或前端路由。
Phase 1:插件框架最小实现
- 独立服务目录、清单、签名和部署兼容性检查;
- Core 登录代理、管理员角色校验、HttpOnly 插件会话和 CSRF;
- Core API BFF、健康检查、审计和只读示例页;
- 本地 contract test、浏览器验收和反向代理样例。
Phase 2:订阅只读插件
- 套餐、用户余额、订阅实例和操作记录只读查询;
- 同档位实例/单独续费/不可取消语义的 UI 展示与测试数据;
- 只接入测试 Core,不连接生产余额。
Phase 3:单独立项的 Core 写能力
- 评审并实现 Core 原子余额购买/续费/撤销接口;
- 增加 Core 幂等存储、审计和购买快照后,再接入插件写操作;
- 通过并发、事务、浏览器和回滚门禁后才启用生产 scope。
13. 实施前检查清单
- 插件只允许 Core
role=admin登录,且不创建用户表。 - 独立端口、反向代理路径和
visibility=admin菜单入口已确定。 - iframe 登录页和新窗口登录页均可用,已知晓 V1 不提供无感 SSO。
- 只读 Core API allowlist、分页、字段脱敏和缓存策略已冻结。
- 同档位多实例、单独续费、用户不可取消和管理员撤销规则已确认。
- 余额购买写操作明确等待 Core 原子接口,不使用现有多个接口拼接。
- 测试 Core、测试管理员和测试订阅数据已准备,生产余额尚未接入。