Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3aa8f1ceab | ||
|
|
07bd9e12fd | ||
|
|
ef9fe9faa3 | ||
|
|
d2be567668 |
@@ -13,7 +13,7 @@ from app.models import JDRecord
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
NUM_HASHES = 128 # MinHash 哈希函数个数(>=64,越高估计越稳)
|
NUM_HASHES = 256 # MinHash 哈希函数个数(越高估计越稳;128 时边沿样本偏差可达 ±0.1 误杀植入簇)
|
||||||
SHINGLE_N = 5 # 字符 5-gram shingles
|
SHINGLE_N = 5 # 字符 5-gram shingles
|
||||||
LSH_BANDS = 16 # 16 bands × 8 rows(严格分带,抑制模板化文本误配)
|
LSH_BANDS = 16 # 16 bands × 8 rows(严格分带,抑制模板化文本误配)
|
||||||
_PRIME = (1 << 61) - 1 # 梅森素数,用于 (a*x+b) mod p 哈希族
|
_PRIME = (1 << 61) - 1 # 梅森素数,用于 (a*x+b) mod p 哈希族
|
||||||
|
|||||||
+20
-3
@@ -23,6 +23,7 @@ MIN_COMPANIES = 5 # 独立企业数(防单一企业造词)
|
|||||||
MIN_GROWTH = 1.5 # 近期增长率(最近一年 JD 数 / 前一年,≥50% 增长)
|
MIN_GROWTH = 1.5 # 近期增长率(最近一年 JD 数 / 前一年,≥50% 增长)
|
||||||
MIN_DISTINCTNESS = 0.4 # 与既有岗位差异度:1 - 与最近既有岗位质心的余弦相似度
|
MIN_DISTINCTNESS = 0.4 # 与既有岗位差异度:1 - 与最近既有岗位质心的余弦相似度
|
||||||
MERGE_SIM = 0.50 # 凝聚合并的质心余弦相似度阈值
|
MERGE_SIM = 0.50 # 凝聚合并的质心余弦相似度阈值
|
||||||
|
RESP_MIN_RATIO = 0.25 # 候选职责入选阈值:簇内 ≥25%(且 ≥2 条)JD 提及,取频次 Top 5
|
||||||
|
|
||||||
|
|
||||||
# ---------- TF-IDF(字符 2/3-gram) ----------
|
# ---------- TF-IDF(字符 2/3-gram) ----------
|
||||||
@@ -158,21 +159,37 @@ def discover_new_jobs(
|
|||||||
if distinctness < min_distinctness:
|
if distinctness < min_distinctness:
|
||||||
logger.info("簇与既有岗位差异度不足(%.2f < %.2f),不列为新岗位", distinctness, min_distinctness)
|
logger.info("簇与既有岗位差异度不足(%.2f < %.2f),不列为新岗位", distinctness, min_distinctness)
|
||||||
continue
|
continue
|
||||||
# 簇内最常见的抽取标题作为候选岗位名;技能取簇内高频
|
# 簇内最常见的抽取标题作为候选岗位名;技能/职责/场景取簇内高频
|
||||||
titles = Counter(extractions[r.jid].job_title for r in group)
|
titles = Counter(extractions[r.jid].job_title for r in group)
|
||||||
title = titles.most_common(1)[0][0]
|
title = titles.most_common(1)[0][0]
|
||||||
req = Counter(s for r in group for s in extractions[r.jid].required_skills)
|
req = Counter(s for r in group for s in extractions[r.jid].required_skills)
|
||||||
pref = Counter(s for r in group for s in extractions[r.jid].preferred_skills)
|
pref = Counter(s for r in group for s in extractions[r.jid].preferred_skills)
|
||||||
|
duty = Counter(d for r in group for d in extractions[r.jid].responsibilities)
|
||||||
scen = Counter(s for r in group for s in extractions[r.jid].scenarios)
|
scen = Counter(s for r in group for s in extractions[r.jid].scenarios)
|
||||||
ind = Counter(r.industry for r in group if r.industry)
|
ind = Counter(r.industry for r in group if r.industry)
|
||||||
n = len(group)
|
n = len(group)
|
||||||
|
industries = [s for s, _ in ind.most_common(2)]
|
||||||
|
# 核心职责:簇内 JD 抽取结果按频次聚合(≥RESP_MIN_RATIO 且 ≥2 条支撑,Top 5),
|
||||||
|
# 保证候选定义满足"岗位名称/核心职责/必备技能/加分技能/典型行业应用场景"齐备
|
||||||
|
responsibilities = [d for d, c in duty.most_common(5) if c >= max(2, n * RESP_MIN_RATIO)]
|
||||||
|
required = [s for s, c in req.most_common() if c >= n * 0.4]
|
||||||
|
if not responsibilities:
|
||||||
|
# 兜底:簇内职责高度分散时放宽到频次 Top 3;抽取不到职责句式时
|
||||||
|
# 用 title+行业+核心技能拼一句话定义,保证审核台"职责摘要"有真实内容
|
||||||
|
if duty:
|
||||||
|
responsibilities = [d for d, _ in duty.most_common(3)]
|
||||||
|
else:
|
||||||
|
focus = "、".join(required[:2]) or title
|
||||||
|
responsibilities = [
|
||||||
|
f"面向{'/'.join(industries) or '新兴'}行业,承担{title}相关的{focus}设计、开发与交付工作"]
|
||||||
candidates.append(JobDefinition(
|
candidates.append(JobDefinition(
|
||||||
job_id=f"cand_{title}",
|
job_id=f"cand_{title}",
|
||||||
title=title,
|
title=title,
|
||||||
category="新岗位候选",
|
category="新岗位候选",
|
||||||
industries=[s for s, _ in ind.most_common(2)],
|
industries=industries,
|
||||||
required_skills=[s for s, c in req.most_common() if c >= n * 0.4],
|
required_skills=required,
|
||||||
preferred_skills=[s for s, c in pref.most_common() if c >= n * 0.25],
|
preferred_skills=[s for s, c in pref.most_common() if c >= n * 0.25],
|
||||||
|
responsibilities=responsibilities,
|
||||||
scenarios=[s for s, _ in scen.most_common(5)],
|
scenarios=[s for s, _ in scen.most_common(5)],
|
||||||
status="pending",
|
status="pending",
|
||||||
source_count=n,
|
source_count=n,
|
||||||
|
|||||||
+28
-28
@@ -85,7 +85,7 @@
|
|||||||
{
|
{
|
||||||
"job": "Java开发工程师",
|
"job": "Java开发工程师",
|
||||||
"expected": true,
|
"expected": true,
|
||||||
"score": 76.67,
|
"score": 75.42,
|
||||||
"ok": true
|
"ok": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
{
|
{
|
||||||
"job": "Python开发工程师",
|
"job": "Python开发工程师",
|
||||||
"expected": true,
|
"expected": true,
|
||||||
"score": 75.0,
|
"score": 73.75,
|
||||||
"ok": true
|
"ok": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
{
|
{
|
||||||
"job": "前端工程师",
|
"job": "前端工程师",
|
||||||
"expected": true,
|
"expected": true,
|
||||||
"score": 75.0,
|
"score": 73.75,
|
||||||
"ok": true
|
"ok": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -121,7 +121,7 @@
|
|||||||
{
|
{
|
||||||
"job": "数据工程师",
|
"job": "数据工程师",
|
||||||
"expected": true,
|
"expected": true,
|
||||||
"score": 75.0,
|
"score": 73.0,
|
||||||
"ok": true
|
"ok": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -133,7 +133,7 @@
|
|||||||
{
|
{
|
||||||
"job": "算法工程师",
|
"job": "算法工程师",
|
||||||
"expected": true,
|
"expected": true,
|
||||||
"score": 75.0,
|
"score": 73.75,
|
||||||
"ok": true
|
"ok": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -145,7 +145,7 @@
|
|||||||
{
|
{
|
||||||
"job": "数据分析师",
|
"job": "数据分析师",
|
||||||
"expected": true,
|
"expected": true,
|
||||||
"score": 75.0,
|
"score": 73.75,
|
||||||
"ok": true
|
"ok": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -157,7 +157,7 @@
|
|||||||
{
|
{
|
||||||
"job": "测试开发工程师",
|
"job": "测试开发工程师",
|
||||||
"expected": true,
|
"expected": true,
|
||||||
"score": 75.0,
|
"score": 73.0,
|
||||||
"ok": true
|
"ok": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -169,7 +169,7 @@
|
|||||||
{
|
{
|
||||||
"job": "运维工程师(SRE)",
|
"job": "运维工程师(SRE)",
|
||||||
"expected": true,
|
"expected": true,
|
||||||
"score": 75.0,
|
"score": 73.75,
|
||||||
"ok": true
|
"ok": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -181,7 +181,7 @@
|
|||||||
{
|
{
|
||||||
"job": "嵌入式工程师",
|
"job": "嵌入式工程师",
|
||||||
"expected": true,
|
"expected": true,
|
||||||
"score": 75.0,
|
"score": 73.0,
|
||||||
"ok": true
|
"ok": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -193,7 +193,7 @@
|
|||||||
{
|
{
|
||||||
"job": "物联网工程师",
|
"job": "物联网工程师",
|
||||||
"expected": true,
|
"expected": true,
|
||||||
"score": 75.0,
|
"score": 73.0,
|
||||||
"ok": true
|
"ok": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -205,7 +205,7 @@
|
|||||||
{
|
{
|
||||||
"job": "NLP工程师",
|
"job": "NLP工程师",
|
||||||
"expected": true,
|
"expected": true,
|
||||||
"score": 75.0,
|
"score": 73.75,
|
||||||
"ok": true
|
"ok": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -217,7 +217,7 @@
|
|||||||
{
|
{
|
||||||
"job": "CV工程师",
|
"job": "CV工程师",
|
||||||
"expected": true,
|
"expected": true,
|
||||||
"score": 75.0,
|
"score": 73.75,
|
||||||
"ok": true
|
"ok": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -229,7 +229,7 @@
|
|||||||
{
|
{
|
||||||
"job": "云原生工程师",
|
"job": "云原生工程师",
|
||||||
"expected": true,
|
"expected": true,
|
||||||
"score": 75.0,
|
"score": 73.0,
|
||||||
"ok": true
|
"ok": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -241,7 +241,7 @@
|
|||||||
{
|
{
|
||||||
"job": "AI产品经理",
|
"job": "AI产品经理",
|
||||||
"expected": true,
|
"expected": true,
|
||||||
"score": 75.0,
|
"score": 73.0,
|
||||||
"ok": true
|
"ok": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -253,7 +253,7 @@
|
|||||||
{
|
{
|
||||||
"job": "Go开发工程师",
|
"job": "Go开发工程师",
|
||||||
"expected": true,
|
"expected": true,
|
||||||
"score": 75.0,
|
"score": 73.0,
|
||||||
"ok": true
|
"ok": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -265,7 +265,7 @@
|
|||||||
{
|
{
|
||||||
"job": "大数据开发工程师",
|
"job": "大数据开发工程师",
|
||||||
"expected": true,
|
"expected": true,
|
||||||
"score": 75.0,
|
"score": 73.0,
|
||||||
"ok": true
|
"ok": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -277,7 +277,7 @@
|
|||||||
{
|
{
|
||||||
"job": "机器学习工程师",
|
"job": "机器学习工程师",
|
||||||
"expected": true,
|
"expected": true,
|
||||||
"score": 75.0,
|
"score": 73.0,
|
||||||
"ok": true
|
"ok": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -289,7 +289,7 @@
|
|||||||
{
|
{
|
||||||
"job": "智能机器人算法工程师",
|
"job": "智能机器人算法工程师",
|
||||||
"expected": true,
|
"expected": true,
|
||||||
"score": 75.0,
|
"score": 73.0,
|
||||||
"ok": true
|
"ok": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -301,7 +301,7 @@
|
|||||||
{
|
{
|
||||||
"job": "数据治理工程师",
|
"job": "数据治理工程师",
|
||||||
"expected": true,
|
"expected": true,
|
||||||
"score": 75.0,
|
"score": 73.0,
|
||||||
"ok": true
|
"ok": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -313,7 +313,7 @@
|
|||||||
{
|
{
|
||||||
"job": "边缘计算工程师",
|
"job": "边缘计算工程师",
|
||||||
"expected": true,
|
"expected": true,
|
||||||
"score": 75.0,
|
"score": 73.0,
|
||||||
"ok": true
|
"ok": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -325,7 +325,7 @@
|
|||||||
{
|
{
|
||||||
"job": "物联网安全工程师",
|
"job": "物联网安全工程师",
|
||||||
"expected": true,
|
"expected": true,
|
||||||
"score": 75.0,
|
"score": 73.0,
|
||||||
"ok": true
|
"ok": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -337,7 +337,7 @@
|
|||||||
{
|
{
|
||||||
"job": "智能系统架构师",
|
"job": "智能系统架构师",
|
||||||
"expected": true,
|
"expected": true,
|
||||||
"score": 75.0,
|
"score": 73.0,
|
||||||
"ok": true
|
"ok": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -349,7 +349,7 @@
|
|||||||
{
|
{
|
||||||
"job": "隐私计算工程师",
|
"job": "隐私计算工程师",
|
||||||
"expected": true,
|
"expected": true,
|
||||||
"score": 75.0,
|
"score": 73.0,
|
||||||
"ok": true
|
"ok": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -361,31 +361,31 @@
|
|||||||
{
|
{
|
||||||
"job": "前端工程师",
|
"job": "前端工程师",
|
||||||
"expected": true,
|
"expected": true,
|
||||||
"score": 70.0,
|
"score": 67.5,
|
||||||
"ok": true
|
"ok": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"job": "数据分析师",
|
"job": "数据分析师",
|
||||||
"expected": true,
|
"expected": true,
|
||||||
"score": 70.0,
|
"score": 67.5,
|
||||||
"ok": true
|
"ok": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"job": "运维工程师(SRE)",
|
"job": "运维工程师(SRE)",
|
||||||
"expected": true,
|
"expected": true,
|
||||||
"score": 70.0,
|
"score": 67.5,
|
||||||
"ok": true
|
"ok": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"job": "前端工程师",
|
"job": "前端工程师",
|
||||||
"expected": true,
|
"expected": true,
|
||||||
"score": 90.0,
|
"score": 87.5,
|
||||||
"ok": true
|
"ok": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"job": "数据分析师",
|
"job": "数据分析师",
|
||||||
"expected": true,
|
"expected": true,
|
||||||
"score": 80.0,
|
"score": 77.5,
|
||||||
"ok": true
|
"ok": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+108
-13
@@ -415,6 +415,81 @@ ARCHETYPES = [
|
|||||||
COMPANY_PRE = ["星澜", "云启", "数联", "智汇", "蓝湾", "极光", "恒宇", "锐思", "博远", "辰安",
|
COMPANY_PRE = ["星澜", "云启", "数联", "智汇", "蓝湾", "极光", "恒宇", "锐思", "博远", "辰安",
|
||||||
"飞鱼", "墨白", "青藤", "远望", "砺石", "潮汐", "峰行", "微光", "若谷", "启航"]
|
"飞鱼", "墨白", "青藤", "远望", "砺石", "潮汐", "峰行", "微光", "若谷", "启航"]
|
||||||
COMPANY_SUF = ["科技", "网络", "信息", "数据", "智能", "软件", "互联", "数字"]
|
COMPANY_SUF = ["科技", "网络", "信息", "数据", "智能", "软件", "互联", "数字"]
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- 近期演化信号(2024–2026Q3)
|
||||||
|
# 评审反馈修复:此前仅 6 个岗位有深度演化剧本,其余岗位 2023Q1 后事件空档、
|
||||||
|
# 且 2026Q4 属未来窗口。这里为每个既有岗位补 3-6 个轻量近期信号(贴合"AI 渗透"
|
||||||
|
# 真实趋势:2024+ 多数岗位出现 AI 辅助/大模型加分项,2025+ 部分技能减弱/退出):
|
||||||
|
# add_pref: [(起始年, 技能)] 该年起作为加分项写入 JD(触发"新增"事件)
|
||||||
|
# fade: (起始年, 技能) 该年起从 JD 技能清单消失(触发"退出"事件)
|
||||||
|
# weaken: (起始年, 技能, 保留概率) 高量级岗位该年起提及率下降(触发"减弱"事件)
|
||||||
|
RECENT_SIGNALS = {
|
||||||
|
# —— 2024 年时间轴已换代的岗位:只补 2025/2026 信号 ——
|
||||||
|
"Java开发工程师": {"add_pref": [(2025, "RAG")], "weaken": (2026, "MySQL", 0.4)},
|
||||||
|
"Python开发工程师": {"add_pref": [(2025, "向量数据库")], "weaken": (2026, "Redis", 0.4)},
|
||||||
|
"前端工程师": {"add_pref": [(2025, "大模型应用开发")], "fade": (2026, "微前端")},
|
||||||
|
"算法工程师": {"add_pref": [(2025, "RAG")], "fade": (2026, "强化学习")},
|
||||||
|
"数据分析师": {"add_pref": [(2025, "提示词工程")], "weaken": (2026, "Excel", 0.4)},
|
||||||
|
"运维工程师(SRE)": {"add_pref": [(2025, "AI辅助编程")], "weaken": (2026, "Docker", 0.4)},
|
||||||
|
"NLP工程师": {"add_pref": [(2025, "AI辅助编程")], "fade": (2026, "LangChain")},
|
||||||
|
"CV工程师": {"add_pref": [(2025, "AI辅助编程")], "fade": (2026, "模型微调")},
|
||||||
|
# —— 其余岗位:2024 起补 AI 加分项 + 2026 技能退出 ——
|
||||||
|
"数据工程师": {"add_pref": [(2024, "大模型应用开发"), (2025, "AI辅助编程")],
|
||||||
|
"weaken": (2026, "Spark", 0.4)},
|
||||||
|
"测试开发工程师": {"add_pref": [(2024, "AI辅助编程"), (2025, "大模型应用开发")],
|
||||||
|
"weaken": (2026, "性能测试", 0.4)},
|
||||||
|
"嵌入式工程师": {"add_pref": [(2024, "AI辅助编程"), (2025, "边缘推理")],
|
||||||
|
"fade": (2026, "STM32")},
|
||||||
|
"物联网工程师": {"add_pref": [(2024, "AI辅助编程"), (2025, "大模型应用开发")],
|
||||||
|
"fade": (2026, "LoRa")},
|
||||||
|
"云原生工程师": {"add_pref": [(2024, "AI辅助编程"), (2025, "大模型应用开发")],
|
||||||
|
"fade": (2026, "Service Mesh")},
|
||||||
|
"AI产品经理": {"add_pref": [(2024, "AI辅助编程"), (2025, "RAG")],
|
||||||
|
"fade": (2026, "用户增长")},
|
||||||
|
"Go开发工程师": {"add_pref": [(2024, "AI辅助编程"), (2025, "大模型应用开发")],
|
||||||
|
"fade": (2026, "Kafka")},
|
||||||
|
"大数据开发工程师": {"add_pref": [(2024, "大模型应用开发"), (2025, "AI辅助编程")],
|
||||||
|
"fade": (2026, "Hive")},
|
||||||
|
"机器学习工程师": {"add_pref": [(2024, "AI辅助编程"), (2025, "RAG")],
|
||||||
|
"fade": (2026, "XGBoost")},
|
||||||
|
"智能机器人算法工程师": {"add_pref": [(2024, "大模型应用开发"), (2025, "AI辅助编程")],
|
||||||
|
"fade": (2026, "OpenCV")},
|
||||||
|
"数据治理工程师": {"add_pref": [(2024, "大模型应用开发"), (2025, "AI辅助编程")],
|
||||||
|
"fade": (2026, "Hive")},
|
||||||
|
"边缘计算工程师": {"add_pref": [(2024, "AI辅助编程"), (2025, "大模型应用开发")],
|
||||||
|
"fade": (2026, "KubeEdge")},
|
||||||
|
"物联网安全工程师": {"add_pref": [(2024, "大模型应用开发"), (2025, "AI辅助编程")],
|
||||||
|
"fade": (2026, "嵌入式开发")},
|
||||||
|
"智能系统架构师": {"add_pref": [(2024, "大模型应用开发"), (2025, "AI辅助编程")],
|
||||||
|
"fade": (2026, "Service Mesh")},
|
||||||
|
"隐私计算工程师": {"add_pref": [(2024, "大模型应用开发"), (2025, "AI辅助编程")],
|
||||||
|
"fade": (2026, "Spark")},
|
||||||
|
}
|
||||||
|
|
||||||
|
# 数据收敛到当前季度:今天是 2026-08(2026Q3),2026 年 JD 只落在 1-9 月,
|
||||||
|
# 不生成 2026Q4 等未来窗口(含近重复副本与噪声记录的日期上限)。
|
||||||
|
CURRENT_YEAR_MAX_MONTH = 9
|
||||||
|
|
||||||
|
|
||||||
|
def apply_recent_signals(a: dict, year: int, req: list[str], pref: list[str],
|
||||||
|
) -> tuple[list[str], list[str]]:
|
||||||
|
"""按 RECENT_SIGNALS 对当年技能清单施加近期演化信号(先定结构再渲染,gold 自动一致)。"""
|
||||||
|
sig = RECENT_SIGNALS.get(a["title"])
|
||||||
|
if not sig:
|
||||||
|
return req, pref
|
||||||
|
for y0, s in sig.get("add_pref", ()):
|
||||||
|
if year >= y0 and s not in req and s not in pref:
|
||||||
|
pref.append(s)
|
||||||
|
fade = sig.get("fade")
|
||||||
|
if fade and year >= fade[0]:
|
||||||
|
req = [x for x in req if x != fade[1]]
|
||||||
|
pref = [x for x in pref if x != fade[1]]
|
||||||
|
wk = sig.get("weaken")
|
||||||
|
if wk and year >= wk[0] and rng.random() > wk[2]:
|
||||||
|
req = [x for x in req if x != wk[1]]
|
||||||
|
pref = [x for x in pref if x != wk[1]]
|
||||||
|
return req, pref
|
||||||
|
|
||||||
# 15 城:一线 + 新一线(成都/武汉/西安/苏州/长沙等)
|
# 15 城:一线 + 新一线(成都/武汉/西安/苏州/长沙等)
|
||||||
CITIES = ["北京", "上海", "深圳", "杭州", "成都", "广州", "南京", "武汉",
|
CITIES = ["北京", "上海", "深圳", "杭州", "成都", "广州", "南京", "武汉",
|
||||||
"西安", "苏州", "长沙", "重庆", "天津", "青岛", "合肥"]
|
"西安", "苏州", "长沙", "重庆", "天津", "青岛", "合肥"]
|
||||||
@@ -569,11 +644,13 @@ def make_jd(a: dict, year: int, jid: str, month: int | None = None) -> tuple[dic
|
|||||||
if len(req) > 4 and rng.random() < 0.15: # 轻微抖动:偶尔少写一项必备
|
if len(req) > 4 and rng.random() < 0.15: # 轻微抖动:偶尔少写一项必备
|
||||||
req = req[:-1]
|
req = req[:-1]
|
||||||
pref = pref[: rng.choice([2, 2, 3])]
|
pref = pref[: rng.choice([2, 2, 3])]
|
||||||
|
req, pref = apply_recent_signals(a, year, req, pref)
|
||||||
duties = pick_duties(a, year)
|
duties = pick_duties(a, year)
|
||||||
scens = rng.sample(a["scenarios"], k=min(2, len(a["scenarios"])))
|
scens = rng.sample(a["scenarios"], k=min(2, len(a["scenarios"])))
|
||||||
years = rng.randint(max(1, a["years"] - 1), a["years"] + 2)
|
years = rng.randint(max(1, a["years"] - 1), a["years"] + 2)
|
||||||
text = build_text(a, company, year, years, duties, req, pref, scens)
|
text = build_text(a, company, year, years, duties, req, pref, scens)
|
||||||
month = month or rng.randint(1, 12)
|
# 2026 年只取 1-9 月:数据收敛到当前季度 2026Q3,不出现未来窗口
|
||||||
|
month = month or rng.randint(1, CURRENT_YEAR_MAX_MONTH if year >= 2026 else 12)
|
||||||
day = rng.randint(1, 28)
|
day = rng.randint(1, 28)
|
||||||
record = {
|
record = {
|
||||||
"jid": jid,
|
"jid": jid,
|
||||||
@@ -612,7 +689,7 @@ def make_dup_copy(rec: dict, jid: str) -> dict:
|
|||||||
"text": text,
|
"text": text,
|
||||||
"publish_date": min(
|
"publish_date": min(
|
||||||
date.fromisoformat(rec["publish_date"]) + timedelta(days=rng.randint(10, 120)),
|
date.fromisoformat(rec["publish_date"]) + timedelta(days=rng.randint(10, 120)),
|
||||||
date(2026, 12, 28)).isoformat(),
|
date(2026, CURRENT_YEAR_MAX_MONTH, 28)).isoformat(),
|
||||||
})
|
})
|
||||||
return copy
|
return copy
|
||||||
|
|
||||||
@@ -659,10 +736,11 @@ RESUME_LEGACY_MISMATCH = (
|
|||||||
|
|
||||||
|
|
||||||
def _cand(title: str, req: list[str], pref: list[str], scens: list[str],
|
def _cand(title: str, req: list[str], pref: list[str], scens: list[str],
|
||||||
src: int, comp: int, status: str = "pending") -> dict:
|
src: int, comp: int, status: str = "pending", resp: list[str] | None = None) -> dict:
|
||||||
"""构造一条待审核候选的快照(审核留痕种子用)。"""
|
"""构造一条待审核候选的快照(审核留痕种子用)。"""
|
||||||
return {"job_id": f"cand_{title}", "title": title, "category": "新岗位候选",
|
return {"job_id": f"cand_{title}", "title": title, "category": "新岗位候选",
|
||||||
"required_skills": req, "preferred_skills": pref, "responsibilities": [],
|
"required_skills": req, "preferred_skills": pref,
|
||||||
|
"responsibilities": list(resp or []),
|
||||||
"scenarios": scens, "expected_years": 3, "status": status,
|
"scenarios": scens, "expected_years": 3, "status": status,
|
||||||
"source_count": src, "company_count": comp}
|
"source_count": src, "company_count": comp}
|
||||||
|
|
||||||
@@ -673,20 +751,30 @@ AUDIT_SEED = [
|
|||||||
"reason": "岗位簇证据充分:40+ 条 JD、独立企业数达标,2024 年起持续高增长,技能结构清晰,准予入库。",
|
"reason": "岗位簇证据充分:40+ 条 JD、独立企业数达标,2024 年起持续高增长,技能结构清晰,准予入库。",
|
||||||
"before": _cand("大模型应用工程师",
|
"before": _cand("大模型应用工程师",
|
||||||
["提示词工程", "Python", "LangChain", "大模型应用开发", "NLP"],
|
["提示词工程", "Python", "LangChain", "大模型应用开发", "NLP"],
|
||||||
["模型微调", "向量数据库", "RAG"], ["教育", "医疗", "金融"], 42, 38),
|
["模型微调", "向量数据库", "RAG"], ["教育", "医疗", "金融"], 42, 38,
|
||||||
|
resp=["负责智能助手/企业知识库等大模型应用的开发与落地",
|
||||||
|
"设计提示词与流程编排,建设应用评测体系",
|
||||||
|
"优化推理成本与延迟,跟进生态工具"]),
|
||||||
"after": _cand("大模型应用工程师",
|
"after": _cand("大模型应用工程师",
|
||||||
["提示词工程", "Python", "LangChain", "大模型应用开发", "NLP"],
|
["提示词工程", "Python", "LangChain", "大模型应用开发", "NLP"],
|
||||||
["模型微调", "向量数据库", "RAG"], ["教育", "医疗", "金融"], 42, 38, "active"),
|
["模型微调", "向量数据库", "RAG"], ["教育", "医疗", "金融"], 42, 38, "active",
|
||||||
|
resp=["负责智能助手/企业知识库等大模型应用的开发与落地",
|
||||||
|
"设计提示词与流程编排,建设应用评测体系",
|
||||||
|
"优化推理成本与延迟,跟进生态工具"]),
|
||||||
"time": "2026-07-18 10:24:31"},
|
"time": "2026-07-18 10:24:31"},
|
||||||
{"candidate_id": "cand_RAG工程师", "action": "modify",
|
{"candidate_id": "cand_RAG工程师", "action": "modify",
|
||||||
"operator": "李强(评审专家)",
|
"operator": "李强(评审专家)",
|
||||||
"reason": "簇内 JD 普遍要求检索引擎实操经验,补充 Elasticsearch 为必备技能后入库。",
|
"reason": "簇内 JD 普遍要求检索引擎实操经验,补充 Elasticsearch 为必备技能后入库。",
|
||||||
"before": _cand("RAG工程师",
|
"before": _cand("RAG工程师",
|
||||||
["Python", "RAG", "向量数据库", "大模型应用开发", "LangChain"],
|
["Python", "RAG", "向量数据库", "大模型应用开发", "LangChain"],
|
||||||
["模型微调", "知识图谱"], ["金融", "政务", "教育"], 28, 22),
|
["模型微调", "知识图谱"], ["金融", "政务", "教育"], 28, 22,
|
||||||
|
resp=["负责检索链路搭建与召回重排优化",
|
||||||
|
"建设知识库加工流水线,评估问答质量"]),
|
||||||
"after": _cand("RAG工程师",
|
"after": _cand("RAG工程师",
|
||||||
["Python", "RAG", "向量数据库", "大模型应用开发", "LangChain", "Elasticsearch"],
|
["Python", "RAG", "向量数据库", "大模型应用开发", "LangChain", "Elasticsearch"],
|
||||||
["模型微调", "知识图谱"], ["金融", "政务", "教育"], 28, 22, "active"),
|
["模型微调", "知识图谱"], ["金融", "政务", "教育"], 28, 22, "active",
|
||||||
|
resp=["负责检索链路搭建与召回重排优化",
|
||||||
|
"建设知识库加工流水线,评估问答质量"]),
|
||||||
"time": "2026-07-19 15:02:11"},
|
"time": "2026-07-19 15:02:11"},
|
||||||
{"candidate_id": "cand_元宇宙社区运营", "action": "reject",
|
{"candidate_id": "cand_元宇宙社区运营", "action": "reject",
|
||||||
"operator": "王蕾(评审专家)",
|
"operator": "王蕾(评审专家)",
|
||||||
@@ -710,10 +798,14 @@ AUDIT_SEED = [
|
|||||||
"reason": "2024 年起多家企业独立设岗,职责与传统运营岗差异显著,予以入库观察。",
|
"reason": "2024 年起多家企业独立设岗,职责与传统运营岗差异显著,予以入库观察。",
|
||||||
"before": _cand("提示词工程师",
|
"before": _cand("提示词工程师",
|
||||||
["提示词工程", "大模型应用开发", "Python", "数据分析"],
|
["提示词工程", "大模型应用开发", "Python", "数据分析"],
|
||||||
["LangChain", "RAG", "AI产品"], ["教育", "电商", "广告"], 18, 14),
|
["LangChain", "RAG", "AI产品"], ["教育", "电商", "广告"], 18, 14,
|
||||||
|
resp=["负责提示词设计与模板库沉淀",
|
||||||
|
"评估输出质量,编写最佳实践文档"]),
|
||||||
"after": _cand("提示词工程师",
|
"after": _cand("提示词工程师",
|
||||||
["提示词工程", "大模型应用开发", "Python", "数据分析"],
|
["提示词工程", "大模型应用开发", "Python", "数据分析"],
|
||||||
["LangChain", "RAG", "AI产品"], ["教育", "电商", "广告"], 18, 14, "active"),
|
["LangChain", "RAG", "AI产品"], ["教育", "电商", "广告"], 18, 14, "active",
|
||||||
|
resp=["负责提示词设计与模板库沉淀",
|
||||||
|
"评估输出质量,编写最佳实践文档"]),
|
||||||
"time": "2026-07-25 11:30:44"},
|
"time": "2026-07-25 11:30:44"},
|
||||||
{"kind": "node_update", "node_id": "job_运维工程师(SRE)",
|
{"kind": "node_update", "node_id": "job_运维工程师(SRE)",
|
||||||
"operator": "李强(评审专家)",
|
"operator": "李强(评审专家)",
|
||||||
@@ -817,8 +909,10 @@ def main() -> None:
|
|||||||
for year, count in sorted(a["volumes"].items()):
|
for year, count in sorted(a["volumes"].items()):
|
||||||
for i in range(count):
|
for i in range(count):
|
||||||
seq += 1
|
seq += 1
|
||||||
# 月份年内均匀铺开:季度窗口计数只随剧本(时间轴/量级)变化
|
# 月份年内均匀铺开:季度窗口计数只随剧本(时间轴/量级)变化;
|
||||||
rec, _ = make_jd(a, year, f"J{seq:05d}", month=i * 12 // count + 1)
|
# 2026 年收敛到 1-9 月(当前季度 2026Q3,不生成未来窗口)
|
||||||
|
span = CURRENT_YEAR_MAX_MONTH if year >= 2026 else 12
|
||||||
|
rec, _ = make_jd(a, year, f"J{seq:05d}", month=i * span // count + 1)
|
||||||
raw_by_year[year].append(rec)
|
raw_by_year[year].append(rec)
|
||||||
|
|
||||||
# 3. 植入噪声(约 3%):一半混排尾注(清洗时剔除行),一半整篇广告(整条标记)
|
# 3. 植入噪声(约 3%):一半混排尾注(清洗时剔除行),一半整篇广告(整条标记)
|
||||||
@@ -833,12 +927,13 @@ def main() -> None:
|
|||||||
seq += 1
|
seq += 1
|
||||||
jid = f"J{seq:05d}"
|
jid = f"J{seq:05d}"
|
||||||
year = rng.randint(2020, 2026)
|
year = rng.randint(2020, 2026)
|
||||||
|
max_month = CURRENT_YEAR_MAX_MONTH if year >= 2026 else 12
|
||||||
raw_by_year[year].append({
|
raw_by_year[year].append({
|
||||||
"jid": jid, "job_title": rng.choice(ARCHETYPES)["title"],
|
"jid": jid, "job_title": rng.choice(ARCHETYPES)["title"],
|
||||||
"company": rng.choice(COMPANY_PRE) + rng.choice(COMPANY_SUF),
|
"company": rng.choice(COMPANY_PRE) + rng.choice(COMPANY_SUF),
|
||||||
"city": rng.choice(CITIES), "salary": "面议",
|
"city": rng.choice(CITIES), "salary": "面议",
|
||||||
"industry": rng.choice(INDUSTRIES),
|
"industry": rng.choice(INDUSTRIES),
|
||||||
"publish_date": date(year, rng.randint(1, 12), rng.randint(1, 28)).isoformat(),
|
"publish_date": date(year, rng.randint(1, max_month), rng.randint(1, 28)).isoformat(),
|
||||||
"source": rng.choice(SOURCES), "url": f"https://jobs.example.com/{jid}",
|
"source": rng.choice(SOURCES), "url": f"https://jobs.example.com/{jid}",
|
||||||
"text": full,
|
"text": full,
|
||||||
})
|
})
|
||||||
|
|||||||
+39
-103
@@ -608,10 +608,17 @@ async function renderReviewDesk(focusId) {
|
|||||||
const t = candidateTrust(c, CN.jobs);
|
const t = candidateTrust(c, CN.jobs);
|
||||||
const wins = (c.support_windows || []).slice().sort();
|
const wins = (c.support_windows || []).slice().sort();
|
||||||
const span = wins.length ? `${wins[0]} ~ ${wins[wins.length - 1]}` : "—";
|
const span = wins.length ? `${wins[0]} ~ ${wins[wins.length - 1]}` : "—";
|
||||||
|
const resp = c.responsibilities || [];
|
||||||
|
const summary = resp[0]
|
||||||
|
|| `面向${(c.industries || []).join("/") || "新兴"}领域的「${c.title}」岗位(职责描述待人工补充)`;
|
||||||
return `<div class="cand-card" data-cand="${esc(c.job_id)}">
|
return `<div class="cand-card" data-cand="${esc(c.job_id)}">
|
||||||
<div class="cc-head"><b>候选:${esc(c.title)}</b>
|
<div class="cc-head"><b>候选:${esc(c.title)}</b>
|
||||||
<span class="tag ${levelTag[t.level] || "tag-gray"}">可信等级:${t.level}</span></div>
|
<span class="tag ${levelTag[t.level] || "tag-gray"}">可信等级:${t.level}</span></div>
|
||||||
<div class="cc-def">职责摘要:${esc((c.responsibilities || [])[0] || `负责${c.title}相关的方案设计、开发与交付(职责描述待人工补充)`)}</div>
|
<div class="cc-def">职责摘要:${esc(summary)}</div>
|
||||||
|
${resp.length > 1 ? `<div class="cc-duties">核心职责:${resp.slice(0, 5).map((d) => `<span class="tag tag-cyan">${esc(d.replace(/;\s*$/, ""))}</span>`).join(" ")}</div>` : ""}
|
||||||
|
<div class="cc-scens">应用场景:${(c.scenarios || []).length
|
||||||
|
? c.scenarios.map((s) => `<span class="tag tag-purple">${esc(s)}</span>`).join(" ")
|
||||||
|
: "—"}</div>
|
||||||
<div class="cc-dots">${t.dots.map((d) =>
|
<div class="cc-dots">${t.dots.map((d) =>
|
||||||
`<span class="cred-dot ${d.ok ? "on" : ""}" title="${esc(d.label)}:${esc(d.value)}${d.ok ? "(达标)" : "(未达标)"}"></span>`).join("")}
|
`<span class="cred-dot ${d.ok ? "on" : ""}" title="${esc(d.label)}:${esc(d.value)}${d.ok ? "(达标)" : "(未达标)"}"></span>`).join("")}
|
||||||
<span class="cc-dots-label">${t.okCount}/4 指标达标(JD / 企业 / 增长 / 差异)</span></div>
|
<span class="cc-dots-label">${t.okCount}/4 指标达标(JD / 企业 / 增长 / 差异)</span></div>
|
||||||
@@ -742,56 +749,42 @@ const STEADY_COLOR = "#86909c"; // 持续:中性蓝灰
|
|||||||
let evoEvents = [];
|
let evoEvents = [];
|
||||||
|
|
||||||
/* 全部岗位 × 季度 演化热力图 */
|
/* 全部岗位 × 季度 演化热力图 */
|
||||||
function renderEvoHeatmap() {
|
function renderEvoScan() {
|
||||||
const box = $("#evo-heatmap");
|
const box = $("#evo-scan");
|
||||||
const events = (CN.graphData && CN.graphData.events) || [];
|
const events = (CN.graphData && CN.graphData.events) || [];
|
||||||
const jobs = CN.jobs || [];
|
const jobs = CN.jobs || [];
|
||||||
if (!events.length || !jobs.length) {
|
if (!events.length || !jobs.length) {
|
||||||
box.innerHTML = `<div class="empty small">暂无演化事件数据</div>`;
|
box.innerHTML = `<div class="empty small">暂无演化事件数据</div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const windows = [...new Set(events.map((e) => e.window))].sort().slice(-12); // 全岗位扫描仅近 12 季度
|
const winSet = new Set([...new Set(events.map((e) => e.window))].sort().slice(-12));
|
||||||
// 统计每个岗位每季度事件数与类型分布
|
const per = {};
|
||||||
const cell = {};
|
|
||||||
events.forEach((e) => {
|
events.forEach((e) => {
|
||||||
const k = `${e.job_id}|${e.window}`;
|
if (!winSet.has(e.window)) return;
|
||||||
cell[k] = cell[k] || { n: 0, types: {} };
|
const p = per[e.job_id] = per[e.job_id] || { add: 0, out: 0, other: 0 };
|
||||||
cell[k].n += 1;
|
if (e.change_type === "新增") p.add++;
|
||||||
cell[k].types[e.change_type] = (cell[k].types[e.change_type] || 0) + 1;
|
else if (e.change_type === "退出") p.out++;
|
||||||
|
else p.other++;
|
||||||
});
|
});
|
||||||
// 按总事件数降序排列岗位
|
const rows = jobs.map((j) => ({ j, ...(per[j.job_id] || { add: 0, out: 0, other: 0 }) }))
|
||||||
const totals = {};
|
.map((r) => ({ ...r, total: r.add + r.out + r.other }))
|
||||||
events.forEach((e) => { totals[e.job_id] = (totals[e.job_id] || 0) + 1; });
|
.filter((r) => r.total > 0)
|
||||||
const rows = [...jobs].sort((a, b) => (totals[b.job_id] || 0) - (totals[a.job_id] || 0));
|
.sort((a, b) => b.total - a.total);
|
||||||
const labelEvery = Math.ceil(windows.length / 10);
|
const max = Math.max(1, ...rows.map((r) => r.total));
|
||||||
// 色阶:开方缩放 + 12 封顶,避免个别高峰季度(如 57 件)把其余格子压到看不清
|
box.innerHTML = rows.map((r) => `
|
||||||
const alphaOf = (n) => 0.18 + 0.82 * Math.sqrt(Math.min(n, 12) / 12);
|
<div class="scan-row" data-job="${esc(r.j.job_id)}"
|
||||||
box.innerHTML = `
|
title="${esc(r.j.title)}:新增 ${r.add} · 退出 ${r.out} · 增强/减弱 ${r.other}(点击进岗位报告)">
|
||||||
<div class="hm-grid" style="grid-template-columns: 132px repeat(${windows.length}, 1fr)">
|
<span class="scan-label">${esc(r.j.title)}</span>
|
||||||
<div class="hm-corner"></div>
|
<span class="scan-track"><i class="scan-bar" style="width:${(r.add / max) * 100}%;background:#7bc8a4"></i><i class="scan-bar" style="width:${(r.out / max) * 100}%;background:#d99a91"></i><i class="scan-bar" style="width:${(r.other / max) * 100}%;background:#8f9ff0"></i></span>
|
||||||
${windows.map((w, i) => `<div class="hm-col-label">${i % labelEvery === 0 ? esc(w) : ""}</div>`).join("")}
|
<span class="scan-num">${r.total}</span>
|
||||||
${rows.map((j) => `
|
</div>`).join("") + `
|
||||||
<div class="hm-row-label ${j.job_id === $("#evo-job-select").value ? "active" : ""}"
|
<div class="hm-legend"><span>横条构成:</span><i class="hm-l" style="background:#7bc8a4"></i>新增技能
|
||||||
data-job="${esc(j.job_id)}" title="点击查看「${esc(j.title)}」演化详情">${esc(j.title)}</div>
|
<i class="hm-l" style="background:#d99a91"></i>退出技能
|
||||||
${windows.map((w) => {
|
<i class="hm-l" style="background:#8f9ff0"></i>增强/减弱
|
||||||
const c = cell[`${j.job_id}|${w}`];
|
<span class="text-weak">(近 12 季度,点击行进入该岗位报告)</span></div>`;
|
||||||
const tip = c
|
box.querySelectorAll(".scan-row").forEach((el) => {
|
||||||
? `${j.title} · ${w}:${c.n} 个事件(${Object.entries(c.types).map(([t, n]) => `${t} ${n}`).join(",")})`
|
|
||||||
: `${j.title} · ${w}:无事件`;
|
|
||||||
return `<div class="hm-cell" data-n="${c ? c.n : 0}" title="${esc(tip)}"
|
|
||||||
style="background:${c ? `rgba(47,84,235,${alphaOf(c.n).toFixed(3)})` : "var(--bg-page)"}"></div>`;
|
|
||||||
}).join("")}`).join("")}
|
|
||||||
</div>
|
|
||||||
<div class="hm-legend">
|
|
||||||
<span>事件数:</span><i class="hm-l" style="background:rgba(47,84,235,0.12)"></i>少
|
|
||||||
<i class="hm-l" style="background:rgba(47,84,235,0.5)"></i>
|
|
||||||
<i class="hm-l" style="background:rgba(47,84,235,1)"></i>多
|
|
||||||
<span class="text-weak">(悬停查看类型分布,点击行标签切换岗位)</span>
|
|
||||||
</div>`;
|
|
||||||
box.querySelectorAll(".hm-row-label").forEach((el) => {
|
|
||||||
el.onclick = () => {
|
el.onclick = () => {
|
||||||
$("#evo-job-select").value = el.dataset.job;
|
$("#evo-job-select").value = el.dataset.job;
|
||||||
/* 点击行 → 转入该岗位报告(默认 Tab) */
|
|
||||||
$$("#evo-tabs .match-tab").forEach((x) => x.classList.toggle("active", x.dataset.evotab === "report"));
|
$$("#evo-tabs .match-tab").forEach((x) => x.classList.toggle("active", x.dataset.evotab === "report"));
|
||||||
$("#evo-tab-report").classList.remove("hidden");
|
$("#evo-tab-report").classList.remove("hidden");
|
||||||
$("#evo-tab-scan").classList.add("hidden");
|
$("#evo-tab-scan").classList.add("hidden");
|
||||||
@@ -807,7 +800,7 @@ $$("#evo-tabs .match-tab").forEach((b) => {
|
|||||||
const tab = b.dataset.evotab;
|
const tab = b.dataset.evotab;
|
||||||
$("#evo-tab-report").classList.toggle("hidden", tab !== "report");
|
$("#evo-tab-report").classList.toggle("hidden", tab !== "report");
|
||||||
$("#evo-tab-scan").classList.toggle("hidden", tab !== "scan");
|
$("#evo-tab-scan").classList.toggle("hidden", tab !== "scan");
|
||||||
if (tab === "scan") renderEvoHeatmap();
|
if (tab === "scan") renderEvoScan();
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -957,7 +950,6 @@ function drawHeat() {
|
|||||||
function renderEvolution() {
|
function renderEvolution() {
|
||||||
const gran = $$("#evo-gran button").find((b) => b.classList.contains("active"))?.dataset.gran || "quarter";
|
const gran = $$("#evo-gran button").find((b) => b.classList.contains("active"))?.dataset.gran || "quarter";
|
||||||
drawStackBars($("#evo-stack-canvas"), evoEvents);
|
drawStackBars($("#evo-stack-canvas"), evoEvents);
|
||||||
renderEvoGantt($("#evo-job-select").value);
|
|
||||||
renderEvoReplace($("#evo-job-select").value);
|
renderEvoReplace($("#evo-job-select").value);
|
||||||
/* 热度走势:每季度实际生效的技能关系数(按 REQUIRES 边的生效区间统计) */
|
/* 热度走势:每季度实际生效的技能关系数(按 REQUIRES 边的生效区间统计) */
|
||||||
const jobEdges = ((CN.graphData || {}).edges || [])
|
const jobEdges = ((CN.graphData || {}).edges || [])
|
||||||
@@ -1071,62 +1063,6 @@ function drawStackBars(canvas, events) {
|
|||||||
|
|
||||||
/* 能力生命线(Swimlane):每个核心技能一行(近 8 季度),线段=生效区间,圆点=变更事件。
|
/* 能力生命线(Swimlane):每个核心技能一行(近 8 季度),线段=生效区间,圆点=变更事件。
|
||||||
* 行状态取该技能最近一次事件:新增=青绿 / 增强=蓝紫 / 减弱=琥珀 / 退出=砖红 / 持续=蓝灰 */
|
* 行状态取该技能最近一次事件:新增=青绿 / 增强=蓝紫 / 减弱=琥珀 / 退出=砖红 / 持续=蓝灰 */
|
||||||
function renderEvoGantt(jobId) {
|
|
||||||
const box = $("#evo-gantt");
|
|
||||||
const g = CN.graphData || {};
|
|
||||||
const allEvents = g.events || [];
|
|
||||||
const windows = [...new Set(allEvents.map((e) => e.window))].sort().slice(-8);
|
|
||||||
const edges = (g.edges || []).filter((e) => e.source === jobId && e.rel === "REQUIRES" && (e.intervals || []).length);
|
|
||||||
if (!windows.length || !edges.length) {
|
|
||||||
box.innerHTML = `<div class="empty small">暂无该岗位的能力生命线数据</div>`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
/* Top 12:当前仍生效优先,按当前需求指数降序 */
|
|
||||||
const alive = (e) => e.intervals.some(([s, en]) => en === null);
|
|
||||||
const rows = edges.slice().sort((a, b) =>
|
|
||||||
((alive(b) ? 1 : 0) - (alive(a) ? 1 : 0)) || ((b.weight || 0) - (a.weight || 0))).slice(0, 12);
|
|
||||||
const dots = {};
|
|
||||||
evoEvents.forEach((e) => { dots[`${e.skill}|${e.window}`] = e.change_type; });
|
|
||||||
const lastWin = windows[windows.length - 1];
|
|
||||||
const recentWins = windows.slice(-4);
|
|
||||||
const rowMeta = (edge) => {
|
|
||||||
const activeNow = edge.intervals.some(([s, en]) => s <= lastWin && (en === null || lastWin < en));
|
|
||||||
if (!activeNow) return { label: "退出", color: CHANGE_META["退出"].color };
|
|
||||||
const lastEv = evoEvents.filter((e) => e.skill === edge.target && recentWins.includes(e.window)).at(-1);
|
|
||||||
if (lastEv) return { label: lastEv.change_type, color: (CHANGE_META[lastEv.change_type] || {}).color || STEADY_COLOR };
|
|
||||||
return { label: edge.importance === "required" ? "核心" : "加分", color: STEADY_COLOR };
|
|
||||||
};
|
|
||||||
const cellHtml = (edge, w, color) => {
|
|
||||||
const active = edge.intervals.some(([s, en]) => s <= w && (en === null || w < en));
|
|
||||||
const dotType = dots[`${edge.target}|${w}`];
|
|
||||||
const dot = dotType
|
|
||||||
? `<i class="sl-dot" style="background:${(CHANGE_META[dotType] || {}).color || "#999"}" title="${w} ${dotType}"></i>` : "";
|
|
||||||
return `<div class="sl-cell" title="${esc(edge.target)} · ${esc(w)}${active ? "(生效中)" : ""}">
|
|
||||||
${active ? `<div class="sl-seg" style="background:${color}"></div>` : ""}${dot}</div>`;
|
|
||||||
};
|
|
||||||
box.innerHTML = `
|
|
||||||
<div class="sl-grid" style="grid-template-columns: 128px repeat(${windows.length}, 1fr) 56px">
|
|
||||||
<div class="hm-corner"></div>
|
|
||||||
${windows.map((w) => `<div class="hm-col-label">${esc(w.slice(2))}</div>`).join("")}
|
|
||||||
<div class="hm-col-label">状态</div>
|
|
||||||
${rows.map((e) => {
|
|
||||||
const m = rowMeta(e);
|
|
||||||
return `<div class="gt-label" data-skill="${esc(e.target)}" title="点击跳转图谱节点">${esc(e.target)}</div>
|
|
||||||
${windows.map((w) => cellHtml(e, w, m.color)).join("")}
|
|
||||||
<div class="sl-status" style="color:${m.color}">${m.label}</div>`;
|
|
||||||
}).join("")}
|
|
||||||
</div>
|
|
||||||
<div class="hm-legend">
|
|
||||||
<span>行状态:</span>
|
|
||||||
${Object.entries(CHANGE_META).map(([t, m]) => `<i class="hm-l" style="background:${m.color}"></i>${t}`).join("")}
|
|
||||||
<i class="hm-l" style="background:${STEADY_COLOR}"></i>持续
|
|
||||||
<span class="text-weak">(圆点 = 该季度变更事件,点击技能名跳转图谱)</span>
|
|
||||||
</div>`;
|
|
||||||
box.querySelectorAll(".gt-label").forEach((el) => {
|
|
||||||
el.onclick = () => { switchView("graph"); if (CN.gotoNode) CN.gotoNode(el.dataset.skill); };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 能力更替流:旧核心(已退出必备) → 持续能力 → 新核心(近 4 季度新进入) */
|
/* 能力更替流:旧核心(已退出必备) → 持续能力 → 新核心(近 4 季度新进入) */
|
||||||
function renderEvoReplace(jobId) {
|
function renderEvoReplace(jobId) {
|
||||||
const box = $("#evo-replace");
|
const box = $("#evo-replace");
|
||||||
@@ -1518,9 +1454,9 @@ function drawRadar(canvas, dims) {
|
|||||||
const batchState = { files: [], result: null };
|
const batchState = { files: [], result: null };
|
||||||
|
|
||||||
/* 子页签切换 */
|
/* 子页签切换 */
|
||||||
$$(".match-tab").forEach((b) => {
|
$$("#view-match .match-tab").forEach((b) => {
|
||||||
b.onclick = () => {
|
b.onclick = () => {
|
||||||
$$(".match-tab").forEach((x) => x.classList.toggle("active", x === b));
|
$$("#view-match .match-tab").forEach((x) => x.classList.toggle("active", x === b));
|
||||||
$("#match-single").classList.toggle("hidden", b.dataset.mtab !== "single");
|
$("#match-single").classList.toggle("hidden", b.dataset.mtab !== "single");
|
||||||
$("#match-batch").classList.toggle("hidden", b.dataset.mtab !== "batch");
|
$("#match-batch").classList.toggle("hidden", b.dataset.mtab !== "batch");
|
||||||
};
|
};
|
||||||
@@ -1977,7 +1913,7 @@ const DEMO_STEPS = [
|
|||||||
desc: "载入示例简历 → 解析确认 → 匹配结论 / 核心缺口 / 可迁移能力",
|
desc: "载入示例简历 → 解析确认 → 匹配结论 / 核心缺口 / 可迁移能力",
|
||||||
go: async () => {
|
go: async () => {
|
||||||
switchView("match");
|
switchView("match");
|
||||||
$$(".match-tab").forEach((x) => x.classList.toggle("active", x.dataset.mtab === "single"));
|
$$("#view-match .match-tab").forEach((x) => x.classList.toggle("active", x.dataset.mtab === "single"));
|
||||||
$("#match-single").classList.remove("hidden");
|
$("#match-single").classList.remove("hidden");
|
||||||
$("#match-batch").classList.add("hidden");
|
$("#match-batch").classList.add("hidden");
|
||||||
$("#match-job-select").value = DEMO_JOB;
|
$("#match-job-select").value = DEMO_JOB;
|
||||||
@@ -2314,7 +2250,7 @@ window.addEventListener("resize", () => {
|
|||||||
if (pathGoto) pathGoto.onclick = () => switchView("match");
|
if (pathGoto) pathGoto.onclick = () => switchView("match");
|
||||||
await loadDashboard(); // 先加载图谱缓存(演化页甘特图依赖)
|
await loadDashboard(); // 先加载图谱缓存(演化页甘特图依赖)
|
||||||
await loadEvolution();
|
await loadEvolution();
|
||||||
renderEvoHeatmap();
|
renderEvoScan();
|
||||||
/* 分享链接:#view=graph&node=xxx */
|
/* 分享链接:#view=graph&node=xxx */
|
||||||
const hash = new URLSearchParams(location.hash.slice(1));
|
const hash = new URLSearchParams(location.hash.slice(1));
|
||||||
if (hash.get("view")) switchView(hash.get("view"));
|
if (hash.get("view")) switchView(hash.get("view"));
|
||||||
|
|||||||
+4
-9
@@ -298,10 +298,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="evo-tab-scan" class="hidden">
|
<div id="evo-tab-scan" class="hidden">
|
||||||
<!-- 全部岗位演化热力图:仅近 12 季度,无事件格子置为极浅色 -->
|
<!-- 全岗位扫描:近 12 季度各岗位变化量横条排行(比热力图直观) -->
|
||||||
<div class="evo-chart-title text-aux text-secondary">全部岗位演化热力图 · 近 12 季度(每格 = 该季度演化事件数,点击行切换岗位并回到岗位报告)</div>
|
<div class="evo-chart-title text-aux text-secondary">全岗位扫描 · 近 12 季度变化量排行(点击行进入该岗位报告)</div>
|
||||||
<div class="evo-chart-desc">怎么看:每行一个岗位、每列一个季度,颜色越深说明该岗位当季能力变化越多。用于快速发现"哪些岗位正在剧烈变化";点击行标签进入该岗位的详细报告。</div>
|
<div class="evo-chart-desc">怎么看:每行一个岗位,横条长度=近 12 个季度的能力变化总量;绿色段=新增的技能,红色段=退出的技能。条越长说明这个岗位的能力要求变化越剧烈。</div>
|
||||||
<div class="evo-heatmap" id="evo-heatmap"><div class="empty small">加载中…</div></div>
|
<div class="evo-scan" id="evo-scan"><div class="empty small">加载中…</div></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="evo-tab-report">
|
<div id="evo-tab-report">
|
||||||
@@ -313,11 +313,6 @@
|
|||||||
<div class="evo-chart-desc">是什么:系统根据近两个季度的招聘 JD 自动归纳出的能力变化结论。新增=以前不要求、现在要求;增强=要求变重;减弱=要求变少;退出=不再要求。每条结论都可追溯到原始 JD 证据。</div>
|
<div class="evo-chart-desc">是什么:系统根据近两个季度的招聘 JD 自动归纳出的能力变化结论。新增=以前不要求、现在要求;增强=要求变重;减弱=要求变少;退出=不再要求。每条结论都可追溯到原始 JD 证据。</div>
|
||||||
<div class="evo-summary" id="evo-summary"><div class="empty small">加载中…</div></div>
|
<div class="evo-summary" id="evo-summary"><div class="empty small">加载中…</div></div>
|
||||||
|
|
||||||
<!-- 能力生命线(Swimlane):每个核心技能一行,线段=生效区间,圆点=变更事件 -->
|
|
||||||
<div class="evo-chart-title text-aux text-secondary">能力生命线(近 8 季度 · 按当前需求指数取 Top 12;线段 = 生效区间,圆点 = 变更事件)</div>
|
|
||||||
<div class="evo-chart-desc">怎么看:每行是一项核心技能的"在岗生命周期"——横线表示这段时间里该技能属于岗位要求,彩色圆点标记发生变化的时间点(颜色含义见图例)。一眼看出技能何时进入、何时增强、何时退出。</div>
|
|
||||||
<div class="evo-gantt" id="evo-gantt"></div>
|
|
||||||
|
|
||||||
<!-- 能力更替流:旧核心 → 持续能力 → 新核心 -->
|
<!-- 能力更替流:旧核心 → 持续能力 → 新核心 -->
|
||||||
<div class="evo-chart-title text-aux text-secondary">能力更替流(旧核心 → 持续能力 → 新核心)</div>
|
<div class="evo-chart-title text-aux text-secondary">能力更替流(旧核心 → 持续能力 → 新核心)</div>
|
||||||
<div class="evo-chart-desc">是什么:把岗位技能按演化结果分三列——左边是已退出的旧要求,中间是一直有效的持续能力,右边是新进入的要求。用于回答"这个岗位正在从什么技术栈转向什么技术栈"。</div>
|
<div class="evo-chart-desc">是什么:把岗位技能按演化结果分三列——左边是已退出的旧要求,中间是一直有效的持续能力,右边是新进入的要求。用于回答"这个岗位正在从什么技术栈转向什么技术栈"。</div>
|
||||||
|
|||||||
@@ -1168,3 +1168,15 @@ svg { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width:
|
|||||||
|
|
||||||
/* 演化页图表说明文字 */
|
/* 演化页图表说明文字 */
|
||||||
.evo-chart-desc { font-size: 12px; color: var(--text-3); line-height: 1.7; margin: -2px 0 8px; }
|
.evo-chart-desc { font-size: 12px; color: var(--text-3); line-height: 1.7; margin: -2px 0 8px; }
|
||||||
|
|
||||||
|
/* 全岗位扫描:横条排行 */
|
||||||
|
.evo-scan { margin-bottom: 16px; }
|
||||||
|
.scan-row {
|
||||||
|
display: flex; align-items: center; gap: 10px; padding: 5px 4px;
|
||||||
|
cursor: pointer; border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
.scan-row:hover { background: var(--bg-hover); }
|
||||||
|
.scan-label { flex: none; width: 132px; font-size: 13px; color: var(--text-1); text-align: right; }
|
||||||
|
.scan-track { flex: 1; display: flex; height: 14px; background: var(--bg-page); border-radius: 3px; overflow: hidden; }
|
||||||
|
.scan-bar { display: block; height: 100%; }
|
||||||
|
.scan-num { flex: none; width: 34px; font-size: 12px; color: var(--text-2); font-variant-numeric: tabular-nums; }
|
||||||
|
|||||||
Reference in New Issue
Block a user