这两批改动此前一直压在工作区未提交,本次一并归档。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,
生产库需手动导入。
13 KiB
招聘考试抓取推送模块 · 完善设计方案
模块路径:
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
// 现状伪代码
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天内」,全量回抓)
- 看板趋势图失真(全部堆在当天)
- 推送早报的「今日新增」虚高
方案:按优先级分段抽取,取第一个可信命中:
- 优先
<meta name="PubDate" / "publishdate" / "og:published_time"> - 其次带语义容器的正则:
<div class="(time|date|pubdate|info)">...2026-09-13... - 再次匹配「发布时间:/发布日期:/日期:」前缀后的日期
- 全部未命中 → 置空,并在
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 结构,让该字段真正生效:
{
"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()
_ = 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 新增列
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 天);- 抓取质量日报(新增数、失败源、异常波动告警)。
六、风险与注意事项
- 抓取频率与合规:政府站对高频访问敏感,
delayMs默认 800ms,单源单次抓取控制在分钟级;建议遵守 robots.txt 与站点条款。 - 词表误伤:评分制若阈值过低会引入噪声(如「招聘会预告」),需先用真实列表页离线验证,再上线。
- 日期置空的影响:P0-2 改为「未识别则置空」后,依赖
publish_date的统计会短期波动,属预期(此前是错误数据)。 - 自动禁用需谨慎:阈值过低会因偶发网络抖动误禁。建议结合「连续」失败(中间成功即归零),并推送告警以便人工复核。
- 表变更走迁移脚本:禁止手工改库;生成代码(若后续跑
gf gen dao)需注意本模块 entity 为手写,勿被覆盖。