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

430 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Sub2ApiPay 改造方案
## 一、概述
基于 Pincc 参考界面,改造 Sub2ApiPay 项目,新增:
- **用户页面**:双 Tab按量付费 / 包月套餐),渠道卡片展示,充值弹窗,订阅购买流程
- **管理员界面**:渠道管理、订阅套餐管理、系统配置
- **数据库存储配置**:支付渠道等配置从环境变量迁移至数据库,支持运行时修改
---
## 二、数据库 Schema 变更
### 2.1 新增模型
```prisma
// 渠道展示配置(管理员配置,对应 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 模型上新增字段,复用整套支付流程:
```prisma
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` 新增方法:
```typescript
// 获取所有分组(管理员)
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
```
类型定义:
```typescript
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`
```typescript
interface CreateOrderInput {
// 现有字段...
orderType?: 'balance' | 'subscription'; // 新增
planId?: string; // 新增(订阅时必填)
}
```
订阅订单创建时的校验逻辑:
1. 验证 `planId` 对应的 SubscriptionPlan 存在且 `forSale=true`
2. 调用 Sub2API 验证 `groupId` 对应的分组仍然存在且 status=active
3. 金额使用 plan.price不允许用户自定义
4. 其余流程(支付方式选择、限额检查等)与余额订单一致
### 5.2 订单履约(修改 `executeRecharge` → `executeFulfillment`
```
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
- **显示配置**PAY*HELP_IMAGE_URL、PAY_HELP_TEXT、PAYMENT_SUBLABEL*\* 等
- **前端定制**:站点名称、联系客服信息等
配置优先级:**数据库配置 > 环境变量**(环境变量作为默认值/回退值)
---
## 八、配置系统改造
### 8.1 `getConfig()` 函数改造
```typescript
// 新的配置读取优先级:
// 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 步)
4. 渠道管理 API + 页面
5. 订阅套餐管理 API + 页面
6. 系统配置 API + 页面
7. 管理员导航侧边栏
### Phase 3订单服务改造预估 2 步)
8. Order 模型扩展 + 订阅订单创建逻辑
9. 订阅履约逻辑executeSubscriptionFulfillment
### Phase 4用户页面改造预估 3-4 步)
10. 用户 APIchannels、subscription-plans、subscriptions/my
11. 按量付费 TabChannelGrid + TopUpModal
12. 包月套餐 TabSubscriptionPlanCard + SubscriptionConfirm
13. 用户订阅展示 + 续费 + 异常处理
### Phase 5配置迁移 & 收尾(预估 1-2 步)
14. getEnv() 改造(数据库优先 + 环境变量回退)
15. 测试 + 端到端验证
---
## 十一、安全考虑
1. **订阅分组校验**:每次展示和下单都实时校验 Sub2API 分组是否存在且 active
2. **价格篡改防护**:订阅订单金额从服务端 SubscriptionPlan.price 读取,不信任客户端传值
3. **支付后分组消失**:订单标记 FAILED + 常驻错误提示 + 审计日志,不自动退款(需人工确认)
4. **敏感配置**:支付密钥在 API 响应中脱敏,前端仅展示 `****...最后4位`
5. **幂等性**:订阅分配使用 `orderId` 作为幂等 key防止重复分配