540 lines
16 KiB
Markdown
540 lines
16 KiB
Markdown
# 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
|
||
**状态:** 待评审
|