76 KiB
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(采集器+报告生成 CLI);API Server Phase 2 评估 |
| 前端层 | 静态 Web 页面展示(Explorer / 报告) | React/Vite(TypeScript)或纯静态 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_openrouter(Go binary,cron 触发,直写 DB)
├── generate_daily_report(Go binary,Markdown 输出到 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 1:PostgreSQL
- 与立交桥技术栈统一
- 利用 PostgreSQL JSONB 存储灵活字段(如 capabilities 数组)
- Schema 设计直接以 PostgreSQL 语法编写(见
db/migrations/) - Phase 2 评估是否需要数据库内任务队列(当前用 cron 直接触发 binary)
2.4 为什么不用这些
| 未选方案 | 原因 |
| SQLite | 不符合与立交桥技术栈统一的要求 | | FastAPI | 技术栈已统一为 Go;Python 框架需要运行时和依赖管理,部署复杂度高于 Go 单二进制 | | Scrapy | 重量级框架,Phase 1 采集规模不需要分布式 | | Celery + RabbitMQ | 增加运维复杂度,当前用 cron + Go binary 替代 | | React/Vue | Phase 1 使用静态页面或最小 React 构建;部署复杂度可控 | | Deno/Bun | 生态不如 Go 成熟,与立交桥技术栈不一致 |
三、数据库设计(DDL)
以下 DDL 以 PostgreSQL 语法编写,与立交桥技术栈统一。
3.1 model_provider(模型商)
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(模型)
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(运营商/云平台)
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(区域定价)
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(价格历史)
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(免费政策)
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(每日报告)
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(用户订阅)
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 内部采集 API(Collector → Server)
POST /api/v1/collect/push
采集器推送采集结果(Phase 2 分布式采集节点使用)
Request:
{
"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:
{
"status": "ok",
"inserted": 365,
"updated": 12,
"errors": 0
}
4.1.5 API 安全与生产规范
认证与鉴权
| 场景 | 机制 | 说明 |
|---|---|---|
| 内部采集器 → DB | 无 API 认证 | 采集器直连 PostgreSQL,通过 DB 用户权限隔离 |
| Phase 2 对外 API | API Key(X-API-Key Header) | 按订阅等级分配不同 Key,DB 存储 bcrypt 哈希 |
| Admin 运维接口 | JWT + RBAC | 仅内部使用,Token 有效期 1h,Refresh 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 |
维护模式 | 系统升级中 |
错误响应格式:
{
"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:
{
"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:
{
"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:
{
"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:
{
"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:
{
"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:
{
"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:
{
"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):
// 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):
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):
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 统一调度,无外部消息队列依赖。
# /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
#!/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 实现)
// 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):
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):
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 2Phase 2Phase 2Phase 2Phase 2成Phase 2本Phase 2计Phase 2算Phase 2器Phase 2Phase 2Phase 2Phase 2Phase 2 Phase 2|Phase 2 Phase 2Phase 2/Phase 2cPhase 2aPhase 2lPhase 2cPhase 2uPhase 2lPhase 2aPhase 2tPhase 2oPhase 2rPhase 2.Phase 2hPhase 2tPhase 2mPhase 2lPhase 2Phase 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 2Phase 2Phase 2Phase 2Phase 2趋Phase 2势Phase 2图Phase 2Phase 2Phase 2Phase 2Phase 2 Phase 2|Phase 2 Phase 2Phase 2/Phase 2tPhase 2rPhase 2ePhase 2nPhase 2dPhase 2sPhase 2.Phase 2hPhase 2tPhase 2mPhase 2lPhase 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 2EPhase 2CPhase 2hPhase 2aPhase 2rPhase 2tPhase 2sPhase 2)Phase 2 Phase 2|Phase 2
Phase 2| 关于我们 | /about.html | 项目介绍、数据来源说明 |
6.3 与后端的数据交互
模式:纯前端 SPA(Single 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):
// 统一 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 模型浏览器页面结构
<!-- 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 配置
# 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 环境变量清单
# .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.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 前缀提取 provider(anthropic/claude-3.5-sonnet → Anthropic)
│ │ ├── 处理免费模型(id 包含 :free 后缀)
│ │ └── 错误处理(401/429/500)
│ ├── [ ] 实现 base collector 抽象类
│ ├── [ ] 实现数据清洗逻辑(去除异常价格、统一单位)
│ └── [ ] 验证:371 模型全部入库,无重复,数据正确
│
├── T1.3 数据映射 + Provider 标准化
│ ├── [ ] 建立 PROVIDER_NAME_MAP(OpenRouter id → 标准厂商名)
│ ├── [ ] 验证:所有 provider 名称统一(无别名)
│ └── [ ] 补充 provider logo_url / description
│
├── T1.4 初始数据导入
│ ├── [ ] 运行 OpenRouter 采集器,导入 371 模型
│ ├── [ ] 质量检查:随机抽 10 条数据,验证价格/上下文长度
│ └── [ ] 导出数据字典文档
│
└── T1.5 采集脚本 + cron 配置
├── [ ] 编写 scripts/run_openrouter_collect.sh
├── [ ] 配置 crontab(08: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.md(v0.3)、FEATURE_LIST.md(v1.1)、BUSINESS_MODEL.md(v1.0)、MARKET_ANALYSIS.md(v3.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 读取示例:
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 模板(无真实值):
# 数据库
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 校验(采集器):
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 安全响应头
// 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):
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(多阶段构建):
# 构建阶段
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(生产版):
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 健康检查端点
// 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 滚动发布与回滚
零停机部署:
- 构建新镜像 →
docker build -t llm-hub:v1.1 - 蓝绿部署:新容器启动 + 健康检查通过
- 切换流量:nginx upstream 指向新容器
- 保留旧容器 5 分钟(快速回滚)
回滚策略:
# 紧急回滚(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):
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 + 文件 + 立即告警 |
日志轮转:
# docker-compose 中的 logrotate 或 docker 原生
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "5"
9.2 Metrics(Prometheus)
指标设计:
| 指标名 | 类型 | 说明 |
|---|---|---|
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):
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)
推荐面板:
- 采集健康:成功率、耗时趋势、各采集器状态
- 数据规模:模型数、定价记录数、日增量
- API 性能:QPS、P99 延迟、错误率
- 资源使用:CPU、内存、DB 连接池、磁盘
- 业务看板:价格变动 Top 10、新模型上线、免费政策变更
9.5 链路追踪(Phase 2)
评估 OpenTelemetry Go SDK:
import "go.opentelemetry.io/otel"
// 采集链路:cron → collector → API → parser → DB
// 追踪维度:采集批次、模型处理耗时、DB 写入耗时
十、测试策略
10.1 单元测试
覆盖率目标:
- 整体 ≥ 80%
- 采集器核心逻辑 ≥ 90%
- 数据库查询 ≥ 85%
- 错误处理路径 100%
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:
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 请求 → 验证响应
前端 E2E(Phase 2):
- Cypress/Playwright:Explorer 页面加载、筛选交互、数据展示
10.4 性能测试
| 测试项 | 目标 | 工具 |
|---|---|---|
| 采集器并发 | 371 模型 < 60s | Go benchmark + pprof |
| API 响应 | P99 < 200ms | k6 / vegeta |
| DB 查询 | 价格对比查询 < 100ms | EXPLAIN ANALYZE |
Go Benchmark 示例:
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 | ~50KB(HTML) |
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 扩展点:
- 读副本:PostgreSQL 流复制,分离报表查询和写入
- 采集器并行:按厂商拆分 goroutine,并发采集
- 缓存层:Redis 缓存热门查询(模型列表、价格对比)
- CDN:前端静态资源 + 日报 HTML 缓存
Phase 3 扩展点:
- 分库分表:
pricing_history按时间分区(PostgreSQL 原生分区) - 对象存储:日报 HTML 长期归档至 MinIO/S3
- 消息队列:采集任务入队,Worker 消费(替代 cron 直接触发)