16 KiB
16 KiB
One-API 多租户 SaaS 系统二开方案
📋 项目概述
目标
在 one-api 基础上进行二次开发,构建多租户 SaaS 系统,支持:
- 主系统统一管理上游 API 渠道
- 多个代理站点独立部署(独立数据库)
- 月套餐令牌系统(日限/周限/月限额度控制)
- 主系统统一计费扣费
核心原则
- 保持可升级性:插件化开发,最小化核心代码改动
- 完全独立部署:每个代理站点独立运行,互不影响
- 统一计费管理:主系统集中管理渠道和计费
🏗️ 系统架构设计
整体架构
┌─────────────────────────────────────────────────────────────┐
│ 主系统 (Master) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 上游渠道池 │ │ 计费中心 │ │ 代理管理 │ │
│ │ OpenAI/Claude│ │ 统计报表 │ │ 额度分配 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ ▲ ▲ ▲ │
└─────────┼──────────────────┼──────────────────┼──────────────┘
│ │ │
│ 渠道请求 │ 计费回传 │ 配置同步
│ │ │
┌─────┴──────────────────┴──────────────────┴─────┐
│ │
┌───▼────────┐ ┌─────────────┐ ┌─────────▼────┐
│代理站点 A │ │ 代理站点 B │ │ 代理站点 C │
│独立数据库 │ │ 独立数据库 │ │ 独立数据库 │
│独立域名 │ │ 独立域名 │ │ 独立域名 │
└────────────┘ └─────────────┘ └──────────────┘
▲ ▲ ▲
│ │ │
终端用户 终端用户 终端用户
核心交互流程
1. 用户请求 → 代理站点
2. 代理站点验证令牌(本地)
3. 代理站点 → 主系统(请求渠道服务)
4. 主系统验证额度 → 转发上游 API
5. 主系统记录消耗 → 返回结果
6. 代理站点 → 返回用户
💾 数据库设计
主系统新增表
1. agent_sites(代理站点表)
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(代理计费日志)
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 表(月套餐令牌)
-- 在现有 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. 月套餐令牌系统
套餐定义
// 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美金/周
},
}
额度检查逻辑
// 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
}
消费扣费
// 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. 主从计费系统
代理站点配置
// 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") // 站点密钥
)
代理站点中继拦截
// 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
}
主系统代理中继接口
// 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
代理站点部署配置
环境变量:
# .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,避免修改主流程
- 独立分支管理:
# 主分支跟随上游 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
🎯 下一步行动
- 确认方案:与团队评审本方案
- 环境准备:搭建开发测试环境
- 数据库设计:创建数据表和索引
- 接口设计:定义 API 接口规范
- 开始编码:按阶段实施开发
文档版本: v1.0 创建时间: 2025-12-29 作者: Claude 状态: 待评审