# 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 **状态:** 待评审