// scoring.js v10 — 简历筛选评分与硬性预筛门槛(纯逻辑,无浏览器依赖,可离线测试) // // v10 变更(2026-08-26,需求方反馈"打招呼范围过宽"): // 1. 新增硬性预筛门槛 hardGate():不相关候选人直接标记"不相关",永不进入打招呼流程 // - 门槛A(岗位/经历):全文须含外贸类关键词(外贸/外贸销售/外贸业务/外贸汽车/外贸助理/ // 外贸主管/外贸经理/外销/国际贸易/国贸/海外销售/出口/跨境电商/二手车出口/汽车出口/整车出口) // - 门槛B(毕业专业):教育经历须含 英语/商务英语/应用英语/俄语/西班牙语/西语/法语/ // 国际经济与贸易/贸易经济 // - 门槛C(技能证书):须含 英语四级/六级/八级(CET-4/6, 专四/专八)、俄语四/六/八级、 // 法语四级/四八级、西班牙语四八级 等指定证书 // - A/B/C 满足任一即算"方向相关";都不满足 → 不相关,不评分不打招呼 // 2. 新增毕业时间窗口:2020-2026 届(教育经历中解析到的最晚年份; // 解析不到时不硬拒,gateReason 标记 year-unknown,可配置 GRAD_YEAR.strict=true 改为硬拒) // 3. 保留 v9 七维评分 + 动态阈值 + 年龄>35 一票否决 + 岗位推荐标签 // // 用法(脚本内):const { hardGate, scoreCandidate } = require('./scoring'); // 离线测试:node scoring.js # 跑内置样例 // node scoring.js --db # 用真实数据库抽样验证 'use strict'; // ========== 可配置筛选条件(需求方 2026-08-26 更新) ========== const CONFIG = { // 门槛A:岗位/经历关键词(任一命中即通过,全文匹配 rawText+workDetail) tradeKeywords: [ '外贸', '外销', '国际贸易', '国贸', '海外销售', '出口', '跨境电商', '跨境', '二手车出口', '汽车出口', '整车出口', '海外市场', ], // 门槛B:毕业专业(教育经历 educationDetail 匹配,rawText 兜底) majorKeywords: [ '英语', '商务英语', '应用英语', '俄语', '西班牙语', '西语', '法语', '国际经济与贸易', '贸易经济', ], // 门槛C:技能证书(certDetail + rawText 匹配) certKeywords: [ // 英语 'CET-4', 'CET-6', 'CET4', 'CET6', '大学英语四级', '大学英语六级', '英语四级', '英语六级', '英语八级', '英语专业四级', '英语专业八级', '专四', '专八', 'TEM-4', 'TEM-8', // 俄语 '俄语四级', '俄语六级', '俄语八级', '俄语专业四级', '俄语专业八级', // 法语 '法语四级', '法语六级', '法语八级', '法语四八级', '法语专业四级', '法语专业八级', // 西班牙语 '西班牙语四级', '西班牙语六级', '西班牙语八级', '西班牙语四八级', '西语四级', '西语六级', '西语八级', '西语四八级', ], // 门槛D:毕业时间窗口 [min, max](含边界;教育经历解析不到年份时见 strict) gradYear: { min: 2020, max: 2026, strict: false }, // 年龄硬否决(v9 沿用) maxAge: 35, // 动态阈值(v9 沿用):行业匹配≥4 时推荐线降 2 分 thresholds: { recommend: 26, recommendIndustry: 24, maybe: 18 }, }; // ========== v9 七维评分关键词(与 jd-requirements.md 映射一致) ========== const INDUSTRY_HIGH = ['二手车', '整车出口', '汽车出口', '车辆出口', '海外汽车', 'VIN', '车架号', '滚装船', '车型配置', '多VIN']; const INDUSTRY_MID = ['汽配', '工程机械', '大宗', '汽车', '车辆']; const TRADE_HIGH = ['询盘', '报价', '谈判', '成交', 'PI', '报关', '清关', '收款', '复购', '客户开发', '订单', 'T/T', 'LC']; // 仅 workDetail const TRADE_MID = ['外贸', '出口', '跨境电商', '海外客户', '国际贸易', '跟单', '外销']; const LANG_TOP = ['雅思', 'IELTS', '托福', 'TOEFL', 'BEC', '俄语', '法语', '西班牙语', '西语', '葡萄牙语', '阿拉伯语']; const LANG_MID = ['CET-6', 'CET6', '大学英语六级', '英语六级', '专八', '英语专业八级', 'TEM-8']; const LANG_LOW = ['CET-4', 'CET4', '大学英语四级', '英语四级', '专四', '英语专业四级', 'TEM-4', '商务英语', '英语口语']; const CHANNEL_HIGH = ['阿里巴巴国际站', '阿里国际站', '独立站', 'LinkedIn', 'Facebook', 'WhatsApp', 'Google', '展会', '中国制造网', 'Instagram']; // 仅 workDetail const CHANNEL_LOW = ['客户开发', '邮件开发', '获客', '社媒推广', '社媒']; const MAJOR_HIGH = ['商务英语', '国际贸易', '英语', '国际经济与贸易']; const MAJOR_LANG = ['俄语', '西班牙语', '西语', '法语', '德语', '日语', '朝鲜语', '翻译', '应用英语']; const MAJOR_OTHER = ['市场营销', '跨境电商', '电子商务', '贸易经济']; function countHits(text, list) { return list.reduce((n, k) => (text.includes(k) ? n + 1 : n), 0); } // ========== 硬性预筛门槛 ========== // 从教育经历解析毕业年份(取出现的最晚年份作为毕业时间) function parseGradYear(educationDetail, rawText) { const src = educationDetail || rawText || ''; const years = (src.match(/(?:19|20)\d{2}/g) || []).map(Number).filter(y => y >= 1990 && y <= 2030); if (!years.length) return null; return Math.max(...years); } /** * 硬性预筛:通过返回 { pass: true, reasons: [...] }; * 不通过返回 { pass: false, reason: '...' }(reason 用于日志与数据库 notes)。 * 数据形态与 extractModalData 输出一致:{ rawText, workDetail, educationDetail, certDetail, age } */ function hardGate(data) { const rawText = data.rawText || ''; const workDetail = data.workDetail || ''; const eduDetail = data.educationDetail || ''; const certDetail = data.certDetail || ''; const reasons = []; // 年龄一票否决(先于其他门槛,v9 沿用) if (data.age && data.age > CONFIG.maxAge) { return { pass: false, reason: `age>${CONFIG.maxAge}`, level: '不推荐' }; } // A:外贸相关【经验】必须命中(2026-08-26 需求方收紧:只要相关经验,不再考虑培养类) // 只匹配 workDetail(工作经历)——求职意向/教育背景里提到外贸不算经验 const tradeHit = CONFIG.tradeKeywords.filter(k => workDetail.includes(k)); const majorSrc = eduDetail || rawText; const majorHit = CONFIG.majorKeywords.filter(k => majorSrc.includes(k)); const certSrc = certDetail + '\n' + rawText; const certHit = CONFIG.certKeywords.filter(k => certSrc.includes(k)); if (!tradeHit.length) { // B/C 只中专业或证书 = 培养类,明确拦截并注明原因便于统计 const traineeSignal = []; if (majorHit.length) traineeSignal.push(`major:${majorHit[0]}`); if (certHit.length) traineeSignal.push(`cert:${certHit[0]}`); return { pass: false, reason: traineeSignal.length ? `trainee-only(${traineeSignal.join('+')})` : 'no-trade-experience', level: '不相关', }; } reasons.push(`经验:${tradeHit.slice(0, 3).join('/')}`); if (majorHit.length) reasons.push(`专业:${majorHit.slice(0, 3).join('/')}`); if (certHit.length) reasons.push(`证书:${certHit.slice(0, 3).join('/')}`); // D:毕业时间窗口 const gradYear = parseGradYear(eduDetail, rawText); if (gradYear !== null && (gradYear < CONFIG.gradYear.min || gradYear > CONFIG.gradYear.max)) { return { pass: false, reason: `grad-year-${gradYear}-out-of-${CONFIG.gradYear.min}-${CONFIG.gradYear.max}`, level: '不相关' }; } if (gradYear === null) reasons.push('year-unknown'); return { pass: true, reasons, gradYear }; } // ========== 活跃度解析(v9 沿用 v5 逻辑) ========== function parseActivity(activity) { if (!activity) return 2; const text = activity.toLowerCase(); if (/^\d+分钟前/.test(text) && /回复|浏览/.test(text)) return 5; if (/半小时前|1小时前/.test(text)) return 4; if (/^\d+小时前/.test(text)) return 4; if (/48小时前在线/.test(text)) return 3; if (/^\d天前(有回复|在线|浏览)/.test(text) || /^[123]天前/.test(text)) return 3; if (/新注册/.test(text)) return 2; if (/^\d+天前/.test(text)) return 1; return 2; } // ========== 经验年数解析(注意"27年应届生"陷阱,先判应届) ========== function parseExperienceYears(exp) { if (!exp) return null; if (/应届生|在校生/.test(exp)) return 0; const m = exp.match(/(\d+)年(\d*个月?)?/); if (m) return parseInt(m[1], 10) + (parseInt((m[2] || '').replace(/[^0-9]/g, ''), 10) || 0) / 12; return null; } // ========== 七维评分(v9 重建版,维度名与数据库 score_details 一致) ========== function scoreCandidate(data) { const rawText = data.rawText || ''; const workDetail = data.workDetail || ''; const eduDetail = data.educationDetail || ''; const fullText = rawText + '\n' + workDetail; const expYears = parseExperienceYears(data.experience || data.positionExp || '') || 0; // 1. 行业匹配(全文):二手车/汽车出口类高分,汽配/工程机械中分 const indH = countHits(fullText, INDUSTRY_HIGH); const indM = countHits(fullText, INDUSTRY_MID); let industryScore = 1; if (indH >= 2) industryScore = 5; else if (indH === 1) industryScore = 4; else if (indM >= 2) industryScore = 3; else if (indM === 1) industryScore = 2; // 2. 外贸核心能力:highTrade 仅从 workDetail 匹配(求职意向不算数),midTrade 全文 const highTrade = countHits(workDetail, TRADE_HIGH); const midTrade = countHits(fullText, TRADE_MID); let tradeScore = 1; if (highTrade >= 2) tradeScore = 5; else if (highTrade === 1) tradeScore = 4; else if (midTrade >= 2) tradeScore = 3; else if (midTrade === 1) tradeScore = 2; // 3. 语言能力:小语种/雅思托福最高;外贸经验强时保底 2 分(语言不一票否决实战派) const langTop = countHits(fullText, LANG_TOP); const langMid = countHits(fullText, LANG_MID); const langLow = countHits(fullText, LANG_LOW); let langScore = 1; if (langTop >= 1) langScore = 5; else if (langMid >= 1) langScore = 4; else if (langLow >= 1) langScore = 3; if (tradeScore <= 2 && highTrade >= 2) langScore = Math.max(langScore, 2); // 4. 客户开发与获客(仅 workDetail,避免实习/意向虚高;区分高/低含金量) const chH = countHits(workDetail, CHANNEL_HIGH); const chL = countHits(workDetail, CHANNEL_LOW); let channelScore = 1; if (chH >= 2) channelScore = 5; else if (chH === 1 && chL >= 1) channelScore = 4; else if (chH === 1) channelScore = 3; else if (chL >= 2) channelScore = 3; else if (chL === 1) channelScore = 2; // 5. 专业背景:高匹配专业+学历综合 const edu = data.education || ''; let eduBase = 1; if (edu === '硕士' || edu === '博士') eduBase = 5; else if (edu === '本科') eduBase = 4; else if (edu === '大专') eduBase = 3; const majorSrc = eduDetail || rawText; let majorScore = 1; if (countHits(majorSrc, MAJOR_HIGH) >= 1 && eduBase >= 3) majorScore = 5; else if (countHits(majorSrc, MAJOR_LANG) >= 1) majorScore = 4; else if (countHits(majorSrc, MAJOR_OTHER) >= 1) majorScore = 3; else if (eduBase >= 4) majorScore = 2; // 6. 稳定性:最长在职时长 + 工作段数 let stabilityScore = 3; if (workDetail) { const dms = workDetail.match(/\((\d+年\s*\d*个月?)\)/g) || workDetail.match(/(\d+年\d*个月?)(?=[\s\n])/g); if (dms && dms.length) { const durations = dms.map(d => { const m = d.match(/(\d+)年\s*(\d*个月?)?/); return (parseInt(m[1], 10) || 0) + (parseInt((m[2] || '').replace(/[^0-9]/g, ''), 10) || 0) / 12; }); const mx = Math.max(...durations); const jc = durations.length; if (mx >= 2 && jc <= 3) stabilityScore = 5; else if (mx >= 1.5 && jc <= 4) stabilityScore = 4; else if (mx >= 1) stabilityScore = 3; else if (durations[0] < 0.5 && jc > 3) stabilityScore = 1; else stabilityScore = 2; } } // 7. 活跃度 const activityScore = parseActivity(data.activity || ''); const total = industryScore + tradeScore + langScore + channelScore + majorScore + stabilityScore + activityScore; // 动态阈值(v9):行业强 → 推荐线降 2 分 const recommendLine = industryScore >= 4 ? CONFIG.thresholds.recommendIndustry : CONFIG.thresholds.recommend; let level = total >= recommendLine ? '推荐' : (total >= CONFIG.thresholds.maybe ? '待定' : '不推荐'); // 岗位推荐标签(v9) let roleTag = '不限'; if (expYears >= 1 && (tradeScore >= 3 || channelScore >= 3 || industryScore >= 3)) roleTag = '有经验岗'; else if (expYears < 1 && (langScore >= 3 || majorScore >= 3)) roleTag = '应届生岗'; return { total, level, roleTag, details: { industryScore, tradeScore, langScore, channelScore, majorScore, stabilityScore, activityScore }, }; } // ========== 总入口:门槛 + 评分 ========== /** * evaluateCandidate(data) → { gate, score, total, level, details } * 门槛不通过时 level 为 '不相关'(或年龄否决的 '不推荐'),score 为 null,绝不打招呼。 */ function evaluateCandidate(data) { const gate = hardGate(data); if (!gate.pass) { return { gate, score: null, total: 0, level: gate.level || '不相关', roleTag: '不限', details: { gateReason: gate.reason } }; } const sc = scoreCandidate(data); return { gate, score: sc.total, total: sc.total, level: sc.level, roleTag: sc.roleTag, details: sc.details }; } module.exports = { CONFIG, hardGate, scoreCandidate, evaluateCandidate, parseGradYear, parseExperienceYears, parseActivity }; // ========== 离线自测(node scoring.js 直接运行时) ========== if (require.main === module) { const samples = [ { name: '外贸业务员(应通过)', data: { age: 27, education: '本科', experience: '3年', rawText: '三年外贸业务员经验,负责海外客户开发、询盘报价,参加展会,阿里巴巴国际站运营', workDetail: 'XX外贸公司 外贸业务员 (2年8个月) 客户开发 询盘 报价', educationDetail: '2018-2022 本科 商务英语', certDetail: '大学英语六级 CET-6' } }, { name: '证券分析(应拦截)', data: { age: 28, education: '本科', experience: '4年', rawText: '证券公司研究员,负责行业研究与投资分析报告', workDetail: 'XX证券 证券分析师 (3年2个月) 行业研究 金融建模', educationDetail: '2014-2018 本科 金融学', certDetail: '证券从业资格' } }, { name: '保险顾问(应拦截)', data: { age: 30, education: '大专', experience: '5年', rawText: '保险代理人,客户资源维护与保单销售', workDetail: 'XX人寿 保险顾问 (4年1个月) 保单销售 客户维护', educationDetail: '2013-2016 大专 市场营销', certDetail: '' } }, { name: '英语应届生(应拦截-培养类)', data: { age: 22, education: '本科', experience: '应届生', rawText: '英语专业应届毕业生,求职外贸方向', workDetail: '', educationDetail: '2022-2026 本科 英语', certDetail: '英语专业四级 专四' } }, { name: '俄语翻译(应拦截-培养类)', data: { age: 24, education: '本科', experience: '1年', rawText: '俄语翻译,协助外贸邮件沟通', workDetail: 'XX贸易 翻译 (1年0个月)', educationDetail: '2018-2022 本科 俄语', certDetail: '俄语专业四级' } }, { name: '2019届外贸(应拦-毕业年份)', data: { age: 29, education: '本科', experience: '6年', rawText: '外贸经理,团队管理,海外市场', workDetail: 'XX进出口 外贸经理 (5年2个月) 外贸 出口', educationDetail: '2015-2019 本科 国际经济与贸易', certDetail: '' } }, { name: '36岁外贸老手(应拦-年龄)', data: { age: 36, education: '本科', experience: '12年', rawText: '外贸总监', workDetail: '外贸 (10年0个月)', educationDetail: '2008-2012 本科 英语', certDetail: '专八' } }, ]; for (const s of samples) { const r = evaluateCandidate(s.data); const verdict = r.gate.pass ? `${r.level} ${r.total}分 [${r.roleTag}] (${r.gate.reasons.join('; ')})` : `⛔ ${r.level} (${r.gate.reason})`; console.log(`${s.name} → ${verdict}`); } }