CareerNexus 职联:岗位能力图谱构建与动态演化分析平台(挑战杯 XH-202621)
- 全链路:多源JD接入/清洗交叉验证/结构化抽取(星火+离线双通道)/时序知识图谱/新岗位发现/演化分析/人岗匹配/学习路径 - WebUI 五视图:驾驶舱/图谱工作台/岗位演化/匹配(单人+批量)/学习路径 - 设置中心(config.json)/Token鉴权/SQLite状态库/语义向量通道(BGE-M3本地或联网API) - 71 个 pytest 用例;数据目录仅保留结构(.gitkeep),数据由 scripts/generate_dataset.py 复现
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
"""匹配引擎测试:评分权重、差距清单、可迁移技能、学习路径拓扑序、简历脱敏。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from app.extract.base import load_lexicon
|
||||
from app.match.engine import MatchEngine, topological_stages
|
||||
from app.match.resume import mask_text, parse_resume
|
||||
from app.models import JobDefinition, ResumeProfile
|
||||
|
||||
JOB = JobDefinition(
|
||||
job_id="job_X", title="X工程师", category="后端开发",
|
||||
required_skills=["Java", "Spring Boot", "MySQL", "Redis"],
|
||||
preferred_skills=["Docker", "Kubernetes"],
|
||||
scenarios=["电商", "金融"], expected_years=3,
|
||||
)
|
||||
|
||||
|
||||
def _engine(**weights) -> MatchEngine:
|
||||
return MatchEngine(lexicon=load_lexicon(), weights=weights or None)
|
||||
|
||||
|
||||
def test_scoring_weights_and_dimensions():
|
||||
"""四维加权:必备50/加分15/经验20/场景15。"""
|
||||
profile = ResumeProfile(
|
||||
skills=["Java", "Spring Boot", "Docker"], # 必备 2/4,加分 1/2
|
||||
years=1.5, # 经验 1.5/3 = 0.5
|
||||
scenarios=["电商"], # 场景 1/2
|
||||
)
|
||||
r = _engine().match(profile, JOB)
|
||||
assert r.dimension_scores == {"required": 50.0, "preferred": 50.0,
|
||||
"experience": 50.0, "scenario": 50.0}
|
||||
# 总分 = 0.5*50 + 0.15*50 + 0.2*50 + 0.15*50 = 50
|
||||
assert r.total_score == 50.0
|
||||
assert not r.is_match # < 60 阈值
|
||||
|
||||
|
||||
def test_custom_weights_override():
|
||||
profile = ResumeProfile(skills=["Java", "Spring Boot", "MySQL", "Redis"],
|
||||
years=3, scenarios=["电商", "金融"])
|
||||
r = _engine(required=1.0, preferred=0.0, experience=0.0, scenario=0.0).match(profile, JOB)
|
||||
assert r.total_score == 100.0 and r.is_match
|
||||
|
||||
|
||||
def test_gap_list_and_transferable():
|
||||
profile = ResumeProfile(skills=["Java", "Spring", "MySQL"], years=3,
|
||||
scenarios=["电商", "金融"])
|
||||
r = _engine().match(profile, JOB)
|
||||
assert r.missing_required == ["Spring Boot", "Redis"]
|
||||
assert r.matched_required == ["Java", "MySQL"]
|
||||
# Spring 与缺失的 Spring Boot 同属“后端框架”→ 可迁移
|
||||
assert "Spring" in r.transferable_skills
|
||||
|
||||
|
||||
def test_learning_path_topological_order():
|
||||
"""缺失技能按前置关系拓扑分层:Spring → Spring Boot → Spring Cloud。"""
|
||||
lexicon = load_lexicon()
|
||||
stages = topological_stages(["Spring Cloud", "Spring Boot", "Spring"], lexicon)
|
||||
assert stages[0]["skills"] == ["Spring"]
|
||||
assert stages[1]["skills"] == ["Spring Boot"]
|
||||
assert stages[2]["skills"] == ["Spring Cloud"]
|
||||
# 全部缺失时拓扑序稳定且不漏项
|
||||
stages2 = topological_stages(["Kubernetes", "Docker", "Linux"], lexicon)
|
||||
flat = [s for st in stages2 for s in st["skills"]]
|
||||
assert flat == ["Linux", "Docker", "Kubernetes"]
|
||||
|
||||
|
||||
def test_learning_path_acceptance():
|
||||
"""每个阶段附带结构化验收标准:产出物/量化验收项/自测问答,且按技能类别差异化。"""
|
||||
lexicon = load_lexicon()
|
||||
stages = topological_stages(["Spring Cloud", "Spring Boot", "Spring"], lexicon)
|
||||
for st in stages:
|
||||
acc = st["acceptance"]
|
||||
assert acc["deliverable"] and len(acc["criteria"]) >= 3 and len(acc["self_check"]) >= 2
|
||||
assert all("{" not in c for c in acc["criteria"] + acc["self_check"]) # 占位符全部替换
|
||||
assert any(s in acc["deliverable"] for s in st["skills"]) # 产出物点名本阶段技能
|
||||
# 类别差异化:后端框架类与产品类的验收内容应不同
|
||||
be = topological_stages(["Spring"], lexicon)[0]["acceptance"]
|
||||
pm = topological_stages(["A/B测试"], lexicon)[0]["acceptance"]
|
||||
assert be["category"] != pm["category"] or be["criteria"] != pm["criteria"]
|
||||
|
||||
|
||||
def test_resume_masking():
|
||||
text = "姓名:王小明\n手机:13812345678\n邮箱:wxm@example.com\n3年开发经验"
|
||||
masked = mask_text(text)
|
||||
assert "王小明" not in masked and "王**" in masked
|
||||
assert "13812345678" not in masked and "138****78" in masked
|
||||
assert "wxm@example.com" not in masked and "w***@example.com" in masked
|
||||
|
||||
|
||||
def test_resume_parse_skills_and_years():
|
||||
profile = parse_resume("姓名:李四\n5年后端开发经验,熟悉Java、MySQL与Docker。")
|
||||
assert profile.years == 5.0
|
||||
assert set(profile.skills) == {"Java", "MySQL", "Docker"}
|
||||
assert "138" not in profile.raw_masked_text
|
||||
Reference in New Issue
Block a user