From caf5b106b6fd4f7f0f94f2281fa6350eab54e410 Mon Sep 17 00:00:00 2001 From: RoyirinRosmontis Date: Wed, 19 Aug 2026 12:53:45 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AD=A6=E4=B9=A0=E8=B7=AF=E5=BE=84=E5=A4=9A?= =?UTF-8?q?=E9=98=B6=E6=AE=B5=E5=8C=96=EF=BC=9A=E6=8B=93=E6=89=91=E5=B1=82?= =?UTF-8?q?=E6=8C=89=E6=AF=8F=E9=98=B6=E6=AE=B5=E2=89=A43=E6=8A=80?= =?UTF-8?q?=E8=83=BD=E6=8B=86=E5=88=86=EF=BC=8C=E9=98=B6=E6=AE=B5=E5=91=BD?= =?UTF-8?q?=E5=90=8D=E6=94=B9=E4=B8=BA=20=E5=9F=BA=E7=A1=80=E8=A1=A5?= =?UTF-8?q?=E9=BD=90=E2=86=92=E5=B2=97=E4=BD=8D=E6=A0=B8=E5=BF=83*?= =?UTF-8?q?=E2=86=92=E9=A1=B9=E7=9B=AE=E9=AA=8C=E8=AF=81=EF=BC=9B=E7=AE=80?= =?UTF-8?q?=E5=8E=86=E8=A3=B8=E5=A7=93=E5=90=8D/=E4=B8=93=E7=A1=95?= =?UTF-8?q?=E8=AF=86=E5=88=AB=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/match/engine.py | 26 ++++++++++++++++++-------- tests/test_match.py | 12 ++++++++++++ webui/app.js | 9 +++++---- 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/app/match/engine.py b/app/match/engine.py index 844936e..81c1471 100644 --- a/app/match/engine.py +++ b/app/match/engine.py @@ -27,27 +27,37 @@ def _coverage(have: set[str], want: list[str]) -> tuple[list[str], list[str], fl return matched, missing, cov -def topological_stages(missing: list[str], lexicon: dict) -> list[dict]: - """把缺失技能按前置关系拓扑排序分阶段(Kahn 分层)。 +def topological_stages(missing: list[str], lexicon: dict, + max_per_stage: int = 3) -> list[dict]: + """把缺失技能按前置关系拓扑排序分阶段(Kahn 分层 + 每层按上限再拆分)。 前置关系取自技能词表 prereqs;前置技能若已掌握或不在缺失集中则视为已满足。 + 同一拓扑层技能过多时按 max_per_stage 拆成多个连续阶段,让路线图呈 + "基础补齐 → 岗位核心 → … → 项目验证"的多阶段结构。 每个阶段附带结构化验收标准(产出物 / 量化验收项 / 自测问答),按技能类别生成。 """ prereq = {s["name"]: [p for p in s.get("prereqs", []) if p in missing] for s in lexicon["skills"] if s["name"] in missing} cat_of = {s["name"]: s.get("category", "") for s in lexicon["skills"]} remaining = set(missing) - stages: list[dict] = [] - stage_no = 1 + layers: list[list[str]] = [] while remaining: ready = sorted(s for s in remaining if not [p for p in prereq.get(s, []) if p in remaining]) if not ready: # 有环兜底:按字典序放出,保证终止 ready = sorted(remaining) - stages.append({"stage": stage_no, "skills": ready, - "goal": f"第 {stage_no} 阶段:掌握 {'、'.join(ready)}", - "acceptance": build_acceptance(ready, cat_of)}) + layers.append(ready) remaining -= set(ready) - stage_no += 1 + + # 拓扑层 → 按每阶段技能数上限拆分 + stages: list[dict] = [] + stage_no = 1 + for layer in layers: + for i in range(0, len(layer), max_per_stage): + chunk = layer[i:i + max_per_stage] + stages.append({"stage": stage_no, "skills": chunk, + "goal": f"第 {stage_no} 阶段:掌握 {'、'.join(chunk)}", + "acceptance": build_acceptance(chunk, cat_of)}) + stage_no += 1 return stages diff --git a/tests/test_match.py b/tests/test_match.py index 79ce2fc..b9aa03a 100644 --- a/tests/test_match.py +++ b/tests/test_match.py @@ -105,3 +105,15 @@ def test_resume_bare_name_and_edu_alias(): assert p.education == "硕士" # 专硕→硕士 assert p.address.startswith("辽宁大连") assert "Python" in p.skills + + +def test_learning_path_stage_chunking(): + """同层技能超 3 个时拆成多个连续阶段;拓扑序不变。""" + stages = topological_stages(["Linux", "Docker", "Kubernetes", "Python", "Java", "Go", "MySQL"], + load_lexicon()) + # 每阶段 ≤3 个技能 + assert all(len(st["skills"]) <= 3 for st in stages) + flat = [s for st in stages for s in st["skills"]] + # 前置关系仍满足:Linux 在 Docker 前,Docker 在 Kubernetes 前 + assert flat.index("Linux") < flat.index("Docker") < flat.index("Kubernetes") + assert len(stages) >= 3 # 7 个技能至少拆成 3 段 diff --git a/webui/app.js b/webui/app.js index 84c9210..0cd53f4 100644 --- a/webui/app.js +++ b/webui/app.js @@ -1806,9 +1806,10 @@ function renderPath(data) { return; } const weeks = stageWeeks(data.learning_path.length); - /* 阶段语义化命名:基础补齐 → 岗位核心 → 项目验证(超出 3 阶段顺延为综合提升) */ - const STAGE_NAMES = ["基础补齐", "岗位核心", "项目验证"]; - const stageName = (i) => STAGE_NAMES[i] || "综合提升"; + const weeks = stageWeeks(data.learning_path.length); + /* 阶段语义化命名:基础补齐 → 岗位核心(可多个)→ 项目验证 */ + const stageName = (i, n) => + i === 0 ? "基础补齐" : i === n - 1 ? "项目验证" : "岗位核心"; box.innerHTML = (data._demo ? `
示范路线(以零已掌握技能生成):完成一次人岗诊断后,此处将自动替换为你的个人成长路线图 @@ -1820,7 +1821,7 @@ function renderPath(data) { ${done ? "✓" : st.stage}
- 阶段 ${st.stage} · ${stageName(i)}(${st.skills.length} 项技能) + 阶段 ${st.stage} · ${stageName(i, data.learning_path.length)}(${st.skills.length} 项技能) 建议 ${weeks[i]} 周
${i > 0 ? `
前置:建议完成阶段 ${st.stage - 1} 后开始(按技能前置关系拓扑排序)
` : ""}