# TG 商户采集系统 — 需求方案书(v3.1) > 本文档描述系统要实现的功能和业务逻辑,供开发者从零设计和实现。 > 版本: v3.1(在 v3 基础上扩展多维度采集 + 商户信息丰富化 + 可控性管理) --- ## 零、版本说明与前置确认清单 ### 版本演进 | 版本 | 主要变化 | |---|---| | v2 | 插件化采集 + 统一清洗 | | v3 | 前置确认清单 / shared 基础设施层 / MySQL / 规则细节修正 / 监控评估 | | **v3.1** | **多维度采集蓝图(8 插件)/ Enrichment 层 / 商户实体聚合 / 可控性与配额管理** | ### v3.1 相对 v3 的变化 | 变化 | 动机 | |---|---| | 采集维度从 2 个插件扩到 8 个插件蓝图(分 M1/M2/M3 落地) | 单维度漏网严重,多维度能交叉印证提升置信度 | | 新增 **Enrichment 层**(raw→clean→enriched) | clean 只能说"是真商户",enriched 才能说"这是什么样的商户" | | 新增 **商户实体聚合** | 同一商户常有多个 TG 号 / 域名,需要聚合成一个实体 | | 新增 **可控性与配额管理** | 任何时候都要能回答"系统在做什么 / 花了多少钱 / 能不能立即停" | | 数据模型从 9 张扩到 15 张 | 持久化实体、enrichment、配额、审计、审核队列 | | 新增 canary 灰度 + 紧急开关(kill switch) | 新插件上线前必须小数据量验证;出合规问题能一键停 | ### 前置确认清单(写代码前必须回答) > 这三件事不确认就开始写代码,等于在错的地基上盖楼。 **1. 合规边界** - 部署所在地?(决定适用法域) - 目标客群所在地?(决定 PIPL / GDPR / 所在国法律是否适用) - 输出用途?(内部研究 / 冷触达销售 / 二次分发 — 每一种法律风险不同) - 目标行业为"机场/VPN/科学上网"时,业务本身的合法性由业务方确认,采集系统只负责技术实现 **2. 月度预算(美元)** - 搜索 API:Brave 免费 5000 次/月大概率一周内耗光,需确认是否可切 Serper(建议预算 $10–30/月起步) - AI API(TG 插件启用后):按关键词密度估,建议 $20/月起步 - 代理池:如果启用住宅代理,$50–200/月;不启用则只能单 IP 低并发运行 - 服务器 + chromedp 内存:建议 2 vCPU / 4 GB 起,chromedp 单页峰值 300 MB - Whois / ICP 查询 API:$10/月起(Enrichment 启用后) **3. 代理池策略** - 方案 A:不用代理 → 网页采集日抓量上限约 1000 页,且只能跑中低反爬站 - 方案 B:自建 IP 池(国内 VPS) → 成本低但维护重 - 方案 C:买住宅代理(Bright Data / IPRoyal / Oxylabs) → 推荐,按流量计费 - **day 1 必须预留代理接口**,即使最初跑单 IP,也不要把 http.Client 写死 三个问题没答案前,本文档以下内容按"合规 OK / 有最低预算 / 代理接口先留桩"默认值进行。 --- ## 一、系统目标 **一句话:从多个维度找到 TG 商户,把联系方式和业务画像全扒下来,清洗、聚合、丰富后输出一张可以直接联系的客户表。** ### 什么是"商户" 系统要找的是**在 TG 上提供产品或服务的人或组织**。 判定标准(满足任意一条即算商户): - 有 TG 联系方式(@xxx 或 t.me/xxx)**并且**有商业意图(接单、代理、价格、购买、咨询、客服、官网、订阅) - 被导航站收录(导航站本身就是商户目录) **不算商户的**:聊天用户、新闻频道、系统 bot。 ### 目标行业(当前) 当前只做**机场 / VPN / 科学上网**。行业规则可配置,以后可扩展。 ### 输入 / 输出 - **输入**:一组关键词 + 一组种子 TG 频道 + 一份导航站白名单 - **输出**:商户实体表(merchant_entities)+ 每个实体的 enrichment 画像 | 字段 | 说明 | |------|------| | 实体 ID | 聚合后的唯一商户标识 | | 商户名 | 主显示名 | | 所有 TG 号 | 主号 + 备用号 + 客服号 | | 所有官网 | 主域 + 备用域 | | 邮箱 / 电话 | 联系方式 | | 来源维度数 | 被多少个维度发现 | | 行业标签 | 机场 / VPN 等 | | 等级 | Hot / Warm / Cold | | 业务画像 | 价格档位 / 支付方式 / 节点地区 / 技术栈 | | 活跃度 | 官网存活 / TG 最新消息时间 | | 人工备注 | 可选 | --- ## 二、整体架构 系统分四层:**采集端(插件)** → **共享基础设施** → **处理端(流水线)** → **丰富化**。 ``` ┌────────────── 采集端(插件,互不依赖) ──────────────────────┐ │ web_search web_directory tg_channel tg_snowball │ │ github forum cert_trans icp_reverse │ │ └──────────────────┬──────────────────┘ │ │ ▼ │ │ merchants_raw │ └─────────────────────────┬────────────────────────────────────┘ │ ▼ ┌─────────────── 共享基础设施(shared/) ──────────────────────┐ │ tgpool / proxypool / searchcache / httpclient / extractor │ │ quota / audit / killswitch │ └─────────────────────────┬────────────────────────────────────┘ │ ▼ ┌────────────── 处理端(固定流水线) ──────────────────────────┐ │ 死号预检 → 黑名单 → 去重 → 实体聚合 → (TG验证) → 打标签 │ │ │ │ │ ▼ │ │ merchants_clean + merchant_entities │ └─────────────────────────┬────────────────────────────────────┘ │ ▼ ┌────────────────── 丰富化(Enrichment) ──────────────────────┐ │ HTTP探测 / Whois / ICP / TG profile / 文本画像 │ │ │ │ │ ▼ │ │ merchant_enrichment │ └──────────────────────────────────────────────────────────────┘ ``` ### 插件隔离规则 ``` 规则 1 每个采集插件是独立的 Go 包(internal/plugins//) 规则 2 插件之间零依赖:A 不能 import B 的任何符号 规则 3 插件和处理端都可以 import internal/shared/* 的共享组件 规则 4 所有采集插件的产出走同一个标准格式(MerchantData) 规则 5 新增插件 = 新建目录 + 实现 Collector 接口 + 配置里注册,不改任何已有代码 规则 6 shared/ 里的包不依赖任何具体插件(否则环形依赖) 规则 7 新插件上线必须先 canary 模式跑通人工审核(见第十三章) ``` ### 标准产出格式 ```go type MerchantData struct { TgUsername string // 必填,没有就不入库 TgLink string MerchantName string Website string Email string Phone string SourceType string // web_search / web_directory / tg_channel / github / ... SourceName string SourceURL string OriginalText string IndustryTag string FetchedAt time.Time Canary bool // v3.1:canary 数据只进 raw,不进 clean } ``` **硬约束**:没有 `TgUsername` 的商户不入库。 ### 插件接口 ```go type Collector interface { Name() string Run(ctx context.Context, cfg map[string]any, emit func(MerchantData)) error Stop() error } ``` --- ## 三、采集维度蓝图 > 单靠网页+TG 两个维度漏网严重。v3.1 规划 8 个维度,分三期落地。同一商户经常出现在多个维度,多维度互补正是提高召回率和置信度的核心手段。 ### 多维度的价值 | 场景 | 单维度的问题 | 多维度的解法 | |---|---|---| | 商户只在私密 TG 群出现 | Web 搜不到 | TG 滚雪球 | | 商户只在 GitHub README | TG 看不到 | github 插件 | | 官网被墙但频道活跃 | Web 抓失败 | TG + forum | | 主域名被封换备用域 | Web 搜不到新域 | cert_transparency | | 验证商户真实性 | 单一来源可能伪造 | 跨维度交叉印证 | `source_count ≥ 2` 的商户自动进入 Hot,这是多维度的直接收益。 ### 插件蓝图(8 个维度) | # | 插件 | 数据源 | 产出密度 | 合规风险 | 优先级 | 里程碑 | |---|---|---|---|---|---|---| | 1 | **web_search** | Google/Bing/Brave/Serper 搜索结果 | ★★★★★ | 低 | P0 | M1 | | 2 | **web_directory** | 已知导航站白名单主动爬取 | ★★★★★ | 低 | P0 | M1 | | 3 | **tg_channel** | TG 频道历史消息 | ★★★ | 中 | P1 | M2 | | 4 | **tg_snowball** | 从已发现频道滚雪球(转发源、@mention) | ★★★★ | 中 | P1 | M2 | | 5 | **github_search** | GitHub code/README 里的 t.me 链接 | ★★ | 低 | P2 | M2 | | 6 | **forum_scraper** | V2EX/hostloc/Reddit 等论坛帖子 | ★★★ | 低 | P2 | M3 | | 7 | **cert_transparency** | 证书透明日志反查同组织域名 | ★★ | 低 | P3 | M3 | | 8 | **icp_reverse** | 通过 ICP 备案号反查同主体域名 | ★★ | 低 | P3 | M3 | ### 里程碑与验收标准 | 里程碑 | 范围 | 周期 | 验收 | |---|---|---|---| | **M1** | web_search + web_directory + 处理端 + Enrichment 最小集 + 可控性骨架 | 2 周 | Hot 商户 ≥ 100,Precision ≥ 85% | | **M2** | + tg_channel + tg_snowball + github_search + 实体聚合 | 1 个月 | 总商户 ≥ 500,source_count≥2 占比 ≥ 30% | | **M3** | + forum + cert_transparency + icp_reverse + 全量可控性仪表 | 2 个月 | 按实体去重后 ≥ 800 家商户 | **不跳级**:M1 没稳定前不做 M2;M2 没稳定前不做 M3。每个新插件上线必须先走 canary 模式(见第十三章)。 ### 维度互补性示例 ``` 商户 A: web_search → 2 个导航站发现 web_directory → airportlist.top 收录 tg_snowball → 频道 @vpn_nav 转发 github_search → awesome-vpn-cn README cert_transparency → 发现备用域名 a-vpn.net source_count = 5 → Hot,置信度 High,实体聚合自动合并 ``` --- ## 四、采集插件 A:网页搜索采集(web_search,P0) ### 为什么是第一优先级 - 一个导航站几秒出 50 个商户,效率最高 - 没有 TG 账号封禁风险 - 导航站里的商户是别人整理好的,质量高 ### 数据流 ``` 关键词 → [searchcache 查缓存] → [搜索 API] → URL 列表 │ ┌──────────┴─────────┐ ▼ ▼ URL 是 t.me/xxx URL 是网页 直接提取 username 进入抓取流程 │ │ │ ┌─────────┴────────┐ │ ▼ ▼ │ 抓 HTML(三层 fallback) 丢弃黑名单域 │ │ │ 解析 HTML,正则提取 │ t.me / 邮箱 / 电话 │ │ └──────┬───┘ ▼ emit → merchants_raw ``` ### 详细逻辑 **第一步:关键词搜索** 1. 从 `keywords` 表读 enabled=true 的关键词 2. 先查 `search_cache`:key = (engine, keyword, page),TTL 默认 7 天 3. 缓存未命中 → 通过 quota 中心拿到额度(见第十三章) → 调搜索 API 4. 结果写回 `search_cache` 5. 每个关键词默认 3–5 页 × 10 条;每次请求之间随机等 2–5 秒 **第二步:URL 分拣** | URL 类型 | 判断 | 处理 | |---|---|---| | `t.me/xxx` | URL 以 `t.me/` 或 `telegram.me/` 开头 | 直接提取 username,emit | | `t.me/joinchat/xxx`、`t.me/+xxx` | 邀请链接 | 标记 invalid,丢弃 | | 黑名单域(twitter/google/youtube 等 80+) | 域名精确匹配 | 丢弃 | | 其他网页 | | 进入第三步 | **第三步:网页抓取(三层 fallback)** 由 `shared/httpclient` 统一暴露: ``` 层 1 net/http + colly(默认) 超时 10s,失败或 403/429 → 升层 层 2 utls 自定义 TLS 指纹(绕 Cloudflare 类反爬) 超时 15s,失败或 JS 渲染空 body → 升层 层 3 chromedp(Headless Chrome) 超时 30s,失败 → 放弃 ``` 每层都走同一个 `proxypool.Next()` 拿出口 IP。并发上限见第十二章。 **第四步:HTML 解析** 1. 收到 HTML 后先判断是否中文站(见下方"中文判断"修正) 2. 用 goquery 遍历: - 所有 `a[href^="https://t.me/"]` 和 `a[href^="tg://"]` → 抽 username - 所有 `a[href^="mailto:"]` → 抽邮箱 - 正文纯文本对电话正则匹配(带上下文关键词过滤) 3. 导航站判断(见下方"导航站启发式"修正) 4. 每个候选 username 组装 MerchantData → emit **中文判断(修正 v2 的 3000 字规则)** ``` 策略 1(默认) 解析 HTML 后取 + <meta description> + 前 5000 字符可见文本 统计中文字符数 / 总字符数,比例 ≥ 15% 判定为中文站 策略 2(补充) 若 HTTP 层返回的是 JS 空壳(<body> 少于 200 字符), 直接升级到 chromedp 渲染后再判断,不误杀 告警 若策略 1 和 2 都不过关但页面里有 t.me/@username, 仍走 emit,只是在 original_text 里标记 lang=unknown ``` **导航站启发式(修正 v2 的 ">5 个 t.me")** ``` 是导航站的充分条件(满足任一即视为高质量导航站): a) 页面上 ≥ 8 个 t.me 链接 且 分布在不同 DOM 父节点(避免评论区灌水) b) URL / title 含 "导航 / nav / 机场推荐 / 订阅" 等关键词 c) 有规律的卡片式布局(<ul><li> 或 <table>,同级节点里重复出现 t.me) 非导航站处理:仍抽取所有 t.me,但标记 SourceType=web_casual 清洗阶段这类商户降权(不会直接 Hot) ``` **电话号码正则(修正 v2 误匹配问题)** ``` 不要用裸 1[3-9]\d{9}。命中时必须满足以下条件之一: a) 正则前/后 20 字符内有关键词:电话|手机|tel|phone|联系|客服 b) 命中位置在 <a href="tel:..."> 里 c) 位置属于 meta / schema.org Contact 块 否则丢弃(避免 QQ 号 / 订单号 / 时间戳误判) ``` ### 搜索 API 选择与缓存 | 方案 | 免费额度 | 付费 | 用法 | |---|---|---|---| | **Brave Search API** | 5000 次/月 | $5/1000 次 | 起步用 | | **Serper.dev** | 2500 次(一次性) | $50/50000 次 | 免费耗完后切 | | **Bing Web Search** | $3/1000 次 | | 备用 | | **DuckDuckGo lite** | 无限 | 免费 | 兜底 | **强制缓存 + 强制配额**: - 同一 `(engine, keyword, page)` 在 7 天内只允许调一次 API(search_cache 表) - 调用前必须通过 quota 中心检查(见第十三章) **可配置**:`config.yaml` 里 `search.provider = brave | serper | bing | ddg`。 --- ## 五、采集插件 B:TG 频道采集(tg_channel,P1) **M1 网页插件稳定运行 2 周以上再启动这个。** ### 数据流 ``` seed_channels → [tgpool 拿账号] → 进频道 → 读历史消息(最近 500 条) │ 每条消息:正则快扫 + 关键词预筛 │ ▼ 触发条件 → AI 精确提取 │ AI 结果 → 回源校验 │ ▼ emit → merchants_raw ``` ### 详细逻辑 1. 从 `seed_channels` 表拿 status=pending 的频道 2. `tgpool.Acquire()` 拿一个可用的 TG 账号 3. ResolveUsername 前先查 `channels.channel_id` 缓存 4. 读最近 500 条消息,断点续传记在 `channels.last_message_id` 5. 消息预筛: - 系统消息 / 非中文 / 长度 < 10 → 跳过 - 正则扫 `@\w+` / `t.me/\w+` / 邮箱 / 电话 — 命中任一才进下一步 6. AI 提取(调 DeepSeek / GLM): - 提示词要求结构化 JSON 输出 - 超时 5 秒 / 失败 → 正则兜底 7. **AI 结果校验(修正 v2 的"正则回原文精确匹配")**: ``` 校验规则:取 AI 输出的每个联系方式(username / email / phone), 去掉标点、空白、@、+86 等前缀,提取核心 token, 去原文里做"去格式化后的子串匹配" 命中即接受;否则丢弃 例:AI 输出 "@abc_123",原文 "加 V:abc_123 / 加Q:456" token = "abc_123" → 子串匹配 → 接受 ``` 8. 通过校验的联系方式组装 MerchantData → emit --- ## 六、其他采集插件概要 本章列出 M2/M3 阶段要做的 6 个补充插件。所有插件共用第七章的 shared 基础设施,统一实现 `Collector` 接口,所以"新增一个维度 = 新建一个目录"。本章只给要点,详细实现在对应里程碑启动时展开。 ### 6.1 web_directory — 导航站主动爬取(P0,M1) **为什么**:web_search 烧 API 额度,但大型导航站列表是已知的(airportlist.top、vpn.nav.vip 等),直接爬比搜索更高效完整。 **流程**: 1. 维护导航站白名单表 `directory_whitelist` 2. 每个站点配置抓取规则(URL 模板、列表页选择器、卡片 CSS) 3. 按 cron 定时抓(默认每天一次) 4. 用 shared/httpclient 抓页面,goquery 按规则解析 5. 新商户 → emit;已知商户 → 更新 last_seen_at **维护成本**:规则失效监控(网站改版会导致选择器失效,需要每日产出量监控告警)。 ### 6.2 tg_snowball — TG 滚雪球(P1,M2) **为什么**:种子频道只有少数几个,但 TG 生态内的转发链和 @mention 链能发现大量新频道。 **流程**: 1. 扫描已采集的 TG 消息,提取所有 `@channel_name` 和"Forwarded from xxx" 2. 新频道自动加入 `channels` 表,source='discovered',status=pending 3. 由 tg_channel 插件继续采集 4. 每日汇总新发现频道数作为指标 **约束**:新频道加入前过滤 bot、违法关键词、非中文频道。 **复用**:不实现新的采集逻辑,只是生产新的种子给 tg_channel。 ### 6.3 github_search — GitHub 代码搜索(P2,M2) **为什么**:大量机场项目放 GitHub,README 直接写 TG 联系方式。GitHub API 免费 5000 req/hour,几乎无限制。 **流程**: 1. 用 GitHub Code Search API 搜 `"t.me" language:Markdown`、`telegram 机场`、`VPN 订阅` 等 2. 拿 repo 列表 → 逐个读 README / description / topics 3. 正则提取 t.me 链接 4. 关联 repo metadata(star、last_push)作为活跃度信号 **产出特点**:数量不多但质量高(README 一般是项目主动填的)。 ### 6.4 forum_scraper — 论坛采集(P2,M3) **为什么**:V2EX、hostloc、Reddit 的"机场推荐"帖是高质量发现源,尤其携带用户真实评价。 **流程**: 1. 配置论坛白名单 + 每个论坛的采集规则(API 或 HTML 抓) 2. 定期抓置顶帖 + 最新主题列表 3. 帖子正文 + 评论都进 extractor 4. 用户评价作为"社区口碑"信号供 Enrichment 使用 **产出特点**:密度中等,但携带用户评价这种独家信号。 ### 6.5 cert_transparency — 证书透明日志反查(P3,M3) **为什么**:机场商户常注册多个域名(主域名被墙就换备用)。通过 SSL 证书透明日志(crt.sh)可以反查同一组织申请的其他域名。 **流程**: 1. 从已发现商户拿主域名 2. 查 crt.sh:`https://crt.sh/?q=%25example.com&output=json` 3. 提取 SAN 里的其他域名 4. 新域名丢给 web_search 做二次采集 5. 新域名若指向新 TG → emit **产出特点**:主要用于补全已知商户的备用域名,偶尔发现新商户。 ### 6.6 icp_reverse — ICP 备案号反查(P3,M3) **为什么**:中国境内合规商户需要 ICP 备案。从已知商户的 ICP 号反查同一主体备案的其他域名,能发现关联商户。 **流程**: 1. 从 Enrichment 层拿商户的 ICP 备案号 2. 查 ICP 反查 API(beianx.cn 或自建) 3. 同主体的其他域名 → 标记为 related_domains 4. 含 TG 的新域名 → emit **产出特点**:只对备案商户有效(机场类目很多不备案,覆盖率低),但一旦命中置信度高。 --- ## 七、共享基础设施(`internal/shared/`) ### 7.1 tgpool — TG 账号池 职责: - 管理多个 TG 账号的 session、健康状态、FloodWait 倒计时 - 提供 `Acquire(ctx) → Account` / `Release(acc)` 接口 - 调用端不感知限速:超限自动切号,全部账号都限速时挂起调用者 状态持久化到 `tg_accounts` 表: | 字段 | 类型 | 说明 | |---|---|---| | id | int | 主键 | | phone | VARBINARY | AES-GCM 加密存储 | | api_id | int | my.telegram.org 申请 | | api_hash | VARBINARY | 加密存储 | | session_path | string | session 文件路径 | | status | string | active / flood_wait / banned / disabled | | flood_wait_until | datetime | FloodWait 解除时间 | | resolve_count_today | int | 今日 ResolveUsername 计数 | | last_used_at | datetime | | 限速策略: - 全局请求频率 ≤ 30 次/分钟(所有账号汇总) - FloodWait < 60s → 账号原地等 - FloodWait 60–300s → 账号进 `flood_wait` 状态,切下一个 - FloodWait > 300s → 标记 `flood_wait`,调度器 5 分钟后重试 - 所有账号都不可用 → 调用方 block 等待,最长 30 分钟后报错 ### 7.2 proxypool — 代理出口池 职责: - 抽象"出口 IP"概念,插件和 httpclient 通过 `proxypool.Next()` 拿代理 - 健康检查:每 5 分钟对每个代理做一次 `GET https://example.com`,连续 3 次失败下线 - 支持三种后端: - `direct` — 不走代理(默认,方便本地开发) - `static_list` — 静态 IP 列表(YAML 配置) - `bright_data` / `iproyal` — 商用住宅代理(按流量) **day 1 只实现 `direct`,但接口必须先留好**。 ### 7.3 searchcache — 搜索结果缓存 持久化到 `search_cache` 表。见第四章"搜索 API 选择与缓存"。 ### 7.4 httpclient — 三层 fallback 客户端 ```go type Client interface { Get(ctx context.Context, url string, opts ...Option) (*Response, error) } ``` 每层通过 `proxypool.Next()` 拿出口。chromedp 层有单独并发闸门(默认 3,见第十二章)。 ### 7.5 extractor — 正则提取器 ```go package extractor func TgUsernames(text string) []string func Emails(text string) []string func Phones(text string, ctxWindow int) []string func PriceTiers(text string) []PriceTier // v3.1 Enrichment 使用 func PaymentMethods(text string) []string func ServerRegions(text string) []string ``` 纯函数、无状态、插件和处理端共用。 ### 7.6 quota — 配额中心(v3.1 新增) ```go type Quota interface { Check(resource string, amount int64) error // 超限返回 ErrQuotaExceeded Consume(resource string, amount int64) error Usage(resource string) (used, budget int64) } ``` 所有外部付费资源(search API / AI tokens / proxy 流量 / tg requests / whois 查询)调用前必须过 `quota.Check`。见第十三章。 ### 7.7 audit — 操作审计(v3.1 新增) ```go type Audit interface { Log(actor, action, targetType, targetID string, payload any) error } ``` 系统级动作都写 audit_logs 表。见第十三章。 ### 7.8 killswitch — 紧急开关(v3.1 新增) ```go type KillSwitch interface { IsEngaged(domain string) bool // domain: "collectors" / "enrichment" / "all" Engage(domain, reason, actor string) error Release(domain, actor string) error } ``` 所有插件和 Enrichment 在每一轮循环开始时查询 `IsEngaged("collectors")`,true 就优雅退出。见第十三章。 --- ## 八、Enrichment 层(商户信息丰富化) > clean 只能告诉你"这是个真商户",enriched 才能告诉你"这是个什么样的商户"。没有 Enrichment,销售拿到的列表只有 TG 号和名字,无法分档、无法个性化触达。 ### 输入 / 输出 - **输入**:`merchants_clean` 里 status=valid 的记录 - **输出**:`merchant_enrichment` 表(每个 merchant_id 一条) ### 丰富字段(15 项,分四组) **A. 官网元数据(HTTP 请求)** | 字段 | 说明 | |---|---| | `site_alive` | 官网 HTTP 2xx | | `site_ssl_days` | SSL 证书剩余天数 | | `site_title` | <title> | | `site_description` | <meta description> | | `tech_stack` | Cloudflare/Nginx/V2Board/SSPanel/Xboard 等 | **B. 域名元数据(Whois / ICP)** | 字段 | 说明 | |---|---| | `domain_registrar` | 注册商 | | `domain_registered_at` | 注册时间(越老越可信) | | `icp_beian_no` | ICP 备案号(若有) | | `icp_subject` | 备案主体名 | **C. TG 活跃度(可选,需要 tgpool)** | 字段 | 说明 | |---|---| | `tg_channel_members` | 频道成员数 | | `tg_last_message_at` | 频道最后消息时间 | | `tg_is_premium` | TG 号是否 Premium | **D. 业务画像(从官网/频道文本提取)** | 字段 | 说明 | |---|---| | `price_tiers` | 价格档位数组 ["9.9元/月", "99元/年"] | | `payment_methods` | 支付方式(支付宝/USDT/PayPal) | | `server_regions` | 节点地区(HK/JP/SG/US) | ### 流水线 ``` merchants_clean (valid) │ ├─[1] 官网 HTTP 探测 → site_* 字段 ├─[2] Whois 查询 → domain_* 字段 ├─[3] ICP 反查 → icp_* 字段 ├─[4] (可选) TG profile → tg_* 字段 ├─[5] 文本画像抽取 → price / payment / region 字段 ▼ merchant_enrichment (upsert by merchant_id) ``` ### 触发策略 | 场景 | 触发 | |---|---| | 新发现 Hot 商户 | 立即触发(同步,< 10s) | | 新发现 Warm 商户 | 延迟触发(加入 enrichment 队列,< 1h) | | 定期刷新已有商户 | 每 7 天一次(site_alive / tg_last_message_at 有时效) | | 手动触发 | 前端按钮 | ### 文本画像抽取(最值钱的部分) 价格、支付方式、节点地区是销售判断商户能不能合作的第一手信息。 **价格**:正则 `\d+(\.\d+)?\s*元[//]\s*(月|年|季|天)` + 上下文关键词("套餐/购买/订阅"),命中后取上下文 30 字作为 `price_context` 保留。 **支付方式**:字典匹配 `["支付宝","微信","USDT","PayPal","银行卡","btc","usdt","trc20","erc20","支付宝扫码"]` **地区**:字典匹配 `["香港","日本","新加坡","美国","台湾","HK","JP","SG","US","TW"]` + 旗帜 emoji `🇭🇰🇯🇵🇸🇬🇺🇸🇹🇼` **不用 AI 做这个**。AI 幻觉成本太高,规则命中率够用。只在规则完全失败时人工扩词表。 ### Enrichment 失败的降级 - HTTP 探测失败(官网被墙) → `site_alive=false`,其他字段尽力填充 - Whois 被限速 → 排队 1 小时后重试 - TG 验证拿不到 → 这一组字段 null,不影响其他字段 - 整体失败 → 商户仍保留在 merchants_clean,只是没 enrichment 数据 ### 实体聚合的触发点 Enrichment 产出的 `icp_subject` 和 `domain_registrar` 是商户实体聚合的主要依据 — 见第九章 9.4。 --- ## 九、处理端:清洗流水线 所有插件的产出进 `merchants_raw` 后统一跑清洗。 ``` merchants_raw │ ├─[1] t.me 死号预检 ← 免费、HTML 扫描 ├─[2] 黑名单 / bot 过滤 ├─[3] 去重合并 ← 按 tg_username 聚合 ├─[4] 实体聚合 ← v3.1 新增,聚合成 merchant_entity ├─[5] (可选) TG 真实性验证 ← 需要 tgpool ├─[6] 打标签 + 分等级 ▼ merchants_clean + merchant_entities ``` ### 9.1 t.me 死号预检 - HTTP GET `https://t.me/{username}`,解析返回 HTML - 用 `tgme_page_photo_image` 标记判断活号,但加**健康监控**: - 每天对 `tg_accounts` 里 1 个已知活号跑基线测试 - 基线失败(Telegram 改 HTML)→ 告警,预检结果暂时置 `unknown` - 并发 10,通过 `shared/httpclient` 第一层即可 - 结果写回 `merchants_clean.is_alive` ### 9.2 黑名单 / bot 过滤 | 规则 | 处理 | |---|---| | 用户名是系统 bot(`@BotFather`、`@SpamBot`、或以 `bot` 结尾) | status=bot | | 用户名形如邀请链接哈希(16–24 位纯随机) | status=invalid | | 原始文本完全无中文 且 无英文商业关键词 | status=invalid | ### 9.3 去重合并 - 按 `tg_username` 聚合 - **raw 入库去重键(修正 v2)**:`UNIQUE(tg_username, source_url_hash, fetched_date)`,fetched_date 是 MySQL 生成列 - 聚合时按信息丰富度保留最好的一条:`has_website > has_email > has_phone > none` - `source_count` 计所有聚合来源 - `all_sources` 存 JSON 数组 ### 9.4 实体聚合(v3.1 新增) **问题**:同一商户常有 3–5 个 TG 号(主号/备用号/客服号),清洗后是 3–5 条 merchants_clean 记录。销售拿到列表会重复联系。 **解法**:引入 `merchant_entities` 表,把属于同一商户的多个 clean 记录聚合成一个实体。 **聚合规则**(按优先级从高到低): | 规则 | 条件 | 置信度 | 动作 | |---|---|---|---| | R1 | 同一 `website` 域名 | High | 自动合并 | | R2 | 同一 `icp_subject` | High | 自动合并 | | R3 | 同一 `email` | Medium | 入 review_queue | | R4 | `merchant_name` 归一化后相同 | Medium | 入 review_queue | | R5 | `merchant_name` 编辑距离 ≤ 2 且 同 industry_tag | Low | 只生成建议 | **流程**: 1. 扫 merchants_clean 里 entity_id IS NULL 的记录 2. 按 R1-R5 顺序尝试匹配已有 entity 3. 命中 High → 更新 merchant.entity_id,合并联系方式到 entity 4. 命中 Medium → 写 review_queue 5. 未命中 → 新建 entity 6. 人工可以手动合并 / 拆分(前端按钮 + audit_log) **聚合结果**:一个实体包含所有成员的 TG 号、邮箱、电话、官网(去重合并),level 取成员最高级。 ### 9.5 可选:TG 真实性验证 只在 `tgpool` 可用时执行: - `ResolveUsername` 验证账号真实性 - 拿到:显示名、是否 Premium、成员数 - 写入 `merchants_clean.tg_profile`(JSON) - 失败不致命:清洗继续 ### 9.6 打标签 + 分等级 **行业标签**:关键词匹配(name + original_text 里含"机场 / 节点 / 订阅 / VPN / 科学上网" → 机场标签)。 **等级(3 桶)**: | 等级 | 条件 | |---|---| | **Hot** | 行业匹配 + `is_alive=true` + (有 website 或 email) + source_count ≥ 2(v3.1 提高门槛) | | **Warm** | 行业匹配 + `is_alive=true` + 只有 TG 号,或 source_count=1 | | **Cold** | 行业不匹配 / 死号 / 信息太少 / SourceType=web_casual | 等级在实体聚合后重算:实体 level = 成员中最高的那个。 --- ## 十、数据模型(15 张表) ### 表 1:keywords — 关键词 | 字段 | 类型 | 说明 | |---|---|---| | id | int | 主键 | | keyword | varchar(128) | 搜索关键词 | | industry_tag | varchar(32) | | | enabled | bool | | | created_at | datetime(3) | | ### 表 2:seed_channels — TG 种子频道 | 字段 | 类型 | 说明 | |---|---|---| | id | int | 主键 | | username | varchar(64) | UNIQUE | | industry_tag | varchar(32) | | | enabled | bool | | | note | varchar(256) | | ### 表 3:channels — TG 频道元数据 + 断点 | 字段 | 类型 | 说明 | |---|---|---| | id | int | 主键 | | username | varchar(64) | UNIQUE | | channel_id | bigint | 缓存 | | access_hash | bigint | 缓存 | | status | varchar(16) | pending / scraped / skipped / error | | last_message_id | int | 断点续传 | | merchants_found | int | | | source | varchar(16) | seed / discovered | | created_at | datetime(3) | | ### 表 4:tg_accounts — TG 账号 见 7.1。`phone` / `api_hash` 用 VARBINARY 加密存储。 ### 表 5:merchants_raw — 原始商户 | 字段 | 类型 | 说明 | |---|---|---| | id | bigint | 主键 | | tg_username | varchar(64) | **必填** | | tg_link | varchar(128) | | | merchant_name | varchar(128) | | | website | varchar(512) | | | email | varchar(128) | | | phone | varchar(32) | | | source_type | varchar(32) | web_search / web_directory / tg_channel / ... | | source_name | varchar(128) | | | source_url | varchar(512) | | | source_url_hash | char(64) | GENERATED AS SHA2(source_url,256) STORED | | original_text | text | | | industry_tag | varchar(32) | | | status | varchar(16) | raw / processing / done | | canary | bool | v3.1 灰度标记 | | fetched_at | datetime(3) | | | fetched_date | date | GENERATED AS DATE(fetched_at) STORED | | created_at | datetime(3) | | **入库去重**:`UNIQUE KEY uk_raw_dedup (tg_username, source_url_hash, fetched_date)` **留存策略**:超过 `raw_retention_days`(默认 90)的 done 记录归档到 `merchants_raw_archive`,或用 RANGE 分区表按月 DROP PARTITION。 ### 表 6:merchants_clean — 清洗后商户 | 字段 | 类型 | 说明 | |---|---|---| | id | bigint | 主键 | | entity_id | bigint | FK → merchant_entities,v3.1 新增 | | tg_username | varchar(64) | UNIQUE | | tg_link | varchar(128) | | | merchant_name | varchar(128) | | | website | varchar(512) | | | email | varchar(128) | | | phone | varchar(32) | | | source_count | int | | | all_sources | json | | | industry_tag | varchar(32) | | | level | varchar(8) | Hot / Warm / Cold | | status | varchar(16) | valid / invalid / bot / duplicate | | is_alive | bool | | | tg_profile | json | | | last_checked_at | datetime(3) | | | created_at | datetime(3) | | `INDEX idx_entity_id (entity_id)` / `INDEX idx_level (level)` ### 表 7:search_cache — 搜索 API 缓存 | 字段 | 类型 | 说明 | |---|---|---| | id | bigint | 主键 | | engine | varchar(16) | brave / serper / bing / ddg | | keyword | varchar(128) | | | page | int | | | result_json | longtext | 原始响应 | | fetched_at | datetime(3) | | | expires_at | datetime(3) | | `UNIQUE KEY (engine, keyword, page)` ### 表 8:task_logs — 任务日志 | 字段 | 类型 | 说明 | |---|---|---| | id | bigint | 主键 | | task_type | varchar(32) | | | task_instance | varchar(64) | | | plugin_name | varchar(32) | | | status | varchar(16) | running / success / failed / stopped / paused | | items_processed | int | | | merchants_added | int | | | errors_count | int | | | started_at | datetime(3) | | | finished_at | datetime(3) | | | detail | text | | ### 表 9:eval_samples — 抽检样本(可选) | 字段 | 类型 | 说明 | |---|---|---| | id | bigint | 主键 | | merchant_id | bigint | FK → merchants_clean | | sample_round | varchar(32) | | | is_true_positive | bool | | | note | text | | | reviewed_at | datetime(3) | | ### 表 10:merchant_entities — 商户实体(v3.1 新增) 见 9.4。 | 字段 | 类型 | 说明 | |---|---|---| | id | bigint | 主键 | | primary_name | varchar(128) | | | primary_website | varchar(512) | | | primary_tg_username | varchar(64) | | | all_tg_usernames | json | | | all_emails | json | | | all_phones | json | | | industry_tag | varchar(32) | | | source_count | int | 去重后来源数 | | level | varchar(8) | 实体级别 | | merged_clean_ids | json | 聚合的 clean ID | | note | text | 人工备注 | | created_at | datetime(3) | | | updated_at | datetime(3) | | ### 表 11:merchant_enrichment — 丰富化数据(v3.1 新增) 见第八章。 | 字段 | 类型 | 说明 | |---|---|---| | id | bigint | 主键 | | merchant_id | bigint | FK → merchants_clean,UNIQUE | | entity_id | bigint | FK → merchant_entities(可选) | | site_alive | bool | | | site_ssl_days | int | | | site_title | varchar(512) | | | site_description | text | | | tech_stack | json | | | domain_registrar | varchar(128) | | | domain_registered_at | date | | | icp_beian_no | varchar(64) | | | icp_subject | varchar(256) | | | tg_channel_members | int | | | tg_last_message_at | datetime(3) | | | tg_is_premium | bool | | | price_tiers | json | | | payment_methods | json | | | server_regions | json | | | last_enriched_at | datetime(3) | | ### 表 12:directory_whitelist — 导航站白名单(v3.1 新增) 见 6.1。 | 字段 | 类型 | 说明 | |---|---|---| | id | int | 主键 | | name | varchar(64) | | | url_template | varchar(512) | 列表页模板,支持 `{page}` | | selector_rules | json | CSS 选择器 | | max_pages | int | | | cron_schedule | varchar(32) | | | enabled | bool | | | last_run_at | datetime(3) | | | last_merchants_found | int | | ### 表 13:quota_usage — 配额使用统计(v3.1 新增) 见第十三章。 | 字段 | 类型 | 说明 | |---|---|---| | id | bigint | 主键 | | resource | varchar(32) | search_brave / search_serper / ai_tokens / proxy_bytes / tg_requests / whois_queries | | period | varchar(8) | day / month | | period_start | date | | | used | bigint | | | budget | bigint | | | last_updated_at | datetime(3) | | `UNIQUE KEY (resource, period, period_start)` ### 表 14:audit_logs — 操作审计(v3.1 新增) 见第十三章。 | 字段 | 类型 | 说明 | |---|---|---| | id | bigint | 主键 | | actor | varchar(64) | 用户名 / system | | action | varchar(64) | start_task / stop_task / update_config / merge_entity / kill_switch / export | | target_type | varchar(32) | | | target_id | varchar(64) | | | payload | json | 改动前后详情 | | created_at | datetime(3) | | `INDEX idx_actor (actor)` / `INDEX idx_action (action)` / `INDEX idx_created_at (created_at)` ### 表 15:review_queue — 人工审核队列(v3.1 新增) 见第十三章。 | 字段 | 类型 | 说明 | |---|---|---| | id | bigint | 主键 | | queue_type | varchar(32) | hot_publish / entity_merge / low_conf_dedup / canary_review | | payload | json | 待审项的全部数据 | | status | varchar(16) | pending / approved / rejected | | assignee | varchar(64) | | | decided_at | datetime(3) | | | note | text | | | created_at | datetime(3) | | `INDEX idx_queue_status (queue_type, status)` --- ## 十一、外部依赖 ### 技术栈 | 层 | 选型 | 说明 | |---|---|---| | 后端语言 | Go | 高并发、单二进制部署 | | Web 框架 | Gin / Echo | | | ORM | GORM | | | 数据库 | **MySQL 8.0+** | InnoDB 引擎,utf8mb4 字符集 | | TG 客户端 | gotd/td | 原生 MTProto | | HTML 解析 | goquery | | | HTTP 客户端 | net/http + colly + utls + chromedp | 三层 fallback | | 前端 | Vue 3 + Vite + TypeScript | | | 配置 | YAML + viper | | | 日志 | zerolog / zap | 结构化 | | 指标 | prometheus client_golang | 第十四章用 | | 定时任务 | robfig/cron | 插件 schedule | ### 外部服务与成本(建议预算) | 服务 | 用途 | 最低预算 | |---|---|---| | 搜索 API(Brave/Serper/Bing) | 关键词搜索 | $10–30/月 | | 代理池(住宅代理) | 网页抓取出口 | $50–200/月(day 1 可不开) | | AI API(DeepSeek/GLM) | TG 消息提取 | $10–20/月 | | Whois API | Enrichment 域名查询 | $5–10/月 | | ICP 反查 API | Enrichment 备案查询 | $5–10/月(国内) | | GitHub API | github_search 插件 | 免费 | | 服务器 | chromedp + DB | 2 vCPU / 4 GB 起 | ### AI 使用策略 | 环节 | 方法 | 说明 | |---|---|---| | 网页联系方式提取 | 纯正则 | 不用 AI | | TG 消息联系方式提取 | 正则预筛 + AI 精提取 + 去格式化子串校验 | 唯一用 AI 的地方 | | 行业分类 | 纯关键词匹配 | | | 导航站识别 | 纯规则 | | | Enrichment 文本画像 | 纯正则 + 字典匹配 | 不用 AI | ### MySQL 建表要点(必读) v3 选 MySQL,建表时必须规避以下坑: **1. 字符集强制 utf8mb4** ```sql CREATE DATABASE tg_scraper DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; ``` 每张表显式加 `DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`。TG 消息含 emoji 和四字节汉字,utf8mb3 会报错。 **2. 索引长度限制** InnoDB 单列索引 key ≤ 3072 字节,utf8mb4 下单列 VARCHAR 索引 ≤ 768 字符。长列用前缀索引或 SHA-256 哈希列代替。 **3. raw 去重键的实现** ```sql CREATE TABLE merchants_raw ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, tg_username VARCHAR(64) NOT NULL, source_url VARCHAR(512) NOT NULL, source_url_hash CHAR(64) GENERATED ALWAYS AS (SHA2(source_url, 256)) STORED, fetched_at DATETIME(3) NOT NULL, fetched_date DATE GENERATED ALWAYS AS (DATE(fetched_at)) STORED, ... PRIMARY KEY (id), UNIQUE KEY uk_raw_dedup (tg_username, source_url_hash, fetched_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` **4. 时间列统一 UTC** ```sql created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) ``` 应用层和 MySQL 都配置 `time_zone='+00:00'`。 **5. 大文本列类型** - `original_text` → TEXT - `all_sources`、`price_tiers`、`selector_rules` 等 JSON 字段 → 用 MySQL 8 的 `JSON` 类型 - `result_json` → LONGTEXT **6. 分区表(上量后启用)** `merchants_raw` 按 `fetched_date` RANGE 分区,到期 DROP PARTITION 比 DELETE 快几个数量级。 **7. GORM + MySQL DSN** ``` user:pass@tcp(host:3306)/tg_scraper?charset=utf8mb4&parseTime=True&loc=UTC&time_zone=%27%2B00%3A00%27 ``` **8. 连接池** ```go sqlDB.SetMaxOpenConns(50) sqlDB.SetMaxIdleConns(10) sqlDB.SetConnMaxLifetime(time.Hour) ``` **9. 敏感字段加密** `tg_accounts.phone`、`tg_accounts.api_hash` 应用层 AES-GCM 加密后存 `VARBINARY(255)`。 --- ## 十二、并发与资源限额 统一在 `config.yaml` 里声明,跨模块生效。 ```yaml concurrency: http_layer1_colly: 50 # 标准 HTTP http_layer2_utls: 20 # 绕 Cloudflare http_layer3_chrome: 3 # Headless Chrome(内存敏感,硬上限) tme_precheck: 10 # t.me 死号预检 search_api_qps: 1 # 搜索 API 串行化 tg_global_per_min: 30 # tgpool 全局 QPS ai_extract: 5 # AI 提取并发 enrichment: 10 # Enrichment 流水线并发 task: max_per_instance: web_search: 2 web_directory: 1 tg_channel: 1 tg_snowball: 1 github_search: 1 forum_scraper: 1 clean: 1 enrichment: 2 memory: chrome_page_max_mb: 400 retention: raw_days: 90 search_cache_days: 7 task_logs_days: 180 audit_logs_days: 365 ``` **关键**:chromedp 并发 3 是硬上限。 --- ## 十三、可控性与配额管理(v3.1 新增) > **目标**:任何时候都能回答三个问题 — 系统现在在做什么?花了多少钱?能不能立刻停? ### 13.1 配额中心 (quota_usage) 所有外部资源消耗走统一配额中心: | 资源 | 单位 | 周期 | 示例上限 | |---|---|---|---| | search_brave | 次数 | 月 | 5000 | | search_serper | 次数 | 月 | 50000 | | search_bing | 次数 | 月 | 10000 | | ai_tokens | tokens | 月 | 1000000 | | proxy_bytes | 字节 | 月 | 50 GB | | tg_requests | 次数 | 日 | 1000 | | whois_queries | 次数 | 月 | 1000 | | icp_queries | 次数 | 月 | 1000 | 调用点在发起请求前先 `quota.Check(resource, amount)`,超限返回错误;调用后 `quota.Consume(resource, amount)` 累加。状态持久化到 `quota_usage` 表。 ### 13.2 熔断策略 | 用量 | 状态 | 行为 | |---|---|---| | < 80% | 正常 | 无限制 | | 80–95% | 告警 | 日志 warn + 前端黄灯 + Bark/Slack 通知 | | 95–100% | 降级 | 停止该资源的新任务,已跑任务继续 | | = 100% | 熔断 | 全部拒绝,触发硬停 | 熔断解除:手动(前端按钮或 API),必须写 audit_logs。 ### 13.3 任务生命周期 ``` pending → running → (paused ↔ running) → success ↘ failed ↘ stopped(人工) ``` 每个任务具备: - **优先级** 0-9(数字大优先,默认 5) - **可暂停** pause 时保留已抓进度 - **可恢复** 从 checkpoint 恢复,不重跑 - **可取消** stopped 状态不能恢复 - **实例隔离** 同 task_type 可多实例并行(见十二章) ### 13.4 插件开关与独立配置 每个插件在 config.yaml 里有独立开关: ```yaml plugins: web_search: enabled: true canary: false schedule: "0 */6 * * *" quota_profile: standard web_directory: enabled: true canary: false schedule: "0 3 * * *" tg_channel: enabled: false canary: true quota_profile: tg_strict tg_snowball: enabled: false github_search: enabled: false forum_scraper: enabled: false cert_transparency: enabled: false icp_reverse: enabled: false ``` **热加载**:改 config.yaml 发 SIGHUP 不用重启。 ### 13.5 Canary 模式(灰度发布) 新插件上线第一次默认 canary=true: - 只处理前 1% 数据(比如只跑 1 个关键词,或只抓 1 个导航站) - 产出写 `merchants_raw` 但 `canary=true` - 清洗阶段 canary 数据不进 clean 表,进 review_queue(queue_type=canary_review) - 人工确认无误 → 改 `canary=false` → 全量跑 ### 13.6 人工审核队列 (review_queue) 四种审核场景: | 队列类型 | 触发 | 审核动作 | |---|---|---| | **hot_publish** | 新商户分级为 Hot | approve → 进 clean;reject → 进 archive | | **entity_merge** | Medium 置信度实体合并建议 | approve → 合并;reject → 保持独立 | | **low_conf_dedup** | 去重有歧义 | approve / reject | | **canary_review** | canary 插件产出 | approve → 插件全量;reject → 回滚配置 | **非阻塞**:hot_publish 审核可关(`config.review.hot_publish_required = false`),默认开启。 ### 13.7 操作审计 (audit_logs) 以下动作强制写 audit: - 启动 / 停止 / 暂停 / 恢复 任务 - 修改任何配置 - 合并 / 拆分 merchant_entity - 触发紧急开关 - 手动调整 quota 配额 - 导出数据(含 merchant_count) - 审核队列决定 日志不可删,只能归档。 ### 13.8 紧急开关(Kill Switch) 配置:`kill_switch.collectors = true` 一瞬间停所有采集插件。清洗和 Enrichment 不停(可独立设 `kill_switch.enrichment`)。 触发场景: - 收到合规问询 - 代理流量突增疑似被当爬虫 - 目标站返回蜜罐数据 - 配额严重超支 前端有大红按钮。触发后写 audit_logs 并发告警。 ### 13.9 可观测性最小集(/dashboard 页面) 前端 dashboard 必须展示: 1. **各资源本月配额用量**(进度条 + 黄/红告警色) 2. **各插件今日产出 merchants_added 趋势**(折线图) 3. **当前运行中任务列表**(task_type / instance / 进度 / 操作) 4. **最近 24h 错误率**(分插件) 5. **紧急开关状态**(大按钮) 6. **最近 10 条 audit_logs** 7. **review_queue 待审数**(hot_publish / entity_merge / canary) 8. **实体去重后的商户总数**(按 level 分组) 这 8 条是"系统在不在健康运行"的最低信息。 --- ## 十四、评估与监控 ### 14.1 定期抽检(人工,1 小时/周) 每周从 `merchant_entities` 按分层抽样: - Hot 抽 20 条 - Warm 抽 20 条 - Cold 抽 10 条 写入 `eval_samples` 表,人工打 `is_true_positive`。聚合指标: - **Precision** - **Hot 级精确率目标 ≥ 85%** - **Warm 级精确率目标 ≥ 60%** 精确率跌破阈值 → 触发规则审查。 ### 14.2 召回率侧估算 维护"已知商户黄金集"(人工收集 30–50 个真实商户作为 ground truth),每次跑完全链路检查命中比例。命中率 < 50% → 关键词 / 导航站覆盖不够。 ### 14.3 运行时监控指标(Prometheus) | 指标 | 说明 | 告警阈值 | |---|---|---| | `search_api_calls_total{engine}` | 搜索 API 调用数 | 接近月额度 | | `search_cache_hit_ratio` | 搜索缓存命中率 | < 60% | | `http_fallback_layer_total{layer}` | 三层 fallback 各自命中数 | layer3 > 20% | | `chrome_page_oom_total` | chromedp OOM 次数 | > 0 | | `tg_flood_wait_total` | FloodWait 次数 | 持续上升 | | `tme_precheck_baseline_fail` | 死号检测基线失败 | > 0 | | `merchants_added_per_day{plugin}` | 每日新增 | 连续 3 天 0 | | `entity_merged_per_day` | 实体合并数 | 突增/突降 | | `enrichment_failure_rate{step}` | Enrichment 各步失败率 | > 30% | | `quota_usage_ratio{resource}` | 配额使用率 | > 80% 告警、> 95% 熔断 | | `review_queue_pending{type}` | 审核待办数 | > 100 积压 | --- ## 十五、前端(只做 3 个页面 + 认证) ### 页面 1:Dashboard(v3.1 新增) 见 13.9。 ### 页面 2:商户实体列表 - 显示 `merchant_entities` 的数据(按实体而非 clean 记录) - 按等级筛选(Hot / Warm / Cold) - 按行业 / 来源维度数 / 技术栈 / 支付方式 / 节点地区筛选 - 搜索(按名字、TG 号、域名) - 排序(发现时间 / 来源维度数 / 最后活跃时间) - 点击进详情页:展示所有 TG 号、所有域名、enrichment 画像、来源列表 - **手动合并 / 拆分**按钮(写 audit_logs) - 导出 CSV / Excel ### 页面 3:任务与审核管理 - 启动 / 暂停 / 恢复 / 停止 任务 - 任务历史与错误详情 - 审核队列(hot_publish / entity_merge / canary) - 配额中心(每资源当月用量 + 调整按钮) - 紧急开关 ### 认证 - 存个人信息,**必须认证** - 单用户 basic auth 起步(env 里存 bcrypt hash) - 扩多人时换 JWT - 所有 API 路由强制登录 - 权限分层:`admin`(配置+紧急开关)/ `operator`(启停任务+审核)/ `viewer`(只读) --- ## 十六、运行方式 ### 单插件运行 每个插件可独立跑: - `go run ./cmd/server --plugin=web_search --dry-run` - 只跑清洗:`go run ./cmd/server --task=clean` - 只跑 Enrichment:`go run ./cmd/server --task=enrichment` ### 全链路运行 `web_search → web_directory → clean → entity_merge → enrichment` 由任务调度器串起来。cron 触发。 ### 任务控制 - 状态:running / paused / success / failed / stopped - 同 task_type 支持 N 个 task_instance 并行 - 测试模式:`--dry-run` 跑少量数据不写库 - 所有任务启停写 audit_logs --- ## 十七、踩过的坑 ### v2 原有(保留) 1. **TG 限速是最大坑** — channel_id + access_hash 必须缓存 2. **t.me 死号可以免费检测** — 扫描 HTML 里的头像标记 3. **AI 会编造联系方式** — 必须回源校验 4. **raw/clean 分开存** — 允许反复清洗 5. **非中文直接跳过** — 但注意 JS 渲染站的误伤 6. **HTTP 抓取要有 fallback** — 但并发必须有上限 7. **不要一上来就全链路** — 先把一个插件做透 ### v3 新增 8. **搜索 API 免费额度会一周耗光** — 必须有 search_cache 9. **chromedp 是内存杀手** — 单页峰值 300 MB,并发 3 是硬上限 10. **电话正则离上下文关键词不超过 20 字符** — 否则 QQ 号全进来 11. **AI 验证用去格式化子串匹配** — 不用精确匹配 12. **tgme_page_photo_image 是未文档化的 HTML 标记** — 每天跑基线测试 13. **raw 去重键要带日期维度** — 跨天同源不被吞 14. **shared/ 层是解决"应该隔离但实际需要共享"的唯一干净解法** ### v3.1 新增 15. **单维度采集漏网严重** — 同一商户常只出现在特定平台,多维度互补是提高召回率的关键 16. **实体聚合必须先做再打等级** — 如果在聚合前打等级,同商户的 3 个 TG 号会被分别评级,数据重复 17. **Enrichment 失败必须降级而非整条删除** — 官网被墙很常见,不该因此丢掉有价值的 TG 商户 18. **canary 必须默认开启** — 新插件第一次跑常有问题,canary 能避免污染 clean 表 19. **配额中心必须在请求前检查** — 在请求后检查等于事后算账,账单已经爆了 20. **kill switch 要在插件主循环每一轮查询** — 不能只在任务启停时查,否则长任务停不下来 21. **审计日志不可删除** — 合规审计要求,只能按时间归档 22. **Hot 级别门槛提到 source_count ≥ 2** — 单来源商户大概率是伪造或低质量 23. **价格 / 支付方式 / 节点地区用规则而非 AI** — Enrichment 场景规则命中率够用,AI 幻觉成本高 --- ## 附录 A:目录结构(Go) ``` tg-lead-scraper/ ├── cmd/ │ └── server/main.go │ ├── internal/ │ ├── plugin/ │ │ ├── interface.go │ │ └── registry.go │ │ │ ├── plugins/ # 采集插件(零依赖) │ │ ├── websearch/ # M1 │ │ ├── webdirectory/ # M1 │ │ ├── tgchannel/ # M2 │ │ ├── tgsnowball/ # M2 │ │ ├── githubsearch/ # M2 │ │ ├── forumscraper/ # M3 │ │ ├── certtransparency/ # M3 │ │ └── icpreverse/ # M3 │ │ │ ├── shared/ # 共享基础设施 │ │ ├── tgpool/ │ │ ├── proxypool/ │ │ ├── searchcache/ │ │ ├── httpclient/ │ │ │ ├── layer1_colly.go │ │ │ ├── layer2_utls.go │ │ │ └── layer3_chrome.go │ │ ├── extractor/ │ │ ├── quota/ # v3.1 │ │ ├── audit/ # v3.1 │ │ └── killswitch/ # v3.1 │ │ │ ├── processor/ # 处理端 │ │ ├── pipeline.go │ │ ├── tmechecker.go │ │ ├── blacklist.go │ │ ├── dedup.go │ │ ├── entity.go # v3.1 实体聚合 │ │ ├── tgverify.go │ │ └── tagger.go │ │ │ ├── enrichment/ # v3.1 丰富化层 │ │ ├── pipeline.go │ │ ├── site_probe.go │ │ ├── whois.go │ │ ├── icp.go │ │ ├── tg_profile.go │ │ └── profile_extract.go # 价格/支付/地区 │ │ │ ├── review/ # v3.1 审核队列 │ │ └── queue.go │ │ │ ├── model/ │ │ ├── merchant.go │ │ ├── entity.go # v3.1 │ │ ├── enrichment.go # v3.1 │ │ ├── channel.go │ │ ├── keyword.go │ │ ├── seed_channel.go │ │ ├── tg_account.go │ │ ├── search_cache.go │ │ ├── directory.go # v3.1 │ │ ├── quota.go # v3.1 │ │ ├── audit.go # v3.1 │ │ ├── review.go # v3.1 │ │ ├── tasklog.go │ │ └── eval_sample.go │ │ │ ├── store/ │ │ ├── db.go │ │ └── *_repo.go │ │ │ ├── task/ │ │ ├── manager.go │ │ └── scheduler.go # v3.1 cron + 优先级 │ │ │ └── eval/ │ └── sampler.go │ ├── api/ │ ├── server.go │ ├── handler/ │ │ ├── merchant.go │ │ ├── entity.go # v3.1 │ │ ├── task.go │ │ ├── review.go # v3.1 │ │ ├── quota.go # v3.1 │ │ └── dashboard.go # v3.1 │ └── middleware/ │ ├── auth.go │ └── audit.go # v3.1 自动写审计 │ ├── frontend/ # Vue 3(dashboard + 实体列表 + 任务与审核) │ ├── config/ │ └── config.yaml │ ├── go.mod / go.sum / Makefile / Dockerfile ``` **依赖方向**: - `plugins/* → shared/*` - `processor → shared/*` - `enrichment → shared/*` - `processor ⊥ plugins`(永远不 import) - `enrichment ⊥ plugins` - `api → processor / enrichment / task / review` --- ## 附录 B:完整数据流图 ``` ┌──────────────── 采集端(8 个插件) ──────────────────┐ │ │ │ keywords → [web_search] ┐ │ │ → [web_directory]┤ │ │ │ │ │ seed → [tg_channel] ├──→ merchants_raw ─┐ │ │ → [tg_snowball] │ │ │ │ │ │ │ │ → [github] │ │ │ │ → [forum] │ │ │ │ → [cert_trans] │ │ │ │ → [icp_reverse] ┘ │ │ └────────────────────────────────────────────────┼─────┘ │ ▼ ┌──────────────── 处理端 ──────────────────────────────┐ │ 死号预检 → 黑名单 → 去重 → 实体聚合 → TG验证 → 标签 │ │ │ │ │ ▼ │ │ merchants_clean + merchant_entities│ └──────────────────────────┬───────────────────────────┘ │ ▼ ┌──────────────── 丰富化 ──────────────────────────────┐ │ HTTP探测 / Whois / ICP / TG profile / 文本画像 │ │ │ │ │ ▼ │ │ merchant_enrichment │ └──────────────────────────┬───────────────────────────┘ │ ┌───────────────┴───────────────┐ ▼ ▼ 前端:dashboard 评估:抽检 + 黄金集 前端:实体列表 监控:Prometheus 前端:任务与审核 审计:audit_logs 配额:quota_usage 熔断:kill_switch ``` --- ## 附录 C:决策日志 | # | 旧做法 | 现做法 | 原因 | 引入版本 | |---|---|---|---|---| | 1 | 5 张表 | 15 张表 | 持久化实体、enrichment、配额、审计、审核 | v3→v3.1 | | 2 | 种子频道塞 keywords 表 | 拆 seed_channels | 两种语义共用一表 | v3 | | 3 | 插件零依赖 / 清洗可调 TG 验证 | 抽 shared/ 层 | 解决隔离与共享矛盾 | v3 | | 4 | 搜索 API 无缓存 | search_cache + quota | 免费额度一周耗光 | v3 | | 5 | HTML 前 3000 字无中文就跳过 | 比例法 + JS 渲染后复判 | 避免 JS 站误杀 | v3 | | 6 | 导航站 >5 个 t.me | 多条件充分集 | 避免评论区灌水 | v3 | | 7 | 电话正则无上下文 | 要求附近有关键词 | 消除误匹配 | v3 | | 8 | AI 回原文精确匹配 | 去格式化子串匹配 | AI 常做格式归一 | v3 | | 9 | 未指定 chromedp 并发 | 硬上限 3 | 避免 OOM | v3 | | 10 | 未提代理 | proxypool day 1 留接口 | 上规模必用 | v3 | | 11 | 未提评估 | 分层抽检 + 黄金集 | 无指标不知好坏 | v3 | | 12 | 未提前端认证 | 强制 + 三级权限 | 个人信息合规 | v3→v3.1 | | 13 | 未提合规 | 前置确认清单 | 代码前必答 | v3 | | 14 | 未提成本 | 预算表 + 配额中心 | 钱包先炸很常见 | v3→v3.1 | | 15 | raw 去重键 (username, url) | +MySQL 生成列 fetched_date | 跨天重采不被吞 | v3 | | 16 | 未提 raw 留存 | retention_days=90 + 分区 | 避免无限增长 | v3 | | 17 | 未提 tgme HTML 基线检查 | 每日基线 + 告警 | 未文档化标记可能变 | v3 | | **18** | **只有 2 个采集插件** | **8 个插件蓝图 + M1/M2/M3** | **单维度漏网严重** | **v3.1** | | **19** | **raw → clean 两层** | **+ Enrichment 层(15 字段)** | **clean 只能说"是真商户"** | **v3.1** | | **20** | **每条 merchants_clean 独立** | **+ merchant_entities 聚合** | **同商户多 TG 号会被重复联系** | **v3.1** | | **21** | **Hot 标准:行业匹配 + 官网/邮箱** | **+source_count ≥ 2** | **单来源大概率低质或伪造** | **v3.1** | | **22** | **无灰度** | **canary=true 默认** | **新插件易出错污染 clean 表** | **v3.1** | | **23** | **无配额中心** | **quota.Check/Consume** | **请求前检查才能防超支** | **v3.1** | | **24** | **无紧急开关** | **kill_switch 三档(collectors/enrichment/all)** | **合规问询要一键停** | **v3.1** | | **25** | **无操作审计** | **audit_logs 强制写入** | **合规审计要求** | **v3.1** | | **26** | **无审核队列** | **review_queue 四类** | **Hot 商户需人工过一眼** | **v3.1** | | **27** | **Enrichment 用 AI 抽价格** | **纯正则 + 字典** | **规则命中率够,AI 幻觉成本高** | **v3.1** |