import type { ResultSetHeader, RowDataPacket } from 'mysql2'; import pool from '../../db.js'; import type { DailyMileageReport, MileageReportHistoryItem, MileageReportHistoryResponse, MileageReportSource, } from '../../../shared/mileage/daily-report.js'; const TABLE = 'lingniu_prod.tab_mileage_daily_report'; const CREATE_TABLE_SQL = ` CREATE TABLE IF NOT EXISTS ${TABLE} ( id BIGINT NOT NULL AUTO_INCREMENT, report_date DATE NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'ARCHIVED', source VARCHAR(20) NOT NULL, vehicle_count INT NOT NULL DEFAULT 0, operating_count INT NOT NULL DEFAULT 0, inventory_count INT NOT NULL DEFAULT 0, daily_mileage DECIMAL(14,2) NOT NULL DEFAULT 0, day_over_day_rate DECIMAL(8,2) NULL, report_payload JSON NOT NULL, generated_at DATETIME NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_report_date (report_date), KEY idx_generated_at (generated_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='里程每日汇报归档快照' `; let ensured = false; export async function ensureDailyReportTable(): Promise { if (ensured) return; await pool.query(CREATE_TABLE_SQL); ensured = true; } function parsePayload(value: unknown): DailyMileageReport | null { if (!value) return null; if (typeof value === 'object') return value as DailyMileageReport; if (typeof value !== 'string') return null; try { return JSON.parse(value) as DailyMileageReport; } catch { return null; } } export async function getDailyReportSnapshot(reportDate: string): Promise { await ensureDailyReportTable(); const [rows] = await pool.query( `SELECT report_payload FROM ${TABLE} WHERE report_date = ? LIMIT 1`, [reportDate], ); return parsePayload(rows[0]?.report_payload); } export async function hasDailyReportSnapshot(reportDate: string): Promise { await ensureDailyReportTable(); const [rows] = await pool.query( `SELECT 1 AS found FROM ${TABLE} WHERE report_date = ? LIMIT 1`, [reportDate], ); return rows.length > 0; } export async function saveDailyReportSnapshot( report: DailyMileageReport, source: MileageReportSource, options: { replace?: boolean } = {}, ): Promise { await ensureDailyReportTable(); const archived: DailyMileageReport = { ...report, status: 'ARCHIVED', source }; const values = [ archived.reportDate, archived.status, source, archived.totals.vehicleCount, archived.totals.operatingCount, archived.totals.inventoryCount, archived.totals.dailyMileage, archived.totals.dayOverDayRate, JSON.stringify(archived), archived.generatedAt.replace('T', ' ').replace(/Z$/, '').slice(0, 19), ]; const insert = options.replace ? 'REPLACE' : 'INSERT IGNORE'; const [result] = await pool.query( `${insert} INTO ${TABLE} (report_date, status, source, vehicle_count, operating_count, inventory_count, daily_mileage, day_over_day_rate, report_payload, generated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, CAST(? AS JSON), ?)`, values, ); return result.affectedRows > 0; } export async function updateDailyReportSnapshotPayload(report: DailyMileageReport): Promise { await ensureDailyReportTable(); const [result] = await pool.query( `UPDATE ${TABLE} SET report_payload = CAST(? AS JSON) WHERE report_date = ?`, [JSON.stringify(report), report.reportDate], ); return result.affectedRows > 0; } export async function listDailyReportHistory(limit = 90): Promise { await ensureDailyReportTable(); const safeLimit = Math.min(366, Math.max(1, limit)); const [rows] = await pool.query( `SELECT DATE_FORMAT(report_date, '%Y-%m-%d') AS report_date, status, source, vehicle_count, operating_count, inventory_count, daily_mileage, day_over_day_rate, DATE_FORMAT(generated_at, '%Y-%m-%dT%H:%i:%s+08:00') AS generated_at FROM ${TABLE} ORDER BY report_date DESC LIMIT ?`, [safeLimit], ); const items: MileageReportHistoryItem[] = rows.map(row => ({ reportDate: row.report_date, status: row.status, source: row.source, generatedAt: row.generated_at, vehicleCount: Number(row.vehicle_count) || 0, operatingCount: Number(row.operating_count) || 0, inventoryCount: Number(row.inventory_count) || 0, dailyMileage: Number(row.daily_mileage) || 0, dayOverDayRate: row.day_over_day_rate == null ? null : Number(row.day_over_day_rate), })); return { items, latestDate: items[0]?.reportDate || null }; }