2026-08-26 22:55:08 +08:00
|
|
|
|
#!/usr/bin/env node
|
|
|
|
|
|
/**
|
2026-09-16 10:18:04 +00:00
|
|
|
|
* 每日筛选汇总生成器 v1.4(2026-09-15 极简紧凑版)
|
2026-08-26 22:55:08 +08:00
|
|
|
|
* 从 recruitment.db 查询当日数据,生成 Markdown 日报。
|
|
|
|
|
|
* 用法:node daily-report.js [YYYY-MM-DD] [--db /path/to.db]
|
2026-09-16 10:09:30 +00:00
|
|
|
|
*
|
2026-09-16 10:18:04 +00:00
|
|
|
|
* v1.4:表格极限压缩——总览改单行文本流(无表头无对齐行),
|
|
|
|
|
|
* 画像表砍到3列。目标:整条消息一屏内、无空行浪费。
|
2026-08-26 22:55:08 +08:00
|
|
|
|
*/
|
|
|
|
|
|
const Database = require('better-sqlite3');
|
|
|
|
|
|
const path = require('path');
|
|
|
|
|
|
|
|
|
|
|
|
const args = process.argv.slice(2);
|
|
|
|
|
|
const dateArg = args.find(a => /^\d{4}-\d{2}-\d{2}$/.test(a));
|
2026-09-16 10:18:04 +00:00
|
|
|
|
const acctArgIdx = args.indexOf('--account');
|
|
|
|
|
|
const ONLY_ACCT = acctArgIdx >= 0 ? args[acctArgIdx + 1] : null; // v1.5: 单账号渲染(分账号发送用)
|
2026-08-26 22:55:08 +08:00
|
|
|
|
const dbArgIdx = args.indexOf('--db');
|
|
|
|
|
|
const DB_PATH = dbArgIdx >= 0 ? args[dbArgIdx + 1] : '/opt/data/recruitment-data/recruitment-live.db';
|
|
|
|
|
|
|
|
|
|
|
|
const day = dateArg || new Date().toISOString().slice(0, 10);
|
|
|
|
|
|
const dayStart = `${day}T00:00`;
|
|
|
|
|
|
const dayEnd = `${day}T23:59`;
|
|
|
|
|
|
|
|
|
|
|
|
const db = new Database(DB_PATH, { readonly: true });
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- 数据查询 ----------
|
|
|
|
|
|
const rows = db.prepare(
|
|
|
|
|
|
`SELECT account, name, age, education, experience, school, score, score_level, role_tag,
|
|
|
|
|
|
greeted, greeted_at, notes
|
|
|
|
|
|
FROM candidates WHERE scraped_at >= ? AND scraped_at <= ?`
|
|
|
|
|
|
).all(dayStart, dayEnd);
|
|
|
|
|
|
|
|
|
|
|
|
// 账号聚合
|
|
|
|
|
|
const byAcct = {};
|
|
|
|
|
|
for (const r of rows) {
|
|
|
|
|
|
const a = byAcct[r.account || '未知'] || (byAcct[r.account || '未知'] = { n: 0, rec: 0, wait: 0, irr: 0, greeted: 0 });
|
|
|
|
|
|
a.n++;
|
|
|
|
|
|
if (r.score_level === '推荐') a.rec++;
|
|
|
|
|
|
else if (r.score_level === '待定') a.wait++;
|
|
|
|
|
|
else a.irr++;
|
|
|
|
|
|
if (r.greeted) a.greeted++;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const total = { n: rows.length, rec: 0, wait: 0, irr: 0, greeted: 0 };
|
|
|
|
|
|
for (const a of Object.values(byAcct)) { total.rec += a.rec; total.wait += a.wait; total.irr += a.irr; total.greeted += a.greeted; }
|
|
|
|
|
|
|
|
|
|
|
|
// 打招呼明细(当日实际打过招呼的人)
|
|
|
|
|
|
const greetedRows = rows.filter(r => r.greeted && r.greeted_at && r.greeted_at.startsWith(day))
|
|
|
|
|
|
.sort((x, y) => (y.score || 0) - (x.score || 0));
|
|
|
|
|
|
|
|
|
|
|
|
// 历史累计
|
|
|
|
|
|
const histTotal = db.prepare('SELECT COUNT(*) c FROM candidates').get().c;
|
|
|
|
|
|
|
2026-09-16 10:18:04 +00:00
|
|
|
|
// ---------- 渲染 ----------
|
2026-08-26 22:55:08 +08:00
|
|
|
|
const WEEK = ['日', '一', '二', '三', '四', '五', '六'];
|
|
|
|
|
|
const wk = WEEK[new Date(day + 'T12:00:00Z').getUTCDay()];
|
|
|
|
|
|
|
|
|
|
|
|
const ACCT_META = {
|
2026-09-16 10:18:04 +00:00
|
|
|
|
hr1: { short: 'hr1', chatLeft: () => process.env.CHAT_LEFT_HR1 || '—', status: () => process.env.HR1_STATUS || '✅' },
|
|
|
|
|
|
hr2: { short: 'hr2', chatLeft: () => process.env.CHAT_LEFT_HR2 || '—', status: () => process.env.HR2_STATUS || '✅' },
|
2026-08-26 22:55:08 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-09-16 10:18:04 +00:00
|
|
|
|
// ---------- 开工判断(v1.5: --account 时只渲染该账号,且标题带账号名)----------
|
2026-09-11 13:00:04 +00:00
|
|
|
|
const activeAccts = ['hr1', 'hr2'].filter(a => (byAcct[a]?.n || 0) > 0);
|
2026-09-16 10:18:04 +00:00
|
|
|
|
const renderAccts = ONLY_ACCT ? activeAccts.filter(a => a === ONLY_ACCT) : activeAccts;
|
2026-09-11 13:00:04 +00:00
|
|
|
|
|
2026-08-26 22:55:08 +08:00
|
|
|
|
const md = [];
|
2026-09-16 10:18:04 +00:00
|
|
|
|
if (renderAccts.length > 0) {
|
|
|
|
|
|
const titleAcct = ONLY_ACCT ? `(${ACCT_META[ONLY_ACCT].short})` : '';
|
|
|
|
|
|
md.push(`📊 **简历筛选日报${titleAcct} ${day} 周${wk}**`);
|
2026-08-26 22:55:08 +08:00
|
|
|
|
md.push('');
|
2026-09-16 10:18:04 +00:00
|
|
|
|
for (const a of renderAccts) {
|
2026-09-16 10:09:30 +00:00
|
|
|
|
const v = byAcct[a];
|
|
|
|
|
|
const p = (n) => v.n ? Math.round(n / v.n * 100) + '%' : '0%';
|
2026-09-16 10:18:04 +00:00
|
|
|
|
md.push(`**${ACCT_META[a].short}** 筛${v.n}·打${v.greeted}|✅${v.rec} ⏳${v.wait} ⛔${v.irr}(${p(v.irr)})|剩${ACCT_META[a].chatLeft()}次 ${ACCT_META[a].status()}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (renderAccts.length > 1 && !ONLY_ACCT) {
|
|
|
|
|
|
const p = (n) => total.n ? Math.round(n / total.n * 100) + '%' : '0%';
|
|
|
|
|
|
md.push(`**合计** 筛${total.n}·打${total.greeted}|✅${total.rec} ⏳${total.wait} ⛔${total.irr}(${p(total.irr)})|库累计${histTotal.toLocaleString()}`);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
md.push(`> 库累计 ${histTotal.toLocaleString()} 人`);
|
2026-09-11 13:00:04 +00:00
|
|
|
|
}
|
2026-09-16 10:09:30 +00:00
|
|
|
|
md.push('');
|
2026-08-26 22:55:08 +08:00
|
|
|
|
|
2026-09-16 10:18:04 +00:00
|
|
|
|
// 打招呼画像(3列极简:姓名 | 基本情况 | 分·级别)
|
|
|
|
|
|
for (const acct of renderAccts) {
|
2026-08-26 22:55:08 +08:00
|
|
|
|
const gr = greetedRows.filter(g => g.account === acct);
|
2026-09-16 10:09:30 +00:00
|
|
|
|
if (!gr.length) continue;
|
2026-09-16 10:18:04 +00:00
|
|
|
|
md.push(`**打招呼 ${gr.length} 人**(${ACCT_META[acct].short})`);
|
2026-09-16 10:09:30 +00:00
|
|
|
|
md.push('');
|
2026-09-16 10:18:04 +00:00
|
|
|
|
md.push(`| 姓名 | 情况 | 分 | 命中 |`);
|
2026-09-16 10:09:30 +00:00
|
|
|
|
md.push(`|:--|:--|:--|:--|`);
|
2026-09-16 10:18:04 +00:00
|
|
|
|
for (const g of gr.slice(0, 5)) {
|
2026-09-16 10:09:30 +00:00
|
|
|
|
const expRaw = (g.experience || '').toString();
|
|
|
|
|
|
const exp = /应届生|在校生/.test(expRaw) ? '应届' : (expRaw.match(/\d+年/) || [])[0] || ((expRaw.match(/^\d+$/)?.[0] || '') + '年' || '—');
|
2026-09-16 10:18:04 +00:00
|
|
|
|
// 命中:notes里的 经验/专业/证书 提取关键词,紧凑展示(经验值/专业语种/证书名)
|
|
|
|
|
|
const hit = (g.notes || '')
|
|
|
|
|
|
.split(/[;;]/).map(s => s.trim()).filter(Boolean)
|
|
|
|
|
|
.map(s => s.replace(/^经验:外贸\/出口.*/, '外贸出口').replace(/^经验:/, '').replace(/^专业:国际经济与贸易$/, '国贸').replace(/^专业:商务英语$/, '商英').replace(/^专业:应用英语$/, '应英').replace(/^专业:/, '').replace(/^证书:/, '').replace(/大学英语四级|英语四级/g, 'CET4').replace(/大学英语六级|英语六级/g, 'CET6').replace(/英语专业四级|英语四级考试/g, '专四').replace(/英语专业八级/g, '专八').replace(/\/.*/, ''))
|
|
|
|
|
|
.join('·');
|
|
|
|
|
|
md.push(`| ${g.name} | ${g.age || '?'}·${g.education || '?'}·${exp} | ${g.score}${g.score_level === '推荐' ? '✅' : ''} | ${hit.slice(0, 20)} |`);
|
2026-08-26 22:55:08 +08:00
|
|
|
|
}
|
2026-09-16 10:18:04 +00:00
|
|
|
|
if (gr.length > 5) md.push(`| …${gr.length}人 | | |`);
|
2026-09-16 10:09:30 +00:00
|
|
|
|
md.push('');
|
2026-08-26 22:55:08 +08:00
|
|
|
|
}
|
2026-09-16 10:18:04 +00:00
|
|
|
|
} // end if
|
2026-08-26 22:55:08 +08:00
|
|
|
|
|
|
|
|
|
|
console.log(md.join('\n'));
|
|
|
|
|
|
db.close();
|