Files
relay-saas/docs/SAAS-PLAN.md
huangzhenpc cb7c48bfa7
Some checks failed
CI / Unit tests (push) Has been cancelled
CI / commit_lint (push) Has been cancelled
first commit: one-api base code + SAAS plan document
2025-12-29 22:52:27 +08:00

540 lines
16 KiB
Markdown
Raw Permalink 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.

# One-API 多租户 SaaS 系统二开方案
## 📋 项目概述
### 目标
在 one-api 基础上进行二次开发,构建多租户 SaaS 系统,支持:
- 主系统统一管理上游 API 渠道
- 多个代理站点独立部署(独立数据库)
- 月套餐令牌系统(日限/周限/月限额度控制)
- 主系统统一计费扣费
### 核心原则
1. **保持可升级性**:插件化开发,最小化核心代码改动
2. **完全独立部署**:每个代理站点独立运行,互不影响
3. **统一计费管理**:主系统集中管理渠道和计费
---
## 🏗️ 系统架构设计
### 整体架构
```
┌─────────────────────────────────────────────────────────────┐
│ 主系统 (Master) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 上游渠道池 │ │ 计费中心 │ │ 代理管理 │ │
│ │ OpenAI/Claude│ │ 统计报表 │ │ 额度分配 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ ▲ ▲ ▲ │
└─────────┼──────────────────┼──────────────────┼──────────────┘
│ │ │
│ 渠道请求 │ 计费回传 │ 配置同步
│ │ │
┌─────┴──────────────────┴──────────────────┴─────┐
│ │
┌───▼────────┐ ┌─────────────┐ ┌─────────▼────┐
│代理站点 A │ │ 代理站点 B │ │ 代理站点 C │
│独立数据库 │ │ 独立数据库 │ │ 独立数据库 │
│独立域名 │ │ 独立域名 │ │ 独立域名 │
└────────────┘ └─────────────┘ └──────────────┘
▲ ▲ ▲
│ │ │
终端用户 终端用户 终端用户
```
### 核心交互流程
```
1. 用户请求 → 代理站点
2. 代理站点验证令牌(本地)
3. 代理站点 → 主系统(请求渠道服务)
4. 主系统验证额度 → 转发上游 API
5. 主系统记录消耗 → 返回结果
6. 代理站点 → 返回用户
```
---
## 💾 数据库设计
### 主系统新增表
#### 1. agent_sites代理站点表
```sql
CREATE TABLE agent_sites (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL, -- 站点名称
domain VARCHAR(255) UNIQUE, -- 站点域名
api_key VARCHAR(64) UNIQUE NOT NULL, -- 站点 API 密钥(用于主从通信)
status INT DEFAULT 1, -- 状态1启用 2禁用
total_quota BIGINT DEFAULT 0, -- 总分配额度
used_quota BIGINT DEFAULT 0, -- 已使用额度
created_time BIGINT,
updated_time BIGINT,
INDEX idx_api_key (api_key),
INDEX idx_status (status)
);
```
#### 2. agent_billing_logs代理计费日志
```sql
CREATE TABLE agent_billing_logs (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
agent_site_id INT NOT NULL, -- 代理站点ID
user_id INT, -- 代理站点的用户ID
token_name VARCHAR(100), -- 令牌名称
model_name VARCHAR(100), -- 模型名称
prompt_tokens INT, -- 输入token数
completion_tokens INT, -- 输出token数
quota INT, -- 消耗额度
channel_id INT, -- 使用的渠道ID
created_time BIGINT,
INDEX idx_agent_site (agent_site_id),
INDEX idx_created_time (created_time)
);
```
### 代理站点扩展字段
#### 扩展 tokens 表(月套餐令牌)
```sql
-- 在现有 tokens 表基础上新增字段
ALTER TABLE tokens ADD COLUMN subscription_type VARCHAR(20); -- 套餐类型daily/weekly/monthly
ALTER TABLE tokens ADD COLUMN daily_quota_limit BIGINT DEFAULT 0; -- 日额度限制
ALTER TABLE tokens ADD COLUMN weekly_quota_limit BIGINT DEFAULT 0; -- 周额度限制
ALTER TABLE tokens ADD COLUMN monthly_quota_limit BIGINT DEFAULT 0; -- 月额度限制
ALTER TABLE tokens ADD COLUMN daily_used_quota BIGINT DEFAULT 0; -- 日已用额度
ALTER TABLE tokens ADD COLUMN weekly_used_quota BIGINT DEFAULT 0; -- 周已用额度
ALTER TABLE tokens ADD COLUMN monthly_used_quota BIGINT DEFAULT 0; -- 月已用额度
ALTER TABLE tokens ADD COLUMN last_reset_daily BIGINT DEFAULT 0; -- 上次日重置时间
ALTER TABLE tokens ADD COLUMN last_reset_weekly BIGINT DEFAULT 0; -- 上次周重置时间
ALTER TABLE tokens ADD COLUMN last_reset_monthly BIGINT DEFAULT 0; -- 上次月重置时间
```
---
## 🔧 核心功能实现
### 1. 月套餐令牌系统
#### 套餐定义
```go
// common/subscription/plans.go (新文件)
package subscription
const (
PlanBasic = "basic" // 100美金/月
PlanStandard = "standard" // 200美金/月
PlanPremium = "premium" // 500美金/月
)
type SubscriptionPlan struct {
Name string
MonthlyQuota int64 // 月总额度(点数)
DailyQuota int64 // 日额度限制
WeeklyQuota int64 // 周额度限制
}
var Plans = map[string]SubscriptionPlan{
PlanBasic: {
Name: "基础版",
MonthlyQuota: 50000000, // 100美金 * 500,000
DailyQuota: 2000000, // ~4美金/天
WeeklyQuota: 15000000, // ~30美金/周
},
PlanStandard: {
Name: "标准版",
MonthlyQuota: 100000000, // 200美金 * 500,000
DailyQuota: 5000000, // ~10美金/天
WeeklyQuota: 35000000, // ~70美金/周
},
PlanPremium: {
Name: "高级版",
MonthlyQuota: 250000000, // 500美金 * 500,000
DailyQuota: 15000000, // ~30美金/天
WeeklyQuota: 90000000, // ~180美金/周
},
}
```
#### 额度检查逻辑
```go
// model/token.go 扩展
func (token *Token) CheckSubscriptionQuota() error {
now := time.Now().Unix()
// 1. 检查并重置日额度
if shouldResetDaily(token.LastResetDaily, now) {
token.DailyUsedQuota = 0
token.LastResetDaily = getDayStart(now)
}
// 2. 检查并重置周额度
if shouldResetWeekly(token.LastResetWeekly, now) {
token.WeeklyUsedQuota = 0
token.LastResetWeekly = getWeekStart(now)
}
// 3. 检查并重置月额度
if shouldResetMonthly(token.LastResetMonthly, now) {
token.MonthlyUsedQuota = 0
token.LastResetMonthly = getMonthStart(now)
}
// 4. 验证额度
if token.DailyQuotaLimit > 0 && token.DailyUsedQuota >= token.DailyQuotaLimit {
return errors.New("日额度已用尽")
}
if token.WeeklyQuotaLimit > 0 && token.WeeklyUsedQuota >= token.WeeklyQuotaLimit {
return errors.New("周额度已用尽")
}
if token.MonthlyQuotaLimit > 0 && token.MonthlyUsedQuota >= token.MonthlyQuotaLimit {
return errors.New("月额度已用尽")
}
return nil
}
```
#### 消费扣费
```go
// model/token.go 扩展
func (token *Token) ConsumeSubscriptionQuota(quota int64) error {
token.DailyUsedQuota += quota
token.WeeklyUsedQuota += quota
token.MonthlyUsedQuota += quota
token.UsedQuota += quota
return DB.Model(token).Updates(map[string]interface{}{
"daily_used_quota": token.DailyUsedQuota,
"weekly_used_quota": token.WeeklyUsedQuota,
"monthly_used_quota": token.MonthlyUsedQuota,
"used_quota": token.UsedQuota,
}).Error
}
```
### 2. 主从计费系统
#### 代理站点配置
```go
// common/config/agent.go (新文件)
package config
var (
IsAgentSite = os.Getenv("AGENT_MODE") == "true"
MasterSystemURL = os.Getenv("MASTER_SYSTEM_URL") // 主系统地址
AgentSiteAPIKey = os.Getenv("AGENT_SITE_API_KEY") // 站点密钥
)
```
#### 代理站点中继拦截
```go
// relay/proxy/master_proxy.go (新文件)
package proxy
// 拦截所有中继请求,转发到主系统
func RelayToMaster(c *gin.Context, meta *relay.RelayMeta) (*http.Response, error) {
// 1. 构造请求到主系统
masterURL := config.MasterSystemURL + "/api/agent/relay"
req, _ := http.NewRequest(c.Request.Method, masterURL, c.Request.Body)
// 2. 添加认证头
req.Header.Set("X-Agent-Site-Key", config.AgentSiteAPIKey)
req.Header.Set("X-Agent-User-Id", strconv.Itoa(meta.UserId))
req.Header.Set("X-Agent-Token-Name", meta.TokenName)
// 3. 转发原始请求头
for k, v := range c.Request.Header {
req.Header[k] = v
}
// 4. 发送请求
client := &http.Client{Timeout: 5 * time.Minute}
resp, err := client.Do(req)
return resp, err
}
```
#### 主系统代理中继接口
```go
// controller/agent_relay.go (新文件)
package controller
func AgentRelay(c *gin.Context) {
// 1. 验证代理站点身份
agentKey := c.GetHeader("X-Agent-Site-Key")
agentSite, err := model.GetAgentSiteByKey(agentKey)
if err != nil {
c.JSON(403, gin.H{"error": "invalid agent site"})
return
}
// 2. 检查代理站点额度
if agentSite.UsedQuota >= agentSite.TotalQuota {
c.JSON(403, gin.H{"error": "agent quota exhausted"})
return
}
// 3. 选择渠道并转发请求
modelName := c.GetString("model")
channel, err := model.CacheGetRandomSatisfiedChannel("default", modelName, false)
// 4. 调用正常的 relay 流程
relayRequest(c, channel, agentSite)
}
func relayRequest(c *gin.Context, channel *model.Channel, agentSite *model.AgentSite) {
// ... 使用现有的 relay 逻辑
// 计费时记录到 agent_billing_logs 表
}
```
---
## 📁 文件结构(插件化设计)
```
one-api/
├── common/
│ ├── subscription/ # 新增:套餐管理
│ │ ├── plans.go # 套餐定义
│ │ └── quota.go # 额度计算
│ └── config/
│ └── agent.go # 新增:代理站点配置
├── model/
│ ├── agent_site.go # 新增:代理站点模型
│ ├── agent_billing.go # 新增:代理计费日志
│ └── token.go # 扩展:添加套餐相关方法
├── controller/
│ ├── agent_site.go # 新增:代理站点管理接口
│ ├── agent_relay.go # 新增:代理中继接口
│ └── subscription.go # 新增:套餐管理接口
├── middleware/
│ └── subscription_check.go # 新增:套餐额度检查中间件
├── relay/
│ └── proxy/
│ └── master_proxy.go # 新增:主系统代理转发
└── docs/
└── SAAS-PLAN.md # 本文档
```
---
## 🔄 工作流程
### 代理站点请求流程
```
1. 用户 → 代理站点 /v1/chat/completions
2. 代理站点中间件验证 Token
- 检查 Token 状态
- 检查套餐额度(日/周/月)
3. 通过 → 转发到主系统 /api/agent/relay
Header: X-Agent-Site-Key, X-Agent-User-Id
4. 主系统验证代理站点身份和额度
5. 主系统选择渠道 → 调用上游 API
6. 主系统计费
- 扣除代理站点额度
- 记录到 agent_billing_logs
7. 返回结果 → 代理站点 → 用户
8. 代理站点更新本地 Token 统计
- 更新 daily/weekly/monthly_used_quota
```
### 代理站点部署配置
**环境变量:**
```bash
# .env
AGENT_MODE=true
MASTER_SYSTEM_URL=https://master.example.com
AGENT_SITE_API_KEY=ask-xxxxxxxxxxxx
SQL_DSN=agent_user:password@tcp(localhost:3306)/agent_db
PORT=3000
```
---
## 🛠️ 实施步骤
### 阶段一基础架构Week 1-2
- [ ] 创建 agent_sites 表和模型
- [ ] 实现代理站点注册和管理接口
- [ ] 开发主系统代理中继接口
- [ ] 实现代理站点转发逻辑
### 阶段二套餐系统Week 3-4
- [ ] 扩展 tokens 表字段
- [ ] 实现套餐定义和管理
- [ ] 开发套餐额度检查逻辑
- [ ] 实现日/周/月自动重置
### 阶段三计费系统Week 5-6
- [ ] 创建 agent_billing_logs 表
- [ ] 实现主系统计费记录
- [ ] 开发代理站点额度同步
- [ ] 实现统计报表接口
### 阶段四前端界面Week 7-8
- [ ] 主系统:代理站点管理页面
- [ ] 主系统:计费统计报表
- [ ] 代理站点:套餐令牌管理页面
- [ ] 代理站点:使用统计页面
### 阶段五测试优化Week 9-10
- [ ] 单元测试
- [ ] 压力测试
- [ ] 安全测试
- [ ] 性能优化
---
## ⚠️ 技术难点与解决方案
### 1. 如何保持可升级性?
**问题:** 二次开发后如何继续跟随 one-api 上游更新?
**解决方案:**
- **插件化设计**:新功能尽量在新文件中实现,减少修改核心文件
- **扩展而非修改**:使用 Go 的组合而非继承,扩展现有结构体
- **Hook 机制**:在关键位置注入 Hook避免修改主流程
- **独立分支管理**
```bash
# 主分支跟随上游
git remote add upstream https://github.com/songquanpeng/one-api.git
# 开发分支
git checkout -b saas-dev
# 定期合并上游
git fetch upstream
git merge upstream/main
```
### 2. 额度透支问题
**问题:** 代理站点可能恶意超额使用
**解决方案:**
- **预扣费机制**:主系统在转发前先扣除预估额度
- **实时额度检查**:每次请求都验证代理站点剩余额度
- **熔断机制**:超额后立即停止服务
- **告警通知**:额度接近用尽时提前通知
### 3. 性能问题
**问题:** 每次请求都要经过主系统,增加延迟
**解决方案:**
- **连接池**:复用 HTTP 连接
- **异步计费**:返回结果后异步记录日志
- **批量提交**:计费数据批量写入
- **Redis 缓存**:缓存代理站点信息和额度
### 4. 高可用性
**问题:** 主系统故障影响所有代理站点
**解决方案:**
- **主系统多节点部署**:负载均衡
- **降级策略**:主系统故障时代理站点使用本地渠道(如果配置)
- **健康检查**:定期检查主系统状态
- **限流保护**:防止单个代理站点占用过多资源
---
## 🔐 安全考虑
### 1. 认证安全
- 代理站点 API Key 使用强随机生成
- 通信使用 HTTPS 加密
- API Key 定期轮换
### 2. 防刷防滥用
- 请求频率限制Rate Limit
- 异常流量检测
- IP 白名单
### 3. 数据安全
- 敏感数据加密存储
- 日志脱敏处理
- 定期备份
---
## 📊 监控与运维
### 关键指标
- 代理站点请求量
- 渠道使用分布
- 额度消耗趋势
- 错误率
- 响应时间
### 告警规则
- 代理站点额度不足(< 10%
- 请求失败率异常(> 5%
- 主系统响应超时
- 数据库连接数过高
---
## 💰 成本估算
### 开发成本
- 后端开发6-8 周
- 前端开发2-3 周
- 测试优化2 周
- 总计10-13 周
### 运维成本(月)
- 主系统服务器:$50-100
- 数据库:$30-50
- Redis$20-30
- 带宽:$50-100
- 总计:$150-280/月
---
## 📚 参考资料
### One-API 原项目
- GitHub: https://github.com/songquanpeng/one-api
- 文档: README.md
### 关键代码位置
- 令牌管理:`model/token.go`
- 计费逻辑:`relay/billing/billing.go`
- 渠道分发:`middleware/distributor.go`
- 认证中间件:`middleware/auth.go`
---
## 🎯 下一步行动
1. **确认方案**:与团队评审本方案
2. **环境准备**:搭建开发测试环境
3. **数据库设计**:创建数据表和索引
4. **接口设计**:定义 API 接口规范
5. **开始编码**:按阶段实施开发
---
**文档版本:** v1.0
**创建时间:** 2025-12-29
**作者:** Claude
**状态:** 待评审