import pool from '../../db.js'; import type { Vehicle, VehicleRow } from '../../types.js'; import { transformRow } from './model.js'; import type { WeeklyTruckIds } from './types.js'; interface Database { query(sql: string, values?: any[]): Promise<[T, ...any[]]>; } type FlowType = 'delivered' | 'returned' | 'replaced'; export type WeeklyDetailType = FlowType | 'pending' | 'new'; export interface FlowDetailRow { id: string; type: FlowType; type_label: string; stat_date: string; truck_id: string; plate_number: string; event_time: string | null; submit_time: string | null; department: string | null; manager: string | null; customer_name: string | null; } interface WeeklyStats { pendingDelivery: number; weeklyNew: number; weeklyRemoved: number; weeklyDelivered: number; weeklyReturned: number; weeklyReplaced: number; } export const MAIN_SQL = `SELECT CAST(vi.id AS CHAR) AS id, vi.plate_number AS 车牌号, vi.vin AS vin, vm.brand AS 车辆品牌, vm.model AS 车辆型号, vi.body_color AS 车辆颜色, vi.rental_company AS 租赁公司, CASE vi.vehicle_source WHEN '0' THEN '自有' WHEN '1' THEN '挂靠' WHEN '2' THEN '外租' WHEN '3' THEN '自有' ELSE vi.actual_ownership END AS 车辆归属状态Label, vm.model AS 车辆型号Label, vm.vehicle_type AS 车辆类型参数, vi.operation_city AS 库存区域, vs.vehicle_status AS 车辆租赁状态, vs.operation_status AS 车辆租赁状态Label, CASE WHEN COALESCE(vs.operation_status, '') = '5' THEN 0 ELSE 1 END AS 是否营运, COALESCE(info_province.NAME, NULLIF(info.province, ''), vi.province_name, vi_province.NAME, NULLIF(vi.province, '')) AS 省, COALESCE(info_city.NAME, NULLIF(info.city, ''), vi.city_name, vi_city.NAME, vi_operation_city.NAME, NULLIF(vi.city, ''), NULLIF(vi.operation_city, '')) AS 市, info.lat AS 纬度, info.lng AS 经度, CASE vm.brand WHEN 'hyundai' THEN CASE WHEN vm.model LIKE '%帕力安%' OR vm.model LIKE '%冷链%' OR vm.model LIKE '%双飞翼%' THEN '帕力安牌' ELSE '现代' END WHEN 'yuejin' THEN '跃进' WHEN 'feichi' THEN '飞驰' WHEN 'sulong' THEN '苏龙' WHEN 'higer' THEN '海格' WHEN 'dongfeng' THEN '东风' WHEN 'yutong' THEN '宇通' WHEN 'chufeng' THEN '楚风' WHEN 'tonghua' THEN '通华' WHEN 'maxus' THEN '大通' WHEN 'mingwei' THEN '明威' WHEN 'wanfeng' THEN '万风' WHEN 'shujie' THEN '舒捷' WHEN 'denza' THEN '腾势' WHEN 'hongyan' THEN '红岩' WHEN 'yuanchang brand' THEN '远程牌' WHEN 'others' THEN '其他' ELSE vm.brand END AS 车辆品牌Label, c.id AS 合同ID, COALESCE(c.contract_code, vor.contract_code, vi.contract_code) AS 合同编码, COALESCE(c.customer_name, vor.customer_name, ci.customer_name) AS 客户名称, c.signing_company AS 合同归属公司, COALESCE(c.business_department_name, vor.business_dept) AS 合同归属部门, NULLIF(vi.registered_ownership, '') AS 主体, COALESCE(c.project_name, vor.project_name) AS 项目名称, COALESCE(c.business_manager_name, vor.business_manager) AS 客户经理, CAST(COALESCE(c.business_manager_id, vi.business_id) AS CHAR) AS 经理ID FROM vehicle_info vi LEFT JOIN vehicle_status vs ON vs.vehicle_id = vi.id AND vs.del_flag = 0 LEFT JOIN vehicle_model vm ON vm.id = vi.vehicle_model_id AND vm.del_flag = '0' LEFT JOIN tab_truck_remote_sync_realtime_info info ON info.plate_number = vi.plate_number AND info.is_deleted = 0 LEFT JOIN common_district info_province ON info_province.CODE = info.province COLLATE utf8mb4_unicode_ci AND info_province.STATUS = 'VALID' LEFT JOIN common_district info_city ON info_city.CODE = info.city COLLATE utf8mb4_unicode_ci AND info_city.STATUS = 'VALID' LEFT JOIN common_district vi_province ON vi_province.CODE = vi.province COLLATE utf8mb4_unicode_ci AND vi_province.STATUS = 'VALID' LEFT JOIN common_district vi_city ON vi_city.CODE = vi.city COLLATE utf8mb4_unicode_ci AND vi_city.STATUS = 'VALID' LEFT JOIN common_district vi_operation_city ON vi_operation_city.CODE = vi.operation_city COLLATE utf8mb4_unicode_ci AND vi_operation_city.STATUS = 'VALID' LEFT JOIN vehicle_lease_order_record vor ON vor.vehicle_id = vi.id AND vor.del_flag = '0' AND vor.id = ( SELECT MAX(vor2.id) FROM vehicle_lease_order_record vor2 WHERE vor2.vehicle_id = vi.id AND vor2.del_flag = '0' ) LEFT JOIN vehicle_lease_contract_info c ON c.order_id = vor.contract_id AND c.del_flag = '0' LEFT JOIN customer_info ci ON ci.id = vi.customer_id AND ci.del_flag = '0' WHERE vi.del_flag = '0' AND COALESCE(vs.operation_status, '') <> '5'`; // Week range: last Saturday 00:00 to this Friday 23:59 // MySQL: WEEKDAY() returns 0=Monday..6=Sunday, Saturday=5 // "上周六-本周五": offset from today to last Saturday export const WEEK_START_SQL = `DATE_SUB(CURDATE(), INTERVAL (WEEKDAY(CURDATE()) + 2) % 7 DAY)`; export const WEEK_END_SQL = `DATE_ADD(${WEEK_START_SQL}, INTERVAL 7 DAY)`; export const WEEKLY_TRUCK_ID_SQLS = [ `SELECT CAST(vehicle_id AS CHAR) AS truck_id FROM vehicle_status WHERE del_flag=0 AND vehicle_status='4' AND COALESCE(operation_status, '') <> '5'`, `SELECT CAST(vehicle_id AS CHAR) AS truck_id FROM delivery_vehicle WHERE del_flag='0' AND vehicle_id IS NOT NULL AND delivery_time >= ${WEEK_START_SQL} AND delivery_time < ${WEEK_END_SQL} AND delivery_status IN (2,3,5)`, `SELECT CAST(vehicle_id AS CHAR) AS truck_id FROM return_vehicle_task WHERE del_flag='0' AND vehicle_id IS NOT NULL AND arrival_time >= ${WEEK_START_SQL} AND arrival_time < ${WEEK_END_SQL} AND status IN (2,3,5)`, `SELECT CAST(new_vehicle_id AS CHAR) AS truck_id FROM vehicle_replacement WHERE del_flag='0' AND new_vehicle_id IS NOT NULL AND replace_time >= ${WEEK_START_SQL} AND replace_time < ${WEEK_END_SQL} AND status=20`, ] as const; // 交车单 SQL const DELIVERED_SQL = `SELECT dv.id, DATE(dv.delivery_time) AS handover_date, CAST(dv.vehicle_id AS CHAR) AS truck_id, dv.plate_number, c.contract_type, COALESCE(dts.customer_name, c.customer_name) AS customer_name FROM delivery_vehicle dv LEFT JOIN delivery_task_subject dts ON dts.id = dv.delivery_task_subject_id AND dts.del_flag = '0' LEFT JOIN vehicle_lease_contract_info c ON c.order_id = dv.contract_id AND c.del_flag = '0' WHERE dv.del_flag = '0' AND dv.vehicle_id IS NOT NULL AND dv.delivery_time IS NOT NULL AND dv.delivery_status IN (2,3,5)`; // 还车单 SQL const RETURNED_SQL = `SELECT r.id, DATE(r.arrival_time) AS handover_date, CAST(r.vehicle_id AS CHAR) AS truck_id, r.plate_number, c.contract_type, COALESCE(dts.customer_name, c.customer_name) AS customer_name FROM return_vehicle_task r LEFT JOIN delivery_task_subject dts ON dts.id = r.delivery_task_subject_id AND dts.del_flag = '0' LEFT JOIN vehicle_lease_contract_info c ON c.order_id = r.contract_id AND c.del_flag = '0' WHERE r.del_flag = '0' AND r.vehicle_id IS NOT NULL AND r.arrival_time IS NOT NULL AND r.status IN (2,3,5)`; // 替换车单 SQL const REPLACED_SQL = `SELECT vr.id, DATE(vr.replace_time) AS handover_date, CAST(vr.new_vehicle_id AS CHAR) AS truck_id, vr.new_vehicle_plate AS plate_number, c.contract_type, c.customer_name FROM vehicle_replacement vr LEFT JOIN vehicle_lease_contract_info c ON c.id = vr.contract_id AND c.del_flag = '0' WHERE vr.del_flag = '0' AND vr.new_vehicle_id IS NOT NULL AND vr.replace_time IS NOT NULL AND vr.status = 20`; export const WEEKLY_DETAIL_SQL: Record = { delivered: `${DELIVERED_SQL} AND dv.delivery_time >= ${WEEK_START_SQL} AND dv.delivery_time < ${WEEK_END_SQL} ORDER BY dv.delivery_time DESC`, returned: `${RETURNED_SQL} AND r.arrival_time >= ${WEEK_START_SQL} AND r.arrival_time < ${WEEK_END_SQL} ORDER BY r.arrival_time DESC`, replaced: `${REPLACED_SQL} AND vr.replace_time >= ${WEEK_START_SQL} AND vr.replace_time < ${WEEK_END_SQL} ORDER BY vr.replace_time DESC`, pending: `SELECT CAST(vi.id AS CHAR) AS truck_id, vi.plate_number, NULL AS handover_date, NULL AS contract_type, NULL AS customer_name FROM vehicle_info vi LEFT JOIN vehicle_status vs ON vs.vehicle_id=vi.id AND vs.del_flag=0 WHERE vi.del_flag='0' AND vs.vehicle_status='4' AND COALESCE(vs.operation_status, '') <> '5'`, new: `SELECT CAST(vi.id AS CHAR) AS truck_id, vi.plate_number, vi.create_time AS handover_date, NULL AS contract_type, NULL AS customer_name FROM vehicle_info vi LEFT JOIN vehicle_status vs ON vs.vehicle_id=vi.id AND vs.del_flag=0 WHERE vi.del_flag='0' AND COALESCE(vs.operation_status, '') <> '5' AND vi.create_time >= ${WEEK_START_SQL} AND vi.create_time < ${WEEK_END_SQL} ORDER BY vi.create_time DESC`, }; export const FLOW_STATS_SQL = ` SELECT * FROM ( SELECT CONCAT(CASE WHEN dv.vehicle_replacement_id IS NOT NULL THEN 'replaced-' ELSE 'delivered-' END, dv.id) AS id, CASE WHEN dv.vehicle_replacement_id IS NOT NULL THEN 'replaced' ELSE 'delivered' END AS type, CASE WHEN dv.vehicle_replacement_id IS NOT NULL THEN '替换交车' ELSE '交车' END AS type_label, DATE_FORMAT(dv.delivery_completion_processed_time, '%Y-%m-%d') AS stat_date, CAST(dv.vehicle_id AS CHAR) AS truck_id, dv.plate_number, DATE_FORMAT(dv.delivery_completion_processed_time, '%Y-%m-%d %H:%i:%s') AS event_time, DATE_FORMAT(dv.create_time, '%Y-%m-%d %H:%i:%s') AS submit_time, c.business_department_name AS department, c.business_manager_name AS manager, COALESCE(dts.customer_name, c.customer_name) AS customer_name FROM delivery_vehicle dv LEFT JOIN delivery_task_subject dts ON dts.id = dv.delivery_task_subject_id AND dts.del_flag = '0' LEFT JOIN vehicle_lease_contract_info c ON c.order_id = dv.contract_id AND c.del_flag = '0' WHERE dv.del_flag = '0' AND dv.vehicle_id IS NOT NULL AND dv.delivery_completion_processed_time IS NOT NULL AND dv.delivery_status IN (2,3,20) AND (dts.status IS NULL OR dts.status <> 10) AND dv.delivery_completion_processed_time >= ? AND dv.delivery_completion_processed_time < DATE_ADD(?, INTERVAL 1 DAY) UNION ALL SELECT CONCAT('returned-', r.id) AS id, 'returned' AS type, '还车' AS type_label, DATE_FORMAT(r.return_completion_processed_time, '%Y-%m-%d') AS stat_date, CAST(r.vehicle_id AS CHAR) AS truck_id, r.plate_number, DATE_FORMAT(r.return_completion_processed_time, '%Y-%m-%d %H:%i:%s') AS event_time, DATE_FORMAT(r.create_time, '%Y-%m-%d %H:%i:%s') AS submit_time, c.business_department_name AS department, c.business_manager_name AS manager, COALESCE(dts.customer_name, c.customer_name) AS customer_name FROM return_vehicle_task r LEFT JOIN delivery_task_subject dts ON dts.id = r.delivery_task_subject_id AND dts.del_flag = '0' LEFT JOIN vehicle_lease_contract_info c ON c.order_id = r.contract_id AND c.del_flag = '0' WHERE r.del_flag = '0' AND r.vehicle_id IS NOT NULL AND r.return_completion_processed_time IS NOT NULL -- 与还车管理历史记录一致:完成、已签章、待签署;日期按运维签章时间归属。 AND r.status IN (2,3,5) AND r.return_completion_processed_time >= ? AND r.return_completion_processed_time < DATE_ADD(?, INTERVAL 1 DAY) ) flow ORDER BY flow.event_time DESC, flow.id DESC `; const CACHE_TTL = 60 * 1000; export class VehicleRepository { private cachedVehicles: Vehicle[] = []; private lastFetchTime = 0; private cachedWeeklyTruckIds: WeeklyTruckIds | null = null; private weeklyTruckIdsLastFetch = 0; private cachedWeeklyStats: WeeklyStats | null = null; private weeklyStatsLastFetch = 0; constructor( private readonly db: Database, private readonly now: () => number = Date.now, private readonly mapVehicleRow: (row: VehicleRow) => Vehicle = transformRow, ) {} async getVehicles(): Promise { const now = this.now(); if (this.cachedVehicles.length > 0 && now - this.lastFetchTime < CACHE_TTL) { return this.cachedVehicles; } const [rows] = await this.db.query(MAIN_SQL); this.cachedVehicles = (rows as VehicleRow[]).map(this.mapVehicleRow); this.lastFetchTime = now; return this.cachedVehicles; } async getWeeklyTruckIds(): Promise { const now = this.now(); if (this.cachedWeeklyTruckIds && now - this.weeklyTruckIdsLastFetch < CACHE_TTL) { return this.cachedWeeklyTruckIds; } const [[pendingRows], [deliveredRows], [returnedRows], [replacedRows]] = await Promise.all( WEEKLY_TRUCK_ID_SQLS.map((sql) => this.db.query(sql)), ); const toSet = (rows: any[]) => new Set(rows.map((r) => String(r.truck_id)).filter((s) => s && s !== 'null')); this.cachedWeeklyTruckIds = { pending: toSet(pendingRows as any[]), delivered: toSet(deliveredRows as any[]), returned: toSet(returnedRows as any[]), replaced: toSet(replacedRows as any[]), }; this.weeklyTruckIdsLastFetch = now; return this.cachedWeeklyTruckIds; } async getWeeklyStats(): Promise { const now = this.now(); if (this.cachedWeeklyStats && now - this.weeklyStatsLastFetch < CACHE_TTL) { return this.cachedWeeklyStats; } const [[pendingRows], [newRows], [removedRows], [deliveredRows], [returnedRows], [replacedRows]] = await Promise.all([ this.db.query(`SELECT COUNT(*) AS cnt FROM vehicle_status WHERE del_flag=0 AND vehicle_status='4' AND COALESCE(operation_status, '') <> '5'`), this.db.query(`SELECT COUNT(*) AS cnt FROM vehicle_info WHERE del_flag='0' AND create_time >= ${WEEK_START_SQL} AND create_time < ${WEEK_END_SQL}`), this.db.query(`SELECT COUNT(*) AS cnt FROM vehicle_info vi LEFT JOIN vehicle_status vs ON vs.vehicle_id=vi.id AND vs.del_flag=0 WHERE (vi.del_flag='1' OR vs.operation_status='5') AND vi.update_time >= ${WEEK_START_SQL} AND vi.update_time < ${WEEK_END_SQL}`), this.db.query(`SELECT COUNT(*) AS cnt FROM delivery_vehicle WHERE del_flag='0' AND delivery_time IS NOT NULL AND delivery_status IN (2,3,5) AND delivery_time >= ${WEEK_START_SQL} AND delivery_time < ${WEEK_END_SQL}`), this.db.query(`SELECT COUNT(*) AS cnt FROM return_vehicle_task WHERE del_flag='0' AND arrival_time IS NOT NULL AND status IN (2,3,5) AND arrival_time >= ${WEEK_START_SQL} AND arrival_time < ${WEEK_END_SQL}`), this.db.query(`SELECT COUNT(*) AS cnt FROM vehicle_replacement WHERE del_flag='0' AND replace_time IS NOT NULL AND status=20 AND replace_time >= ${WEEK_START_SQL} AND replace_time < ${WEEK_END_SQL}`), ]); this.cachedWeeklyStats = { pendingDelivery: (pendingRows as any[])[0]?.cnt || 0, weeklyNew: (newRows as any[])[0]?.cnt || 0, weeklyRemoved: (removedRows as any[])[0]?.cnt || 0, weeklyDelivered: (deliveredRows as any[])[0]?.cnt || 0, weeklyReturned: (returnedRows as any[])[0]?.cnt || 0, weeklyReplaced: (replacedRows as any[])[0]?.cnt || 0, }; this.weeklyStatsLastFetch = now; return this.cachedWeeklyStats; } invalidateRefreshCaches(): void { this.lastFetchTime = 0; this.weeklyStatsLastFetch = 0; } async getRealtimeMileageRows(): Promise { const [rows] = await this.db.query(` SELECT plate_number, day_mileage FROM tab_truck_remote_sync_realtime_info WHERE is_deleted = 0 AND plate_number IS NOT NULL `); return rows; } async getAttendanceMileageRows(): Promise { const [rows] = await this.db.query(`SELECT plate_number, day_mileage FROM tab_truck_remote_sync_realtime_info WHERE is_deleted = 0 AND plate_number IS NOT NULL`); return rows; } async getWeeklyDetailRows(type: WeeklyDetailType): Promise { const [rows] = await this.db.query(WEEKLY_DETAIL_SQL[type]); return rows; } async getFlowDetailRows(start: string, end: string): Promise { const [rows] = await this.db.query(FLOW_STATS_SQL, [start, end, start, end]); return rows as FlowDetailRow[]; } async getWeeklyDebugData(): Promise<{ dateRange: any; deliveredAll: any; deliveredRecent: any; latestTake: any; returnedRecent: any; latestReturn: any; }> { const [[dateRange]] = await this.db.query(`SELECT ${WEEK_START_SQL} AS week_start, ${WEEK_END_SQL} AS week_end, CURDATE() AS today, WEEKDAY(CURDATE()) AS weekday`); const [[deliveredAll]] = await this.db.query(`SELECT COUNT(*) AS cnt FROM delivery_vehicle WHERE del_flag='0' AND delivery_time IS NOT NULL AND delivery_status IN (2,3,5)`); const [[deliveredRecent]] = await this.db.query(`SELECT COUNT(*) AS cnt FROM delivery_vehicle WHERE del_flag='0' AND delivery_time IS NOT NULL AND delivery_status IN (2,3,5) AND delivery_time >= ${WEEK_START_SQL} AND delivery_time < ${WEEK_END_SQL}`); const [[latestTake]] = await this.db.query(`SELECT MAX(delivery_time) AS latest FROM delivery_vehicle WHERE del_flag='0' AND delivery_time IS NOT NULL AND delivery_status IN (2,3,5)`); const [[returnedRecent]] = await this.db.query(`SELECT COUNT(*) AS cnt FROM return_vehicle_task WHERE del_flag='0' AND arrival_time IS NOT NULL AND status IN (2,3,5) AND arrival_time >= ${WEEK_START_SQL} AND arrival_time < ${WEEK_END_SQL}`); const [[latestReturn]] = await this.db.query(`SELECT MAX(arrival_time) AS latest FROM return_vehicle_task WHERE del_flag='0' AND arrival_time IS NOT NULL AND status IN (2,3,5)`); return { dateRange, deliveredAll, deliveredRecent, latestTake, returnedRecent, latestReturn }; } async getRealtimeProvinceRows(): Promise { const [rows] = await this.db.query(`SELECT plate_number, province FROM tab_truck_remote_sync_realtime_info WHERE is_deleted = 0 AND plate_number IS NOT NULL`); return rows; } } export const vehicleRepository = new VehicleRepository(pool as unknown as Database);