Files
sub2apipay/.claude/plan.md
2026-03-14 03:45:37 +08:00

16 KiB
Raw Blame History

Sub2ApiPay 改造方案

一、概述

基于 Pincc 参考界面,改造 Sub2ApiPay 项目,新增:

  • 用户页面:双 Tab按量付费 / 包月套餐),渠道卡片展示,充值弹窗,订阅购买流程
  • 管理员界面:渠道管理、订阅套餐管理、系统配置
  • 数据库存储配置:支付渠道等配置从环境变量迁移至数据库,支持运行时修改

二、数据库 Schema 变更

2.1 新增模型

// 渠道展示配置(管理员配置,对应 Sub2API 的 group
model Channel {
  id             String   @id @default(cuid())
  groupId        Int      @unique @map("group_id")     // Sub2API group ID
  name           String                                  // 显示名称
  platform       String   @default("claude")             // 分类: claude/openai/gemini/codex
  rateMultiplier Decimal  @db.Decimal(10, 4) @map("rate_multiplier") // 倍率
  description    String?  @db.Text                       // 描述
  models         String?  @db.Text                       // JSON数组: 支持的模型列表
  features       String?  @db.Text                       // JSON数组: 功能特性列表
  sortOrder      Int      @default(0) @map("sort_order") // 排序
  enabled        Boolean  @default(true)                  // 是否启用
  createdAt      DateTime @default(now()) @map("created_at")
  updatedAt      DateTime @updatedAt @map("updated_at")

  @@index([sortOrder])
  @@map("channels")
}

// 订阅套餐配置(管理员配置价格后才可售卖)
model SubscriptionPlan {
  id            String   @id @default(cuid())
  groupId       Int      @unique @map("group_id")       // Sub2API group ID
  name          String                                    // 套餐名称
  description   String?  @db.Text                         // 描述
  price         Decimal  @db.Decimal(10, 2)               // CNY 价格
  originalPrice Decimal? @db.Decimal(10, 2) @map("original_price") // 原价(划线价)
  validityDays  Int      @default(30) @map("validity_days") // 有效期天数
  features      String?  @db.Text                         // JSON数组: 特性描述
  forSale       Boolean  @default(false) @map("for_sale") // 是否启用售卖
  sortOrder     Int      @default(0) @map("sort_order")
  createdAt     DateTime @default(now()) @map("created_at")
  updatedAt     DateTime @updatedAt @map("updated_at")

  orders        Order[]

  @@index([forSale, sortOrder])
  @@map("subscription_plans")
}

// 系统配置(键值对,支持运行时修改)
model SystemConfig {
  key       String   @id
  value     String   @db.Text
  group     String   @default("general")  // general / payment / limits / display
  label     String?                        // 配置项显示名称
  updatedAt DateTime @updatedAt @map("updated_at")

  @@index([group])
  @@map("system_configs")
}

2.2 扩展 Order 模型

在现有 Order 模型上新增字段,复用整套支付流程:

model Order {
  // ... 现有字段不变 ...

  // 新增:订单类型
  orderType     String   @default("balance") @map("order_type")  // "balance" | "subscription"

  // 新增订阅相关orderType="subscription" 时有值)
  planId              String?           @map("plan_id")
  plan                SubscriptionPlan? @relation(fields: [planId], references: [id])
  subscriptionGroupId Int?              @map("subscription_group_id")  // Sub2API group ID
  subscriptionDays    Int?              @map("subscription_days")      // 购买时的有效天数

  // 新增索引
  @@index([orderType])
}

设计理由:订阅订单和余额充值订单共享同一套支付流程(创建→支付→回调→履约),仅在最终「履约」步骤不同:

  • balance:调用 createAndRedeem() 充值余额
  • subscription:调用 Sub2API POST /admin/subscriptions/assign 分配订阅

三、Sub2API Client 扩展

src/lib/sub2api/client.ts 新增方法:

// 获取所有分组(管理员)
async function getAllGroups(): Promise<Sub2ApiGroup[]>;
// GET /api/v1/admin/groups/all

// 获取单个分组
async function getGroup(groupId: number): Promise<Sub2ApiGroup | null>;
// GET /api/v1/admin/groups/:id

// 分配订阅(支付成功后调用)
async function assignSubscription(
  userId: number,
  groupId: number,
  validityDays: number,
  notes?: string,
): Promise<Sub2ApiSubscription>;
// POST /api/v1/admin/subscriptions/assign

// 获取用户的订阅列表
async function getUserSubscriptions(userId: number): Promise<Sub2ApiSubscription[]>;
// GET /api/v1/admin/users/:id/subscriptions

// 延长订阅(续费)
async function extendSubscription(subscriptionId: number, days: number): Promise<void>;
// POST /api/v1/admin/subscriptions/:id/extend

类型定义:

interface Sub2ApiGroup {
  id: number;
  name: string;
  description: string;
  platform: string;
  status: string;
  rate_multiplier: number;
  subscription_type: string;
  daily_limit_usd: number | null;
  weekly_limit_usd: number | null;
  monthly_limit_usd: number | null;
  default_validity_days: number;
  sort_order: number;
}

interface Sub2ApiSubscription {
  id: number;
  user_id: number;
  group_id: number;
  starts_at: string;
  expires_at: string;
  status: string;
  daily_usage_usd: number;
  weekly_usage_usd: number;
  monthly_usage_usd: number;
  notes: string | null;
}

四、API 路由新增

4.1 用户 API

方法 路径 说明
GET /api/channels 获取已启用的渠道列表用户token验证 + 校验Sub2API分组是否存在
GET /api/subscription-plans 获取可售卖的订阅套餐列表(同上校验)
POST /api/orders 扩展:支持 order_type: "subscription" + plan_id
GET /api/subscriptions/my 获取当前用户的活跃订阅列表

4.2 管理员 API

方法 路径 说明
GET /api/admin/channels 渠道列表含Sub2API分组同步状态
POST /api/admin/channels 创建/更新渠道
PUT /api/admin/channels/[id] 更新渠道
DELETE /api/admin/channels/[id] 删除渠道
GET /api/admin/sub2api/groups 从Sub2API拉取所有分组供选择
GET /api/admin/subscription-plans 订阅套餐列表
POST /api/admin/subscription-plans 创建套餐
PUT /api/admin/subscription-plans/[id] 更新套餐
DELETE /api/admin/subscription-plans/[id] 删除套餐
GET /api/admin/subscriptions 所有用户的订阅列表
GET /api/admin/config 获取系统配置
PUT /api/admin/config 批量更新系统配置

五、订单服务改造

5.1 订单创建(扩展 createOrder

interface CreateOrderInput {
  // 现有字段...
  orderType?: 'balance' | 'subscription'; // 新增
  planId?: string; // 新增(订阅时必填)
}

订阅订单创建时的校验逻辑:

  1. 验证 planId 对应的 SubscriptionPlan 存在且 forSale=true
  2. 调用 Sub2API 验证 groupId 对应的分组仍然存在且 status=active
  3. 金额使用 plan.price不允许用户自定义
  4. 其余流程(支付方式选择、限额检查等)与余额订单一致

5.2 订单履约(修改 executeRechargeexecuteFulfillment

if (order.orderType === 'subscription') {
  // 1. 再次验证 Sub2API 分组存在
  const group = await getGroup(order.subscriptionGroupId)
  if (!group || group.status !== 'active') {
    // 标记 FAILEDreason = "订阅分组已不存在"
    // 前端展示常驻错误提示
    return
  }
  // 2. 调用 Sub2API 分配订阅
  await assignSubscription(order.userId, order.subscriptionGroupId, order.subscriptionDays)
  // 3. 标记 COMPLETED
} else {
  // 原有余额充值逻辑不变
  await createAndRedeem(...)
}

5.3 订阅退款

订阅订单的退款需要额外步骤:撤销 Sub2API 中的订阅(DELETE /admin/subscriptions/:id)。 如果撤销失败,标记为 REFUND_FAILED 并记录审计日志,需人工介入。


六、用户页面改造

6.1 页面结构(参考 Pincc 的 top-up-main.png

/pay 页面
├── 顶部标题区:"选择适合你的 订阅套餐"
├── 双 Tab 切换:[ 按量付费 | 包月套餐 ]
│
├── Tab 1: 按量付费
│   ├── Banner: 按量付费模式说明(倍率换算、余额通用等)
│   ├── 渠道卡片网格3列从 /api/channels 获取)
│   │   └── 每张卡片:平台标签 + 名称 + 倍率 + 余额换算 + 描述 + 模型标签 + 功能标签 + "立即充值" 按钮
│   └── 点击"立即充值" → 弹出充值金额选择弹窗(参考 top-up.png
│       └── 金额网格(管理员可配置档位)→ "确认充值" → 支付方式选择 → 支付流程
│
├── Tab 2: 包月套餐
│   ├── 订阅套餐卡片(从 /api/subscription-plans 获取)
│   │   └── 每张卡片:套餐名 + 价格/月 + 划线原价 + 限额特性列表 + "立即开通" 按钮
│   └── 点击"立即开通" → 确认订单页(参考 subscribe.png
│       └── 套餐详情 + 价格 + 选择支付方式 + "立即购买"
│       (注:我们的用户已通过 token 认证,不需要 Pincc 的邮箱/密码输入框)
│
├── 用户已有订阅展示区
│   └── 活跃订阅列表 + 到期提醒 + "续费" 按钮
│
└── 底部:购买流程说明 + 温馨提示

条件逻辑

  • 如果管理员 没有配置渠道Channel 表为空)→ 直接显示现有的充值界面PaymentForm不显示卡片
  • 如果管理员 配置了渠道 → 显示渠道卡片网格,点击"立即充值"弹出金额选择弹窗
  • 如果管理员 没有配置订阅套餐SubscriptionPlan 无 forSale=true→ 隐藏"包月套餐" Tab

6.2 新增组件

组件 说明
ChannelCard.tsx 渠道卡片(平台标签、倍率、模型标签等)
ChannelGrid.tsx 渠道卡片网格容器
TopUpModal.tsx 充值金额选择弹窗
SubscriptionPlanCard.tsx 订阅套餐卡片
SubscriptionConfirm.tsx 订阅确认订单页
UserSubscriptions.tsx 用户已有订阅展示
MainTabs.tsx 按量付费/包月套餐 Tab 切换
PurchaseFlow.tsx 购买流程说明4步骤图标

6.3 异常处理

  • 支付成功但订阅分组不存在:前端显示常驻红色告警框,包含:
    • 错误说明:"您已成功支付,但订阅分组已下架,无法自动开通"
    • 订单信息(订单号、金额、支付时间)
    • 引导:"请联系客服处理,提供订单号 xxx"

七、管理员页面新增

7.1 页面路由

路由 说明
/admin/channels 渠道管理(列表 + 编辑弹窗,参考 channel-conf.png
/admin/subscriptions 订阅套餐管理 + 已有订阅列表
/admin/settings 系统配置(支付渠道配置、业务参数等)

7.2 渠道管理页(/admin/channels

  • 顶部操作栏:[从 Sub2API 同步分组] [新建渠道]
  • 渠道列表表格:名称 | 分类 | 倍率 | Sub2API状态 | 排序 | 启用 | 操作
  • 编辑弹窗(参考 channel-conf.png
    • 渠道名称、分类(下拉)、倍率、描述
    • 支持模型textarea每行一个
    • 功能特性textarea每行一个
    • 排序、启用开关
  • "从 Sub2API 同步":拉取所有分组 → 显示差异 → 可选批量导入

7.3 订阅套餐管理页(/admin/subscriptions

两个区域:

  1. 套餐配置

    • 列表:套餐名 | 关联分组 | 价格 | 有效天数 | 启用售卖 | Sub2API状态 | 操作
    • 新建/编辑表单:选择 Sub2API 分组 → 配置名称、价格、原价、有效天数、特性描述、启用售卖
  2. 已有订阅

    • 从 Sub2API 查询所有订阅记录
    • 表格:用户 | 分组 | 开始时间 | 到期时间 | 状态 | 用量

7.4 系统配置页(/admin/settings

分组展示:

  • 支付渠道配置PAYMENT_PROVIDERS、各支付商的 Key/密钥等(敏感字段脱敏显示)
  • 业务参数ORDER_TIMEOUT_MINUTES、MIN/MAX_RECHARGE_AMOUNT、MAX_DAILY_RECHARGE_AMOUNT 等
  • 充值档位配置:自定义充值金额选项(如 50/100/500/1000
  • 显示配置PAYHELP_IMAGE_URL、PAY_HELP_TEXT、PAYMENT_SUBLABEL* 等
  • 前端定制:站点名称、联系客服信息等

配置优先级:数据库配置 > 环境变量(环境变量作为默认值/回退值)


八、配置系统改造

8.1 getConfig() 函数改造

// 新的配置读取优先级:
// 1. 数据库 SystemConfig 表(运行时可修改)
// 2. 环境变量(作为回退/初始值)

async function getConfig(key: string): Promise<string | undefined> {
  const dbConfig = await prisma.systemConfig.findUnique({ where: { key } })
  if (dbConfig) return dbConfig.value
  return process.env[key]
}

// 批量获取带缓存避免频繁查DB
async function getConfigs(keys: string[]): Promise<Record<string, string>> { ... }

8.2 缓存策略

  • 使用内存缓存Map + TTL 30秒避免每次请求都查数据库
  • 管理员更新配置时清除缓存
  • 支付商密钥等敏感配置仍可通过环境变量传入(数据库中存储 __FROM_ENV__ 标记表示使用环境变量值)

九、管理员入口

管理员通过以下方式进入:

  1. Sub2API 管理面板中跳转(携带 admin token
  2. 直接访问 /admin?token=xxx(现有机制)

管理员页面新增导航侧边栏:

  • 订单管理(现有)
  • 数据概览(现有)
  • 渠道管理(新增)
  • 订阅管理(新增)
  • 系统配置(新增)

十、实施顺序

Phase 1数据库 & 基础设施(预估 2-3 步)

  1. Prisma schema 变更 + migration
  2. SystemConfig 服务层CRUD + 缓存)
  3. Sub2API client 扩展(分组/订阅 API

Phase 2管理员 API & 页面(预估 4-5 步)

  1. 渠道管理 API + 页面
  2. 订阅套餐管理 API + 页面
  3. 系统配置 API + 页面
  4. 管理员导航侧边栏

Phase 3订单服务改造预估 2 步)

  1. Order 模型扩展 + 订阅订单创建逻辑
  2. 订阅履约逻辑executeSubscriptionFulfillment

Phase 4用户页面改造预估 3-4 步)

  1. 用户 APIchannels、subscription-plans、subscriptions/my
  2. 按量付费 TabChannelGrid + TopUpModal
  3. 包月套餐 TabSubscriptionPlanCard + SubscriptionConfirm
  4. 用户订阅展示 + 续费 + 异常处理

Phase 5配置迁移 & 收尾(预估 1-2 步)

  1. getEnv() 改造(数据库优先 + 环境变量回退)
  2. 测试 + 端到端验证

十一、安全考虑

  1. 订阅分组校验:每次展示和下单都实时校验 Sub2API 分组是否存在且 active
  2. 价格篡改防护:订阅订单金额从服务端 SubscriptionPlan.price 读取,不信任客户端传值
  3. 支付后分组消失:订单标记 FAILED + 常驻错误提示 + 审计日志,不自动退款(需人工确认)
  4. 敏感配置:支付密钥在 API 响应中脱敏,前端仅展示 ****...最后4位
  5. 幂等性:订阅分配使用 orderId 作为幂等 key防止重复分配