Files
ln-bi/src/server/routes/mileage/daily-report-store.ts
T

137 lines
4.6 KiB
TypeScript

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<void> {
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<DailyMileageReport | null> {
await ensureDailyReportTable();
const [rows] = await pool.query<RowDataPacket[]>(
`SELECT report_payload FROM ${TABLE} WHERE report_date = ? LIMIT 1`,
[reportDate],
);
return parsePayload(rows[0]?.report_payload);
}
export async function hasDailyReportSnapshot(reportDate: string): Promise<boolean> {
await ensureDailyReportTable();
const [rows] = await pool.query<RowDataPacket[]>(
`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<boolean> {
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<ResultSetHeader>(
`${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<boolean> {
await ensureDailyReportTable();
const [result] = await pool.query<ResultSetHeader>(
`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<MileageReportHistoryResponse> {
await ensureDailyReportTable();
const safeLimit = Math.min(366, Math.max(1, limit));
const [rows] = await pool.query<RowDataPacket[]>(
`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 };
}