Files
sub2api-add/docs/SUBSCRIPTION_PLUGIN_V1_RFC.md
T
Qiufeng 5feae3ad41
Business Plugins CI / check (plugin-admin) (push) Successful in 3m13s
Business Plugins CI / check (subscription-admin) (push) Successful in 1m41s
chore: initialize standalone business plugin repository
2026-08-27 23:36:08 +08:00

13 KiB
Raw Blame History

余额订阅业务插件 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 代码、数据库迁移、现有前端路由或 .s2plugin ABI。

2. 业务语义(供后续 Core 原子接口使用)

订阅插件以后需要保持当前业务语义,写操作必须由 Core 在单事务内完成:

  1. 支持多个同档位:同一用户可以拥有多个相同套餐/档位的订阅实例。每个实例有独立的 subscription_id、期限、配额、用量和审计记录;实例数量受套餐的上限字段约束。
  2. 支持单独续费:续费请求必须指定 subscription_id,只延长目标实例的期限或按 Core 规则生成续费记录,不影响同用户的其他同档位实例。
  3. 不可由用户取消:插件和用户端不提供“取消订阅”入口。管理员撤销属于单独的受控操作,需 Core 权限、原因、二次确认和审计;它不等同于用户取消。
  4. 购买时快照:价格、货币、有效期、额度、Group 覆盖和实例规则以购买时版本写入 Core 快照,后续编辑套餐不静默改写已购买实例。
  5. 余额付款:余额扣减、订阅创建/续费、余额流水、幂等记录和审计必须在 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 原子命令至少需要:

  1. 校验操作者为管理员并通过资源级授权;
  2. 锁定用户余额、套餐购买上限和目标订阅资源;
  3. 校验套餐可售、Group 覆盖、余额和实例上限;
  4. 写入购买快照;
  5. 扣余额并写余额流水;
  6. 创建新实例或仅续费指定 subscription_id;
  7. 写唯一 operation、幂等记录和管理员审计;
  8. 事务提交后失效相关缓存。

任一步失败都回滚整笔命令。插件不接触 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

首版只读页面建议顺序:

  1. 概览:Core 连接状态、插件版本、最后同步时间和健康状态;
  2. 套餐:价格、货币、有效期、额度、覆盖 Group 和可售状态;
  3. 用户订阅:按用户 ID、脱敏名称、订阅状态、实例档位和到期时间查询;
  4. 订阅详情:展示单个 subscription_id 的期限、窗口额度、用量和来源版本;
  5. 操作记录:展示操作者、Core request ID、结果和时间;
  6. 设置: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、测试管理员和测试订阅数据已准备,生产余额尚未接入。