'use strict'; const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); const ExcelJS = require('exceljs'); const config = require('../config'); const db = require('../infra/db'); const { buildBackupPayload } = require('./backup'); const EXPORT_TYPES = new Set([ 'employees', 'online-exams', 'practical-exams', 'team-analysis', 'analysis-report', 'question-details', 'backup', ]); function safeNumber(value) { const number = Number(value); return Number.isFinite(number) ? number : null; } function jsonCell(value) { if (value == null) return null; return typeof value === 'object' ? JSON.stringify(value) : value; } function normalizeRows(rows) { return rows.map((row) => Object.fromEntries( Object.entries(row).map(([key, value]) => [key, jsonCell(value)]), )); } function addSheet(workbook, name, rows) { const normalized = normalizeRows(rows); const data = normalized.length > 0 ? normalized : [{ message: '无数据' }]; const keys = [...new Set(data.flatMap((row) => Object.keys(row)))]; const sheet = workbook.addWorksheet(name.slice(0, 31)); sheet.columns = keys.map((key) => ({ header: key, key, width: Math.min(40, Math.max(12, key.length + 4)) })); sheet.addRows(data); sheet.getRow(1).font = { bold: true }; sheet.views = [{ state: 'frozen', ySplit: 1 }]; } function periodWhere(alias, params, values) { const clauses = []; const year = safeNumber(params.year); const month = safeNumber(params.month); if (year != null) { clauses.push(`${alias}.exam_year = ?`); values.push(year); } if (month != null) { clauses.push(`${alias}.exam_month = ?`); values.push(month); } if (params.team) { clauses.push('e.team = ?'); values.push(String(params.team)); } if (params.employeeId) { clauses.push(`${alias}.employee_id = ?`); values.push(String(params.employeeId)); } return clauses; } async function employeesWorkbook(params = {}) { const values = []; const where = params.employeeId ? 'AND employee_id = ?' : 'AND (employment_status = \'在职\' OR employment_status IS NULL)'; if (params.employeeId) values.push(String(params.employeeId)); const rows = await db.query( `SELECT employee_id, internal_employee_id, name, team, position, native_place, nationality, political_status, education, education_type, graduate_school, graduate_date, skill_level, hire_date, birth_date, status, employment_status, source, details, extras FROM employees WHERE deleted_at IS NULL ${where} ORDER BY team, employee_id`, values, ); const workbook = new ExcelJS.Workbook(); addSheet(workbook, '员工档案', rows); return workbook; } async function onlineWorkbook(params) { const values = []; const clauses = periodWhere('o', params, values); const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : ''; const exams = await db.query( `SELECT o.employee_id, e.name AS employee_name, e.team, e.position, o.exam_year, o.exam_month, o.paper_class, o.score, o.result, o.faults, o.choice_faults, o.submit_time, o.elapsed_seconds, o.details FROM online_exams o LEFT JOIN employees e ON e.employee_id = o.employee_id AND e.deleted_at IS NULL ${where} ORDER BY o.exam_year, o.exam_month, e.team, o.employee_id`, values, ); const missingValues = []; const missingClauses = []; if (safeNumber(params.year) != null && safeNumber(params.month) != null) { missingClauses.push('m.missing_month = ?'); missingValues.push(`${safeNumber(params.year)}-${String(safeNumber(params.month)).padStart(2, '0')}`); } else if (safeNumber(params.year) != null) { missingClauses.push('m.missing_month LIKE ?'); missingValues.push(`${safeNumber(params.year)}-%`); } if (params.team) { missingClauses.push('e.team = ?'); missingValues.push(String(params.team)); } const missingWhere = missingClauses.length ? `WHERE ${missingClauses.join(' AND ')}` : ''; const missing = await db.query( `SELECT m.employee_id, e.name AS employee_name, e.team, e.position, m.missing_month, m.reason, m.import_time, m.details FROM missing_exams m LEFT JOIN employees e ON e.employee_id = m.employee_id AND e.deleted_at IS NULL ${missingWhere} ORDER BY m.missing_month, e.team, m.employee_id`, missingValues, ); const workbook = new ExcelJS.Workbook(); addSheet(workbook, '在线考试', exams); addSheet(workbook, '缺考', missing); return workbook; } async function practicalWorkbook(params) { const values = []; const clauses = periodWhere('p', params, values); const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : ''; const rows = await db.query( `SELECT p.employee_id, e.name AS employee_name, e.team, e.position, p.exam_year, p.exam_month, p.paper_id, p.pc_class AS paper_class, p.score, p.result, p.faults, p.create_time, p.details FROM practical_exams p LEFT JOIN employees e ON e.employee_id = p.employee_id AND e.deleted_at IS NULL ${where} ORDER BY p.exam_year, p.exam_month, e.team, p.employee_id`, values, ); const workbook = new ExcelJS.Workbook(); addSheet(workbook, '实操考试', rows); return workbook; } async function questionDetailsWorkbook(params) { const values = []; const clauses = periodWhere('o', params, values); const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : ''; const rows = await db.query( `SELECT o.employee_id, e.name AS employee_name, e.team, o.exam_year, o.exam_month, o.paper_class, o.score, o.result, JSON_EXTRACT(o.details, '$.questionScores') AS question_scores, o.details FROM online_exams o LEFT JOIN employees e ON e.employee_id = o.employee_id AND e.deleted_at IS NULL ${where} ORDER BY o.exam_year, o.exam_month, e.team, o.employee_id`, values, ); const workbook = new ExcelJS.Workbook(); addSheet(workbook, '题目作答原始明细', rows); return workbook; } async function teamWorkbook(params) { const year = safeNumber(params.year); const month = safeNumber(params.month); const values = []; const clauses = []; if (year != null) { clauses.push('year = ?'); values.push(year); } if (month != null) { clauses.push('month = ?'); values.push(month); } if (params.team) { clauses.push('team = ?'); values.push(String(params.team)); } const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : ''; const [online, practical, kilometers] = await Promise.all([ db.query(`SELECT * FROM online_exam_stats_monthly ${where} ORDER BY year, month, team`, values), db.query(`SELECT * FROM practical_exam_stats_monthly ${where} ORDER BY year, month, team`, values), db.query(`SELECT * FROM kilometers_stats_team_monthly ${where} ORDER BY year, month, team`, values), ]); const workbook = new ExcelJS.Workbook(); addSheet(workbook, '在线考试班组统计', online); addSheet(workbook, '实操考试班组统计', practical); addSheet(workbook, '班组公里数', kilometers); return workbook; } function escapeHtml(value) { return String(value ?? '').replace(/[&<>"']/g, (char) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', })[char]); } async function analysisHtml(params) { const summaryRows = await db.query("SELECT payload, computed_at FROM dashboard_summary WHERE scope_key = 'global' LIMIT 1"); const summary = summaryRows[0]?.payload || {}; const team = params.team ? ` · ${escapeHtml(params.team)}` : ''; return `
统计时间:${escapeHtml(summaryRows[0]?.computed_at || new Date().toISOString())}