import type { RowDataPacket } from 'mysql2'; import { DAILY_REPORT_TABLE as TABLE } from '../../db/schema/mileage-report.js'; /** * 里程域的数据访问。 * SQL 集中在此;缓存/口径计算仍在 cache.ts、daily-report-* 等模块里。 */ /** 只需要 query 能力的连接。 */ export interface Database { query(sql: string, values?: any[]): Promise<[T, ...any[]]>; } // --------------------------------------------------------------------------- // 里程域 // --------------------------------------------------------------------------- /** 车辆关联信息(客户 / 部门 / 经理 / 租赁状态 / 主体 / 项目 / 品牌)。 */ export const VEHICLE_INFO_SQL = `SELECT vi.plate_number AS plate, vi.vin AS vin, COALESCE(c.customer_name, vor.customer_name, ci.customer_name) AS customer, COALESCE(c.business_department_name, vor.business_dept) AS department, COALESCE(c.business_manager_name, vor.business_manager) AS manager, CAST(COALESCE(c.business_manager_id, vi.business_id) AS CHAR) AS manager_id, CASE vs.operation_status WHEN '1' THEN '租赁' WHEN '2' THEN '自营' WHEN '3' THEN '可运营' WHEN '4' THEN '待运营' WHEN '5' THEN '退出运营' ELSE vs.operation_status END AS rent_status, NULLIF(vi.registered_ownership, '') AS entity, COALESCE(c.project_name, vor.project_name) AS project, COALESCE(vi_operation_city.NAME, NULLIF(vi.operation_city, '')) AS inventory_location, 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 brand_label 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 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'`; /** 按车牌批量查询车辆关联信息。 */ export async function loadVehicleInfoByPlates(db: Database, args: { plates: string[] }): Promise { const { plates } = args; const [rows] = await db.query(`${VEHICLE_INFO_SQL} AND vi.plate_number IN (${plates.map(() => '?').join(',')})`, plates); return rows; } /** 考核目标下的车牌。 */ export async function loadTargetVehiclePlates(db: Database, args: { targetId: string }): Promise { const { targetId } = args; const [rows] = await db.query(`SELECT plate_number FROM lingniu_prod.tab_mileage_assessment_vehicle WHERE target_id = ? AND is_deleted = 0`, [targetId]); return rows; } /** 监控缓存的考核目标 × 车牌(全量)。 */ export async function loadMonitoringTargetRows(db: Database): Promise { const [rows] = await db.query(`SELECT t.id, t.target_name, v.plate_number FROM lingniu_prod.tab_mileage_assessment_target t JOIN lingniu_prod.tab_mileage_assessment_vehicle v ON v.target_id = t.id AND v.is_deleted = 0 WHERE t.is_deleted = 0`, []); return rows; } /** 考核目标列表。 */ export async function loadAssessmentTargetRows(db: Database): Promise { const [rows] = await db.query(`SELECT * FROM lingniu_prod.tab_mileage_assessment_target WHERE is_deleted = 0 ORDER BY id`, []); return rows; } /** 各目标的车辆统计。 */ export async function loadAssessmentVehicleStats(db: Database): Promise { const [rows] = await db.query(`SELECT target_id, COUNT(*) as total, SUM(today_mileage) as today_total, SUM(current_mileage) as cumulative_total, AVG(current_year_completion_rate) as avg_completion, SUM(CASE WHEN is_qualified = 1 THEN 1 ELSE 0 END) as qualified_count, SUM(CASE WHEN current_year_is_qualified = 1 THEN 1 ELSE 0 END) as year_qualified_count, SUM(CASE WHEN current_year_completion_rate >= 0.5 THEN 1 ELSE 0 END) as half_qualified_count, SUM(current_year_mileage_task) as current_year_target, SUM(current_year_mileage) as current_year_completed, MAX(current_year_assessment_end_date) as year_end_date FROM lingniu_prod.tab_mileage_assessment_vehicle WHERE is_deleted = 0 GROUP BY target_id`, []); return rows; } /** 首年目标统计。 */ export async function loadAssessmentFirstYearStats(db: Database): Promise { const [rows] = await db.query(`SELECT v.target_id, COUNT(*) as first_year_total, SUM(t.annual_mileage_per_vehicle) as first_year_target, SUM(LEAST(v.current_mileage, t.annual_mileage_per_vehicle)) as first_year_completed, SUM(GREATEST(t.annual_mileage_per_vehicle - v.current_mileage, 0)) as first_year_remaining, SUM(LEAST(v.current_mileage, t.annual_mileage_per_vehicle)) / NULLIF(SUM(t.annual_mileage_per_vehicle), 0) as first_year_completion_rate, SUM(CASE WHEN v.current_mileage >= t.annual_mileage_per_vehicle THEN 1 ELSE 0 END) as first_year_qualified_count, SUM(CASE WHEN v.current_mileage >= t.annual_mileage_per_vehicle * 0.5 THEN 1 ELSE 0 END) as first_year_half_qualified_count, DATE_FORMAT(MIN(v.assessment_start_date), '%Y-%m-%d') as first_year_start_date, DATE_FORMAT(MAX(DATE_SUB(DATE_ADD(v.assessment_start_date, INTERVAL 1 YEAR), INTERVAL 1 DAY)), '%Y-%m-%d') as first_year_end_date FROM lingniu_prod.tab_mileage_assessment_vehicle v JOIN lingniu_prod.tab_mileage_assessment_target t ON t.id = v.target_id AND t.is_deleted = 0 WHERE v.is_deleted = 0 GROUP BY v.target_id`, []); return rows; } /** 年度考核统计。 */ export async function loadAssessmentYearlyStats(db: Database): Promise { const [rows] = await db.query(`SELECT v.target_id, y.year_number, COUNT(*) as vehicle_count, SUM(t.annual_mileage_per_vehicle * y.year_number) as target_mileage, SUM(LEAST(v.current_mileage, t.annual_mileage_per_vehicle * y.year_number)) as completed_mileage, SUM(GREATEST(t.annual_mileage_per_vehicle * y.year_number - v.current_mileage, 0)) as remaining_mileage, SUM(LEAST(v.current_mileage, t.annual_mileage_per_vehicle * y.year_number)) / NULLIF(SUM(t.annual_mileage_per_vehicle * y.year_number), 0) as completion_rate, SUM(CASE WHEN v.current_mileage >= t.annual_mileage_per_vehicle * y.year_number THEN 1 ELSE 0 END) as qualified_count, SUM(CASE WHEN v.current_mileage >= t.annual_mileage_per_vehicle * y.year_number * 0.5 THEN 1 ELSE 0 END) as half_qualified_count, DATE_FORMAT(MIN(DATE_ADD(v.assessment_start_date, INTERVAL y.year_number - 1 YEAR)), '%Y-%m-%d') as start_date, DATE_FORMAT(MAX(DATE_SUB(DATE_ADD(v.assessment_start_date, INTERVAL y.year_number YEAR), INTERVAL 1 DAY)), '%Y-%m-%d') as end_date FROM lingniu_prod.tab_mileage_assessment_vehicle v JOIN lingniu_prod.tab_mileage_assessment_target t ON t.id = v.target_id AND t.is_deleted = 0 JOIN ( SELECT 1 as year_number UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 ) y ON y.year_number <= LEAST(t.assessment_years, v.current_year_number) WHERE v.is_deleted = 0 GROUP BY v.target_id, y.year_number ORDER BY v.target_id, y.year_number`, []); return rows; } /** 年度分期统计。 */ export async function loadAssessmentYearlyPeriodStats(db: Database): Promise { const [rows] = await db.query(`SELECT v.target_id, y.year_number, DATE_FORMAT(DATE_ADD(v.assessment_start_date, INTERVAL y.year_number - 1 YEAR), '%Y-%m-%d') as start_date, DATE_FORMAT(DATE_SUB(DATE_ADD(v.assessment_start_date, INTERVAL y.year_number YEAR), INTERVAL 1 DAY), '%Y-%m-%d') as end_date, COUNT(*) as cnt FROM lingniu_prod.tab_mileage_assessment_vehicle v JOIN lingniu_prod.tab_mileage_assessment_target t ON t.id = v.target_id AND t.is_deleted = 0 JOIN ( SELECT 1 as year_number UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 ) y ON y.year_number <= LEAST(t.assessment_years, v.current_year_number) WHERE v.is_deleted = 0 GROUP BY v.target_id, y.year_number, v.assessment_start_date ORDER BY v.target_id, y.year_number, v.assessment_start_date`, []); return rows; } /** 分期统计。 */ export async function loadAssessmentPeriodStats(db: Database): Promise { const [rows] = await db.query(`SELECT target_id, DATE_FORMAT(assessment_start_date, '%Y-%m-%d') as start_date, DATE_FORMAT(assessment_end_date, '%Y-%m-%d') as end_date, COUNT(*) as cnt FROM lingniu_prod.tab_mileage_assessment_vehicle WHERE is_deleted = 0 GROUP BY target_id, assessment_start_date, assessment_end_date ORDER BY target_id, assessment_start_date`, []); return rows; } /** 考核车辆明细(全量)。 */ export async function loadTargetVehicleRows(db: Database): Promise { const [rows] = await db.query(`SELECT target_id, plate_number FROM lingniu_prod.tab_mileage_assessment_vehicle WHERE is_deleted = 0`, []); return rows; } /** 单目标年度考核车辆。 */ export async function loadYearlyAssessments(db: Database, args: { targetId: string }): Promise { const { targetId } = args; const [rows] = await db.query(`SELECT plate_number, today_mileage, vehicle_total_mileage, completion_rate, is_qualified, current_year_is_qualified, daily_required_mileage FROM lingniu_prod.tab_mileage_assessment_vehicle WHERE target_id = ? AND is_deleted = 0 ORDER BY today_mileage DESC`, [targetId]); return rows; } /** 读取日报归档负载。 */ export async function loadDailyReportPayload(db: Database, args: { reportDate: string }): Promise { const { reportDate } = args; const [rows] = await db.query(`SELECT report_payload FROM ${TABLE} WHERE report_date = ? LIMIT 1`, [reportDate]); return rows; } /** 日报是否已归档。 */ export async function dailyReportSnapshotExists(db: Database, args: { reportDate: string }): Promise { const { reportDate } = args; const [rows] = await db.query(`SELECT 1 AS found FROM ${TABLE} WHERE report_date = ? LIMIT 1`, [reportDate]); return rows; } /** 写入日报归档(REPLACE 或 INSERT IGNORE)。 */ export async function insertDailyReportSnapshot(db: Database, args: { replace: boolean; values: unknown[] }): Promise { const { replace, values } = args; const [rows] = await db.query(`${replace ? "REPLACE" : "INSERT IGNORE"} 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 rows; } /** 更新日报负载。 */ export async function updateDailyReportPayload(db: Database, args: { report: unknown; reportDate: string }): Promise { const { report, reportDate } = args; const [rows] = await db.query(`UPDATE ${TABLE} SET report_payload = CAST(? AS JSON) WHERE report_date = ?`, [JSON.stringify(report), reportDate]); return rows; } /** 日报归档历史。 */ export async function listDailyReportHistoryRows(db: Database, args: { safeLimit: number }): Promise { const { safeLimit } = args; const [rows] = await db.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]); return rows; } /** 日报所需的考核车辆行。 */ export async function loadReportAssessmentRows(db: Database): Promise { const [rows] = await db.query(`SELECT t.id AS target_id, t.target_name, t.annual_mileage_per_vehicle, v.id AS assessment_vehicle_id, v.plate_number, DATE_FORMAT(v.assessment_start_date, '%Y-%m-%d') AS assessment_start_date, DATE_FORMAT(v.assessment_end_date, '%Y-%m-%d') AS assessment_end_date, v.total_mileage_task, v.current_mileage, v.completion_rate, v.current_year_mileage_task, v.current_year_mileage, v.current_year_completion_rate, v.current_year_is_qualified, v.daily_required_mileage, DATE_FORMAT(v.current_year_assessment_end_date, '%Y-%m-%d') AS current_year_assessment_end_date FROM lingniu_prod.tab_mileage_assessment_target t JOIN lingniu_prod.tab_mileage_assessment_vehicle v ON v.target_id = t.id AND v.is_deleted = 0 WHERE t.is_deleted = 0 ORDER BY t.id, v.plate_number`, []); return rows; } /** 全量车辆关联信息。 */ export async function loadVehicleInfoMapRows(db: Database): Promise { const [rows] = await db.query(VEHICLE_INFO_SQL); return rows; }