Files
zhaopin/scripts/daily-report.js
T

136 lines
5.6 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.0(2026-08-26)
* 从 recruitment.db 查询当日数据,生成 Markdown 日报。
* 用法:node daily-report.js [YYYY-MM-DD] [--db /path/to.db]
* 输出:Markdown 文本到 stdout(cron 任务用 hermes send -f 发送)
*/
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 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;
// 最近一次聊天剩余(从最近 scrape_log 无记录,需运行时传入;此处用环境变量或显示待补)
const chatLeft = process.env.CHAT_LEFT_HR1 ? `hr1 剩 ${process.env.CHAT_LEFT_HR1}` : 'hr1 剩 ?';
const chatLeft2 = process.env.CHAT_LEFT_HR2 ? `hr2 剩 ${process.env.CHAT_LEFT_HR2}` : 'hr2 剩 ?';
// ---------- Markdown 渲染 ----------
const WEEK = ['日', '一', '二', '三', '四', '五', '六'];
const wk = WEEK[new Date(day + 'T12:00:00Z').getUTCDay()];
const ACCT_META = {
hr1: { label: 'hr1(Chrome)', chatLeft: () => process.env.CHAT_LEFT_HR1 ? `剩 ${process.env.CHAT_LEFT_HR1} 次` : '—', status: () => process.env.HR1_STATUS || '✅ 在线' },
hr2: { label: 'hr2(Edge)', chatLeft: () => process.env.CHAT_LEFT_HR2 ? `剩 ${process.env.CHAT_LEFT_HR2} 次` : '—', status: () => process.env.HR2_STATUS || '✅ 在线' },
};
// ---------- 开工判断(2026-09-02 用户定稿)----------
// 未开工 = 当日无任何筛选数据。两个都没开工 → 输出空串(不发日报);
// 只有一个开工 → 日报只包含该账号,未开工的整块剔除。
const activeAccts = ['hr1', 'hr2'].filter(a => (byAcct[a]?.n || 0) > 0);
const md = [];
if (activeAccts.length > 0) {
md.push(`# 📊 智联简历筛选日报`);
md.push('');
md.push(`**${day} 周${wk}** · 岗位:二手车出口外贸业务员`);
md.push('---');
md.push('');
// 总览(只列开工账号;单账号时省略对比表)
md.push(`## ■ 今日总览`);
md.push('');
if (activeAccts.length === 2) {
md.push(`| 指标 | hr1 Chrome | hr2 Edge | 合计 |`);
md.push(`|:---|---:|---:|---:|`);
md.push(`| 新增筛选 | ${byAcct.hr1?.n || 0} | ${byAcct.hr2?.n || 0} | **${total.n}** |`);
md.push(`| 打招呼 | ${byAcct.hr1?.greeted || 0} | ${byAcct.hr2?.greeted || 0} | **${total.greeted}** |`);
} else {
const a = activeAccts[0];
md.push(`| 指标 | ${ACCT_META[a].label} |`);
md.push(`|:---|---:|`);
md.push(`| 新增筛选 | **${byAcct[a].n}** |`);
md.push(`| 打招呼 | **${byAcct[a].greeted}** |`);
}
md.push('');
// 分账号小节(只列开工账号,2026-09-02)
for (const acct of activeAccts) {
const meta = ACCT_META[acct];
const v = byAcct[acct];
md.push(`## ■ ${meta.label}`);
md.push('');
const p = (n) => ((n / v.n) * 100).toFixed(1) + '%';
md.push(`| 结果 | 人数 | 占比 |`);
md.push(`|:---|---:|---:|`);
md.push(`| ✅ 推荐 | ${v.rec} | ${p(v.rec)} |`);
md.push(`| ⏳ 待定 | ${v.wait} | ${p(v.wait)} |`);
md.push(`| ⛔ 拦截 | ${v.irr} | ${p(v.irr)} |`);
md.push('');
const gr = greetedRows.filter(g => g.account === acct);
if (gr.length) {
md.push(`**打招呼画像(${gr.length} 人)**`);
md.push('');
md.push(`| 姓名 | 年龄 | 学历 | 经验 | 评分 | 级别 |`);
md.push(`|:---|---:|:---:|---:|---:|:---:|`);
const top = gr.slice(0, 5);
for (const g of top) {
const expRaw = (g.experience || '').toString();
const exp = /应届生|在校生/.test(expRaw) ? '应届' : (expRaw.match(/\d+年/) || [])[0] || ((expRaw.match(/^\d+$/)?.[0] || '') + '年' || '—');
md.push(`| ${g.name} | ${g.age || '—'} | ${g.education || '—'} | ${exp} | ${g.score || '—'} | ${g.score_level} |`);
}
if (gr.length > 5) md.push(`| …共 ${gr.length} 人 | | | | | |`);
md.push('');
}
}
md.push(`## ■ 资源与状态`);
md.push('');
md.push(`| 账号 | 聊天剩余 | 状态 |`);
md.push(`|:---|---:|:---|`);
for (const a of activeAccts) {
md.push(`| ${ACCT_META[a].label.replace('(', ' ').replace(')', '')} | ${ACCT_META[a].chatLeft()} | ${ACCT_META[a].status()} |`);
}
md.push('');
md.push(`- 历史累计:${histTotal.toLocaleString()} 人`);
} // end if (activeAccts.length > 0)
console.log(md.join('\n'));
db.close();