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