design-spec.md 27 KB

商户查找系统 - 设计规格书

日期: 2026-04-09 目标: 用 Go + React 完全重写现有 Python TG Lead Scraper 系统


一、系统概述

从 Telegram 频道、搜索引擎、GitHub、导航网站自动挖掘商户联系方式,经过清洗去重验证后按质量打分,输出可用商户清单。

技术栈:

  • 后端: Go (Gin + GORM + asynq)
  • 前端: React + Ant Design + zustand
  • 数据库: MySQL (复用现有 Docker 容器, root/root123)
  • 缓存/队列: Redis (复用现有 Docker 容器, 无密码, db3)
  • TG API: gotd/td (MTProto)
  • 网页爬取: colly (静态) + chromedp (JS 渲染)
  • LLM: OpenAI 兼容接口 (可配置切换 OpenAI/Claude/GLM 等)
  • 搜索: Serper API
  • 部署: Docker Compose (仅 Go API + Nginx, 复用已有 MySQL/Redis)

二、架构

单体服务 + 异步任务队列。API 和 Worker 在同一个 Go 进程内。

┌─────────────────────────────────────┐
│           Go 单体服务               │
│  ┌──────────┐  ┌──────────────────┐ │
│  │ HTTP API │  │  Task Worker     │ │
│  │ (Gin)    │  │  (7阶段Pipeline) │ │
│  └──────────┘  └──────────────────┘ │
│        ↕              ↕             │
│  ┌──────────┐  ┌──────────────────┐ │
│  │  MySQL   │  │  Redis           │ │
│  │  (持久化) │  │  (队列+缓存+锁) │ │
│  └──────────┘  └──────────────────┘ │
└─────────────────────────────────────┘
         ↑
   React SPA (Nginx)

三、项目结构

spider/
├── cmd/
│   └── server/
│       └── main.go              # 入口,启动 API + Worker
├── internal/
│   ├── config/                  # 配置加载 (YAML + DB managed_settings)
│   │   └── config.go
│   ├── model/                   # MySQL 表结构 (GORM)
│   │   ├── seed.go
│   │   ├── keyword.go
│   │   ├── setting.go
│   │   ├── channel.go
│   │   ├── nav_site.go
│   │   ├── merchant_raw.go
│   │   ├── merchant_clean.go
│   │   ├── task.go
│   │   └── config_revision.go
│   ├── handler/                 # HTTP handler (Gin)
│   │   ├── task.go
│   │   ├── merchant.go
│   │   ├── channel.go
│   │   ├── nav_site.go
│   │   ├── seed.go
│   │   ├── keyword.go
│   │   ├── config.go
│   │   └── dashboard.go
│   ├── service/                 # 业务逻辑层
│   │   ├── task_service.go
│   │   ├── merchant_service.go
│   │   └── config_service.go
│   ├── pipeline/                # 7阶段 Pipeline 调度器
│   │   ├── pipeline.go          # 调度器主逻辑
│   │   ├── phase1_discover.go   # TG 频道裂变
│   │   ├── phase2_search.go     # Serper 搜索
│   │   ├── phase3_github.go     # GitHub README 挖掘
│   │   ├── phase4_scrape.go     # TG 消息采集
│   │   ├── phase5_crawl.go      # 网页爬取
│   │   ├── phase6_clean.go      # 清洗三关
│   │   └── phase7_score.go      # 评分
│   ├── telegram/                # gotd/td 封装
│   │   ├── client.go            # TG 客户端封装
│   │   └── account_manager.go   # 多账号轮换 + FloodWait
│   ├── search/                  # 搜索引擎封装
│   │   └── serper.go            # Serper API
│   ├── crawler/                 # 网页爬取
│   │   ├── static.go            # colly 静态爬取
│   │   └── dynamic.go           # chromedp JS 渲染
│   ├── llm/                     # LLM 统一接口
│   │   └── client.go            # OpenAI 兼容接口封装
│   ├── extractor/               # 联系方式提取
│   │   ├── regex.go             # 正则提取
│   │   └── llm_extractor.go     # LLM 辅助提取
│   └── worker/                  # asynq 任务 Worker
│       └── worker.go
├── web/                         # React 前端
│   ├── src/
│   │   ├── pages/
│   │   │   ├── Dashboard.tsx        # 总览仪表盘
│   │   │   ├── Tasks.tsx            # 任务管理
│   │   │   ├── MerchantsRaw.tsx     # 原始商户表
│   │   │   ├── MerchantsClean.tsx   # 清洗商户表
│   │   │   ├── Channels.tsx         # 频道管理
│   │   │   ├── NavSites.tsx         # 导航网页
│   │   │   ├── Seeds.tsx            # 种子管理
│   │   │   ├── Keywords.tsx         # 关键词管理
│   │   │   └── Settings.tsx         # 系统配置
│   │   ├── components/
│   │   │   ├── Layout.tsx
│   │   │   ├── TaskControl.tsx      # 7阶段独立启动按钮
│   │   │   └── DataTable.tsx        # 通用表格组件
│   │   ├── api/
│   │   │   └── index.ts             # axios 封装
│   │   └── store/
│   │       └── index.ts             # zustand 状态管理
│   ├── package.json
│   └── vite.config.ts
├── deploy/
│   ├── docker-compose.yml
│   ├── Dockerfile.api
│   ├── Dockerfile.web
│   └── nginx.conf
├── configs/
│   └── config.yaml              # 默认配置
└── go.mod

四、数据模型

4.1 MySQL 表 (数据库名: spider)

CREATE DATABASE IF NOT EXISTS spider DEFAULT CHARACTER SET utf8mb4;
USE spider;

-- 种子管理
CREATE TABLE managed_seeds (
    id           BIGINT AUTO_INCREMENT PRIMARY KEY,
    channel_name VARCHAR(255) NOT NULL UNIQUE,
    status       ENUM('active','inactive') DEFAULT 'active',
    note         VARCHAR(500),
    created_at   DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at   DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

-- 关键词管理
CREATE TABLE managed_keywords (
    id           BIGINT AUTO_INCREMENT PRIMARY KEY,
    keyword      VARCHAR(255) NOT NULL UNIQUE,
    category     VARCHAR(100),
    status       ENUM('active','inactive') DEFAULT 'active',
    created_at   DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- 运行参数
CREATE TABLE managed_settings (
    id           BIGINT AUTO_INCREMENT PRIMARY KEY,
    key_name     VARCHAR(255) NOT NULL UNIQUE,
    value        TEXT NOT NULL,
    value_type   ENUM('int','float','bool','string','json') NOT NULL,
    effect_level ENUM('runtime','new_task') DEFAULT 'runtime',
    description  VARCHAR(500),
    updated_at   DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

-- 发现的 TG 频道
CREATE TABLE channels (
    id              BIGINT AUTO_INCREMENT PRIMARY KEY,
    username        VARCHAR(255) NOT NULL UNIQUE,
    title           VARCHAR(500),
    member_count    INT DEFAULT 0,
    about           TEXT,
    source          ENUM('seed','snowball','search','github') NOT NULL,
    source_detail   VARCHAR(500),
    status          ENUM('pending','scraped','failed','skipped') DEFAULT 'pending',
    last_message_id INT DEFAULT 0,
    relevance_score FLOAT,
    created_at      DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at      DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_status (status),
    INDEX idx_source (source)
);

-- 候选导航网页
CREATE TABLE nav_sites (
    id             BIGINT AUTO_INCREMENT PRIMARY KEY,
    url            VARCHAR(2048) NOT NULL,
    domain         VARCHAR(255),
    source         VARCHAR(100),
    status         ENUM('pending','scraped','filtered','failed') DEFAULT 'pending',
    filter_reason  VARCHAR(255),
    merchant_count INT DEFAULT 0,
    created_at     DATETIME DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_status (status),
    UNIQUE INDEX idx_url (url(500))
);

-- 原始商户
CREATE TABLE merchants_raw (
    id               BIGINT AUTO_INCREMENT PRIMARY KEY,
    merchant_name    VARCHAR(500),
    tg_username      VARCHAR(255),
    website          VARCHAR(2048),
    email            VARCHAR(255),
    phone            VARCHAR(100),
    industry         VARCHAR(100),
    source_type      ENUM('tg_scrape','web_crawl','github') NOT NULL,
    source_id        VARCHAR(500),
    original_message TEXT,
    status           ENUM('raw','glm_parsed') DEFAULT 'raw',
    created_at       DATETIME DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_status (status),
    INDEX idx_tg_username (tg_username)
);

-- 清洗后商户
CREATE TABLE merchants_clean (
    id               BIGINT AUTO_INCREMENT PRIMARY KEY,
    raw_id           BIGINT,
    merchant_name    VARCHAR(500),
    tg_username      VARCHAR(255),
    website          VARCHAR(2048),
    email            VARCHAR(255),
    phone            VARCHAR(100),
    industry         VARCHAR(100),
    status           ENUM('valid','invalid','bot','duplicate','group') NOT NULL,
    tg_first_name    VARCHAR(255),
    tg_last_name     VARCHAR(255),
    is_premium       TINYINT(1) DEFAULT 0,
    last_online      DATETIME,
    active_level     ENUM('active','moderate','inactive'),
    member_count     INT DEFAULT 0,
    quality_score    FLOAT DEFAULT 0,
    source_count     INT DEFAULT 1,
    source_links     JSON,
    created_at       DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at       DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE INDEX idx_tg_username (tg_username),
    INDEX idx_status (status),
    INDEX idx_quality (quality_score),
    INDEX idx_industry (industry)
);

-- 任务记录
CREATE TABLE tasks (
    id           BIGINT AUTO_INCREMENT PRIMARY KEY,
    task_type    ENUM('full','discover','search','github','scrape','crawl','clean','score') NOT NULL,
    status       ENUM('pending','running','completed','failed','stopped') DEFAULT 'pending',
    params       JSON,
    progress     JSON,
    result       JSON,
    error_msg    TEXT,
    started_at   DATETIME,
    finished_at  DATETIME,
    created_at   DATETIME DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_status (status)
);

-- 配置变更审计
CREATE TABLE config_revisions (
    id           BIGINT AUTO_INCREMENT PRIMARY KEY,
    setting_key  VARCHAR(255) NOT NULL,
    old_value    TEXT,
    new_value    TEXT,
    changed_by   VARCHAR(100) DEFAULT 'admin',
    created_at   DATETIME DEFAULT CURRENT_TIMESTAMP
);

4.2 Redis 用途 (db3)

Key 模式 类型 用途 TTL
spider:task:queue:* asynq 内部 任务队列 asynq 管理
spider:task:progress:{id} Hash 实时进度 24h
spider:task:lock:{type} String 同类型任务互斥锁 自动释放
spider:tg:floodwait:{account} String 账号冷却截止时间 按实际冷却
spider:cache:channel:{username} Hash 频道信息缓存 24h
spider:cache:settings Hash managed_settings 热缓存 5min
spider:dedup:merchant:{username} String 采集时快速去重 7d

所有 key 加 spider: 前缀避免和其他服务冲突。


五、API 接口

前缀: /api/v1

5.1 任务管理

POST   /tasks/start
  Body: {
    "task_type": "full|discover|search|github|scrape|crawl|clean|score",
    "target": "可选, 频道名或关键词",
    "test_run": { "item_limit": 10, "message_limit": 50 },  // 可选
    "skip_phases": ["scrape"]  // 可选
  }
  Response: { "task_id": 1, "status": "pending" }

POST   /tasks/:id/stop
  Body: { "force": false }

GET    /tasks
  Query: ?status=running&page=1&page_size=20
  Response: { "items": [...], "total": 100 }

GET    /tasks/:id
  Response: { "id":1, "task_type":"full", "status":"running", "progress": {...} }

GET    /tasks/:id/logs
  WebSocket 实时推送日志

5.2 商户数据

GET    /merchants/raw
  Query: ?status=raw&source_type=tg_scrape&page=1&page_size=20
  默认排序: created_at DESC

GET    /merchants/clean
  Query: ?status=valid&industry=机场&min_score=60&sort=quality_score&order=desc&page=1&page_size=20

GET    /merchants/:id
  Response: 商户详情 (raw 或 clean 自动判断)

GET    /merchants/stats
  Response: { "raw_total":1946, "clean_total":125, "valid":62, "by_source":{...}, "by_industry":{...} }

5.3 频道管理

GET    /channels
  Query: ?status=pending&source=snowball&page=1&page_size=20

GET    /channels/stats
  Response: { "total":1907, "by_status":{...}, "by_source":{...} }

5.4 导航网页

GET    /nav-sites
  Query: ?status=pending&page=1&page_size=20

5.5 种子管理

GET    /seeds
POST   /seeds             Body: { "channel_name": "@bbs3000", "note": "综合频道" }
PUT    /seeds/:id          Body: { "status": "inactive", "note": "..." }
DELETE /seeds/:id

5.6 关键词管理

GET    /keywords           Query: ?category=机场&status=active
POST   /keywords           Body: { "keywords": ["机场推荐","发卡网"], "category": "机场" }  // 支持批量
PUT    /keywords/:id       Body: { "keyword": "...", "category": "..." }
DELETE /keywords/:id

5.7 系统配置

GET    /config/settings
PUT    /config/settings/:key   Body: { "value": "500" }  // 自动写审计日志

5.8 仪表盘

GET    /dashboard
  Response: {
    "channels_total": 1907,
    "merchants_raw_total": 1946,
    "merchants_clean_total": 125,
    "merchants_valid": 62,
    "nav_sites_total": 646,
    "recent_tasks": [...最近5个任务],
    "running_task": null | {...}
  }

六、Pipeline 7 阶段详细逻辑

Phase 1: discover (频道裂变)

输入: managed_seeds (status=active) 输出: channels 表

  1. 从 managed_seeds 拿所有 active 种子
  2. 用 gotd/td 连接 TG,进入每个种子频道
  3. 读最近 100 条消息,提取:
    • forward_from 频道
    • 消息内 t.me/xxx 链接
    • TG 推荐频道
  4. 新频道作为第二层种子继续裂变
  5. 限制: max_depth=3, 每层 max_channels_per_layer(200), 总数 max_channels_total(500)
  6. 每个频道间 sleep 5s
  7. 写入 channels 表, source='seed'(第一层) 或 'snowball'(裂变层)

FloodWait 处理: 由 AccountManager 统一管理

Phase 2: search (搜索引擎)

输入: managed_keywords (status=active) 输出: channels + nav_sites

  1. 从 managed_keywords 拿所有 active 关键词
  2. 对每个关键词调 Serper API,翻页获取结果
  3. 分拣结果 URL:
    • t.me/xxx → 写 channels 表, source='search'
    • 域名含 nav/list/catalog/directory → 写 nav_sites 表
    • 博客/社交媒体/产品官网 → 丢弃
  4. 关键词间 sleep 2s

Phase 3: github (GitHub 采集)

输入: 预设 query 列表 (从 managed_keywords 生成) 输出: channels 表

  1. 用 GitHub Search API 搜索 repo (按 star 排序)
  2. 下载每个 repo 的 README.md
  3. 过滤: README 前 5000 字必须含中文
  4. 正则匹配 t.me/xxx 链接
  5. 链接前后 200 字必须含中文
  6. repo 间 sleep 2s, query 间 sleep 5s
  7. 写入 channels 表, source='github'

注意: 加 GitHub token 到配置,提升到 30 req/min

Phase 4: scrape (TG 消息采集)

输入: channels 表 (status=pending) 输出: merchants_raw

  1. 取 status='pending' 的频道
  2. 对每个频道: a. get_entity 解析频道 b. LLM 相关性评估 (频道名+简介+成员数) → 不相关则 skip c. 读频道简介 (about) d. 读置顶消息 (limit=20) e. 遍历历史消息 (limit=500, 断点续传用 last_message_id)
  3. 每条消息:
    • 系统消息跳过
    • 非中文跳过
    • 正则快速判是否含联系方式
    • 有联系方式 → LLM 精准解析提取商户 (failover 到正则)
    • 写入 merchants_raw, source_type='tg_scrape'
  4. 消息间 sleep 由 managed_settings 控制

Phase 5: crawl (网页爬取)

输入: nav_sites 表 (status=pending) 输出: merchants_raw

  1. 取 status='pending' 的网页
  2. 预过滤 (规则引擎):
    • 黑名单域名 (80+) → filtered
    • 黑名单扩展名 (40+) → filtered
    • 黑名单路径 → filtered
    • 正向信号 (nav/directory/catalog) → 通过
    • 不确定 → LLM 二次过滤
  3. 通过的网页:
    • colly 抓取 → 失败则 chromedp (JS 渲染) → 仍失败标 failed
    • HTML 前 5000 字非中文跳过
    • 解析商户链接
  4. 每个商户链接:
    • @tg_username → t.me 死号预检 (抓网页看有无头像)
    • 活号 → 写 merchants_raw, source_type='web_crawl'
    • 只有网站 → 爬商户首页 + /contact, /about 等子页,正则提取联系方式

Phase 6: clean (清洗三关)

输入: merchants_raw (status=raw) 输出: merchants_clean

第一关: 黑名单过滤 (本地, 毫秒级)

  • 系统 bot 用户名 (26个 + xxxbot 后缀) → 标 bot
  • 邀请链接哈希 (16-24位 base64 + 高熵) → 标 invalid
  • original_message 非空且不含中文 → 标 invalid

第二关: 去重 (本地, 毫秒级)

  • 同 tg_username 多条记录,按信息丰富度打分 (有 website/email/phone 加分)
  • 保留最丰富的一条,其余标 duplicate
  • 合并所有 source 链接到 keeper 的 source_links

第三关: TG 真实性验证 (需 TG API)

  • gotd/td get_entity 验证 username
  • 返回 User 且非 bot → valid (顺便拿 first_name/last_name/is_premium/last_online)
  • 返回 Bot → bot
  • 返回 Channel/Chat → group
  • UsernameNotOccupied → invalid
  • FloodWait 处理同上

Phase 7: score (评分)

输入: merchants_clean (status=valid) 输出: 更新 merchants_clean.quality_score

6 维度加权打分 (总权重 1.0):

维度 权重 规则
member_count 0.25 <100→10, <1k→30, <1w→50, <10w→80, ≥10w→100
premium 0.15 is_premium=true→100, false→0
activity 0.25 active→100, moderate→50, inactive→20
multi_source 0.20 source_count≥4→100, 3→70, 2→40, 1→10
has_website 0.10 website非空→100, 空→0
has_email 0.05 email非空→100, 空→0

quality_score = 各维度得分 × 权重 之和 (0-100)


七、TG 账号管理

// AccountManager 管理多个 TG 账号的连接和限速
type AccountManager struct {
    accounts []TGAccount
    mu       sync.Mutex
    redis    *redis.Client
}

type TGAccount struct {
    Phone     string
    SessionFile string
    Client    *telegram.Client   // gotd/td client
    CoolUntil time.Time
}

// Acquire 获取一个当前可用的账号
// 跳过正在冷却的账号,全部冷却则返回错误
func (m *AccountManager) Acquire(ctx context.Context) (*TGAccount, error)

// Release 归还账号,如果触发了 FloodWait 则标记冷却
// 冷却截止时间同步到 Redis,重启不丢失
func (m *AccountManager) Release(acc *TGAccount, floodWait time.Duration)

// FloodWait 策略:
// ≤60s  → 当前账号等待后重试
// >60s  → Release + 切换账号
// >300s → 整轮 break,标记所有账号最小冷却 300s

八、LLM 统一接口

// LLMClient 封装 OpenAI 兼容接口
type LLMClient struct {
    client   *openai.Client
    model    string        // 可配置: gpt-4o / claude-3-sonnet / glm-4 等
    baseURL  string        // 可配置: 指向不同提供商
    timeout  time.Duration
}

// 用途 1: 频道相关性评估
func (c *LLMClient) EvalChannelRelevance(name, about string, memberCount int) (float64, error)

// 用途 2: 消息商户解析
func (c *LLMClient) ParseMerchant(message string) (*MerchantInfo, error)

// 用途 3: 行业分类
func (c *LLMClient) ClassifyIndustry(name, about string) (string, error)

// 用途 4: 导航站判断
func (c *LLMClient) IsNavSite(url string) (bool, float64, error)

配置示例 (config.yaml):

llm:
  provider: "openai"        # openai / claude / glm
  base_url: "https://api.openai.com/v1"
  api_key: "sk-xxx"
  model: "gpt-4o-mini"
  timeout: 30s

九、联系方式提取器

// 正则提取 (优先, 零成本)
func ExtractByRegex(text string) *ContactInfo

// 正则模式:
// TG 用户名: @[a-zA-Z][a-zA-Z0-9_]{4,31}
// TG 链接: t\.me/[a-zA-Z0-9_]{5,32}
// 变体: t点me, t . me, tg: 等
// 邮箱: 标准 email 正则
// 电话: +国际区号格式
// 网址: https?://...
// 微信变体: 加V, 加v, vx, wx, 微信 后跟联系方式

// LLM 提取 (正则没提取到但文本可能含非标准联系方式时)
func ExtractByLLM(client *LLMClient, text string) *ContactInfo

十、网页爬取策略

请求优先级:
1. colly (静态 HTTP, 最快)
2. chromedp (无头浏览器, 处理 JS 渲染)

预过滤规则引擎:
- 黑名单域名: t.me, twitter.com, google.com, facebook.com ... (80+)
- 黑名单扩展名: .apk, .zip, .pdf, .exe ... (40+)
- 黑名单路径: /api/, /login/, ?ref= ...
- 正向信号: URL 含 nav/directory/catalog/list → 直接通过
- 不确定 → LLM 二次过滤 (置信度 ≥0.6 放行)

t.me 死号预检:
- 抓 t.me/{username} 网页
- 没有头像元素 → 判定死号, 直接丢弃
- 有头像 → 活号, 继续入库

十一、前端页面

使用 React + Ant Design + zustand + vite

页面清单

页面 路径 功能
仪表盘 / 各表计数、运行中任务、最近任务
任务管理 /tasks 7阶段独立启动按钮、full pipeline、任务列表、实时进度、停止
原始商户 /merchants/raw 表格 (分页/过滤/按created_at desc)
清洗商户 /merchants/clean 表格 (分页/过滤/排序by score)
频道列表 /channels 表格 (分页/状态过滤/来源过滤)
导航网页 /nav-sites 表格 (分页/状态过滤)
种子管理 /seeds CRUD 表格
关键词管理 /keywords CRUD 表格 (支持批量添加)
系统配置 /settings 三个 tab: 种子/关键词/流水线参数
系统日志 /logs 实时运行日志 (WebSocket)

布局

┌────────────────────────────────────┐
│  Logo    商户查找系统         Admin │  ← 顶栏
├──────┬─────────────────────────────┤
│      │                             │
│ 仪表盘│        内容区域              │
│ 任务  │                             │
│ 商户  │                             │
│ 频道  │                             │
│ 网页  │                             │
│ 配置  │                             │
│ 日志  │                             │
│      │                             │
├──────┴─────────────────────────────┤
└────────────────────────────────────┘
         ← 侧边栏导航

十二、配置文件

# configs/config.yaml

server:
  port: 8080

mysql:
  host: "mysql"           # Docker 容器名
  port: 3306
  user: "root"
  password: "root123"
  database: "spider"

redis:
  host: "redis"           # Docker 容器名
  port: 6379
  password: ""
  db: 3

telegram:
  accounts:
    - phone: "+1234567890"
      session_file: "sessions/account_01.session"
    - phone: "+0987654321"
      session_file: "sessions/account_02.session"
  app_id: 12345
  app_hash: "abcdef1234567890"

llm:
  provider: "openai"
  base_url: "https://api.openai.com/v1"
  api_key: "sk-xxx"
  model: "gpt-4o-mini"
  timeout: 30s

serper:
  api_key: "xxx"
  results_per_page: 10
  max_pages: 3

github:
  token: ""               # 可选, 有则 30 req/min

十三、Docker Compose 部署

# deploy/docker-compose.yml
version: "3.8"

services:
  api:
    build:
      context: ..
      dockerfile: deploy/Dockerfile.api
    ports:
      - "8080:8080"
    volumes:
      - ../configs:/app/configs
      - ../sessions:/app/sessions
    depends_on: []
    restart: unless-stopped
    networks:
      - default
      - external_db    # 连接已有 MySQL/Redis 的网络

  web:
    build:
      context: ..
      dockerfile: deploy/Dockerfile.web
    ports:
      - "80:80"
    depends_on:
      - api
    restart: unless-stopped

networks:
  external_db:
    external: true       # 已有 MySQL/Redis 所在的 Docker network
    name: <现有网络名>    # 需要确认

十四、managed_settings 初始值

key value type effect_level description
pipeline.skip_phases [] json new_task 默认跳过的阶段
pipeline.checkpoint_interval 30 int runtime 进度上报间隔(秒)
tg_scraper.message_limit_per_channel 500 int runtime 每频道最大消息数
tg_scraper.delay_per_message 1.0 float runtime 消息间延迟(秒)
tg_scraper.delay_per_channel 5.0 float runtime 频道间延迟(秒)
tg_scraper.delay_per_verify 3.0 float runtime 验证间延迟(秒)
clean.timeout_seconds 3600 int runtime 清洗阶段超时
search.timeout_seconds 3600 int runtime 搜索阶段超时
snowball.max_channels_per_layer 200 int runtime 每层最大频道数
snowball.max_channels_total 500 int runtime 总最大频道数
tme_validator.enabled true bool runtime 启用t.me死号预检
tme_validator.rate_per_min 60 int runtime 预检限速
tme_validator.concurrency 10 int runtime 预检并发数

十五、相比原 Python 系统的改进点

15.1 架构层面

  1. Go 单二进制部署 — 无 Python venv 依赖地狱
  2. MySQL 替代 SQLite — 支持并发写入、不会锁库
  3. Redis 任务队列 — 替代文件 pipeline_state.json,支持分布式锁
  4. 结构化配置 — YAML + DB managed_settings,热加载

15.2 TG 限速改进

  1. 独立节流器 — cleaner verify 有自己的 rate limiter,不和 scraper 共享,避免互相干扰
  2. 冷却状态持久化 — FloodWait 状态存 Redis,服务重启不丢失,不会重复触发限速
  3. 采集时实时去重 — Redis dedup key,同一 username 不会重复写入 raw 表,减少后续清洗压力

15.3 采集改进

  1. GitHub 配 token — 从 10 req/min 提升到 30 req/min
  2. chromedp 替代 playwright — 纯 Go 方案,无需额外安装 Node/浏览器依赖
  3. LLM 可切换 — 不绑定 GLM,支持 OpenAI/Claude/GLM 随时切换

15.4 数据质量

  1. merchant_name 清洗 — 入库前 strip HTML 标签,修复原系统的污染问题
  2. 采集层去重前置 — Redis 布隆过滤或 set,在写 merchants_raw 之前就去重

15.5 运维

  1. WebSocket 实时日志 — 替代轮询日志文件
  2. Docker Compose 一键部署 — 不依赖 systemd 手动配置