137 lines
4.6 KiB
TypeScript
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 };
|
|
}
|