Files
zhaopin/scripts/daily-report.js
T

110 lines
5.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* 每日筛选汇总生成器 v1.4(2026-09-15 极简紧凑版)
* 从 recruitment.db 查询当日数据,生成 Markdown 日报。
* 用法:node daily-report.js [YYYY-MM-DD] [--db /path/to.db]
*
* v1.4:表格极限压缩——总览改单行文本流(无表头无对齐行),
* 画像表砍到3列。目标:整条消息一屏内、无空行浪费。
*/
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));
const acctArgIdx = args.indexOf('--account');
const ONLY_ACCT = acctArgIdx >= 0 ? args[acctArgIdx + 1] : null; // v1.5: 单账号渲染(分账号发送用)
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;
// ---------- 渲染 ----------
const WEEK = ['日', '一', '二', '三', '四', '五', '六'];
const wk = WEEK[new Date(day + 'T12:00:00Z').getUTCDay()];
const ACCT_META = {
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 || '✅' },
};
// ---------- 开工判断(v1.5: --account 时只渲染该账号,且标题带账号名)----------
const activeAccts = ['hr1', 'hr2'].filter(a => (byAcct[a]?.n || 0) > 0);
const renderAccts = ONLY_ACCT ? activeAccts.filter(a => a === ONLY_ACCT) : activeAccts;
const md = [];
if (renderAccts.length > 0) {
const titleAcct = ONLY_ACCT ? `(${ACCT_META[ONLY_ACCT].short})` : '';
md.push(`📊 **简历筛选日报${titleAcct} ${day} 周${wk}**`);
md.push('');
for (const a of renderAccts) {
const v = byAcct[a];
const p = (n) => v.n ? Math.round(n / v.n * 100) + '%' : '0%';
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()} 人`);
}
md.push('');
// 打招呼画像(3列极简:姓名 | 基本情况 | 分·级别)
for (const acct of renderAccts) {
const gr = greetedRows.filter(g => g.account === acct);
if (!gr.length) continue;
md.push(`**打招呼 ${gr.length} 人**(${ACCT_META[acct].short})`);
md.push('');
md.push(`| 姓名 | 情况 | 分 | 命中 |`);
md.push(`|:--|:--|:--|:--|`);
for (const g of gr.slice(0, 5)) {
const expRaw = (g.experience || '').toString();
const exp = /应届生|在校生/.test(expRaw) ? '应届' : (expRaw.match(/\d+年/) || [])[0] || ((expRaw.match(/^\d+$/)?.[0] || '') + '年' || '—');
// 命中: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)} |`);
}
if (gr.length > 5) md.push(`| …${gr.length}人 | | |`);
md.push('');
}
} // end if
console.log(md.join('\n'));
db.close();