service.xpcool.com/docs/recruitment-crawler-design.md
夏犀麟 ad4567fc01 feat: 招聘爬虫健壮性 + 通知记录增强(归档工作区留存改动)
这两批改动此前一直压在工作区未提交,本次一并归档。go build ./... 通过。

一、招聘爬虫健壮性
- 新增 internal/service/recruitment/source_config.go:
  crawl_source.config(VARCHAR(1000) JSON)的源级抓取参数解析 —— 增量窗口天数、
  单次详情页上限、最大翻页数、详情间隔限速、自定义标准词/排除词。
  全部字段可选,JSON 解析失败即回退默认值,保证历史数据零迁移可用,
  且一个源的脏配置不会拖垮整个调度。
- 新增 internal/service/recruitment/keywords.go(含 keywords_test.go 与 testdata/):
  链接筛选由「URL 形态命中 AND 文本语义命中」改为评分制 —— URL 形态加分、
  标准词加分、排除词大幅减分,达阈值即入选。原与逻辑会系统性漏抓
  「补充工作人员的通知」「公开选调公务员简章」等标题变体,且静默无报错。
- 新增 manifest/sql/recruitment/004_crawl_source_status.sql:
  crawl_source 增加「最近一次运行状态」冗余列,抓取结束时写入,
  使 Sources() 查询零 JOIN 零扫描 —— 规避原实现全表扫描只增不减的 crawl_log、
  随运行时间线性劣化的问题。幂等,可重复执行。
- internal/service/recruitment/crawler.go:在既有「静态两级 / SPA / curl 回退」
  流程上做锚点抽取与过滤、编码回退的健壮性增强。
- internal/service/recruitment/bark.go、recruitment.go:配合上述调整。

二、通知记录增强
- api/notice、internal/controller/notice、internal/service/notice、
  internal/model/{dto,entity,do}/notice.go:通知日志查询与操作扩展 ——
  批次号、状态、重试次数、来源、耗时、详情、删除、清空。
- 新增 internal/model/dto/notice_meta.go(字典选项元数据,供前端筛选器取选项)。
- 新增 manifest/sql/017_notice_log_enhance.sql:notice_log 扩展列 +
  删除/清空/详情/字典选项接口权限,幂等(ALTER 走 information_schema 判断,
  菜单 ON DUPLICATE KEY UPDATE,角色绑定 INSERT IGNORE)。

三、仓库卫生
- .gitignore 补 rc_*.log 与 rc_verify*,挡掉本地离线验证产物
  (含 30MB 的 rc_verify.exe),避免误入库。

设计依据见 docs/recruitment-crawler-design.md。

注:017 与 recruitment/004 两个 SQL 为增量迁移,DB 结构变更不自动 DDL,
生产库需手动导入。
2026-09-14 01:22:39 +08:00

273 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 招聘考试抓取推送模块 · 完善设计方案
> 模块路径`api/recruitment/``internal/controller/recruitment/``internal/service/recruitment/`
> 独立数据库`recruitment`与主库 `service` 隔离
> 文档定位本次抓取健壮性专项设计供评审后实施
---
## 现状总览
### 1.1 架构分层
```
┌─────────────────────────────────────────────────────────────┐
│ 调度层 auto_job 表(主库 service.auto_job由 job 模块驱动) │
│ ├─ recruit-crawl-hourly 每小时 触发 crawl --all │
│ └─ recruit-push-daily 每日08:00 触发 push --daily │
└───────────────────────────┬─────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 服务层 internal/service/recruitment/ │
│ crawler.go 抓取主流程(静态两级 / SPA 占位 / curl 回退)│
│ scheduler.go RegisterTasks 注册 cron 到 auto_job │
│ bark.go Bark 推送(单条 + 早报汇总) │
│ recruitment.go 查询/统计/订阅 CRUD │
└───────────────────────────┬─────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 数据层 recruitment 库 │
│ crawl_source 数据源配置source_type 1静态 2SPA 3登录 4附件
│ recruitment_info 公告主表UNIQUE fingerprint 去重) │
│ crawl_log / push_log 运行日志 │
│ push_subscription / organization │
└─────────────────────────────────────────────────────────────┘
```
### 1.2 已具备的能力
| 能力 | 实现位置 | 说明 |
|---|---|---|
| 静态列表抓取 | `genericStaticCrawl` | 列表页 详情页两级抓取 |
| WAF | `fetchHTML` | **curl 子进程优先**失败回退 Go `gclient` |
| 编码兼容 | `fetchHTML` | gbk/gb2312 utf-8 转码 |
| 去重 | `fingerprint` | `源ID + 标题 + 日期 + URL` 哈希唯一索引 |
| 跨源聚合 | `group_key` | 标题+日期归一化识别同公告多源转载 |
| 增量控制 | `crawlInfo` | `time.Since(publishDate) > 30天` 跳过 |
| 调度 | `auto_job` | 支持启停 / cron / 运行日志 |
| 推送 | `bark.go` | 订阅按地区+分类过滤支持早报汇总 |
| 观测 | `crawl_log` / `fail_count` | 记录抓取量错误连续失败次数 |
### 1.3 数据源现状`002_seed_sources.sql`
| | 类型 | 地区 | 状态 |
|---|---|---|---|
| 贵阳市人社局-人事招考 | 1 静态 | 贵阳 | **启用** |
| 贵阳市政府-人事招考 | 1 静态 | 贵阳 | **启用** |
| 贵州人事考试信息网 | 2 SPA | 省直 | 禁用 POC |
| 贵州国资央企招聘平台 | 2 SPA | 省直 | 禁用 POC |
| 贵州茅台集团 | 4 附件型 | 省直 | 禁用 POC |
> **关键结论**当前招聘聚合实际只覆盖 **2 个贵阳本地静态源**省直/央企/国企类公告完全未覆盖
---
## 问题清单
按严重度分级本次专项聚焦 **P0 / P1**
### P0-1 列表页链接过滤过严存在系统性漏抓
**位置**`crawler.go` `filterArticleAnchors`
```go
// 现状伪代码
if urlHint && textHint { // 两个条件必须同时满足才认为是公告
keep(a)
}
```
**问题**`textHint` 要求锚文本包含招聘/考录/招考/公告等词政府站的公告标题变体极多例如
- XX局关于2026年**补充**工作人员的**通知** 招聘
- XX市2026年**公开选调**公务员**简章** 无标准词
- XX单位**引进**高层次人才**启事**
- 列表页若用more分页锚点锚文本为空
**影响**漏抓是静默的`crawl_log` 只显示 fetched 变少不会报错这是当前最影响数据完整性的缺陷
**方案**改为**URL 形态命中为主文本命中放宽**的组合评分
```
score = 0
if url 匹配详情页正则(/art/、/\d{6,}\.html、/info/、content?id= then score += 2
if 文本命中标准词(招聘|考录|招考|选调|遴选|引进|人才|公告|简章|启事|通知) then score += 2
if 文本命中排除词(政策解读|常见问题|办事指南|下载中心|联系我们) then score -= 5
if 锚文本为空 或 长度<6 then score -= 2
if score >= 2 then keep(a)
```
同时把词表外置到 `crawl_source.config`不同站点可定制无需改代码
### P0-2 发布日期误取整页第一个日期
**位置**`crawler.go` `extractDetail` `firstDate(html)`
**问题**政府详情页头部常有今天是2026年9月13日 星期五`firstDate` 取全页第一个日期即取到它导致 `publish_date` 全部错成抓取当天进而
- 增量窗口判断失效永远30天内全量回抓
- 看板趋势图失真全部堆在当天
- 推送早报的今日新增虚高
**方案**按优先级分段抽取**第一个可信命中**
1. 优先 `<meta name="PubDate" / "publishdate" / "og:published_time">`
2. 其次带语义容器的正则`<div class="(time|date|pubdate|info)">...2026-09-13...`
3. 再次匹配发布时间/发布日期/日期前缀后的日期
4. 全部未命中 **置空**并在 `crawl_log.error` 日期未识别而非盲目取第一个
### P1-3 增量窗口硬编码 30
**位置**`crawler.go` `crawlInfo` `30*24*time.Hour`
**方案**读取 `crawl_source.config.incrDays`默认 30允许按源配置`Force` 手动触发时忽略该限制现有 `force` 语义保留
### P1-4 `crawl_source.config` 字段完全未被使用
**现状**表里有 `config VARCHAR(1000) COMMENT '适配器扩展配置(JSON)'`代码中**零引用**
**方案**定义并启用 `SourceConfig` 结构让该字段真正生效
```jsonc
{
"incrDays": 30, // 增量窗口天数
"listSelector": "ul.list li a", // 列表链接选择器(覆盖默认猜测)
"detailSelector": ".content", // 详情正文容器
"includeWords": ["招聘","选调"], // 覆盖默认标准词
"excludeWords": ["政策解读"], // 覆盖默认排除词
"datePatterns": ["发布时间:(\\d{4}-\\d{2}-\\d{2})"],
"maxPages": 3, // 列表分页最大翻页数
"delayMs": 800 // 详情页抓取间隔,避免过快
}
```
### P1-5 `Sources()` LIMIT 全表扫描
**位置**`recruitment.go` `Sources()`
```go
_ = dao.CrawlLog.Ctx(ctx).OrderDesc("id").Scan(&logs) // 无分页
```
**问题**`crawl_log` 只增不减每次打开数据源状态页都全量拉取到内存随运行时间线性劣化
**方案**改为**取每个源最近 1 **两种实现任选
- 子查询`WHERE id IN (SELECT MAX(id) FROM crawl_log GROUP BY source_id)`
- 或新增 `crawl_source.last_log_*` 冗余列写入时同步查询零 JOIN
推荐后者顺带解决 P1-6
### P1-6 失败无告警无自动禁用
**现状**`fail_count` 累加了**没有任何消费者**源静默失效改版/封禁不会被发现
**方案**
- 连续失败达阈值默认 5 自动置 `enabled=0`并推送一条数据源已自动禁用告警
- 抓取成功 `fail_count` 归零当前是否归零需确认应立即归零
- 可选失败达 3 次时先推送一次预警未禁用
### P2-7 调试日志残留
**位置**`crawler.go` 6 `g.Log().Warningf(ctx, "[recruit-debug] ...")`
**方案**删除或降为 `Debugf`生产日志不应被调试信息污染
### P2-8 Bark 模块英文日志翻译遗漏
**位置**`bark.go` 92 / 98 / 128 / 174 行附近`load subscriptions failed`
**方案**按项目中文优先约定统一中文化
### P2-9 `push_time` 字段是死配置
**位置**`push_subscription.push_time` vs `scheduler.go` `recruit-push-daily` 固定 `0 0 8 * * *`
**问题**订阅里设 09:30 不生效永远 08:00 推送
**方案**本次不做列入后续`recruit-push-daily` 改为每小时跑一次只处理 `push_time` 落在当前小时的订阅或用动态 cron 按订阅分时注册
---
## SPA / 附件型源接入后续阶段
本次不做但设计上预留三个禁用源的技术路径预判
| | 预判路径 | 难度 |
|---|---|---|
| 贵州人事考试信息网 | hash 路由背后通常是 `POST /api/xxx/list` 返回 JSON需抓包定位接口 | |
| 贵州国资央企招聘平台iguopin | 国聘系平台接口形态统一通常有公开列表 API | |
| 茅台集团官网 | 附件型列表页详情页PDF 附件解析 PDF 文本 | |
**建议**新增 `source_type=5 接口型``config` 存接口地址与字段映射 JSONPath 提取这样 SPA 源无需模拟浏览器直接调底层接口稳定且快
---
## 表结构变更本次仅 P1 相关
沿用不破坏现有数据原则全部为**新增列**无需数据迁移
### 4.1 `crawl_source` 新增列
```sql
ALTER TABLE `crawl_source`
ADD COLUMN `last_log_at` DATETIME NULL DEFAULT NULL COMMENT '最近一次抓取时间(冗余,避免全表扫描 crawl_log)' AFTER `fail_count`,
ADD COLUMN `last_log_fetched` INT NOT NULL DEFAULT 0 COMMENT '最近一次抓取条数(冗余)' AFTER `last_log_at`,
ADD COLUMN `last_log_new` INT NOT NULL DEFAULT 0 COMMENT '最近一次新增条数(冗余)' AFTER `last_log_fetched`,
ADD COLUMN `last_log_error` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '最近一次错误信息(冗余)' AFTER `last_log_new`;
```
### 4.2 迁移脚本
新增 `manifest/sql/recruitment/004_crawl_source_status.sql`幂等`ADD COLUMN IF NOT EXISTS` 或部署前判存在
---
## 实施计划
### 阶段一抓取健壮性本次P0 + P1
| # | 任务 | 涉及文件 |
|---|---|---|
| 1 | 链接过滤改为评分制 + 词表可配 | `crawler.go` |
| 2 | 发布日期分段抽取 + 未识别记日志 | `crawler.go` |
| 3 | 增量窗口读 `config.incrDays` | `crawler.go` |
| 4 | 启用 `SourceConfig` 结构含选择器覆盖限速 | `crawler.go`新增 `source_config.go` |
| 5 | `Sources()` 查最近日志改为冗余列 | `recruitment.go` + 表变更 |
| 6 | 失败达阈值自动禁用 + 告警推送 | `crawler.go` + `bark.go` |
| 7 | 删除调试日志`bark.go` 中文化 | `crawler.go``bark.go` |
| 8 | 迁移脚本 `004_crawl_source_status.sql` | `manifest/sql/recruitment/` |
**验收标准**
- 2 个启用源抓取条数不低于手工核对数量目标不漏抓
- `publish_date` 与页面实际发布日期一致抽样 10
- `crawl_log` 可按源查看最近一次结果
- 手动触发 `force=true` 可绕过增量窗口全量回溯
### 阶段二推送体系后续
- `push_time` 真正生效订阅分时推送
- 推送结果统计订阅级成功率
- 失败重试Bark 失败重试 2 指数退避
### 阶段三源扩展后续
- `source_type=5 接口型` + JSONPath 映射
- 接入贵州人事考试信息网国资央企平台
- 茅台附件型PDF 下载 + 文本抽取
### 阶段四运维增强后续
- `crawl_log` / `push_log` 定期归档保留 90
- 抓取质量日报新增数失败源异常波动告警
---
## 风险与注意事项
1. **抓取频率与合规**政府站对高频访问敏感`delayMs` 默认 800ms单源单次抓取控制在分钟级建议遵守 robots.txt 与站点条款
2. **词表误伤**评分制若阈值过低会引入噪声招聘会预告需先用真实列表页离线验证再上线
3. **日期置空的影响**P0-2 改为未识别则置空依赖 `publish_date` 的统计会短期波动属预期此前是错误数据
4. **自动禁用需谨慎**阈值过低会因偶发网络抖动误禁建议结合连续失败中间成功即归零并推送告警以便人工复核
5. **表变更走迁移脚本**禁止手工改库生成代码若后续跑 `gf gen dao`需注意本模块 entity **手写**勿被覆盖