Files
llm-intelligence/TECHNICAL_DESIGN.md
2026-05-13 14:42:45 +08:00

2224 lines
76 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# LLM Intelligence Hub — 技术设计文档 v1.1
> 文档版本v1.1
> 日期2026-05-09
> 负责人宰相AI 辅助)
> 状态Phase 1 执行中,技术栈已修正为 Go+PostgreSQL
---
## 一、系统架构概览
### 1.1 整体架构
```
┌──────────────────────────────────────────────────────────────────────┐
│ LLM Intelligence Hub │
├──────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ 报告 │ │ Web UI │ │ AI Agent / MCP Client │ │
│ │ Phase 2才推送│ │ (Explorer+报告) │ │ (REST API / MCP) │ │
│ └──────┬──────┘ └──────┬──────┘ └────────────┬────────────┘ │
│ │ │ │ │
│ ┌──────▼──────────────────▼────────────────────────▼────────────┐ │
│ │ Service Layer (Go) │ │
│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌──────────┐ │ │
│ │ │ Report │ │ API │ │ Scheduler │ │ Notifier │ │ │
│ │ │ Generator │ │ Server │ │ (cron) │ │ (告警) │ │ │
│ │ └────────────┘ └────────────┘ └────────────┘ └──────────┘ │ │
│ └───────────────────────────┬────────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────────▼────────────────────────────────────┐ │
│ │ Data Access Layer (database/sql + pq) │ │
│ └───────────────────────────┬────────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────────▼────────────────────────────────────┐ │
│ │ Storage Layer (PostgreSQL) │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ Data Collection Layer │ │
│ │ ┌─────────────┐ ┌──────────────┐ ┌──────────────────────┐ │ │
│ │ │ OpenRouter │ │ Phase 2才扩充厂商/中转平台 │ │ 中转平台采集器 │ │ │
│ │ │ Collector │ │ (10家厂商) │ │ (硅基流动等) │ │ │
│ │ └─────────────┘ └──────────────┘ └──────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
```
### 1.2 各层职责
| 层级 | 职责 | 技术选型 |
| **数据采集层** | 从 OpenRouter 抓取模型元数据、定价 | **Go** + `net/http` + `encoding/json` |
| **存储层** | 结构化数据持久化 | PostgreSQL与立交桥技术栈统一 |
| **服务层** | 报告生成、API 服务、调度 | **Go 1.22.2**(采集器+报告生成 CLIAPI Server Phase 2 评估 |
| **前端层** | 静态 Web 页面展示Explorer / 报告) | React/ViteTypeScript或纯静态 HTML |
### 1.3 技术架构决策(与立交桥技术栈统一)
**核心约束**与立交桥技术栈保持一致Phase 1 直接使用 PostgreSQL。
| 决策 | 选型 | 理由 |
| 数据库 | **PostgreSQL** | 与立交桥统一;支持 JSONB/数组类型 |
| 采集语言 | **Go** | 与立交桥统一;静态编译、单二进制部署、并发性能优 |
| API 框架 | **Phase 2 评估** | Phase 1 暂无 API 服务需求(直写 DB + 静态页面) |
| 前端 | **React/Vite 或纯静态 HTML** | 视部署方式定Phase 1 最小可用 |
| HTTP 客户端 | `net/http`(标准库) | Go 原生,无需第三方依赖 |
| 数据库驱动 | `github.com/lib/pq` | 最成熟的 PostgreSQL Go 驱动 |
| 调度 | **系统 cron + Go binary** | `go build` 产出单二进制cron 直接调用 |
| 日志/监控 | **标准库 log + 文件输出** | Go 标准库够用Phase 2 再接入结构化日志 |
| 告警 | **Phase 2** | 钉钉/飞书 Webhook 推送 |
**Phase 1 单机部署拓扑**
```
Phase 1 单机部署
├── PostgreSQL DB
├── fetch_openrouterGo binarycron 触发,直写 DB
├── generate_daily_reportGo binaryMarkdown 输出到 reports/daily/
└── frontend/静态页面CDN 或本地 nginx 托管)
```
---
## 二、技术选型详解
### 2.1 语言与运行时
| 组件 | 选型 | 版本 | 依据 |
| 主力语言 | **Go** | 1.22.2 | 与立交桥统一;静态编译单二进制部署;并发采集性能优;标准库完备 |
| 前端 | **Vanilla JS / React** | — | Phase 1 最小可用React 用于 Explorer 复杂交互 |
### 2.2 框架与工具库
| 用途 | 库/工具 | 用途说明 |
| HTTP 请求 | `net/http`(标准库) | 数据采集主库,无需第三方依赖 |
| JSON 解析 | `encoding/json`(标准库) | OpenRouter API 响应反序列化 |
| 数据库 | `database/sql` + `github.com/lib/pq` | PostgreSQL 连接与查询 |
| 报告生成 | Go `text/template` / `html/template` | Markdown/HTML 报告模板渲染 |
| 图表 | **ECharts**(前端) | 浏览器端可视化(价格趋势/排行榜) |
| 调度 | **系统 cron** | Go binary 直接由 cron 调用 |
| 日志 | `log` 标准库 | 简单文件输出Phase 2 评估 zap/slog |
| 日期处理 | `time` 标准库 | Go 原生日期解析与格式化 |
| 货币换算 | **Phase 2** | USD/CNY/EUR 汇率获取(当前仅记录原始币种) |
| 测试 | `testing` + `net/http/httptest` | Go 标准库测试框架 |
### 2.3 数据库
**Phase 1PostgreSQL**
- 与立交桥技术栈统一
- 利用 PostgreSQL JSONB 存储灵活字段(如 capabilities 数组)
- Schema 设计直接以 PostgreSQL 语法编写(见 `db/migrations/`
- Phase 2 评估是否需要数据库内任务队列(当前用 cron 直接触发 binary
### 2.4 为什么不用这些
| 未选方案 | 原因 |
| SQLite | 不符合与立交桥技术栈统一的要求 |
| FastAPI | 技术栈已统一为 GoPython 框架需要运行时和依赖管理,部署复杂度高于 Go 单二进制 |
| Scrapy | 重量级框架Phase 1 采集规模不需要分布式 |
| Celery + RabbitMQ | 增加运维复杂度,当前用 cron + Go binary 替代 |
| React/Vue | Phase 1 使用静态页面或最小 React 构建;部署复杂度可控 |
| Deno/Bun | 生态不如 Go 成熟,与立交桥技术栈不一致 |
---
## 三、数据库设计DDL
> 以下 DDL 以 PostgreSQL 语法编写,与立交桥技术栈统一。
### 3.1 model_provider模型商
```sql
CREATE TABLE model_provider (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE, -- "OpenAI", "百度", "DeepSeek"
name_cn TEXT, -- 中文名:"百度智能云"
country TEXT NOT NULL, -- "US" / "CN" / "EU"
website TEXT, -- 官网 URL
founded_year INTEGER, -- 成立年份
description TEXT, -- 简介
logo_url TEXT, -- 厂商 Logo
status TEXT NOT NULL DEFAULT 'active', -- active / deprecated
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by TEXT DEFAULT 'system', -- 创建者审计
updated_by TEXT DEFAULT 'system', -- 更新者审计
deleted_at TIMESTAMP -- 软删除标记NULL=未删除)
);
CREATE INDEX idx_provider_country ON model_provider(country);
CREATE INDEX idx_provider_status ON model_provider(status);
CREATE INDEX idx_provider_deleted ON model_provider(deleted_at);
```
### 3.2 model模型
```sql
CREATE TABLE model (
id BIGSERIAL PRIMARY KEY,
provider_id INTEGER NOT NULL,
name TEXT NOT NULL, -- "GPT-4o", "ERNIE-4.0"
version TEXT, -- "2025-12", "V3.2"
modality TEXT NOT NULL, -- text / vision / audio / video / code
context_length INTEGER NOT NULL DEFAULT 0, -- 上下文窗口0=未知
capabilities TEXT, -- JSON数组: ["function_calling","vision"]
release_date DATE, -- 发布日期
status TEXT NOT NULL DEFAULT 'active', -- active / deprecated / discontinued
parent_model_id INTEGER, -- 父模型ID区分 Turbo/Lite 变体)
elo_score REAL, -- ELO 分数OpenRouter
benchmark_scores TEXT, -- JSON: {"mmlu": 88.5, "humaneval": 90.2}
source_url TEXT, -- 来源 URL
data_confidence TEXT DEFAULT 'official', -- official / inferred / unverified / stale
retrieved_at TIMESTAMP, -- 数据抓取时间(数据新鲜度)
batch_id TEXT, -- 采集批次号(血缘追踪)
collector_version TEXT DEFAULT 'v1.0', -- 采集器版本
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by TEXT DEFAULT 'system',
updated_by TEXT DEFAULT 'system',
deleted_at TIMESTAMP,
FOREIGN KEY (provider_id) REFERENCES model_provider(id) ON DELETE CASCADE,
UNIQUE(provider_id, name, version)
);
CREATE INDEX idx_model_provider ON model(provider_id);
CREATE INDEX idx_model_modality ON model(modality);
CREATE INDEX idx_model_status ON model(status);
CREATE INDEX idx_model_name ON model(name);
CREATE INDEX idx_model_deleted ON model(deleted_at);
CREATE INDEX idx_model_retrieved ON model(retrieved_at);
CREATE INDEX idx_model_confidence ON model(data_confidence);
```
### 3.3 operator运营商/云平台)
```sql
CREATE TABLE operator (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE, -- "AWS Bedrock", "硅基流动"
name_cn TEXT, -- 中文名
type TEXT NOT NULL, -- cloud / reseller / official
country TEXT NOT NULL, -- 运营主体国籍
website TEXT, -- 控制台地址
api_endpoint TEXT, -- API 基础 URL
auth_type TEXT NOT NULL, -- api_key / oauth / sts
is_cn_accessible BOOLEAN DEFAULT 1, -- 国内是否可访问
stability_grade TEXT DEFAULT 'B', -- A/B/C/D 稳定性评级
status TEXT NOT NULL DEFAULT 'active', -- active / deprecated
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by TEXT DEFAULT 'system',
updated_by TEXT DEFAULT 'system',
deleted_at TIMESTAMP
);
CREATE INDEX idx_operator_type ON operator(type);
CREATE INDEX idx_operator_country ON operator(country);
CREATE INDEX idx_operator_deleted ON operator(deleted_at);
```
### 3.4 region_pricing区域定价
```sql
CREATE TABLE region_pricing (
id BIGSERIAL PRIMARY KEY,
operator_id INTEGER NOT NULL,
model_id INTEGER NOT NULL,
region TEXT NOT NULL DEFAULT 'GLOBAL', -- CN / US / EU / GLOBAL
currency TEXT NOT NULL, -- CNY / USD / EUR
input_price_per_mtok REAL NOT NULL, -- 元/百万Token
output_price_per_mtok REAL NOT NULL,
unit TEXT DEFAULT 'per_mtok', -- per_mtok / per_1k / per_token
free_tier_id INTEGER, -- 关联 free_tier 表
rate_limit TEXT, -- JSON: {"rpm": 60, "tpm": 100000}
free_limitations TEXT, -- JSON数组: ["仅限国内IP","新用户专享"]
last_updated DATE NOT NULL,
source_url TEXT,
data_confidence TEXT DEFAULT 'official', -- official / inferred / expired
retrieved_at TIMESTAMP, -- 数据抓取时间
batch_id TEXT, -- 采集批次号
collector_version TEXT DEFAULT 'v1.0',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by TEXT DEFAULT 'system',
updated_by TEXT DEFAULT 'system',
deleted_at TIMESTAMP,
FOREIGN KEY (operator_id) REFERENCES operator(id) ON DELETE CASCADE,
FOREIGN KEY (model_id) REFERENCES model(id) ON DELETE CASCADE,
UNIQUE(operator_id, model_id, region, currency)
);
CREATE INDEX idx_pricing_operator ON region_pricing(operator_id);
CREATE INDEX idx_pricing_model ON region_pricing(model_id);
CREATE INDEX idx_pricing_region ON region_pricing(region);
CREATE INDEX idx_pricing_currency ON region_pricing(currency);
CREATE INDEX idx_pricing_input_cost ON region_pricing(input_price_per_mtok);
CREATE INDEX idx_pricing_deleted ON region_pricing(deleted_at);
CREATE INDEX idx_pricing_retrieved ON region_pricing(retrieved_at);
```
### 3.5 pricing_history价格历史
```sql
CREATE TABLE pricing_history (
id BIGSERIAL PRIMARY KEY,
region_pricing_id INTEGER NOT NULL,
model_id INTEGER NOT NULL,
operator_id INTEGER NOT NULL,
region TEXT NOT NULL,
currency TEXT NOT NULL,
old_input_price REAL,
new_input_price REAL NOT NULL,
old_output_price REAL,
new_output_price REAL NOT NULL,
change_pct REAL, -- 变动百分比(自动计算)
change_type TEXT NOT NULL, -- increase / decrease / new_model / discontinued
recorded_at DATE NOT NULL, -- 记录日期
source_url TEXT,
batch_id TEXT, -- 采集批次号
collector_version TEXT DEFAULT 'v1.0',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (region_pricing_id) REFERENCES region_pricing(id) ON DELETE CASCADE,
FOREIGN KEY (model_id) REFERENCES model(id) ON DELETE CASCADE,
FOREIGN KEY (operator_id) REFERENCES operator(id) ON DELETE CASCADE
);
CREATE INDEX idx_history_model ON pricing_history(model_id);
CREATE INDEX idx_history_operator ON pricing_history(operator_id);
CREATE INDEX idx_history_recorded ON pricing_history(recorded_at);
CREATE INDEX idx_history_change_type ON pricing_history(change_type);
-- Phase 2: 按 recorded_at 分区,提升历史查询性能
-- CREATE TABLE pricing_history_partitioned (...) PARTITION BY RANGE (recorded_at);
```
### 3.6 free_tier免费政策
```sql
CREATE TABLE free_tier (
id BIGSERIAL PRIMARY KEY,
operator_id INTEGER NOT NULL,
model_id INTEGER, -- NULL表示该平台全部免费额度
free_model_name TEXT, -- 免费模型名称(展示用)
quota_type TEXT NOT NULL, -- daily / monthly / one_time / unlimited
quota_amount REAL, -- 配额数量
quota_unit TEXT, -- requests / tokens / minutes
tpm_limit INTEGER, -- tokens per minute 限制
rpm_limit INTEGER, -- requests per minute 限制
daily_req_limit INTEGER, -- 每日请求上限
monthly_req_limit INTEGER, -- 每月请求上限
token_limit_per_req INTEGER, -- 单次请求Token上限
requires_credit_card BOOLEAN DEFAULT 0, -- 是否需要绑定信用卡
requires_verification BOOLEAN DEFAULT 0, -- 是否需要实名认证
region_restrictions TEXT, -- JSON: ["仅限部分地区"]
applicable_scenarios TEXT, -- JSON: ["仅限新用户"]
special_notes TEXT, -- 特殊说明
effective_from DATE,
effective_until DATE, -- NULL表示长期有效
last_updated DATE,
source_url TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by TEXT DEFAULT 'system',
updated_by TEXT DEFAULT 'system',
deleted_at TIMESTAMP,
FOREIGN KEY (operator_id) REFERENCES operator(id) ON DELETE CASCADE,
FOREIGN KEY (model_id) REFERENCES model(id) ON DELETE SET NULL
);
CREATE INDEX idx_free_operator ON free_tier(operator_id);
CREATE INDEX idx_free_model ON free_tier(model_id);
CREATE INDEX idx_free_quota_type ON free_tier(quota_type);
CREATE INDEX idx_free_deleted ON free_tier(deleted_at);
```
### 3.7 daily_report每日报告
```sql
CREATE TABLE daily_report (
id BIGSERIAL PRIMARY KEY,
report_date DATE NOT NULL UNIQUE,
new_models TEXT, -- JSON数组新上线模型
price_changes TEXT, -- JSON数组价格变动
free_changes TEXT, -- JSON数组免费政策变更
top_recommendations TEXT, -- JSON对象场景推荐
cost_alerts TEXT, -- JSON数组成本告警
html_content TEXT, -- 完整HTML报告内容
summary_md TEXT, -- Markdown摘要
status TEXT NOT NULL DEFAULT 'generated', -- generated / failed / partial
generated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
error_message TEXT,
created_by TEXT DEFAULT 'system',
updated_by TEXT DEFAULT 'system'
);
CREATE INDEX idx_report_date ON daily_report(report_date);
CREATE INDEX idx_report_status ON daily_report(status);
```
### 3.8 user_subscription用户订阅
```sql
CREATE TABLE user_subscription (
id BIGSERIAL PRIMARY KEY,
user_id TEXT NOT NULL, -- 统一用户ID
email TEXT,
phone TEXT,
subscription_tier TEXT NOT NULL DEFAULT 'free', -- free / pro / team / enterprise
subscription_start DATE,
subscription_end DATE,
notify_channels TEXT, -- JSON: ["feishu","email","dingtalk"]
feishu_webhook TEXT,
dingtalk_webhook TEXT,
email_webhook TEXT,
model_watchlist TEXT, -- JSON数组关注模型
operator_watchlist TEXT, -- JSON数组关注平台
price_alert_threshold REAL DEFAULT 10.0, -- 告警阈值(%
monthly_token_limit INTEGER, -- 月度Token限制
monthly_token_used INTEGER DEFAULT 0,
stripe_customer_id TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by TEXT DEFAULT 'system',
updated_by TEXT DEFAULT 'system',
deleted_at TIMESTAMP,
UNIQUE(email)
);
CREATE INDEX idx_sub_user ON user_subscription(user_id);
CREATE INDEX idx_sub_tier ON user_subscription(subscription_tier);
CREATE INDEX idx_sub_deleted ON user_subscription(deleted_at);
```
---
## 四、API 设计
### 4.1 内部采集 APICollector → Server
#### POST /api/v1/collect/push
采集器推送采集结果Phase 2 分布式采集节点使用)
**Request:**
```json
{
"batch": [
{
"provider_name": "OpenAI",
"model_name": "GPT-4o",
"version": "2025-01",
"operator_name": "OpenRouter",
"region": "GLOBAL",
"currency": "USD",
"input_price": 2.50,
"output_price": 10.0,
"context_length": 128000,
"capabilities": ["vision", "function_calling", "json_mode"],
"free_tier": null,
"source_url": "https://openrouter.ai/api/v1/models"
}
],
"collected_at": "2026-05-04T08:00:00+08:00"
}
```
**Response:**
```json
{
"status": "ok",
"inserted": 365,
"updated": 12,
"errors": 0
}
```
---
### 4.1.5 API 安全与生产规范
#### 认证与鉴权
| 场景 | 机制 | 说明 |
|------|------|------|
| **内部采集器 → DB** | 无 API 认证 | 采集器直连 PostgreSQL通过 DB 用户权限隔离 |
| **Phase 2 对外 API** | API KeyX-API-Key Header | 按订阅等级分配不同 KeyDB 存储 bcrypt 哈希 |
| **Admin 运维接口** | JWT + RBAC | 仅内部使用Token 有效期 1hRefresh Token 7d |
#### 限流策略
| 订阅等级 | QPS | 日调用上限 | 并发连接 |
|----------|-----|-----------|---------|
| Free | 2 | 100 | 1 |
| Pro | 10 | 5,000 | 3 |
| Team | 50 | 50,000 | 10 |
| Enterprise | 协商 | 无限 | 协商 |
- 限流实现Token Bucket内存+ Redis分布式Phase 2
- 超限响应:`429 Too Many Requests``Retry-After` 头部
#### 错误码规范
| HTTP Status | 业务码 | 含义 | 示例 |
|-------------|--------|------|------|
| 200 | — | 成功 | — |
| 400 | `BAD_REQUEST` | 参数非法 | `page_size > 100` |
| 401 | `UNAUTHORIZED` | 认证失败 | API Key 无效/过期 |
| 403 | `FORBIDDEN` | 权限不足 | Free 用户访问 Team 功能 |
| 404 | `NOT_FOUND` | 资源不存在 | 模型 ID 不存在 |
| 429 | `RATE_LIMITED` | 限流触发 | 超出 QPS/日配额 |
| 500 | `INTERNAL_ERROR` | 服务端错误 | DB 连接失败 |
| 503 | `SERVICE_UNAVAILABLE` | 维护模式 | 系统升级中 |
**错误响应格式:**
```json
{
"error": {
"code": "RATE_LIMITED",
"message": "Quota exceeded: 100/100 daily calls used",
"retry_after": 3600,
"request_id": "req_abc123"
}
}
```
#### CORS 策略
```
Access-Control-Allow-Origin: https://llm-hub.example.com
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, X-API-Key
Access-Control-Max-Age: 86400
```
- 生产环境:只允许特定域名
- 开发环境:`*`(仅本地)
#### 请求/响应约束
| 约束 | 值 | 说明 |
|------|-----|------|
| 请求体大小 | ≤ 1MB | 防止超大 JSON 攻击 |
| 响应体大小 | ≤ 5MB | 分页控制 |
| 超时 | 30s | API 请求硬超时 |
| 参数校验 | 严格 | 未知参数返回 400 |
---
### 4.2 对外 REST API
#### GET /api/v1/models
查询模型列表
**Query Parameters:**
| 参数 | 类型 | 默认值 | 说明 |
| `provider` | string | — | 模型商名称过滤 |
| `modality` | string | — | text/vision/audio/video/code |
| `min_context` | int | — | 最小上下文长度 |
| `max_input_price` | float | — | 最大输入价格(/MTok |
| `has_free` | bool | false | 仅显示有免费额的模型 |
| `search` | string | — | 关键词搜索(模型名/capabilities |
| `sort` | string | `input_price` | 排序字段 |
| `order` | string | `asc` | asc/desc |
| `page` | int | 1 | 页码 |
| `page_size` | int | 20 | 每页数量max 100 |
**Response:**
```json
{
"total": 523,
"page": 1,
"page_size": 20,
"models": [
{
"id": 42,
"name": "DeepSeek V4-Flash",
"provider": "DeepSeek",
"provider_cn": "深度求索",
"modality": "text",
"context_length": 1048576,
"capabilities": ["function_calling", "json_mode"],
"status": "active",
"lowest_price": {
"operator": "硅基流动",
"currency": "CNY",
"input": 0.14,
"output": 0.028,
"region": "CN"
}
}
]
}
```
#### GET /api/v1/models/{id}
查询单个模型详情
**Response:**
```json
{
"id": 42,
"name": "DeepSeek V4-Flash",
"provider": {
"id": 5,
"name": "DeepSeek",
"country": "CN"
},
"version": "V4-Flash",
"modality": "text",
"context_length": 1048576,
"capabilities": ["function_calling", "json_mode"],
"release_date": "2026-04-15",
"status": "active",
"elo_score": 1382.5,
"pricing": [
{
"operator": "硅基流动",
"region": "CN",
"currency": "CNY",
"input": 0.14,
"output": 0.028,
"source_url": "https://siliconflow.cn"
},
{
"operator": "OpenRouter",
"region": "GLOBAL",
"currency": "USD",
"input": 0.02,
"output": 0.004
}
],
"free_tier": {
"quota_type": "monthly",
"quota_amount": 5000000,
"quota_unit": "tokens",
"requires_credit_card": false
}
}
```
#### GET /api/v1/cost
成本计算器
**Query Parameters:**
| 参数 | 类型 | 必填 | 说明 |
| `input_tokens` | int | 是 | 输入 Token 数 |
| `output_tokens` | int | 否 | 输出 Token 数(默认=input_tokens×0.3 |
| `modality` | string | 否 | 模态过滤 |
| `region` | string | 否 | 区域CN/US/GLOBAL |
| `currency` | string | CNY | 显示货币 |
| `top_n` | int | 10 | 返回前N个最低价 |
**Response:**
```json
{
"input_tokens": 1000000,
"output_tokens": 300000,
"currency": "CNY",
"results": [
{
"rank": 1,
"model": "DeepSeek V4-Flash",
"provider": "DeepSeek",
"operator": "硅基流动",
"input_cost": 0.14,
"output_cost": 0.0084,
"total_cost": 0.1484,
"total_cost_usd": 0.020
},
{
"rank": 2,
"model": "Kimi K2.5",
"provider": "Moonshot",
"operator": "硅基流动",
"input_cost": 0.23,
"output_cost": 0.021,
"total_cost": 0.251,
"total_cost_usd": 0.034
}
]
}
```
#### GET /api/v1/recommend
模型推荐
**Query Parameters:**
| 参数 | 类型 | 必填 | 说明 |
| `use_case` | string | 是 | 场景coding/writing/reasoning/free/vision |
| `min_context` | int | — | 最小上下文需求 |
| `budget` | float | — | 预算上限(/MTok input |
| `region` | string | CN | 区域偏好 |
| `limit` | int | 5 | 返回数量 |
**Response:**
```json
{
"use_case": "coding",
"recommendations": [
{
"rank": 1,
"model": "Kimi K2.6",
"provider": "Moonshot",
"reason": "SWE-Bench Pro 超越 GPT-5.4,编码能力最强",
"input_price": 0.95,
"currency": "CNY",
"free_option": null
},
{
"rank": 2,
"model": "GLM-5.1",
"provider": "智谱",
"reason": "编码能力接近 Opus 4.6,性价比高",
"input_price": 1.40,
"currency": "CNY",
"free_option": null
}
]
}
```
#### GET /api/v1/reports
每日报告列表
**Query Parameters:**
| 参数 | 类型 | 默认值 | 说明 |
| `from` | date | 30天前 | 开始日期 |
| `to` | date | 今天 | 结束日期 |
| `page` | int | 1 | 页码 |
**Response:**
```json
{
"total": 30,
"reports": [
{
"id": 30,
"report_date": "2026-05-04",
"status": "generated",
"summary": "新上线3个模型价格变动2项免费政策更新1项",
"generated_at": "2026-05-04T08:00:45+08:00"
}
]
}
```
#### GET /api/v1/reports/{date}
获取指定日期报告内容
**Response:**
```json
{
"id": 30,
"report_date": "2026-05-04",
"html_content": "<html>...</html>",
"new_models": [
{"name": "xAI Grok 4.1 Fast", "provider": "xAI", "input_price": 0.20, "currency": "USD"}
],
"price_changes": [
{
"model": "Claude Opus 4.6",
"operator": "Anthropic",
"old_price": 15.0,
"new_price": 5.0,
"change_pct": -66.7,
"currency": "USD"
}
],
"free_changes": [
{
"model": "Gemini 2.5 Pro",
"operator": "Google",
"change": "免费层下线,需付费使用"
}
],
"top_recommendations": {
"coding": {"model": "Kimi K2.6", "provider": "Moonshot"},
"writing": {"model": "GLM-5.1", "provider": "智谱"},
"free": {"model": "DeepSeek R1", "provider": "DeepSeek"},
"cheapest": {"model": "Step 3.5 Flash", "provider": "字节"}
}
}
```
#### GET /api/v1/health
健康检查
**Response:**
```json
{
"status": "ok",
"version": "1.0.0",
"db_record_count": {
"models": 523,
"providers": 22,
"operators": 31,
"pricing_records": 1847
},
"last_collect_time": "2026-05-04T08:00:12+08:00",
"last_report_time": "2026-05-04T08:00:45+08:00"
}
```
---
## 五、数据采集 Pipeline
### 5.1 OpenRouter 采集流程
```
┌─────────────────┐
│ 每日 08:00 │
│ cron 触发 │
└────────┬────────┘
┌─────────────────────────────────────────────────┐
│ GET https://openrouter.ai/api/v1/models │
│ Headers: Authorization: Bearer <OPENROUTER_KEY>│
└────────┬────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ 解析响应 JSON │
│ 字段映射: │
│ id → model.name (如 "anthropic/claude-3.5-sonnet")│
│ name → display_name │
│ pricing.input * 1e6 → input_price_per_mtok │
│ pricing.output * 1e6 → output_price_per_mtok│
│ context_length → context_length │
│ supported_parameters → capabilities │
│ opensource → modality (text/vision/etc) │
└────────┬────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ 识别 provider_name (从 id 前缀提取) │
│ 示例: "anthropic/claude-3.5-sonnet" → │
│ provider="Anthropic", model="Claude 3.5 Sonnet"│
└────────┬────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ Upsert: │
│ INSERT OR REPLACE INTO model_provider (...) │
│ INSERT OR REPLACE INTO model (...) │
│ INSERT OR REPLACE INTO region_pricing (...) │
└────────┬────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ 检测价格变动: │
│ SELECT old_price FROM pricing_history │
│ WHERE model_id = x AND operator_id = y │
│ IF new_price != old_price: │
│ INSERT INTO pricing_history (...) │
│ IF abs(change_pct) > 5%: 标记为高亮变动 │
└─────────────────────────────────────────────────┘
```
### 5.2 国内厂商采集流程 — Phase 2
每个国内厂商独立采集器(`collectors/` 目录统一接口输出Go
```go
// collectors/collector.go
package collectors
import "time"
// Collector 所有采集器必须实现的接口
type Collector interface {
Name() string
Collect() ([]CollectedRecord, error)
Schedule() string // cron 表达式,如 "0 8 * * *"
Timeout() time.Duration
RetryCount() int
}
```
#### 采集器清单Phase 1
#### 统一字段映射
每个采集器输出标准化 `CollectedRecord`Go struct
```go
type CollectedRecord struct {
ProviderName string `json:"provider_name"`
ProviderNameCN string `json:"provider_name_cn"`
ModelName string `json:"model_name"`
ModelVersion string `json:"model_version"`
Modality string `json:"modality"`
ContextLength int `json:"context_length"`
Capabilities []string `json:"capabilities"`
OperatorName string `json:"operator_name"`
OperatorType string `json:"operator_type"`
Region string `json:"region"`
Currency string `json:"currency"`
InputPricePerMTok float64 `json:"input_price_per_mtok"`
OutputPricePerMTok float64 `json:"output_price_per_mtok"`
FreeTier *FreeTierRecord `json:"free_tier,omitempty"`
SourceURL string `json:"source_url"`
CollectedAt time.Time `json:"collected_at"`
}
```
统一 `ProviderMapper`Go map
```go
var ProviderNameMap = map[string]struct {
Provider string
Model string
Version string
}{
// DeepSeek
"deepseek-ai/DeepSeek-V3": {Provider: "DeepSeek", Model: "V3.2", Version: "2026-03"},
"deepseek-ai/DeepSeek-V4": {Provider: "DeepSeek", Model: "V4", Version: "2026-04"},
"deepseek-ai/DeepSeek-R1": {Provider: "DeepSeek", Model: "R1", Version: "2026-01"},
// 阿里
"qwen/Qwen3-VL-32B": {Provider: "阿里云", Model: "Qwen3-VL-32B", Version: "2026-03"},
"qwen/Qwen3-VL-8B": {Provider: "阿里云", Model: "Qwen3-VL-8B", Version: "2026-03"},
// Moonshot
"moonshotai/Kimi-K2.6": {Provider: "Moonshot", Model: "K2.6", Version: "2026-04"},
"moonshotai/Kimi-K2.5": {Provider: "Moonshot", Model: "K2.5", Version: "2026-03"},
// 智谱
"zhipuai/GLM-5.1": {Provider: "智谱", Model: "GLM-5.1", Version: "2026-03"},
"zhipuai/GLM-4.7": {Provider: "智谱", Model: "GLM-4.7", Version: "2025-12"},
// ... 其他厂商
}
```
### 5.3 每日调度设计
**调度策略**:系统 cron 统一调度,无外部消息队列依赖。
```crontab
# /etc/crontab
# 每日 08:00 触发全量采集 + 报告生成
0 8 * * * root /opt/llm-hub/scripts/run_daily.sh >> /var/log/llm-hub/daily.log 2>&1
# 每日 09:00 触发数据备份
0 9 * * * root /opt/llm-hub/scripts/backup.sh >> /var/log/llm-hub/backup.log 2>&1
```
```bash
#!/bin/bash
# run_daily.sh
set -e
LOG_FILE="/var/log/llm-hub/daily.log"
HUB_DIR="/opt/llm-hub"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a $LOG_FILE
}
log "开始每日采集任务"
# 1. 采集 OpenRouter海外模型优先级最高
cd $HUB_DIR
./scripts/fetch_openrouter >> $LOG_FILE 2>&1
log "OpenRouter 采集完成"
# 2. Phase 2 才并行采集国内厂商DeepSeek/阿里/Kimi/智谱等)
# 3. 生成每日报告
./scripts/generate_daily_report >> $LOG_FILE 2>&1
log "日报生成完成"
# 4. Phase 2 才检测价格变动并告警
log "每日任务完成"
```
### 5.4 失败重试 + 告警机制Go 实现)
```go
// pkg/retry/retry.go
package retry
import (
"fmt"
"log"
"time"
)
// Do 指数退避重试
func Do(maxAttempts int, delay time.Duration, backoff float64, fn func() error) error {
var err error
for attempt := 1; attempt <= maxAttempts; attempt++ {
if err = fn(); err == nil {
return nil
}
if attempt == maxAttempts {
return fmt.Errorf("all %d attempts failed: %w", maxAttempts, err)
}
wait := time.Duration(float64(delay) * pow(backoff, float64(attempt-1)))
log.Printf("Attempt %d/%d failed: %v. Retrying in %v...", attempt, maxAttempts, err, wait)
time.Sleep(wait)
}
return err
}
func pow(x, y float64) float64 {
result := 1.0
for i := 0; i < int(y); i++ {
result *= x
}
return result
}
```
**采集器调用示例Go**
```go
func collectWithRetry(collector collectors.Collector) error {
return retry.Do(3, 10*time.Second, 2.0, func() error {
records, err := collector.Collect()
if err != nil {
return err
}
return saveToDB(records)
})
}
```
**告警触发逻辑Go**
```go
func checkAndAlertPriceChange(modelID, operatorID int, newPrice float64) {
oldPrice := getLastPrice(modelID, operatorID)
if oldPrice == 0 {
return // 首次录入,不告警
}
changePct := (newPrice - oldPrice) / oldPrice * 100
if math.Abs(changePct) > 10 {
alertMsg := fmt.Sprintf(
"⚠️ 价格变动告警\n模型: %s\n平台: %s\n原价: %.4f\n新价: %.4f\n变动: %+.1f%%",
getModelName(modelID), getOperatorName(operatorID), oldPrice, newPrice, changePct,
)
sendFeishuAlert(alertMsg)
log.Printf("[ALERT] %s", alertMsg)
}
}
```
**告警规则:**
| 条件 | 动作 |
| 单个采集器失败 | 记录日志,保留旧数据,发送低优先级告警 |
| 连续 3 天同一采集器失败 | 发送高优先级告警(钉钉/飞书) |
| 价格变动 > 10% | 立即触发告警 |
| 价格变动 > 20% | 立即触发告警 + 暂停该平台数据(人工确认) |
| 报告生成失败 | 发送告警,保留前一天报告 |
| 数据库写入失败 | 立即告警,回滚事务 |
---
## 六、前端架构
### 6.1 技术栈
| 组件 | 选型 | 理由 |
| **页面框架** | React 18 + Vite + TypeScript | 组件化、类型安全、构建优化 |
| **图表库** | ECharts 5 + echarts-for-react | 功能全面、中文支持好 |
| **图标** | Lucide React | 现代化图标、Tree-shaking |
| **搜索** | 前端 Fuse.js | 轻量模糊搜索、< 100KB |
| **布局** | Tailwind CSS | 原子化 CSS、响应式、定制灵活 |
| **构建** | Vite | 快速 HMR、Rollup 打包、生产优化 |
### 6.2 页面清单
| 页面 | 路径 | 功能说明 |
| **首页 / 报告列表** | `/` | 展示最新每日报告入口,显示近期报告摘要 |
| **报告详情** | `/reports/{date}.html` | 单日报告完整内容(新模型/价格变动/推荐) |
| **模型浏览器** | `/explorer.html` | 组合筛选 + 卡片/表格视图 + 搜索 |
| **模型详情** | `/model/{id}.html` | 模型完整信息 + 全平台定价对比 |
Phase 2|Phase 2 Phase 2~Phase 2~Phase 2*Phase 2*Phase 2成Phase 2本Phase 2计Phase 2算Phase 2器Phase 2*Phase 2*Phase 2~Phase 2~Phase 2 Phase 2|Phase 2 Phase 2`Phase 2/Phase 2cPhase 2aPhase 2lPhase 2cPhase 2uPhase 2lPhase 2aPhase 2tPhase 2oPhase 2rPhase 2.Phase 2hPhase 2tPhase 2mPhase 2lPhase 2`Phase 2 Phase 2|Phase 2 Phase 2TPhase 2oPhase 2kPhase 2ePhase 2nPhase 2 Phase 2用Phase 2量Phase 2 Phase 2→Phase 2 Phase 2多Phase 2平Phase 2台Phase 2成Phase 2本Phase 2对Phase 2比Phase 2排Phase 2行Phase 2 Phase 2|Phase 2
Phase 2Phase 2|Phase 2 Phase 2~Phase 2~Phase 2*Phase 2*Phase 2趋Phase 2势Phase 2图Phase 2*Phase 2*Phase 2~Phase 2~Phase 2 Phase 2|Phase 2 Phase 2`Phase 2/Phase 2tPhase 2rPhase 2ePhase 2nPhase 2dPhase 2sPhase 2.Phase 2hPhase 2tPhase 2mPhase 2lPhase 2`Phase 2 Phase 2|Phase 2 Phase 2价Phase 2格Phase 2/Phase 2模Phase 2型Phase 2能Phase 2力Phase 2历Phase 2史Phase 2趋Phase 2势Phase 2Phase 2EPhase 2CPhase 2hPhase 2aPhase 2rPhase 2tPhase 2sPhase 2Phase 2 Phase 2|Phase 2
Phase 2| **关于我们** | `/about.html` | 项目介绍、数据来源说明 |
### 6.3 与后端的数据交互
**模式**:纯前端 SPASingle Page Application通过 Fetch API 调用后端 REST API。
```
前端静态文件Phase 2才 Nginx 托管)
├── GET /api/v1/models → Go API 返回 JSON
├── GET /api/v1/models/{id} → 模型详情 JSON
├── GET /api/v1/cost → 成本计算 JSON
├── GET /api/v1/recommend → 推荐结果 JSON
└── GET /api/v1/reports/{date} → 报告 JSON
```
**前端数据层dataService.js**
```javascript
// 统一 API 调用封装
const API_BASE = '/api/v1';
async function apiGet(endpoint, params = {}) {
const url = new URL(`${API_BASE}${endpoint}`, window.location.origin);
Object.entries(params).forEach(([k, v]) => v != null && url.searchParams.set(k, v));
const resp = await fetch(url);
if (!resp.ok) throw new Error(`API error: ${resp.status}`);
return resp.json();
}
// 主要接口封装
const api = {
models: {
list: (params) => apiGet('/models', params),
detail: (id) => apiGet(`/models/${id}`)
},
cost: {
calculate: (params) => apiGet('/cost', params)
},
recommend: (params) => apiGet('/recommend', params),
reports: {
list: (params) => apiGet('/reports', params),
get: (date) => apiGet(`/reports/${date}`)
}
};
```
### 6.4 模型浏览器页面结构
```html
<!-- explorer.html -->
<!-- 筛选栏 -->
<div class="row mb-3">
<div class="col-md-2">
<select id="filter-provider" class="form-select">
<option value="">全部厂商</option>
<option value="DeepSeek">DeepSeek</option>
<option value="阿里云">阿里云</option>
<!-- ... -->
</select>
</div>
<div class="col-md-2">
<select id="filter-modality" class="form-select">
<option value="">全部模态</option>
<option value="text">文字</option>
<option value="vision">视觉</option>
<option value="code">代码</option>
</select>
</div>
<div class="col-md-2">
<input type="number" id="filter-max-price" class="form-control"
placeholder="最大输入价(¥/MT)">
</div>
<div class="col-md-3">
<input type="text" id="search-keyword" class="form-control"
placeholder="搜索模型名称...">
</div>
<div class="col-md-3">
<div class="btn-group" role="group">
<button class="btn btn-outline-primary active" data-view="card">卡片</button>
<button class="btn btn-outline-primary" data-view="table">表格</button>
</div>
</div>
</div>
<!-- 结果区域 -->
<div id="results" class="row">
<!-- 动态渲染卡片或表格 -->
</div>
<!-- 分页 -->
<nav><ul class="pagination" id="pagination"></ul></nav>
```
---
## 十二、快速部署参考(历史版本)
> 本节保留早期简洁部署方案,生产环境请参考「八、部署与运维架构」。
### 7.1 Docker 配置
```yaml
# docker-compose.yml
version: '3.8'
services:
# --- Phase 1 核心服务 ---
collector:
build:
context: .
dockerfile: Dockerfile.collector
volumes:
- ./data:/opt/llm-hub/data # PostgreSQL 数据持久化
- ./logs:/var/log/llm-hub # 日志持久化
- ./reports:/opt/llm-hub/reports # 报告输出
env_file:
- .env
restart: unless-stopped
networks:
- llm-hub-net
api:
build:
context: .
dockerfile: Dockerfile.api
ports:
- "5000:5000"
volumes:
- ./data:/opt/llm-hub/data
- ./reports:/opt/llm-hub/reports
env_file:
- .env
restart: unless-stopped
depends_on:
- collector
networks:
- llm-hub-net
# --- Phase 2 才引入 Nginx内网访问 + 静态文件服务)---
networks:
llm-hub-net:
driver: bridge
```
### 7.2 内网部署要求
**部署前提**
- 一台可访问外网的服务器(境外更好,便于访问 OpenRouter
- 域名(可选,用于 HTTPS + 钉钉/飞书 Webhook 回调)
- Docker + Docker Compose
**网络访问需求**
| 目的地 | 用途 | 协议 |
| `openrouter.ai` | 采集海外模型数据 | HTTPS |
| `api.deepseek.com` | 采集 DeepSeek 定价 | HTTPS |
| `dashscope.aliyuncs.com` | 采集阿里云定价 | HTTPS |
| `api.moonshot.cn` | 采集 Kimi 定价 | HTTPS |
| `open.bigmodel.cn` | 采集智谱定价 | HTTPS |
| `api.siliconflow.cn` | 采集硅基流动定价 | HTTPS |
| `oapi.dingtalk.com` | Phase 2 钉钉告警 | HTTPS |
| `open.feishu.cn` | Phase 2 飞书告警 | HTTPS |
| **无需访问** | 国内云厂商定价页(如阿里云控制台) | — |
### 7.3 环境变量清单
```bash
# .env 文件Phase 1 最小配置)
# === 数据库 ===
DATABASE_URL=postgresql://user:pass@localhost:5432/llmhub
# === OpenRouter ===
OPENROUTER_API_KEY=sk-or-v1-xxxxx
# === 国内厂商 API Keys ===
DEEPSEEK_API_KEY=sk-xxxxx
DASHSCOPE_API_KEY=sk-xxxxx
MOONSHOT_API_KEY=sk-xxxxx
ZHIPU_API_KEY=xxxxx
MINIMAX_API_KEY=xxxxx
VOLCENGINE_API_KEY=xxxxx
VOLCENGINE_SECRET_KEY=xxxxx
TENCENT_SECRET_ID=xxxxx
TENCENT_SECRET_KEY=xxxxx
BAIDU_QIANFAN_API_KEY=xxxxx
BAIDU_QIANFAN_SECRET_KEY=xxxxx
SILICONFLOW_API_KEY=sk-xxxxx
# === 告警配置Phase 2 才启用)===
# DINGTALK_WEBHOOK=https://oapi.dingtalk.com/robot/send?access_token=xxxxx
# FEISHU_WEBHOOK=https://open.feishu.cn/open-apis/bot/v2/hook/xxxxx
ALERT_THRESHOLD_PCT=10
ALERT_THRESHOLD_CRITICAL_PCT=20
# === 邮件配置(可选)===
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=noreply@example.com
SMTP_PASS=xxxxx
# === 备份配置 ===
BACKUP_OSS_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com
BACKUP_OSS_BUCKET=llm-hub-backup
BACKUP_OSS_KEY=xxxxx
BACKUP_OSS_SECRET=xxxxx
# === 系统 ===
LOG_LEVEL=INFO
TZ=Asia/Shanghai
```
### 7.4 Nginx 配置
```nginx
# nginx.conf
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
# --- 静态文件服务(前端)---
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# 前端静态页面
location / {
try_files $uri $uri/ /index.html;
}
# 每日报告 HTML
location /reports/ {
alias /usr/share/nginx/html/reports/;
expires 7d;
add_header Cache-Control "public, immutable";
}
# --- API 反向代理 ---
location /api/ {
proxy_pass http://api:5000/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 60s;
}
# 健康检查(无需认证)
location /health {
proxy_pass http://api:5000/api/v1/health;
proxy_set_header Host $host;
}
}
}
```
---
## 十三、Phase 1 技术路线3个月
### 8.1 Sprint 划分
| Sprint | 周期 | 目标 | 交付物 |
| **Sprint 0** | Week 1 | 技术方案确认 + 环境搭建 | `TECHNICAL_DESIGN.md` 终稿;开发环境就绪 |
| **Sprint 1** | Week 2-3 | OpenRouter 采集器 + 数据库 Schema | 371 海外模型入库;数据库 DDL 可执行 |
| **Sprint 2** | Week 4-5 | PostgreSQL migration + 日报生成器 | 三张表落地Markdown 报告输出到 reports/daily/ |
| **Sprint 2** | Week 4-5 | 每日报告生成 + Explorer 页面 | Markdown 报告生成Explorer 页面上线Markdown 报告可输出到 reports/daily/ |
| **Sprint 4** | Week 8-9 | 模型浏览器 + 搜索筛选 | `/explorer.html` 上线;卡片/表格视图 |
| **Sprint 5** | Week 10-11 | Explorer 页面完善 + Dashboard 占位图 | 表格/筛选排序;价格趋势占位图 |
| **Sprint 6** | Week 12 | 收尾 + 部署 + 验证脚本 | Docker Compose 部署文档;验证脚本;备份策略 |
### 8.2 Sprint 1 详细任务OpenRouter 采集器)
```
Sprint 1 目标:从 OpenRouter API 采集 371 模型,建立基础数据库
任务分解:
├── T1.1 数据库 Schema 部署
│ ├── [ ] 创建所有 DDL 表model_provider/model/operator/region_pricing/...
│ ├── [ ] 编写 PostgreSQL Schema 部署脚本deploy.sh
│ └── [ ] 验证:查询所有表,返回空表,数量正确
├── T1.2 OpenRouter 采集器实现
│ ├── [ ] 实现 `scripts/fetch_openrouter.go`Go
│ │ ├── 调用 GET https://openrouter.ai/api/v1/models
│ │ ├── 解析 id/name/pricing/context_length/capabilities
│ │ ├── 从 id 前缀提取 provideranthropic/claude-3.5-sonnet → Anthropic
│ │ ├── 处理免费模型id 包含 :free 后缀)
│ │ └── 错误处理401/429/500
│ ├── [ ] 实现 base collector 抽象类
│ ├── [ ] 实现数据清洗逻辑(去除异常价格、统一单位)
│ └── [ ] 验证371 模型全部入库,无重复,数据正确
├── T1.3 数据映射 + Provider 标准化
│ ├── [ ] 建立 PROVIDER_NAME_MAPOpenRouter id → 标准厂商名)
│ ├── [ ] 验证:所有 provider 名称统一(无别名)
│ └── [ ] 补充 provider logo_url / description
├── T1.4 初始数据导入
│ ├── [ ] 运行 OpenRouter 采集器,导入 371 模型
│ ├── [ ] 质量检查:随机抽 10 条数据,验证价格/上下文长度
│ └── [ ] 导出数据字典文档
└── T1.5 采集脚本 + cron 配置
├── [ ] 编写 scripts/run_openrouter_collect.sh
├── [ ] 配置 crontab08:00 每日执行)
├── [ ] 编写失败重试逻辑
└── [ ] 验证:手动运行脚本成功,数据入库
```
### 8.3 Phase 1 关键技术决策记录
| 决策 | 选型 | 记录时间 | 理由 |
| Phase 1 数据库用 PostgreSQL | ✅ 确认 | Sprint 0 | 与立交桥技术栈统一;支持 JSONB/数组类型;数据库内队列 |
| 数据采集用 Go net/http | ✅ 确认 | Sprint 0 | 标准库,无需第三方依赖,静态编译 |
| 报告生成用 Go html/template | ✅ 确认 | Sprint 0 | 标准库模板,无需第三方依赖 |
| 告警用 Webhook 直推 | ✅ 确认 | Sprint 0 | 无需消息队列,降低复杂度 |
| OpenRouter ELO 数据暂不采集 | ⚠️ 延期 | Sprint 1 | ELO API 可能收费Phase 1 跳过 |
| 国内厂商优先级DeepSeek > 阿里 > Kimi > 智谱 > MiniMax > 火山 > 腾讯 > 百度 | ✅ 确认 | Sprint 2 | 按市场热度排序 |
### 8.4 质量检查清单Phase 1 上线前)
#### 功能验证
- [ ] OpenRouter 371 模型全部入库,覆盖率 100%
# (Phase 2 才采集国内厂商)
- [ ] 每日 08:00 cron 触发采集,报告自动生成
- [ ] 报告内容包含:新模型、价格变动(>5% 高亮)、场景推荐
- [ ] `/explorer.html` 搜索响应 < 500ms
# (Phase 2 才实现告警推送)
#### 数据质量验证
- [ ] 每条数据有 `source_url` 来源标注
- [ ] 置信度分级标注official / inferred / expired
- [ ] 价格单位统一为 ¥/MTok 或 $/MTok
- [ ] 同模型多源价格差异 > 20% 时标注"待核实"
- [ ] 采集失败写入日志,保留旧数据
#### 部署验证
- [ ] `docker-compose up` 可正常启动所有服务
- [ ] PostgreSQL 数据库持久化到 `data/` 目录
- [ ] 报告 HTML 生成到 `reports/` 目录
# Phase 2 才引入 Nginx
- [ ] API `/api/v1/health` 返回 200
- [ ] 备份脚本每日推送至 OSS 成功
#### 性能验证
- [ ] 371 模型采集完成 < 5 分钟
- [ ] 报告生成 < 30 秒
- [ ] API 查询响应 < 500ms/models, 20 条)
- [ ] 并发 10 个采集器同时运行,内存 < 2GB
---
## 附录:目录结构
```
llm-intelligence/
├── TECHNICAL_DESIGN.md # 本文档
├── PRD.md # 产品需求文档
├── FEATURE_LIST.md # 功能清单
├── BUSINESS_MODEL.md # 商业模式
├── MARKET_ANALYSIS.md # 市场调研
├── Dockerfile.collector # 采集器镜像
├── Dockerfile.api # API 服务镜像
├── docker-compose.yml # 容器编排
├── .env.example # 环境变量模板
├── nginx.conf # Nginx 配置
├── collectors/ # 数据采集器
│ ├── collector.go # 采集器接口定义
│ ├── openrouter.go # OpenRouter 采集器Go
│ └── [deepseek.go] # Phase 2: DeepSeek 采集器
├── services/ # 服务层
│ ├── db.go # 数据库连接池封装Go database/sql + pq
│ ├── queries.go # 预编译 SQL 查询
│ └── models.go # Go struct 模型定义(对应 DB schema
├── api/ # REST API
│ ├── main.go # HTTP 服务入口Go net/http 或 Phase 2 框架)
│ ├── routes.go # 路由注册
│ └── middleware.go # 认证/限流/日志中间件
├── static/ # 前端静态文件
│ ├── index.html # 首页/报告列表
│ ├── explorer.html # 模型浏览器
│ ├── calculator.html # 成本计算器
│ ├── trends.html # 趋势分析
│ ├── css/
│ │ └── style.css
│ └── js/
│ ├── dataService.js # API 调用封装
│ ├── explorer.js # 模型浏览器逻辑
│ ├── calculator.js # 计算器逻辑
│ └── charts.js # ECharts 封装
├── templates/ # Go html/template 模板
│ └── report.html # 每日报告 HTML 模板
├── reports/ # 生成的报告 HTML 输出
│ └── 2026-05-04.html
├── scripts/ # 运维脚本
│ ├── run_daily.sh # 每日采集 + 报告脚本
│ ├── backup.sh # 数据库备份脚本
│ ├── migrate.sh # PostgreSQL Schema 部署脚本
│ └── init_db.sql # 数据库初始化(权限/扩展)
├── internal/ # Go 内部包
│ ├── collectors/ # 采集器接口 + 实现
│ │ ├── collector.go # Collector 接口定义
│ │ └── openrouter.go # OpenRouter 采集器
│ ├── db/ # 数据库连接 + 查询封装
│ │ ├── db.go # 连接池管理
│ │ └── queries.go # 预编译 SQL
│ ├── retry/ # 重试工具包
│ │ └── retry.go # 指数退避重试
│ └── report/ # 报告生成
│ └── generator.go # Markdown/HTML 生成
├── db/migrations/ # PostgreSQL 迁移
│ └── 001_phase1_core_tables.sql
├── frontend/ # React 前端
│ ├── src/
│ │ ├── pages/
│ │ │ └── Explorer.tsx # 模型浏览器
│ │ ├── data/
│ │ │ └── models.json # 静态数据(开发用)
│ │ └── App.tsx
│ ├── package.json
│ └── vite.config.ts
├── reports/daily/ # Markdown 日报输出
│ └── daily_report_YYYY-MM-DD.md
├── tests/ # 测试
│ ├── integration/ # 集成测试testcontainers-go
│ └── e2e/ # E2E 测试
├── logs/ # 日志文件运行时生成logrotate
│ ├── collector.log
│ ├── api.log
│ └── backup.log
├── Dockerfile # 多阶段构建
├── docker-compose.yml # 生产编排
├── Makefile # 常用命令
├── go.mod # Go 依赖
└── .env.example # 环境变量模板
```
---
**文档状态:** 生产级设计 v1.1 完成 ✅
**修订内容2026-05-09**
- 技术栈统一为 Go 1.22.2 + PostgreSQL
- DDL 补充审计字段created_by/updated_by/deleted_at+ 数据血缘字段retrieved_at/batch_id/collector_version
- API 安全章节:认证/限流/错误码/CORS
- 新增 5 个生产级章节:安全/部署运维/可观测性/测试策略/容量规划
**下一步行动:**
- [ ] T-3.2 Dashboard 组件完善Explorer 页面数据对接)
- [ ] T-4.3 cron 每日自动采集 + 日报生成
---
_文档编制宰相AI 辅助)_
_基于 PRD.mdv0.3、FEATURE_LIST.mdv1.1、BUSINESS_MODEL.mdv1.0、MARKET_ANALYSIS.mdv3.0_
---
## 七、安全设计
### 7.1 安全原则
本项目遵循**最小权限原则**和**纵深防御**策略:
| 层级 | 防御措施 |
|------|----------|
| 网络层 | TLS 1.3 全链路加密、防火墙白名单 |
| 应用层 | 输入校验、SQL 参数化查询、CSRF 防护 |
| 数据层 | 敏感字段加密、DB 用户权限最小化 |
| 运维层 | 密钥外部化、日志脱敏、定期轮换 |
### 7.2 API 密钥管理
**OpenRouter API Key**
| 存储方式 | 优先级 | 说明 |
|----------|--------|------|
| **环境变量** `OPENROUTER_API_KEY` | ✅ 推荐 | 系统级配置,不进入代码仓库 |
| **Docker Secrets** | ✅ 容器环境 | `docker secret` 或 K8s secret |
| **.env 文件** | ⚠️ 开发环境 | 必须加入 `.gitignore` |
| ❌ **硬编码** | 禁止 | 任何提交含密钥 = 安全事件 |
**Go 读取示例:**
```go
apiKey := os.Getenv("OPENROUTER_API_KEY")
if apiKey == "" {
log.Fatal("OPENROUTER_API_KEY not set")
}
```
### 7.3 数据库凭证管理
**连接字符串**
```
postgresql://llm_hub:${DB_PASSWORD}@db:5432/llm_intelligence?sslmode=require
```
| 环境 | 密码来源 |
|------|----------|
| 开发 | `.env` 文件(不提交 git |
| CI/CD | GitHub Actions Secrets |
| 生产 | Docker Secret / 云 KMS |
**`.env.example` 模板(无真实值):**
```bash
# 数据库
DB_HOST=localhost
DB_PORT=5432
DB_NAME=llm_intelligence
DB_USER=llm_hub
DB_PASSWORD= # 必填,留空示例
DB_SSLMODE=require
# API 密钥
OPENROUTER_API_KEY= # 必填
# 日志级别
LOG_LEVEL=info
```
### 7.4 传输安全
- **DB ↔ App**`sslmode=require`,禁止明文连接
- **App ↔ 外部 API**`tls.Config{MinVersion: tls.VersionTLS13}`
- **前端 ↔ 后端**HTTPS 强制HSTS max-age=31536000
- **内部通信**:若 Phase 2 引入微服务mTLS 双向认证
### 7.5 输入验证与防注入
**JSON Schema 校验**(采集器):
```go
type OpenRouterModel struct {
ID string `json:"id" validate:"required"`
Name string `json:"name"`
Pricing Pricing `json:"pricing" validate:"required"`
ContextLength int `json:"context_length" validate:"gte=0,lte=10000000"`
}
```
**SQL 防注入**
- 全部使用 `database/sql` 参数化查询(`$1, $2`
- 禁止字符串拼接 SQL
- ORM/查询 builder Phase 2 评估
### 7.6 敏感数据保护
**`user_subscription` 表敏感字段**
| 字段 | 存储方式 | 说明 |
|------|----------|------|
| `email` | AES-256-GCM 加密 | 密钥由 KMS 管理 |
| `phone` | AES-256-GCM 加密 | 仅显示后 4 位 |
| `feishu_webhook` | AES-256-GCM 加密 | Webhook URL 含 token |
| `dingtalk_webhook` | AES-256-GCM 加密 | 同上 |
**日志脱敏**
- 日志中 email 显示为 `l***@example.com`
- webhook URL 中 token 部分替换为 `***`
- DB 密码显示为 `[REDACTED]`
### 7.7 安全响应头
```go
// HTTP 中间件
func securityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Security-Policy", "default-src 'self'")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
next.ServeHTTP(w, r)
})
}
```
### 7.8 数据库最小权限
| 用户 | 权限 | 用途 |
|------|------|------|
| `llm_hub_app` | SELECT, INSERT, UPDATE | 应用服务账号 |
| `llm_hub_collector` | SELECT, INSERT, UPDATE | 采集器账号 |
| `llm_hub_readonly` | SELECT | 只读查询(报表/审计) |
| `llm_hub_admin` | ALL | 迁移/运维(人工使用) |
---
## 八、部署与运维架构
### 8.1 CI/CD 流水线
**GitHub Actions 工作流**`.github/workflows/ci.yml`
```yaml
name: CI/CD
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with: { go-version: '1.22' }
- run: go vet ./...
- run: gofmt -l . | tee /dev/stderr | wc -l | xargs test 0 -eq
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env: { POSTGRES_PASSWORD: test }
steps:
- uses: actions/checkout@v4
- run: go test -race -coverprofile=coverage.out ./...
- run: go tool cover -func=coverage.out | grep total
build:
needs: [lint, test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: go build -o bin/fetch_openrouter ./scripts/fetch_openrouter.go
- run: go build -o bin/generate_daily_report ./scripts/generate_daily_report.go
- uses: actions/upload-artifact@v4
with: { name: binaries, path: bin/ }
deploy:
needs: build
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with: { name: binaries }
# SSH/SCP 到生产服务器,或 docker push
```
### 8.2 容器化
**Dockerfile多阶段构建**
```dockerfile
# 构建阶段
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o fetch_openrouter scripts/fetch_openrouter.go
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o generate_daily_report scripts/generate_daily_report.go
# 运行阶段
FROM alpine:latest
RUN apk --no-cache add ca-certificates tzdata
WORKDIR /root/
COPY --from=builder /app/fetch_openrouter .
COPY --from=builder /app/generate_daily_report .
COPY --from=builder /app/db/migrations ./migrations
CMD ["./fetch_openrouter"]
```
**docker-compose.yml生产版**
```yaml
version: '3.8'
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: llm_intelligence
POSTGRES_USER: llm_hub
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
volumes:
- postgres_data:/var/lib/postgresql/data
- ./db/migrations:/docker-entrypoint-initdb.d
secrets:
- db_password
healthcheck:
test: ["CMD-SHELL", "pg_isready -U llm_hub"]
interval: 10s
timeout: 5s
retries: 5
app:
build: .
environment:
DB_CONN: "host=db dbname=llm_intelligence sslmode=require"
OPENROUTER_API_KEY_FILE: /run/secrets/openrouter_key
secrets:
- openrouter_key
- db_password
depends_on:
db:
condition: service_healthy
restart: unless-stopped
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./frontend/dist:/usr/share/nginx/html:ro
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on: [app]
volumes:
postgres_data:
secrets:
db_password:
file: ./secrets/db_password.txt
openrouter_key:
file: ./secrets/openrouter_key.txt
```
### 8.3 健康检查端点
```go
// GET /api/v1/health
func healthHandler(w http.ResponseWriter, r *http.Request) {
health := struct {
Status string `json:"status"`
Version string `json:"version"`
DB string `json:"db"`
DBRecordCount map[string]int `json:"db_record_count"`
LastCollectTime time.Time `json:"last_collect_time"`
LastReportTime time.Time `json:"last_report_time"`
Uptime string `json:"uptime"`
}{
Status: "ok",
Version: os.Getenv("APP_VERSION"),
}
// DB 连通性检查
if err := db.Ping(); err != nil {
health.Status = "degraded"
health.DB = "unreachable: " + err.Error()
w.WriteHeader(http.StatusServiceUnavailable)
} else {
health.DB = "ok"
health.DBRecordCount = getTableCounts()
}
// 数据新鲜度检查
if time.Since(lastCollectTime) > 25*time.Hour {
health.Status = "stale_data"
}
json.NewEncoder(w).Encode(health)
}
```
### 8.4 配置管理
**环境变量清单**
| 变量 | 必填 | 默认值 | 说明 |
|------|------|--------|------|
| `DB_CONN` | ✅ | — | PostgreSQL 连接字符串 |
| `OPENROUTER_API_KEY` | ✅ | — | API 密钥 |
| `LOG_LEVEL` | ❌ | `info` | debug/info/warn/error |
| `APP_VERSION` | ❌ | `dev` | 应用版本号 |
| `COLLECT_TIMEOUT` | ❌ | `60s` | 采集超时 |
| `REPORT_OUTPUT_DIR` | ❌ | `./reports/daily` | 日报输出目录 |
| `FRONTEND_DATA_DIR` | ❌ | `./frontend/src/data` | 前端数据目录 |
| `ENABLE_METRICS` | ❌ | `false` | Prometheus 指标开关 |
**环境区分策略**
```
.env.development # 本地开发
.env.staging # 预生产(连接 staging DB
.env.production # 生产(仅服务器上存在,不提交 git
```
### 8.5 滚动发布与回滚
**零停机部署**
1. 构建新镜像 → `docker build -t llm-hub:v1.1`
2. 蓝绿部署:新容器启动 + 健康检查通过
3. 切换流量nginx upstream 指向新容器
4. 保留旧容器 5 分钟(快速回滚)
**回滚策略**
```bash
# 紧急回滚30 秒内)
docker-compose stop app && docker-compose up -d app --no-deps --scale app=1
# 或切换到上一个镜像标签
docker tag llm-hub:v1.0 llm-hub:latest && docker-compose up -d app
```
---
## 九、可观测性体系
### 9.1 日志体系
**结构化日志Go slog**
```go
import "log/slog"
var logger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: parseLogLevel(os.Getenv("LOG_LEVEL")),
AddSource: true,
}))
// 使用示例
logger.Info("collection completed",
slog.String("collector", "openrouter"),
slog.Int("records", 365),
slog.Duration("duration", 12*time.Second),
)
```
**日志分级**
| 级别 | 场景 | 输出目标 |
|------|------|----------|
| DEBUG | 开发调试、详细 SQL | 本地 stdout生产关闭 |
| INFO | 正常流程、采集完成 | stdout + 文件 |
| WARN | 降级处理、数据延迟 | stdout + 文件 + 告警通道 |
| ERROR | 失败、异常、DB 断开 | stdout + 文件 + 立即告警 |
**日志轮转**
```yaml
# docker-compose 中的 logrotate 或 docker 原生
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "5"
```
### 9.2 MetricsPrometheus
**指标设计**
| 指标名 | 类型 | 说明 |
|--------|------|------|
| `collector_duration_seconds` | Histogram | 采集耗时(按采集器标签) |
| `collector_records_total` | Counter | 采集记录数 |
| `collector_errors_total` | Counter | 采集失败次数(按错误类型标签) |
| `api_requests_total` | Counter | API 请求数(按 endpoint/method/status |
| `api_request_duration_seconds` | Histogram | API 响应时间 P50/P95/P99 |
| `db_connection_active` | Gauge | 当前活跃 DB 连接数 |
| `db_connection_wait_duration` | Histogram | 等待连接池时间 |
| `data_freshness_hours` | Gauge | 数据新鲜度(距上次采集小时数) |
**Go 实现prometheus/client_golang**
```go
import "github.com/prometheus/client_golang/prometheus"
var collectorDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "collector_duration_seconds",
Help: "Collector run duration",
Buckets: prometheus.DefBuckets,
},
[]string{"collector"},
)
func init() {
prometheus.MustRegister(collectorDuration)
}
```
### 9.3 告警分级
| 等级 | 触发条件 | 通知方式 | 响应时间 |
|------|----------|----------|----------|
| **P0** | 服务不可用、DB 断开、所有采集器失败 | 飞书/钉钉/短信 + 电话 | 15 分钟 |
| **P1** | 单个采集器失败 > 24h、API P99 > 2s | 飞书/钉钉 | 1 小时 |
| **P2** | 价格异常变动 > 20%、数据延迟 > 48h | 飞书群 | 4 小时 |
**告警升级策略**
- P0 告警 15 分钟未恢复 → 升级至电话通知
- P1 告警 1 小时未恢复 → 升级至 P0 通道
- 同一问题 24h 内重复触发 → 合并为汇总告警(避免轰炸)
### 9.4 运行看板Grafana
**推荐面板**
1. **采集健康**:成功率、耗时趋势、各采集器状态
2. **数据规模**:模型数、定价记录数、日增量
3. **API 性能**QPS、P99 延迟、错误率
4. **资源使用**CPU、内存、DB 连接池、磁盘
5. **业务看板**:价格变动 Top 10、新模型上线、免费政策变更
### 9.5 链路追踪Phase 2
评估 OpenTelemetry Go SDK
```go
import "go.opentelemetry.io/otel"
// 采集链路cron → collector → API → parser → DB
// 追踪维度采集批次、模型处理耗时、DB 写入耗时
```
---
## 十、测试策略
### 10.1 单元测试
**覆盖率目标**
- 整体 ≥ 80%
- 采集器核心逻辑 ≥ 90%
- 数据库查询 ≥ 85%
- 错误处理路径 100%
**Go 测试示例**
```go
// scripts/fetch_openrouter_test.go
func TestParseModelID(t *testing.T) {
tests := []struct {
input string
wantProvider string
wantModel string
}{
{"anthropic/claude-3.5-sonnet", "Anthropic", "Claude 3.5 Sonnet"},
{"deepseek-ai/DeepSeek-V4", "DeepSeek", "V4"},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
gotProvider, gotModel := parseModelID(tt.input)
assert.Equal(t, tt.wantProvider, gotProvider)
assert.Equal(t, tt.wantModel, gotModel)
})
}
}
```
**Mock 策略**
- HTTP 外部调用httptest 模拟 OpenRouter API
- DB 查询sqlmock 模拟数据库响应
- 时间:手动注入 `time.Now()` 替代
### 10.2 集成测试
**testcontainers-go + PostgreSQL**
```go
import "github.com/testcontainers/testcontainers-go"
import "github.com/testcontainers/testcontainers-go/modules/postgres"
func TestCollectorToDB(t *testing.T) {
ctx := context.Background()
pg, err := postgres.Run(ctx, "postgres:16-alpine",
postgres.WithDatabase("test_db"),
postgres.WithUsername("test"),
postgres.WithPassword("test"),
)
require.NoError(t, err)
defer pg.Terminate(ctx)
connStr, _ := pg.ConnectionString(ctx)
db := setupTestDB(connStr)
// 运行采集器
records, err := collectOpenRouter(ctx, db, mockAPIKey)
require.NoError(t, err)
require.Greater(t, len(records), 0)
// 验证 DB 写入
var count int
db.QueryRow("SELECT COUNT(*) FROM model").Scan(&count)
require.Greater(t, count, 0)
}
```
### 10.3 E2E 测试
**覆盖范围**
- `fetch_openrouter` 二进制 → DB 有数据
- `generate_daily_report` 二进制 → `reports/daily/` 产出 Markdown
- API 端到端:启动服务 → curl 请求 → 验证响应
**前端 E2EPhase 2**
- Cypress/PlaywrightExplorer 页面加载、筛选交互、数据展示
### 10.4 性能测试
| 测试项 | 目标 | 工具 |
|--------|------|------|
| 采集器并发 | 371 模型 < 60s | Go benchmark + pprof |
| API 响应 | P99 < 200ms | k6 / vegeta |
| DB 查询 | 价格对比查询 < 100ms | EXPLAIN ANALYZE |
**Go Benchmark 示例**
```go
func BenchmarkCollectOpenRouter(b *testing.B) {
for i := 0; i < b.N; i++ {
collectOpenRouter(ctx, mockClient)
}
}
```
---
## 十一、容量规划
### 11.1 数据增长模型
| 数据源 | 日增量 | 年增量 | 存储/条 |
|--------|--------|--------|---------|
| OpenRouter 模型 | ~371 条 model | ~135K | ~2KB |
| 定价记录 | ~371 条 pricing | ~135K | ~1KB |
| 价格历史 | 变动时插入,预估 ~50/天 | ~18K | ~0.5KB |
| 日报 | 1 条/天 | ~365 | ~50KBHTML |
**1 年总估算(含索引膨胀 ×1.5**
- 模型+定价表:~400MB
- 价格历史:~20MB
- 日报:~30MB
- 日志90 天轮转):~500MB
- **总计:~1GB/年**PostgreSQL 单机轻松承载)
### 11.2 QPS 与并发规划
| 阶段 | 预期并发 | QPS 目标 | 策略 |
|------|----------|----------|------|
| Phase 1 | 1-5 用户 | 10 QPS | 单机 + 连接池 10 |
| Phase 2 | 50-100 用户 | 100 QPS | nginx 反代 + 连接池 50 |
| Phase 3 | 1K+ 用户 | 500+ QPS | 读副本 + CDN + 缓存 |
### 11.3 性能基准
| 指标 | 目标 | 测量方式 |
|------|------|----------|
| 采集器单次运行 | < 60s | cron 日志 |
| 日报生成 | < 30s | 命令计时 |
| API 响应 P50 | < 50ms | Prometheus histogram |
| API 响应 P99 | < 200ms | Prometheus histogram |
| DB 查询(价格对比) | < 100ms | `EXPLAIN ANALYZE` |
| 前端首屏加载 | < 2s | Lighthouse |
### 11.4 扩展策略
**Phase 2 扩展点**
1. **读副本**PostgreSQL 流复制,分离报表查询和写入
2. **采集器并行**:按厂商拆分 goroutine并发采集
3. **缓存层**Redis 缓存热门查询(模型列表、价格对比)
4. **CDN**:前端静态资源 + 日报 HTML 缓存
**Phase 3 扩展点**
1. **分库分表**`pricing_history` 按时间分区PostgreSQL 原生分区)
2. **对象存储**:日报 HTML 长期归档至 MinIO/S3
3. **消息队列**采集任务入队Worker 消费(替代 cron 直接触发)