内容
# CanYouGrab API
域名可用性查询 API,基于 FastAPI + DNS(Unbound 解析器)构建,支持订阅计费。
**在线服务:**
- API:`https://api.canyougrab.it`
- 开发门户:`https://portal.canyougrab.it`
- 身份验证:`https://auth.canyougrab.it`(Auth0 自定义域名)
## 架构概述
```
┌──────────────────────────────────────────────────────────────────┐
│ 开发门户 │
│ (Zudoku/React on portal.canyougrab.it) │
│ 使用情况仪表盘 · API 密钥 · 定价 · API 参考(OAS) │
└──────────────────┬───────────────────────────────────────────────┘
│ Auth0 JWT(门户) / Bearer API 密钥(API)
▼
┌──────────────────────────────────────────────────────────────────┐
│ FastAPI 后端(v5.0.0) │
│ api.canyougrab.it:8000 │
│ │
│ ┌─────────────┐ ┌──────────┐ ┌──────────┐ ┌─────────────┐ │
│ │ app.py │ │ keys.py │ │billing.py│ │ auth.py │ │
│ │ /check/bulk │ │ /keys │ │/billing │ │ API 密钥 + │ │
│ │ /usage │ │ CRUD │ │/stripe │ │ JWT 身份验证 │ │
│ │ │ │ 旋转 │ │ 网络钩子 │ │ │ │
│ └──────┬──────┘ └──────────┘ └────┬─────┘ └─────────────┘ │
│ │ │ │
└─────────┼────────────────────────────┼───────────────────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Valkey(Redis) │ │ Stripe API │
│ 作业队列 + │ │ 订阅 │
│ 速率限制 │ │ 网络钩子 │
└────────┬────────┘ └─────────────────┘
│
▼
┌─────────────────┐ ┌──────────────────────────────┐
│ RQ Worker │──DNS──▶ │ Unbound 解析器 │
│ (worker.py) │ │ (专用 DigitalOcean droplet) │
│ 线程池(10) │ │ 通过 VPC 的 NS 查询 │
│ 通过 RQ 队列 │ └──────────────────────────────┘
└─────────────────┘
┌──────────────────────────────┐
───────────────────▶│ PostgreSQL │
(身份验证,用例, │ API 密钥 + 使用日志 │
计费) └──────────────────────────────┘
```
## 目录结构
```
zuplo/
├── backend/ # Python FastAPI 后端
│ ├── app.py # 主 API:/check/bulk,/usage,/health
│ ├── auth.py # API 密钥身份验证(SHA-256)+ Auth0 JWT 身份验证(RS256)
│ ├── billing.py # Stripe 结账,门户,网络钩子,卡片归档,详细使用情况
│ ├── keys.py # API 密钥 CRUD:创建,列出,旋转,吊销(+ Turnstile)
│ ├── antifraud.py # 反欺诈:Turnstile,设备指纹,风险评分
│ ├── email_utils.py # 电子邮件规范化 + 处置电子邮件检测
│ ├── dns_client.py # 基于 DNS 的域名可用性检查,通过 Unbound
│ ├── queries.py # PostgreSQL 查询:使用情况跟踪,身份验证,计费
│ ├── valkey_client.py # Redis/Valkey 作业队列客户端(RQ 支持)
│ ├── rq_tasks.py # RQ 任务函数:process_domain_job()
│ ├── worker.py # RQ worker 进程(每个作业的 ThreadPoolExecutor)
│ ├── migrations/ # SQL 迁移
│ │ └── 001_free_tier_antifraud.sql
│ └── requirements.txt # Python 依赖项
├── portal/ # 开发门户(Zuplo + Zudoku)
│ ├── config/
│ │ ├── routes.oas.json # OpenAPI 3.1 规范(公共 API 文档)
│ │ └── policies.json # Zuplo 策略(空 - 所有路由直接)
│ ├── docs/ # Zudoku 文档门户
│ │ ├── src/
│ │ │ ├── config.ts # API_BASE,Turnstile 站点密钥,Stripe PK
│ │ │ ├── UsageDashboard.tsx # 使用情况 + 计费仪表盘组件
│ │ │ ├── PricingPage.tsx # 计划选择 + Stripe 结账
│ │ │ ├── PricingPlans.tsx # 定价卡网格组件
│ │ │ └── CardSetupPage.tsx # Stripe Elements 卡片归档用于 Free+
│ │ ├── public/ # 静态资产(徽标,横幅,CSS 覆盖)
│ │ ├── zudoku.config.tsx # 门户配置:主题,导航,Auth0,API 密钥管理
│ │ └── package.json # 前端依赖项(React 19,Zudoku)
│ ├── package.json # 工作区根目录(Zuplo v6,TypeScript v5)
│ └── README.md # Zuplo 样板(非特定于项目)
├── mcp-server/ # MCP 包,用于 ChatGPT,Claude 和远程 MCP 客户端
│ ├── pyproject.toml # MCP 包元数据 + 版本
│ ├── server.json # MCP 注册表/服务器元数据
│ ├── uv.lock # 锁定的 MCP 运行时依赖项
│ └── src/canyougrab_mcp/
│ ├── __init__.py
│ └── server.py # stdio + streamable-http MCP 入口点
├── .github/workflows/
│ ├── deploy.yml # 生产部署(在标签推送时 v*)
│ └── deploy-dev.yml # 开发部署(在推送到开发分支时)
├── .claude/
│ └── launch.json # 本地开发服务器配置(端口 9200)
└── package.json # 根工作区配置
```
## API 端点
### 公共 API(API 密钥身份验证:`Authorization: Bearer cyg_...`)
| 方法 | 路径 | 描述 |
|--------|------|-------------|
| `POST` | `/api/check/bulk` | 检查最多 100 个域名。长轮询直到结果准备好(最大 30 秒)。 |
| `GET` | `/api/account/usage` | 身份验证消费者的使用情况摘要。 |
| `GET` | `/api/account/quota-check` | 轻量级每月 + 每分钟配额检查。 |
| `GET` | `/health` | 健康检查(无身份验证)。 |
### 门户 API(Auth0 JWT 身份验证)
| 方法 | 路径 | 描述 |
|--------|------|-------------|
| `POST` | `/api/keys` | 创建新的 API 密钥。 |
| `GET` | `/api/keys` | 列出用户的 API 密钥。 |
| `POST` | `/api/keys/{id}/rotate` | 旋转密钥(吊销旧密钥,创建新密钥)。 |
| `DELETE` | `/api/keys/{id}` | 吊销(软删除)密钥。 |
| `POST` | `/api/billing/checkout` | 创建 Stripe 结账会话。 |
| `POST` | `/api/billing/portal` | 创建 Stripe 客户门户会话。 |
| `POST` | `/api/billing/setup-card` | 为 Free+ 创建 SetupIntent。 |
| `POST` | `/api/billing/confirm-free-plus` | 验证指纹并升级到 Free+。 |
| `GET` | `/api/billing/card-status` | 检查用户是否存有卡片。 |
| `GET` | `/api/billing/usage/detailed` | 门户仪表盘的每个密钥使用情况明细。 |
| `POST` | `/api/antifraud/turnstile/verify` | 验证 Cloudflare Turnstile 令牌。 |
| `POST` | `/api/antifraud/device/register` | 注册设备指纹(Fingerprint Pro)。 |
| `GET` | `/api/antifraud/risk` | 获取身份验证用户的风险评估。 |
| `POST` | `/api/antifraud/assess-signup` | 在注册时运行多信号风险评估。 |
### 内部 / 网络钩子
| 方法 | 路径 | 描述 |
|--------|------|-------------|
| `POST` | `/api/account/usage/detailed` | 多消费者使用情况明细。 |
| `POST` | `/api/stripe/webhook` | Stripe 网络钩子接收器(签名验证)。 |
## 核心请求流程
### 域名可用性检查
```
客户端 FastAPI Valkey Worker Unbound
│ │ │ │ │
│ POST /api/check/bulk │ │ │ │
│ { domains: [...] } │ │ │ │
│─────────────────────────▶│ │ │ │
│ │── 验证密钥 ──────────│ │ │
│ │── 检查分钟速率 ────▶│ INCR ratelimit:id:min │ │
│ │── 检查每月配额 ──▶│(PostgreSQL) │ │
│ │── 记录使用情况 ─────────▶│(PostgreSQL) │ │
│ │── create_job() ─────────▶│ HSET job:{uuid} │ │
│ │ │ RQ 入队(带重试) │ │
│ │ │ │ │
│ │ │◀── RQ Worker 队列 ───│ │
│ │ │ │ │
│ │ │──── 声明作业 ────────▶│ │
│ │ │ │── ThreadPool(10) │
│ │ │ │── 检查域名 DNS()
│ │ │ │── NS 查询 ───────▶│
│ │ │ │◀── NOERROR/NXDOMAIN│
│ │ │ │ │
│ │ │◀── 完成作业 ───────│ │
│ │ │ │ │
│ │◀─ 轮询 get_job_status() ─│ │ │
│ │ (0.3 秒间隔,30 秒 │ │ │
│ │ 最大超时) │ │ │
│◀─────────────────────────│ │ │ │
│ { results: [...] } │ │ │ │
```
### 计费 / 订阅流程
```
用户 → 定价页面 → 选择计划 → Auth0 登录
→ POST /api/billing/checkout(JWT 身份验证)
→ 查找/创建 Stripe 客户(由 auth0_sub 元数据链接)
→ 创建 Stripe 结账会话 → 重定向到 Stripe
→ 用户支付 → Stripe 触发网络钩子
→ POST /api/stripe/webhook(HMAC-SHA256 验证)
→ checkout.session.completed → 获取订阅 → 获取价格 ID
→ 将价格映射到计划 → 更新 api_keys SET plan, lookups_limit
```
## 订阅计划
| 计划 | 每月查询次数 | 每分钟速率限制 | 域名/请求 | 价格 |
|------|----------------|----------------------|-----------------|-------|
| 免费 | 500 | 30/分钟 | 30 | $0 |
| 免费+ | 10,000 | 100/分钟 | 100 | $0(存卡) |
| 基础 | 20,000 | 300/分钟 | 100 | $10/月 |
| 专业版 | 50,000 | 1,000/分钟 | 100 | $20/月 |
| 企业 | 300,000 | 3,000/分钟 | 100 | $30/月 |
## 身份验证
### API 密钥身份验证(公共 API 消费者)
- 格式:`Authorization: Bearer cyg_<token>`
- 密钥在 `api_keys` 表中进行 SHA-256 哈希(不存储明文)
- 前缀(`cyg_XXXXXXXX...`)存储在门户中以供显示
- 完整密钥仅在创建时返回一次
- 吊销时软删除(`revoked_at` 时间戳,不物理删除)
### JWT 身份验证(门户/仪表盘)
- Auth0 租户:`dev-mqe5tavp6dr62e7u`
- 自定义域名:`auth.canyougrab.it`
- 算法:RS256,带有 JWKS 验证(1 小时缓存)
- 受众:`https://api.canyougrab.it`
- 社会登录:Google,Apple(Sign in with Apple)
## 通过 DNS 的域名可用性
域名可用性通过查询专用 [Unbound 递归 DNS 解析器](https://github.com/ericismaking/canyougrab-unbound) 运行在单独的 DigitalOcean 专用 droplet 上进行检查。工作器发送 NS 记录查询,通过 VPC 私有网络,并解释响应:
| DNS 响应 | `available` | 含义 |
|---|---|---|
| NOERROR + NS 记录 | `false` | 域名已注册且已委派 |
| NXDOMAIN | `true` | 域名不在区域 - 可能可用 |
| NoAnswer(NOERROR,无 NS) | `false` | 已注册但停放/未委派 |
| SERVFAIL / 超时 | `null` | 不明确 - 检查失败 |
Unbound 解析器会积极缓存(注册域名 7 天,NXDOMAIN 5 分钟),并直接查询 TLD 权威服务器,避免公共解析器速率限制。
## 数据库模式
PostgreSQL 用于身份验证、使用情况跟踪和计费 - 不用于域名查找。
### PostgreSQL 表
| 表 | 目的 | 关键列 |
|-------|---------|-------------|
| `api_keys` | 用户 API 密钥 | `id`, `user_sub`, `email_normalized`, `key_hash`, `key_prefix`, `plan`, `lookups_limit`, `revoked_at` |
| `usage_log_daily` | 每日使用情况汇总 | `consumer`, `lookups`, `recorded_at`(日期,消费者 + 每天唯一) |
| `usage_log_minute` | 每分钟使用情况汇总 | `consumer`, `lookups`, `minute_start`(时间戳,消费者 + 每分钟唯一) |
| `card_fingerprints` | 一个免费帐户每个卡片 | `user_sub`, `stripe_fingerprint`(唯一对) |
| `device_fingerprints` | 基于设备的多帐户检测 | `user_sub`, `visitor_id`(Fingerprint Pro) |
| `account_risk` | 综合风险评分 | `user_sub`(唯一),`risk_score`, `risk_signals`(JSONB) |
### Valkey(Redis)数据结构
| 键模式 | 类型 | 目的 | TTL |
|-------------|------|---------|-----|
| `job:{uuid}` | Hash | 作业状态,域名,结果 | 1 小时 |
| `rq:queue:canyougrab-jobs` | 列表 | RQ 作业队列(由 RQ 管理)| — |
| `rq:job:{uuid}` | Hash | RQ 作业元数据(重试,状态)| 配置 |
| `ratelimit:{consumer}:{YYYYMMDDHHmm}` | 字符串 | 每分钟速率限制计数器(INCR)| 60s |
## 基础设施
### 服务器
| 环境 | 域名 | 目的 |
|-------------|--------|---------|
| 生产 | `api.canyougrab.it` | FastAPI + 工作器 |
| 开发 | `dev.canyougrab.it` | FastAPI + 工作器 |
| 门户 | `portal.canyougrab.it` | Zudoku 静态站点 |
| 身份验证 | `auth.canyougrab.it` | Auth0 自定义域名 |
### 外部服务
| 服务 | 目的 |
|---------|---------|
| **DigitalOcean** | 专用服务器(API 服务器,Unbound 解析器),托管 PostgreSQL,托管 Valkey |
| **Cloudflare** | DNS,CDN,SSL for canyougrab.it 区域,Turnstile 机器人预防 |
| **Auth0** | 用户身份验证,社交登录(Google + Apple),JWT 发行 |
| **Stripe** | 订阅计费,结账,客户门户,网络钩子 |
| **GitHub Actions** | CI/CD 部署管道 |
## 部署管道
### 生产部署
**触发器:** 推送一个匹配 `v*` 的 git 标签(例如 `v1.0.5`)
```
git tag v1.0.5 && git push origin v1.0.5
```
**管道**(`.github/workflows/deploy.yml`):
1. GitHub Actions 在 `v*` 标签推送时触发
2. 使用 `DEPLOY_SSH_KEY` 通过 SSH 登录生产服务器
3. 在服务器上使用目标修订版进行引导(`git fetch` + `git checkout <tag>`)
4. 运行存储库托管的部署脚本:`/opt/canyougrab-repo/scripts/deploy-host.sh`
### 开发部署
**触发器:** 推送到 `dev` 分支
```
git push origin dev
```
**管道**(`.github/workflows/deploy-dev.yml`):
1. GitHub Actions 在推送到 `dev` 时触发
2. 使用相同的 `DEPLOY_SSH_KEY` 通过 SSH 登录开发服务器
3. 在服务器上使用 `dev` 分支进行引导(`git fetch` + `git checkout dev`)
4. 运行存储库托管的部署脚本:`/opt/canyougrab-repo/scripts/deploy-host.sh`
### 托管库主机部署脚本
两个管道都使用小型内联 SSH 引导程序来更新 `/opt/canyougrab-repo` 到目标修订版,然后调用 `scripts/deploy-host.sh`。托管库部署脚本处理:
- `pip install -r requirements.txt` 用于后端依赖项
- 将后端、门户和 MCP 源代码树同步到其运行时目录
- 如果主机还运行 `canyougrab-mcp.service`,则安装或刷新 `mcp-server` 包/运行时
- 通过 systemd 重启 FastAPI、worker 和 MCP 服务
如果 `/mcp` 由与 API 相同的托管主机提供,则不能仅进行后端部署。MCP 服务必须在同一部署期间更新和重启,否则 OAuth 元数据和实时 MCP 行为可能会出现分歧。
现有的 `/opt/deploy.sh` 可以作为手动兼容性桩保留,但自动化管道应将存储库副本的 `scripts/deploy-host.sh` 作为真实来源。
### 分支策略
| 分支 | 目的 | 部署到 |
|--------|---------|------------|
| `main` | 生产版本 | 标签 → `api.canyougrab.it` |
| `dev` | 开发/测试 | 自动 → `dev.canyougrab.it` |
| 功能分支 | 正在进行的工作 | 无自动部署 |
## 环境变量
### 后端(服务器上必需)
```bash
# DNS 解析器(专用 droplet 上的 Unbound)
DNS_RESOLVER_HOSTNAME=unbound.canyougrab.internal # VPC 内部主机名(通过 socket.gethostbyname 解析)
DNS_RESOLVER_PORT=53
DNS_QUERY_TIMEOUT= # 秒为单位的每个查询超时
# PostgreSQL(DigitalOcean 托管数据库 - 仅用于身份验证,使用情况,计费)
POSTGRES_HOST= # 数据库集群主机名
POSTGRES_PORT=5432
POSTGRES_DB=canyougrab
POSTGRES_USER=canyougrab
POSTGRES_PASSWORD= # 数据库密码
POSTGRES_SSLMODE=require
# Valkey / Redis(DigitalOcean 托管数据库)
VALKEY_HOST= # Valkey 集群主机名
VALKEY_PORT=25061
VALKEY_USERNAME=default
VALKEY_PASSWORD= # Valkey 密码
# Auth0
AUTH0_DOMAIN=dev-mqe5tavp6dr62e7u.us.auth0.com
AUTH0_AUDIENCE=https://api.canyougrab.it
# Stripe
STRIPE_SECRET_KEY= # sk_live_...(生产)或 sk_test_...(开发)
STRIPE_WEBHOOK_SECRET= # whsec_...(每个环境)
STRIPE_PRICE_BASIC= # price_...(Basic 计划的实时价格 ID)
STRIPE_PRICE_PRO= # price__(Pro 计划的实时价格 ID)
STRIPE_PRICE_BUSINESS= # price_...(Business 计划的实时价格 ID)
```
# Cloudflare Turnstile(密钥创建时的机器人防护)
TURNSTILE_SECRET_KEY= # 0x4AAAA... (来自Cloudflare仪表盘)
# 门户
PORTAL_URL=https://portal.canyougrab.it
# Worker
BATCH_CONCURRENCY=10 # 域名检查的线程池大小
VALKEY_QUEUE_NAME=canyougrab-jobs # RQ队列名称(默认:canyougrab-jobs)
# 监控(可选 — 仅当安装了监控堆栈时)
SLACK_ALERTS_WEBHOOK_URL= # Slack入站Webhook,用于Alertmanager
RQ_METRICS_PORT=9122 # Prometheus指标导出器端口
# 自动缩放器(可选 — 仅在启用自动缩放器的API主机上)
DO_API_TOKEN= # DigitalOcean API令牌
DO_WORKER_SNAPSHOT_ID= # 新Worker droplets的快照ID
AUTOSCALER_MIN_WORKERS=1
AUTOSCALER_MAX_WORKERS=5
AUTOSCALER_SCALE_UP_THRESHOLD=50
AUTOSCALER_SCALE_DOWN_IDLE_MINUTES=10
### GitHub Actions Secrets
| Secret | 用途 |
|--------|---------|
| `DEPLOY_HOST` | 生产服务器IP |
| `DEV_DEPLOY_HOST` | 开发服务器IP |
| `DEPLOY_SSH_KEY` | 两台服务器的SSH私钥 |
## 本地开发
### 后端
```bash
cd backend
pip install -r requirements.txt
# 启动API服务器
uvicorn app:app --reload --port 8000
# 启动Worker(单独的终端)
python worker.py
```
需要`DNS_RESOLVER_HOST`指向Unbound实例(或使用`8.8.8.8`进行基本测试),以及PostgreSQL和Valkey/Redis(本地或隧道到托管实例)。
### 门户
```bash
cd portal/docs
npm install
npm run dev # 启动Zudoku开发服务器(通过zuplo)
```
或使用配置的启动服务器:
```bash
# 从仓库根目录
npx zuplo dev # 在端口9200上启动
```
门户开发服务器在`portal/docs/src/config.ts`中将`API_BASE`硬编码为`https://api.canyougrab.it`。要针对本地后端开发,请将其更改为`http://localhost:8000`。
## SEO:.it TLD地理目标要求
由于canyougrab.it使用.it国家代码TLD(意大利),所有面向用户的HTML(门户,文档)必须包含信号,表明内容针对英语使用者:
1. **`<html lang="en">`** 在每个页面上。
2. **Hreflang标签** 在`<head>`中:
```html
<link rel="alternate" hreflang="en-US" href="{page_url}" />
<link rel="alternate" hreflang="en" href="{page_url}" />
<link rel="alternate" hreflang="x-default" href="{page_url}" />
```
3. **Canonical标签** — 自引用`<link rel="canonical" href="{page_url}" />`。
4. **所有内容均为英文** — 任何页面上不得出现意大利语文本。
这适用于门户(`portal.canyougrab.it`)、API文档和任何其他公开渲染的HTML。
## 关键设计决策
- **实时DNS查询**:域名可用性通过NS查询到专用[Unbound递归解析器](https://github.com/ericismaking/canyougrab-unbound)进行检查。这样可以获得实时结果(无24小时区域文件延迟),自动适用于任何TLD,并避免了每日批量区域文件加载的运维复杂性。Unbound会积极缓存(注册域名7天),因此重复查询时间约为1毫秒。
- **可空可用性**:DNS的故障模式比数据库查询多。当Unbound返回SERVFAIL或超时时,API返回`available: null`而不是可能危险的假阳性。消费者应将`null`视为“无法确定”。
- **批量检查的作业队列**:批量端点使用RQ(Redis Queue)由Valkey支持,派遣作业并具有自动重试(2次重试,5秒/30秒延迟)和失败作业跟踪。Worker在线程池中并行化DNS查询。
- **长轮询**:批量检查端点保持HTTP连接打开(每0.3秒轮询Valkey,最高45秒),而不是要求客户端实现轮询。作业结果存储在自定义Valkey哈希中(不是RQ的内置结果存储),因此轮询逻辑独立于队列框架。
- **具有SHA-256哈希的API密钥**:密钥在存储前进行哈希(像密码一样),因此数据库泄露不会暴露原始密钥。
- **Stripe元数据链接**:Stripe客户通过Stripe客户对象上的`auth0_sub`元数据链接到Auth0用户,避免了单独的映射表。
- **使用双重跟踪**:同时记录每日(`usage_log_daily`)和每分钟(`usage_log_minute`)使用情况,分别用于每月配额和每分钟速率限制。
- **无Zuplo网关策略**:该项目最初使用Zuplo作为API网关,但迁移到了直接的FastAPI(提交`3abef70`)。Zuplo门户/文档框架仍用于开发人员门户。
## 服务器上的进程
### 核心服务(每台主机)
1. **FastAPI** (uvicorn):在端口8000上提供所有HTTP端点
2. **RQ Worker** (worker.py):处理RQ队列中的域名检查作业
3. **MCP服务器** (如果`/mcp`托管在此服务器上):为远程MCP客户端提供服务
### 监控堆栈(API主机,可选)
4. **Prometheus** (localhost:9090):抓取指标,评估警报规则
5. **Alertmanager** (localhost:9093):将警报路由到Slack
6. **redis_exporter** (localhost:9121):将Valkey指标导出到Prometheus
7. **RQ指标导出器** (localhost:9122):导出队列深度,工作线程数,失败作业
8. **Grafana** (localhost:3000,nginx代理):队列健康仪表盘
9. **自动缩放器** (可选):通过DO API缩放工作线程上下
使用以下命令安装监控堆栈:`sudo bash scripts/setup-monitoring.sh`
主机上的所有活动服务必须通过systemd进行管理,并在部署期间重启。所有自动部署都应通过`scripts/deploy-host.sh`路由,该脚本负责重启主机上安装的服务。
MCP 配置
以下是此 MCP Server 的配置信息,可直接复制到 Cursor 或其他 MCP 客户端中使用。
mcp.json
连接信息
猜你喜欢
暂无推荐服务器