AI API 中转平台 — 完整开发流程
April 9, 2026
技术栈与部署方案
- 前端:Next.js(App Router)+ TailwindCSS + shadcn/ui
- 后端:NestJS + Prisma + PostgreSQL
- 部署:前端 Vercel / 后端美国服务器(Docker)
一、项目结构
整个项目使用 pnpm workspace 管理 monorepo,分为 web(前端)、server(后端)和可选的 shared(共享类型)三个包。
ai-relay/
├── pnpm-workspace.yaml
├── packages/
│ ├── web/ # Next.js 前端(App Router)
│ │ └── src/
│ │ ├── app/
│ │ │ ├── (auth)/ # 登录/注册
│ │ │ ├── (dashboard)/ # 控制面板(需登录)
│ │ │ │ ├── overview/ # 总览/统计
│ │ │ │ ├── keys/ # API Key 管理
│ │ │ │ ├── logs/ # 调用日志
│ │ │ │ ├── channels/ # 渠道管理(Admin)
│ │ │ │ ├── playground/ # API 测试
│ │ │ │ └── settings/ # 个人设置
│ │ │ └── page.tsx # 首页/Landing
│ │ ├── components/
│ │ │ ├── ui/ # shadcn/ui 组件
│ │ │ ├── layout/ # Sidebar, Header
│ │ │ └── dashboard/ # 业务组件
│ │ ├── lib/ # API 封装、Token 管理
│ │ └── hooks/
│ │
│ └── server/ # NestJS 后端
│ ├── src/
│ │ ├── modules/
│ │ │ ├── auth/ # 认证(JWT)
│ │ │ ├── user/ # 用户管理
│ │ │ ├── api-key/ # API Key CRUD
│ │ │ ├── proxy/ # 核心:代理转发
│ │ │ ├── channel/ # 上游渠道管理
│ │ │ ├── log/ # 请求日志
│ │ │ └── stats/ # 数据统计
│ │ ├── common/ # Guards、Interceptors、Filters
│ │ └── prisma/ # Prisma Service
│ ├── prisma/
│ │ └── schema.prisma
│ └── Dockerfile
│
└── packages/shared/ # 共享类型(可选)
text
二、数据库设计
使用 Prisma + PostgreSQL,核心有四张表:User、ApiKey、RequestLog、Channel。
用户表
-- User 模型
-- id: String 主键 cuid
-- email: String 唯一
-- password: String bcrypt 哈希
-- name: String? 可选
-- role: Role 默认 USER (USER / ADMIN)
-- balance: Float 默认 0,余额(可选计费)
-- isActive: Boolean 默认 true
-- createdAt: DateTime 自动
-- updatedAt: DateTime 自动
-- 关联: apiKeys[], logs[]
sql
API Key 表
-- ApiKey 模型
-- id: String 主键 cuid
-- key: String 唯一,格式 sk-xxxxxxxxxxxxxxxx
-- name: String 备注名
-- userId: String 外键 -> User
-- rateLimit: Int 默认 60,每分钟最大请求数
-- enabled: Boolean 默认 true
-- models: String[] 默认空,允许的模型(空=不限)
-- expiresAt: DateTime? 过期时间
-- totalUsed: Int 默认 0,累计请求数
-- createdAt: DateTime 自动
-- 索引: key, userId
-- 关联: logs[]
sql
请求日志表
-- RequestLog 模型
-- id: String 主键 cuid
-- apiKeyId: String 外键 -> ApiKey
-- userId: String 外键 -> User
-- model: String 请求的模型名
-- provider: String openai / anthropic / ...
-- endpoint: String /v1/chat/completions
-- promptTokens: Int 默认 0
-- outputTokens: Int 默认 0
-- totalTokens: Int 默认 0
-- duration: Int 默认 0,响应耗时(ms)
-- status: Int HTTP 状态码
-- ip: String? 可选
-- error: String? 错误信息
-- createdAt: DateTime 自动
-- 索引: [userId, createdAt], [apiKeyId, createdAt], [createdAt]
sql
上游渠道表
-- Channel 模型
-- id: String 主键 cuid
-- name: String 渠道名称
-- provider: String openai / anthropic / custom
-- baseUrl: String 上游 API 基础 URL
-- apiKey: String 上游 API Key(加密存储)
-- models: String[] 支持的模型列表
-- priority: Int 默认 0,优先级(越高越优先)
-- weight: Int 默认 1,权重(同优先级加权轮询)
-- enabled: Boolean 默认 true
-- maxRetry: Int 默认 2
-- timeout: Int 默认 60000,超时时间(ms)
-- status: ChannelStatus 默认 ACTIVE (ACTIVE / DEGRADED / DISABLED)
-- createdAt: DateTime 自动
-- updatedAt: DateTime 自动
sql
三、后端 API 接口设计
3.1 认证接口
- POST
/api/auth/register— 注册 - POST
/api/auth/login— 登录,返回 JWT - GET
/api/auth/profile— 获取当前用户信息 - POST
/api/auth/refresh— 刷新 Token
3.2 API Key 管理
- GET
/api/keys— 获取我的 Key 列表 - POST
/api/keys— 创建新 Key - PATCH
/api/keys/:id— 更新 Key - DELETE
/api/keys/:id— 删除 Key
3.3 AI 代理接口(核心)
- POST
/v1/chat/completions— 兼容 OpenAI Chat 格式 - POST
/v1/completions— 兼容 OpenAI Completions - POST
/v1/embeddings— 嵌入接口 - GET
/v1/models— 返回可用模型列表
鉴权方式:Authorization: Bearer sk-xxxxxxxx(使用平台生成的 API Key)
3.4 渠道管理(Admin)
- GET
/api/channels— 渠道列表 - POST
/api/channels— 添加渠道 - PATCH
/api/channels/:id— 更新渠道 - DELETE
/api/channels/:id— 删除渠道 - POST
/api/channels/:id/test— 测试渠道连通性
3.5 统计与日志接口
- GET
/api/stats/overview— 总览(今日请求数/Token/费用) - GET
/api/stats/daily— 每日趋势(近 30 天) - GET
/api/stats/models— 按模型统计 - GET
/api/logs— 调用日志(分页+筛选)
四、核心模块 — Proxy 代理转发
这是整个平台最核心的模块,负责接收客户端请求并转发到上游 AI 服务。
4.1 请求处理流程
客户端请求
|
v
[API Key 鉴权] --- 无效 ---> 401 Unauthorized
| 有效
v
[限流检查] --- 超限 ---> 429 Too Many Requests
| 通过
v
[模型权限检查] --- 无权 ---> 403 Forbidden
| 通过
v
[选择上游 Channel]
| 加权轮询 + 优先级
v
[构建上游请求]
| 模型映射、格式转换
v
[发送请求到上游]
|
|-- 非流式 ---> 等待响应 ---> 解析 token 用量 ---> 返回
|
+-- 流式 SSE ---> 逐块透传 ---> 累计 token ---> 结束后记录
|
v
[记录日志] -- 异步写入数据库
text
4.2 Proxy Controller
// proxy.controller.ts
// @Controller('v1')
class ProxyController {
// POST /v1/chat/completions
// @UseGuards(ApiKeyAuthGuard)
async chatCompletions(req, res) {
const isStream = req.body.stream === true;
if (isStream) {
// SSE 流式透传
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
await this.proxyService.streamProxy(req, res);
} else {
const result = await this.proxyService.proxy(req);
res.json(result);
}
}
// GET /v1/models
// @UseGuards(ApiKeyAuthGuard)
async listModels() {
return this.proxyService.listModels();
}
}
javascript
4.3 流式代理核心逻辑
// proxy.service.ts
class ProxyService {
// 选择上游渠道(加权轮询)
async selectChannel(model) {
const channels = await this.getActiveChannels(model);
return this.weightedRoundRobin(channels);
}
// 流式代理
async streamProxy(req, res) {
const channel = await this.selectChannel(req.body.model);
const startTime = Date.now();
const upstream = await fetch(
channel.baseUrl + '/v1/chat/completions',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + decrypt(channel.apiKey),
},
body: JSON.stringify(req.body),
},
);
// 逐块透传 SSE
const reader = upstream.body.getReader();
const decoder = new TextDecoder();
while (true) {
const result = await reader.read();
if (result.done) break;
const chunk = decoder.decode(result.value, { stream: true });
res.write(chunk);
}
res.end();
// 异步记录日志
this.logService.create({ /* ... */ });
}
}
javascript
4.4 API Key 鉴权 Guard
// api-key-auth.guard.ts
// @Injectable()
class ApiKeyAuthGuard {
async canActivate(context) {
const request = context.switchToHttp().getRequest();
const authHeader = request.headers['authorization'];
if (!authHeader || !authHeader.startsWith('Bearer sk-')) {
throw new UnauthorizedException('Invalid API Key');
}
const key = authHeader.replace('Bearer ', '');
const apiKey = await this.apiKeyService.validate(key);
if (!apiKey || !apiKey.enabled) {
throw new UnauthorizedException('API Key disabled or not found');
}
if (apiKey.expiresAt && apiKey.expiresAt < new Date()) {
throw new UnauthorizedException('API Key expired');
}
// 挂载到 request 上供后续使用
request.apiKey = apiKey;
request.user = apiKey.user;
return true;
}
}
javascript
五、前端页面设计
5.1 页面路由
/— Landing 首页(公开)/login— 登录(公开)/register— 注册(公开)/overview— 数据总览(需登录)/keys— API Key 管理(需登录)/logs— 调用日志(需登录)/playground— API 测试(需登录)/channels— 渠道管理(Admin)/settings— 个人设置(需登录)
5.2 Dashboard 布局
整个后台使用经典的侧边栏 + 主内容区布局,移动端侧边栏收起为 Sheet 抽屉。
5.3 Overview 总览页
总览页包含以下组件:
- 统计卡片:今日请求数、今日 Token 用量、活跃 Key 数、可用渠道数
- 趋势折线图:近 30 天请求量变化(Recharts LineChart)
- 模型用量饼图:各模型调用占比(Recharts PieChart)
- 最近请求列表:实时展示最近调用记录
5.4 API Keys 管理页
列表展示所有 Key,支持创建、复制、启停和删除操作。Key 只在创建时展示完整值,之后仅显示 sk-***...后4位。
5.5 Playground 测试页
左侧为参数配置面板(模型选择、Temperature、Max Tokens、Key 选择),右侧为对话界面,支持流式输出。
5.6 API 请求封装
// lib/api.ts
const BASE_URL = process.env.NEXT_PUBLIC_API_URL;
async function fetchAPI(path, options) {
const token = getToken();
const headers = {
'Content-Type': 'application/json',
};
if (token) {
headers['Authorization'] = 'Bearer ' + token;
}
if (options && options.headers) {
Object.assign(headers, options.headers);
}
const res = await fetch(BASE_URL + path, {
...options,
headers,
});
if (!res.ok) {
const error = await res.json().catch(function() { return {}; });
throw new ApiError(res.status, error.message);
}
return res.json();
}
javascript
六、分阶段开发步骤
Phase 1:项目初始化(Day 1)
# 创建项目 & 初始化 monorepo
mkdir ai-relay && cd ai-relay
pnpm init
# 创建 Next.js 前端
cd packages
pnpm create next-app@latest web -- \
--typescript --tailwind --eslint --app --src-dir
# 安装 shadcn/ui
cd web
pnpm dlx shadcn@latest init
pnpm dlx shadcn@latest add button card input label \
table dialog dropdown-menu toast tabs badge
# 安装前端依赖
pnpm add swr recharts lucide-react js-cookie
# 创建 NestJS 后端
cd ../
npx @nestjs/cli new server --package-manager pnpm --skip-git
# 安装后端依赖
cd server
pnpm add @nestjs/passport passport passport-jwt @nestjs/jwt
pnpm add @nestjs/config @nestjs/throttler
pnpm add @prisma/client bcryptjs class-validator class-transformer
pnpm add nanoid
# 初始化 Prisma
npx prisma init
bash
Phase 2:后端核心开发(Day 2-4)
按顺序开发:Prisma Schema -> Auth -> User -> ApiKey -> Channel -> Proxy -> Log -> Stats
- Prisma Schema — 编写数据模型,执行
prisma migrate dev --name init - Auth 模块 — 注册/登录,JWT 签发与校验
- API Key 模块 — 生成
sk-+ nanoid(48),CRUD + 启停 - Channel 模块 — 上游渠道管理,API Key 使用 AES 加密存储
- Proxy 模块(核心)— API Key 鉴权、模型路由、加权轮询、流式透传、失败重试
- Log + Stats 模块 — Interceptor 自动采集日志,SQL 聚合统计
Phase 3:前端开发(Day 5-7)
按顺序开发:Layout -> Auth -> Overview -> Keys -> Logs -> Playground -> Settings
Phase 4:部署配置(Day 8)
后端使用 Docker Compose 部署,前端部署到 Vercel。
七、部署方案
后端 Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install
COPY . .
RUN npx prisma generate
RUN pnpm build
FROM node:20-alpine
WORKDIR /app
COPY /app/dist ./dist
COPY /app/node_modules ./node_modules
COPY /app/prisma ./prisma
COPY /app/package.json ./
EXPOSE 3001
CMD ["node", "dist/main.js"]
dockerfile
Docker Compose
version: '3.8'
services:
api:
build: ./packages/server
ports:
- '3001:3001'
environment:
DATABASE_URL: postgresql://relay:password@db:5432/relay
JWT_SECRET: your-jwt-secret-here
ENCRYPTION_KEY: your-aes-key-here
depends_on:
- db
- redis
restart: unless-stopped
db:
image: postgres:16-alpine
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_USER: relay
POSTGRES_PASSWORD: password
POSTGRES_DB: relay
restart: unless-stopped
redis:
image: redis:7-alpine
restart: unless-stopped
volumes:
pgdata:
yaml
Nginx 配置(SSE 支持)
server {
listen 443 ssl http2;
server_name api.yourdomain.com;
location / {
proxy_pass http://127.0.0.1:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# SSE 关键配置
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 300s;
}
}
nginx
部署命令
# 在服务器上
git clone your-repo && cd your-repo
# 启动后端
docker compose up -d
# 执行数据库迁移
docker compose exec api npx prisma migrate deploy
# 配置 SSL
sudo certbot --nginx -d api.yourdomain.com
bash
前端连接 Git 仓库后在 Vercel 部署,设置 Root Directory 为 packages/web,环境变量 NEXT_PUBLIC_API_URL 指向后端地址。
八、环境变量
后端 .env
# 数据库
DATABASE_URL="postgresql://relay:password@localhost:5432/relay"
# JWT
JWT_SECRET="your-super-secret-jwt-key"
JWT_EXPIRES_IN="7d"
JWT_REFRESH_EXPIRES_IN="30d"
# 加密(上游 API Key 加密存储)
ENCRYPTION_KEY="32-byte-hex-string-for-aes-256"
# Redis
REDIS_URL="redis://localhost:6379"
# 服务
PORT=3001
NODE_ENV=production
bash
前端 .env.local
NEXT_PUBLIC_API_URL=https://api.yourdomain.com
bash
九、后续优化路线
- Redis 缓存 — API Key 验证缓存,减少数据库查询
- 模型映射 — Claude 格式与 OpenAI 格式自动转换
- 计费系统 — 按 token 消耗扣费/充值
- 渠道健康检查 — 定时测试,自动禁用故障渠道
- Admin 后台 — 用户管理、全局统计、系统配置
- 多租户 — 支持多用户独立管理自己的渠道
- Webhook 告警 — 余额不足/异常通知
- 速率限制升级 — 滑动窗口 + 令牌桶