这两批改动此前一直压在工作区未提交,本次一并归档。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,
生产库需手动导入。
206 lines
5.8 KiB
Go
206 lines
5.8 KiB
Go
// Package recruitment 的链接评分规则单元测试。
|
||
//
|
||
// 用贵阳市人社局「人事招考」列表页的真实锚点样本做回归,防止评分规则
|
||
// 调整后再次出现「导航页被误抓」或「真实公告被漏抓」两类问题。
|
||
package recruitment
|
||
|
||
import "testing"
|
||
|
||
// TestScoreAnchor 覆盖真实列表页中的典型锚点形态。
|
||
func TestScoreAnchor(t *testing.T) {
|
||
incWords := defaultIncludeWords
|
||
excWords := defaultExcludeWords
|
||
minScore := defaultMinScore
|
||
|
||
cases := []struct {
|
||
name string
|
||
a anchor
|
||
want bool // true=应保留(公告正文),false=应过滤(导航/栏目页)
|
||
}{
|
||
// ---------- 应保留:真实公告正文 ----------
|
||
{
|
||
name: "公告正文-带文件后缀",
|
||
a: anchor{
|
||
URL: "http://rsj.guiyang.gov.cn/zfxxgk/fdzdgklm/zfxxgkrsxx/rszk/202609/t20260908_90834079.html",
|
||
Text: "2026年贵阳贵安面向退役军人选拔培养中小学教师公告",
|
||
},
|
||
want: true,
|
||
},
|
||
{
|
||
name: "公告正文-标题不含标准词但URL是正文",
|
||
a: anchor{
|
||
URL: "http://rsj.guiyang.gov.cn/zfxxgk/fdzdgklm/zfxxgkrsxx/rszk/202603/t20260303_89596144.html",
|
||
Text: "贵阳贵安2026年公开招聘事业单位工作人员岗位取消情况公告",
|
||
},
|
||
want: true,
|
||
},
|
||
{
|
||
name: "公告正文-仅URL可判定",
|
||
a: anchor{
|
||
URL: "http://rsj.guiyang.gov.cn/zfxxgk/fdzdgklm/zfxxgkrsxx/rszk/202512/t20251225_89089652.html",
|
||
Text: "关于做好2026年度考试录用公务员工作的通知",
|
||
},
|
||
want: true,
|
||
},
|
||
|
||
// ---------- 应过滤:栏目目录页(以 / 结尾) ----------
|
||
{
|
||
name: "栏目页-人事招考",
|
||
a: anchor{
|
||
URL: "http://rsj.guiyang.gov.cn/zfxxgk/fdzdgklm/zfxxgkrsxx/rszk/",
|
||
Text: "人事招考",
|
||
},
|
||
want: false,
|
||
},
|
||
{
|
||
name: "栏目页-重点领域公开",
|
||
a: anchor{
|
||
URL: "http://rsj.guiyang.gov.cn/zfxxgk/fdzdgklm/fdzdgklmzdlygk/",
|
||
Text: "重点领域公开",
|
||
},
|
||
want: false,
|
||
},
|
||
{
|
||
name: "栏目页-政府非公开信息目录",
|
||
a: anchor{
|
||
URL: "http://rsj.guiyang.gov.cn/zfxxgk/fdzdgklm/zffgkxxml/",
|
||
Text: "政府非公开信息目录",
|
||
},
|
||
want: false,
|
||
},
|
||
{
|
||
name: "栏目页-建议提案公开",
|
||
a: anchor{
|
||
URL: "http://rsj.guiyang.gov.cn/zfxxgk/fdzdgklm/jytagk/",
|
||
Text: "建议提案公开",
|
||
},
|
||
want: false,
|
||
},
|
||
{
|
||
name: "栏目页-依申请公开",
|
||
a: anchor{
|
||
URL: "http://rsj.guiyang.gov.cn/zfxxgk/ysqgk/index.html",
|
||
Text: "依申请公开",
|
||
},
|
||
want: false,
|
||
},
|
||
{
|
||
name: "导航页-政府信息公开制度",
|
||
a: anchor{
|
||
URL: "http://rsj.guiyang.gov.cn/zfxxgk/zfxxgkzd/",
|
||
Text: "政府信息 公开制度",
|
||
},
|
||
want: false,
|
||
},
|
||
{
|
||
name: "导航页-政府信息公开年报",
|
||
a: anchor{
|
||
URL: "http://rsj.guiyang.gov.cn/zfxxgk/zfxxgknb/",
|
||
Text: "政府信息 公开年报",
|
||
},
|
||
want: false,
|
||
},
|
||
{
|
||
name: "导航页-返回首页",
|
||
a: anchor{
|
||
URL: "http://rsj.guiyang.gov.cn/",
|
||
Text: "返回首页",
|
||
},
|
||
want: false,
|
||
},
|
||
{
|
||
name: "导航页-机构概况",
|
||
a: anchor{
|
||
URL: "http://rsj.guiyang.gov.cn/zfxxgk/fdzdgklm/zfxxgkjggk/",
|
||
Text: "机构概况",
|
||
},
|
||
want: false,
|
||
},
|
||
|
||
// ---------- 应过滤:友链/外部站点(URL 以 .html 结尾易误判) ----------
|
||
{
|
||
name: "友链-贵阳人才网",
|
||
a: anchor{
|
||
URL: "https://www.gyrc.cn/jobPosition/result.html",
|
||
Text: "贵阳人才网 - 贵阳市人才服务中心官方网站",
|
||
},
|
||
want: false,
|
||
},
|
||
{
|
||
name: "友链-信用中国",
|
||
a: anchor{
|
||
URL: "http://cx.guiyang.gov.cn/index.html",
|
||
Text: "信用中国(贵州贵阳)",
|
||
},
|
||
want: false,
|
||
},
|
||
{
|
||
name: "工具页-依申请公开",
|
||
a: anchor{
|
||
URL: "http://rsj.guiyang.gov.cn/zfxxgk/ysqgk/index.html",
|
||
Text: "贵阳市人力资源和社会保障局- 依申请公开",
|
||
},
|
||
want: false,
|
||
},
|
||
}
|
||
|
||
for _, c := range cases {
|
||
t.Run(c.name, func(t *testing.T) {
|
||
got := scoreAnchor(c.a, incWords, excWords, minScore)
|
||
if got != c.want {
|
||
t.Errorf("scoreAnchor() = %v, want %v\n url=%s\n text=%s",
|
||
got, c.want, c.a.URL, c.a.Text)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestCleanText 验证标题清洗:去除标签与折叠空白。
|
||
func TestCleanText(t *testing.T) {
|
||
cases := []struct{ in, want string }{
|
||
{"\n\t\t\t人事招考", "人事招考"},
|
||
{"贵阳贵安 <b>2026</b> 年招聘公告", "贵阳贵安 2026 年招聘公告"},
|
||
{" 前后空白 ", "前后空白"},
|
||
}
|
||
for _, c := range cases {
|
||
if got := cleanText(c.in); got != c.want {
|
||
t.Errorf("cleanText(%q) = %q, want %q", c.in, got, c.want)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestIsImageFileName 验证图片文件名识别(用于剔除图片型锚点噪声)。
|
||
func TestIsImageFileName(t *testing.T) {
|
||
yes := []string{"ysqgk3.png", "banner.jpg", "logo.gif", "icon.SVG", "pic.webp"}
|
||
no := []string{"", "2026年招聘公告", "index.html", "关于招聘的通知"}
|
||
for _, s := range yes {
|
||
if !isImageFileName(s) {
|
||
t.Errorf("isImageFileName(%q) = false, want true", s)
|
||
}
|
||
}
|
||
for _, s := range no {
|
||
if isImageFileName(s) {
|
||
t.Errorf("isImageFileName(%q) = true, want false", s)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestExtractAnchorsImageFallback 验证图片型锚点不会以「文件名」形式进入候选。
|
||
// 复现真实页面:<a href=".../index.html"><img src="ysqgk3.png" title="ysqgk3.png"></a>
|
||
func TestExtractAnchorsImageFallback(t *testing.T) {
|
||
html := `<html><body>
|
||
<a href="http://rsj.guiyang.gov.cn/zfxxgk/ysqgk/index.html" id='gknb'>
|
||
<img src="../../images/ysqgk3.png" alt="ysqgk3.png" title="ysqgk3.png" /></a>
|
||
</body></html>`
|
||
got := extractAnchors(html, "http://rsj.guiyang.gov.cn/")
|
||
for _, a := range got {
|
||
if isImageFileName(a.Text) {
|
||
t.Errorf("图片型锚点应以文件名泄漏进候选: text=%q url=%s", a.Text, a.URL)
|
||
}
|
||
}
|
||
// 该锚点应被完全丢弃(无可读文本)。
|
||
if len(got) != 0 {
|
||
t.Errorf("期望丢弃图片型锚点,实际保留 %d 个: %+v", len(got), got)
|
||
}
|
||
}
|