Files
relay-saas/docs/SAAS-PLAN.md
huangzhenpc cb7c48bfa7
Some checks failed
CI / Unit tests (push) Has been cancelled
CI / commit_lint (push) Has been cancelled
first commit: one-api base code + SAAS plan document
2025-12-29 22:52:27 +08:00

16 KiB
Raw Blame History

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代理站点表

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 原项目

关键代码位置

  • 令牌管理: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 状态: 待评审