first commit: one-api base code + SAAS plan document
Some checks failed
CI / Unit tests (push) Has been cancelled
CI / commit_lint (push) Has been cancelled

This commit is contained in:
huangzhenpc
2025-12-29 22:52:27 +08:00
commit cb7c48bfa7
564 changed files with 61468 additions and 0 deletions

53
docs/API.md Normal file
View File

@@ -0,0 +1,53 @@
# 使用 API 操控 & 扩展 One API
> 欢迎提交 PR 在此放上你的拓展项目。
例如,虽然 One API 本身没有直接支持支付,但是你可以通过系统扩展的 API 来实现支付功能。
又或者你想自定义渠道管理策略,也可以通过 API 来实现渠道的禁用与启用。
## 鉴权
One API 支持两种鉴权方式Cookie 和 Token对于 Token参照下图获取
![image](https://github.com/songquanpeng/songquanpeng.github.io/assets/39998050/c15281a7-83ed-47cb-a1f6-913cb6bf4a7c)
之后,将 Token 作为请求头的 Authorization 字段的值即可,例如下面使用 Token 调用测试渠道的 API
![image](https://github.com/songquanpeng/songquanpeng.github.io/assets/39998050/1273b7ae-cb60-4c0d-93a6-b1cbc039c4f8)
## 请求格式与响应格式
One API 使用 JSON 格式进行请求和响应。
对于响应体,一般格式如下:
```json
{
"message": "请求信息",
"success": true,
"data": {}
}
```
## API 列表
> 当前 API 列表不全,请自行通过浏览器抓取前端请求
如果现有的 API 没有办法满足你的需求,欢迎提交 issue 讨论。
### 获取当前登录用户信息
**GET** `/api/user/self`
### 为给定用户充值额度
**POST** `/api/topup`
```json
{
"user_id": 1,
"quota": 100000,
"remark": "充值 100000 额度"
}
```
## 其他
### 充值链接上的附加参数
One API 会在用户点击充值按钮的时候,将用户的信息和充值信息附加在链接上,例如:
`https://example.com?username=root&user_id=1&transaction_id=4b3eed80-55d5-443f-bd44-fb18c648c837`
你可以通过解析链接上的参数来获取用户信息和充值信息,然后调用 API 来为用户充值。
注意,不是所有主题都支持该功能,欢迎 PR 补齐。

539
docs/SAAS-PLAN.md Normal file
View File

@@ -0,0 +1,539 @@
# 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
**状态:** 待评审