Undo the full-tree rollback in 6ea1b3d and restore the pre-rollback v1.2.0 code. Preserve the new asset flow rules, abnormal inventory separation, range drilldowns and date picker; adapt the regression test to the restored routes layout.
323 lines
14 KiB
TypeScript
323 lines
14 KiB
TypeScript
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<T = any>(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<RowDataPacket[]> {
|
|
const { plates } = args;
|
|
const [rows] = await db.query<RowDataPacket[]>(`${VEHICLE_INFO_SQL} AND vi.plate_number IN (${plates.map(() => '?').join(',')})`, plates);
|
|
return rows;
|
|
}
|
|
|
|
|
|
/** 考核目标下的车牌。 */
|
|
export async function loadTargetVehiclePlates(db: Database, args: { targetId: string }): Promise<RowDataPacket[]> {
|
|
const { targetId } = args;
|
|
const [rows] = await db.query<RowDataPacket[]>(`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<RowDataPacket[]> {
|
|
const [rows] = await db.query<RowDataPacket[]>(`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<RowDataPacket[]> {
|
|
const [rows] = await db.query<RowDataPacket[]>(`SELECT * FROM lingniu_prod.tab_mileage_assessment_target WHERE is_deleted = 0 ORDER BY id`, []);
|
|
return rows;
|
|
}
|
|
|
|
|
|
/** 各目标的车辆统计。 */
|
|
export async function loadAssessmentVehicleStats(db: Database): Promise<RowDataPacket[]> {
|
|
const [rows] = await db.query<RowDataPacket[]>(`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<RowDataPacket[]> {
|
|
const [rows] = await db.query<RowDataPacket[]>(`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<RowDataPacket[]> {
|
|
const [rows] = await db.query<RowDataPacket[]>(`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<RowDataPacket[]> {
|
|
const [rows] = await db.query<RowDataPacket[]>(`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<RowDataPacket[]> {
|
|
const [rows] = await db.query<RowDataPacket[]>(`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<RowDataPacket[]> {
|
|
const [rows] = await db.query<RowDataPacket[]>(`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<RowDataPacket[]> {
|
|
const { targetId } = args;
|
|
const [rows] = await db.query<RowDataPacket[]>(`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<RowDataPacket[]> {
|
|
const { reportDate } = args;
|
|
const [rows] = await db.query<RowDataPacket[]>(`SELECT report_payload FROM ${TABLE} WHERE report_date = ? LIMIT 1`, [reportDate]);
|
|
return rows;
|
|
}
|
|
|
|
|
|
/** 日报是否已归档。 */
|
|
export async function dailyReportSnapshotExists(db: Database, args: { reportDate: string }): Promise<RowDataPacket[]> {
|
|
const { reportDate } = args;
|
|
const [rows] = await db.query<RowDataPacket[]>(`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<number> {
|
|
const { replace, values } = args;
|
|
const [rows] = await db.query<any>(`${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<number> {
|
|
const { report, reportDate } = args;
|
|
const [rows] = await db.query<any>(`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<RowDataPacket[]> {
|
|
const { safeLimit } = args;
|
|
const [rows] = await db.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]);
|
|
return rows;
|
|
}
|
|
|
|
|
|
/** 日报所需的考核车辆行。 */
|
|
export async function loadReportAssessmentRows(db: Database): Promise<RowDataPacket[]> {
|
|
const [rows] = await db.query<RowDataPacket[]>(`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<RowDataPacket[]> {
|
|
const [rows] = await db.query<RowDataPacket[]>(VEHICLE_INFO_SQL);
|
|
return rows;
|
|
}
|