From 91202bdf7193734b593aaddca47232a06db9422b Mon Sep 17 00:00:00 2001 From: kkfluous Date: Wed, 17 Jun 2026 11:53:39 +0800 Subject: [PATCH] fix asset module database migration --- docker-compose.yml | 8 +- src/modules/assets/types.ts | 1 + src/server/auth/login.ts | 14 +- src/server/routes/ele/index.ts | 4 +- src/server/routes/mileage/cache.ts | 8 +- src/server/routes/mileage/targets.ts | 22 +- src/server/routes/mileage/trend.ts | 2 +- src/server/routes/mileage/vehicle-info.ts | 56 ++- src/server/routes/scheduling/suggestions.ts | 39 +- src/server/routes/vehicles.ts | 481 +++++++++++--------- src/server/types.ts | 2 + 11 files changed, 346 insertions(+), 291 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index f5d50be..dce0546 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,11 +5,11 @@ services: image: harbor.lnh2e.com/lingniu-v1/ln-bi:main-1.0.0 network_mode: host environment: - DB_HOST: "47.101.148.99" + DB_HOST: "rm-bp179zbv481rnw3e2no.mysql.rds.aliyuncs.com" DB_PORT: "3306" - DB_USER: "root" - DB_PASSWORD: "LN#Passw0rd@2026" - DB_NAME: "lingniu_prod" + DB_USER: "oneos_db_prod" + DB_PASSWORD: "adASHJcviqwjkbn23ngt1" + DB_NAME: "ln_asset_management" HYDROGEN_DB_HOST: "47.99.185.173" HYDROGEN_DB_PORT: "3306" HYDROGEN_DB_USER: "root" diff --git a/src/modules/assets/types.ts b/src/modules/assets/types.ts index 425fec2..9aef3a5 100644 --- a/src/modules/assets/types.ts +++ b/src/modules/assets/types.ts @@ -106,6 +106,7 @@ export interface VehicleListItem { city: string | null; status: string; ownership: string; + rentCompany?: string | null; contractNo: string | null; customerName: string | null; subjectOrg: string | null; diff --git a/src/server/auth/login.ts b/src/server/auth/login.ts index 50df3f4..cfa608a 100644 --- a/src/server/auth/login.ts +++ b/src/server/auth/login.ts @@ -52,11 +52,15 @@ app.get('/exchange', async (c) => { // 查询 depCode 对应的部门名称 let depName = ''; if (userInfo.depCode) { - const [rows] = await pool.execute( - 'SELECT dep_name FROM tab_department WHERE dep_code = ? AND is_deleted = 0 LIMIT 1', - [userInfo.depCode] - ) as [{ dep_name: string }[], unknown]; - depName = rows[0]?.dep_name || ''; + try { + const [rows] = await pool.execute( + 'SELECT dep_name FROM tab_department WHERE dep_code = ? AND is_deleted = 0 LIMIT 1', + [userInfo.depCode] + ) as [{ dep_name: string }[], unknown]; + depName = rows[0]?.dep_name || ''; + } catch (e: any) { + if (e?.code !== 'ER_NO_SUCH_TABLE') throw e; + } } const payload: JwtPayload = { diff --git a/src/server/routes/ele/index.ts b/src/server/routes/ele/index.ts index a3d9d8e..7801e61 100644 --- a/src/server/routes/ele/index.ts +++ b/src/server/routes/ele/index.ts @@ -149,8 +149,8 @@ async function buildPlateLookup(plates: Set): Promise '?').join(','); const [rows] = await pool.query( `SELECT plate_number, CAST(id AS CHAR) AS truck_id - FROM tab_truck - WHERE is_deleted = 0 AND plate_number IN (${placeholders})`, + FROM vehicle_info + WHERE del_flag = '0' AND plate_number IN (${placeholders})`, arr, ); const map = new Map(); diff --git a/src/server/routes/mileage/cache.ts b/src/server/routes/mileage/cache.ts index 5bce187..eb5b929 100644 --- a/src/server/routes/mileage/cache.ts +++ b/src/server/routes/mileage/cache.ts @@ -74,8 +74,8 @@ interface TargetRow { async function fetchTargetRows(): Promise { return pool.execute( `SELECT t.id, t.target_name, v.plate_number - FROM tab_mileage_assessment_target t - JOIN tab_mileage_assessment_vehicle v ON v.target_id = t.id AND v.is_deleted = 0 + 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` ).then(([rows]) => rows as TargetRow[]); } @@ -102,10 +102,10 @@ function buildPlateTargetNamesMap(targetRows: TargetRow[]): Map> { // v_vehicle_daily_stats.total_km 对 G7S 数据源常为 NULL(G7 只回传日增量), - // 业务库 tab_mileage_assessment_vehicle.vehicle_total_mileage 是累加后的权威累计值, + // 业务库 lingniu_prod.tab_mileage_assessment_vehicle.vehicle_total_mileage 是累加后的权威累计值, // 用它兜底保证 totalKm 汇总完整。 const [rows] = await pool.execute( - 'SELECT plate_number, vehicle_total_mileage FROM tab_mileage_assessment_vehicle WHERE is_deleted = 0' + 'SELECT plate_number, vehicle_total_mileage FROM lingniu_prod.tab_mileage_assessment_vehicle WHERE is_deleted = 0' ) as [{ plate_number: string; vehicle_total_mileage: string | number | null }[], unknown]; const map = new Map(); for (const r of rows) { diff --git a/src/server/routes/mileage/targets.ts b/src/server/routes/mileage/targets.ts index 4f1dbb3..638712d 100644 --- a/src/server/routes/mileage/targets.ts +++ b/src/server/routes/mileage/targets.ts @@ -10,7 +10,7 @@ const app = new Hono(); app.get('/', async (c) => { try { const [targets] = await pool.execute( - 'SELECT * FROM tab_mileage_assessment_target WHERE is_deleted = 0 ORDER BY id' + 'SELECT * FROM lingniu_prod.tab_mileage_assessment_target WHERE is_deleted = 0 ORDER BY id' ) as [any[], unknown]; const [vehicleStats] = await pool.execute(` @@ -25,7 +25,7 @@ app.get('/', async (c) => { 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 tab_mileage_assessment_vehicle WHERE is_deleted = 0 + FROM lingniu_prod.tab_mileage_assessment_vehicle WHERE is_deleted = 0 GROUP BY target_id `) as [any[], unknown]; @@ -44,8 +44,8 @@ app.get('/', async (c) => { 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 tab_mileage_assessment_vehicle v - JOIN tab_mileage_assessment_target t ON t.id = v.target_id AND t.is_deleted = 0 + 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 `) as [any[], unknown]; @@ -67,8 +67,8 @@ app.get('/', async (c) => { 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 tab_mileage_assessment_vehicle v - JOIN tab_mileage_assessment_target t ON t.id = v.target_id AND t.is_deleted = 0 + 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) @@ -91,8 +91,8 @@ app.get('/', async (c) => { 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 tab_mileage_assessment_vehicle v - JOIN tab_mileage_assessment_target t ON t.id = v.target_id AND t.is_deleted = 0 + 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) @@ -114,7 +114,7 @@ app.get('/', async (c) => { 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 tab_mileage_assessment_vehicle WHERE is_deleted = 0 + 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 `) as [any[], unknown]; @@ -135,7 +135,7 @@ app.get('/', async (c) => { } const [targetVehicleRows] = await pool.execute( - 'SELECT target_id, plate_number FROM tab_mileage_assessment_vehicle WHERE is_deleted = 0' + 'SELECT target_id, plate_number FROM lingniu_prod.tab_mileage_assessment_vehicle WHERE is_deleted = 0' ) as [{ target_id: number; plate_number: string }[], unknown]; const targetIdPlatesMap = new Map(); @@ -245,7 +245,7 @@ app.get('/:id/vehicles', async (c) => { `SELECT plate_number, today_mileage, vehicle_total_mileage, completion_rate, is_qualified, current_year_is_qualified, daily_required_mileage - FROM tab_mileage_assessment_vehicle + FROM lingniu_prod.tab_mileage_assessment_vehicle WHERE target_id = ? AND is_deleted = 0 ORDER BY today_mileage DESC`, [targetId] diff --git a/src/server/routes/mileage/trend.ts b/src/server/routes/mileage/trend.ts index b757fc0..1114355 100644 --- a/src/server/routes/mileage/trend.ts +++ b/src/server/routes/mileage/trend.ts @@ -12,7 +12,7 @@ app.get('/', async (c) => { let plates: string[] = []; if (targetId) { const [vehicleRows] = await pool.execute( - 'SELECT plate_number FROM tab_mileage_assessment_vehicle WHERE target_id = ? AND is_deleted = 0', + 'SELECT plate_number FROM lingniu_prod.tab_mileage_assessment_vehicle WHERE target_id = ? AND is_deleted = 0', [targetId] ) as [{ plate_number: string }[], unknown]; plates = vehicleRows.map(r => r.plate_number); diff --git a/src/server/routes/mileage/vehicle-info.ts b/src/server/routes/mileage/vehicle-info.ts index 104ef9d..cc949a0 100644 --- a/src/server/routes/mileage/vehicle-info.ts +++ b/src/server/routes/mileage/vehicle-info.ts @@ -3,24 +3,42 @@ import type { VehicleInfoRow } from './types.js'; /** 车辆关联信息 SQL(客户名、部门、经理、租赁状态、主体、项目) */ export const VEHICLE_INFO_SQL = `SELECT - truck.plate_number AS plate, - cus.customer_name AS customer, - dep.dep_name AS department, - u.user_name AS manager, - CAST(c.bd AS CHAR) AS manager_id, - dic_status.dic_name AS rent_status, - org_truck.org_name AS entity, - c.project_name AS project -FROM tab_truck truck -LEFT JOIN tab_truck_status_info si ON si.truck_id = truck.id AND si.is_deleted = 0 -LEFT JOIN tab_contract c ON c.id = si.contract_id AND c.is_deleted = 0 -LEFT JOIN tab_customer cus ON cus.id = c.customer_id AND cus.is_deleted = 0 -LEFT JOIN tab_user u ON u.id = c.bd AND u.is_deleted = 0 -LEFT JOIN tab_department dep ON dep.id = u.dep_id AND dep.is_deleted = 0 -LEFT JOIN tab_dic dic_status ON dic_status.parent_code = 'dic_truck_rent_status' - AND dic_status.dic_code = truck.truck_rent_status AND dic_status.is_deleted = 0 -LEFT JOIN tab_org org_truck ON org_truck.id = truck.org_id AND org_truck.is_deleted = 0 -WHERE truck.is_deleted = 0 AND truck.is_operation = 1`; + vi.plate_number AS plate, + 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 +FROM vehicle_info vi +LEFT JOIN vehicle_status vs + ON vs.vehicle_id = vi.id + AND vs.del_flag = 0 +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'`; /** 查询所有车辆关联信息,返回 plate→info 的 Map */ export async function fetchVehicleInfoMap(): Promise> { @@ -36,7 +54,7 @@ export async function fetchVehicleInfoMap(): Promise export async function fetchVehicleInfoByPlates(plates: string[]): Promise> { if (plates.length === 0) return new Map(); const [rows] = await pool.execute( - `${VEHICLE_INFO_SQL} AND truck.plate_number IN (${plates.map(() => '?').join(',')})`, + `${VEHICLE_INFO_SQL} AND vi.plate_number IN (${plates.map(() => '?').join(',')})`, plates ) as [VehicleInfoRow[], unknown]; const map = new Map(); diff --git a/src/server/routes/scheduling/suggestions.ts b/src/server/routes/scheduling/suggestions.ts index 073ae94..1b6e0d6 100644 --- a/src/server/routes/scheduling/suggestions.ts +++ b/src/server/routes/scheduling/suggestions.ts @@ -28,16 +28,17 @@ function inferTypeFromTargetName(targetName: string): string { } /** - * Classify vehicle type from dic_type.dic_name (e.g. "4.5吨冷链车", "4.5吨货车", "18吨双飞翼货车"). - * The typeName is the full label from the dictionary, modelRaw is the numeric dic_code. + * Classify vehicle type from ln_asset_management.vehicle_model. + * modelRaw is vehicle_model.vehicle_type, which is not the old dic_truck_type code. */ function classifyVehicleType(typeName: string, _modelRaw: string): string { const t = (typeName || '').trim(); if (t.includes('4.5') && t.includes('冷链')) return '4.5T冷链'; if (t.includes('4.5')) return '4.5T普货'; if (t.includes('18')) return '18T'; - if (t.includes('49') || t.includes('牵引')) return '49T'; if (t.includes('挂车')) return '挂车'; + if (t.includes('49')) return '49T'; + if (t.includes('35')) return '35T'; return t || '其他'; } @@ -54,7 +55,7 @@ app.get('/', async (c) => { // ---- Query 1: Assessment targets ---- const [targets] = await pool.execute( - 'SELECT id, target_name, annual_mileage_per_vehicle FROM tab_mileage_assessment_target WHERE is_deleted = 0 ORDER BY id', + 'SELECT id, target_name, annual_mileage_per_vehicle FROM lingniu_prod.tab_mileage_assessment_target WHERE is_deleted = 0 ORDER BY id', ) as [any[], unknown]; const targetMap = new Map(); @@ -71,21 +72,20 @@ app.get('/', async (c) => { current_mileage, current_year_mileage, current_year_mileage_task, completion_rate, is_qualified, current_year_is_qualified, daily_required_mileage, current_year_assessment_end_date - FROM tab_mileage_assessment_vehicle WHERE is_deleted = 0 + FROM lingniu_prod.tab_mileage_assessment_vehicle WHERE is_deleted = 0 `) as [any[], unknown]; // ---- Query 3: Vehicle info (customer, dept, manager) ---- const vehicleInfoMap = await fetchVehicleInfoMap(); - // ---- Query 4: Vehicle types from tab_truck ---- - // Include soft-deleted trucks: many assessment vehicles have is_deleted=1 in tab_truck - // but are still active in the assessment. We need their type info. + // ---- Query 4: Vehicle types from vehicle_info ---- const [truckTypeRows] = await pool.execute(` - SELECT truck.plate_number, dic_type.dic_name AS type_name, truck.model AS model_raw - FROM tab_truck truck - LEFT JOIN tab_dic dic_type ON dic_type.parent_code = 'dic_truck_type' - AND dic_type.dic_code = truck.model AND dic_type.is_deleted = 0 - WHERE truck.is_operation = 1 + SELECT vi.plate_number, vm.model AS type_name, vm.vehicle_type AS model_raw + 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' + WHERE vi.del_flag = '0' + AND COALESCE(vs.operation_status, '') <> '5' `) as [any[], unknown]; const truckTypeMap = new Map(); @@ -161,12 +161,13 @@ app.get('/', async (c) => { // ---- Query 7: Inventory vehicles (rent_status = 0) ---- const [inventoryTruckRows] = await pool.execute(` - SELECT truck.plate_number, dic_type.dic_name AS type_name, truck.model AS model_raw - FROM tab_truck truck - LEFT JOIN tab_dic dic_type ON dic_type.parent_code = 'dic_truck_type' - AND dic_type.dic_code = truck.model AND dic_type.is_deleted = 0 - WHERE truck.is_deleted = 0 AND truck.is_operation = 1 - AND truck.truck_rent_status = 0 + SELECT vi.plate_number, vm.model AS type_name, vm.vehicle_type AS model_raw + 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' + WHERE vi.del_flag = '0' + AND COALESCE(vs.operation_status, '') IN ('3','4') + AND COALESCE(vs.vehicle_status, '') <> '4' `) as [any[], unknown]; // ---- Build assessment vehicle lookup for inventory cross-reference ---- diff --git a/src/server/routes/vehicles.ts b/src/server/routes/vehicles.ts index 8c83895..af314d3 100644 --- a/src/server/routes/vehicles.ts +++ b/src/server/routes/vehicles.ts @@ -17,75 +17,100 @@ import type { Context } from 'hono'; const app = new Hono(); const MAIN_SQL = `SELECT - CAST(truck.id AS CHAR) AS id, - truck.plate_number AS 车牌号, - truck.vin AS vin, - truck.brand AS 车辆品牌, - truck.model AS 车辆型号, - truck.color AS 车辆颜色, - truck.rent_from_company AS 租赁公司, - dic_ascription_status.dic_name AS 车辆归属状态Label, - dic_type.dic_name AS 车辆型号Label, - truck.stock_area AS 库存区域, - truck.truck_rent_status AS 车辆租赁状态, - dic_status.dic_name AS 车辆租赁状态Label, - truck.is_operation AS 是否营运, - info.province AS 省, - info.city AS 市, + 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 '挂靠' + 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 经度, - dic_brand.dic_name AS 车辆品牌Label, - si.contract_id AS 合同ID, - COALESCE(c.contract_no, si.contract_no) AS 合同编码, - cus.customer_name AS 客户名称, - org.org_name AS 合同归属公司, - dep.dep_name AS 合同归属部门, - org_truck.org_name AS 主体, - c.project_name AS 项目名称, - u.user_name AS 客户经理, - CAST(c.bd AS CHAR) AS 经理ID -FROM tab_truck truck + 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.id = truck.id -LEFT JOIN tab_dic dic_type - ON dic_type.parent_code = 'dic_truck_type' - AND dic_type.dic_code = truck.model - AND dic_type.is_deleted = 0 -LEFT JOIN tab_dic dic_status - ON dic_status.parent_code = 'dic_truck_rent_status' - AND dic_status.dic_code = truck.truck_rent_status - AND dic_status.is_deleted = 0 -LEFT JOIN tab_dic dic_brand - ON dic_brand.parent_code = 'dic_vehicle_brand' - AND dic_brand.dic_code = truck.brand - AND dic_brand.is_deleted = 0 -LEFT JOIN tab_truck_status_info si - ON si.truck_id = truck.id - AND si.is_deleted = 0 -LEFT JOIN tab_contract c - ON c.id = si.contract_id - AND c.is_deleted = 0 -LEFT JOIN tab_customer cus - ON cus.id = c.customer_id - AND cus.is_deleted = 0 -LEFT JOIN tab_org org - ON org.id = c.org_id - AND org.is_deleted = 0 -LEFT JOIN tab_org org_truck - ON org_truck.id = truck.org_id - AND org_truck.is_deleted = 0 -LEFT JOIN tab_dic dic_ascription_status - ON dic_ascription_status.parent_code = 'dic_truck_ascription_status' - AND dic_ascription_status.dic_code = truck.ascription_status - AND dic_ascription_status.is_deleted = 0 -LEFT JOIN tab_user u - ON u.id = c.bd - AND u.is_deleted = 0 -LEFT JOIN tab_department dep - ON dep.id = u.dep_id - AND dep.is_deleted = 0 -WHERE truck.is_deleted = 0 - AND truck.is_operation = 1`; + 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'`; // Region mapping: province/city -> display region const REGIONS = ['嘉兴', '广东', '北京', '新疆', '其他'] as const; @@ -148,23 +173,32 @@ function countByType(vehicles: Vehicle[]): VehicleTypeCounts { return counts; } -// Map rental status to frontend status -// Actual DB values: 在库(0), 自营(1), 租赁(2), 待交车(7), 挂靠(8), 异动(12) -function mapStatus(rentStatus: string | null): 'Operating' | 'Inventory' | 'Pending' | 'Abnormal' { - if (!rentStatus) return 'Inventory'; - const s = rentStatus.trim(); - if (s === '租赁' || s === '自营' || s === '挂靠') return 'Operating'; - if (s === '在库') return 'Inventory'; - if (s === '待交车') return 'Pending'; - if (s === '异动') return 'Abnormal'; +// Map operation status to frontend status. +// ln_asset_management.vehicle_status.operation_status: +// 1=租赁, 2=自营, 3=可运营, 4=待运营, 5=退出运营. +function mapStatus(operationStatus: string | null, vehicleStatus: string | null): 'Operating' | 'Inventory' | 'Pending' | 'Abnormal' { + const op = (operationStatus || '').trim(); + const vehicle = (vehicleStatus || '').trim(); + if (vehicle === '4') return 'Pending'; + if (vehicle === '14') return 'Abnormal'; + if (op === '1' || op === '2') return 'Operating'; + if (op === '3' || op === '4') return 'Inventory'; + if (op === '租赁' || op === '自营') return 'Operating'; + if (op === '可运营' || op === '待运营' || op === '在库') return 'Inventory'; + if (op === '异动') return 'Abnormal'; return 'Inventory'; } -// Map ownership from truck_rent_status (rentStatusLabel) -// DB values: 自营(1), 租赁(2), 挂靠(8) → these are the operating subtypes +// Map ownership from vehicle_info.vehicle_source. +// DB values: 0/自有, 1/外租, 2/挂靠. function mapOwnership(rentStatusLabel: string | null): string { if (!rentStatusLabel) return 'Unknown'; const s = rentStatusLabel.trim(); + if (s === '0') return 'Self'; + if (s === '1') return 'Leased'; + if (s === '2') return 'Hanging'; + if (s === '自有') return 'Self'; + if (s === '外租') return 'Leased'; if (s === '自营') return 'Self'; if (s === '租赁') return 'Leased'; if (s === '挂靠') return 'Hanging'; @@ -179,26 +213,38 @@ function resolveCity(city: string | null, province: string | null): string { return p || '其他'; } -// Derive vehicle type category from model label -// Actual DB values: 4.5吨冷链车, 4.5吨货车, 18吨双飞翼货车, 18吨厢式货车, 49吨牵引车头, 35吨牵引车头, -// 重型集装箱半挂车, 重型平板半挂车, 氢能叉车, SJ型蓄电池观光车, 公务用车/小客车, 挂靠油车 -function deriveType(modelLabel: string | null, brandLabel: string | null): string { +// Derive page category from ln_asset_management.vehicle_model. +// vehicle_type is a new-system category code, not the old lingniu_prod.dic_truck_type code: +// 1 = 4.5T, 2 = 18T / other truck-like models, 3 = tractor head, 5/6 = trailers. +function deriveType(modelLabel: string | null, vehicleTypeCode: string | null): string { const label = (modelLabel || '').trim(); + const code = (vehicleTypeCode || '').trim(); + if (label.includes('半挂车') || code === '5' || code === '6') return '挂车'; if (label.includes('4.5吨')) return '4.5T'; if (label.includes('18吨')) return '18T'; if (label.includes('49吨')) return '49T'; if (label.includes('35吨')) return '35T'; + if (code === '1') return '4.5T'; + if (code === '3') return '49T'; if (label.includes('叉车')) return '叉车'; - if (label.includes('半挂车')) return '挂车'; return '其他车型'; } +function normalizeModelLabel(modelLabel: string | null): string | null { + const label = (modelLabel || '').trim(); + if (label === '帕力安牌4.5吨冷链车') return '4.5吨冷链车'; + if (label === '帕力安牌18吨双飞翼货车') return '18吨双飞翼货车'; + if (label === '海格牌18吨双飞翼货车') return '18吨双飞翼货车'; + return label || null; +} + // Tag → alias mapping with sort order // tag is generated as: brand-modelLabel-color[+rentCompany if 外租] // Some tags are merged (e.g. 嘉氢 red + 嘉氢 blue/green → one alias) const MODEL_ALIAS_MAP: Record = { // 4.5T 普货 '现代-4.5吨货车-白色广州开发区交投氢能运营管理有限公司': { alias: '现代4.5T普货(交投)', order: 101 }, + '现代-4.5吨货车-白色': { alias: '现代4.5T普货', order: 101 }, '现代-4.5吨货车-白': { alias: '现代4.5T普货(恒运)', order: 102 }, // 4.5T 冷链 '帕力安牌-4.5吨冷链车-白色广州开发区交投氢能运营管理有限公司': { alias: '现代4.5T冷链(交投)', order: 201 }, @@ -216,9 +262,11 @@ const MODEL_ALIAS_MAP: Record = { '宇通-49吨牵引车头-白': { alias: '49T宇通', order: 401 }, '飞驰-49吨牵引车头-白/蓝/绿': { alias: '49T飞驰', order: 402 }, '飞驰-49吨牵引车头-白/蓝/绿嘉兴氢能产业发展股份有限公司': { alias: '49T飞驰(嘉氢)', order: 403 }, + '飞驰-49吨牵引车头-红': { alias: '49T飞驰(红)', order: 402 }, '飞驰-49吨牵引车头-红嘉兴氢能产业发展股份有限公司': { alias: '49T飞驰(嘉氢)', order: 403 }, // merge with above '飞驰-49吨牵引车头-红浙江氢能产业发展有限公司': { alias: '49T飞驰(浙氢-红)', order: 404 }, '飞驰-49吨牵引车头-白/蓝/绿浙江氢能产业发展有限公司': { alias: '49T飞驰(浙氢-蓝白绿)', order: 405 }, + '楚风-49吨牵引车头-蓝/黑': { alias: '49T楚风', order: 406 }, '楚风-49吨牵引车头-蓝/黑海珀特科技(北京)有限公司': { alias: '49T楚风(海珀特)', order: 406 }, // 其他 '红岩-35吨牵引车头-红色': { alias: '35T油车', order: 501 }, @@ -232,9 +280,11 @@ const MODEL_ALIAS_MAP: Record = { '万风-重型平板半挂车-红': { alias: '挂车', order: 503 }, '舒捷-SJ型蓄电池观光车-蓝白': { alias: '观光车', order: 504 }, '东风-挂靠油车-白色': { alias: '公务车/挂靠车', order: 505 }, + '东风-挂靠油车-白': { alias: '公务车/挂靠车', order: 505 }, '腾势-公务用车/小客车-黑': { alias: '公务车/挂靠车', order: 505 }, '腾势-公务用车/小客车-白': { alias: '公务车/挂靠车', order: 505 }, '其他-公务用车/小客车-蓝色': { alias: '公务车/挂靠车', order: 505 }, + '其他-公务用车/小客车-灰': { alias: '公务车/挂靠车', order: 505 }, '远程牌-公务用车/小客车-白': { alias: '公务车/挂靠车', order: 505 }, '大通-公务用车/小客车-灰': { alias: '公务车/挂靠车', order: 505 }, }; @@ -247,7 +297,7 @@ function deriveModelTag( rentCompany: string | null, ): string { const brand = (brandLabel || '').trim(); - const model = (modelLabel || '').trim(); + const model = (normalizeModelLabel(modelLabel) || '').trim(); const c = (color || '').trim(); const isRented = ownershipLabel?.trim() === '外租'; const company = isRented ? (rentCompany || '').trim() : ''; @@ -272,15 +322,16 @@ function transformRow(row: VehicleRow): Vehicle { id: row.id, plateNumber: row.车牌号 || '', vin: row.vin || '', - type: deriveType(row.车辆型号Label, row.车辆品牌Label), + type: deriveType(row.车辆型号Label, row.车辆类型参数), model: deriveModelTag(row.车辆品牌Label, row.车辆型号Label, row.车辆颜色, row.车辆归属状态Label, row.租赁公司), color: row.车辆颜色 || '', location: region, region, province: row.省, city: row.市, - status: mapStatus(row.车辆租赁状态Label), - ownership: mapOwnership(row.车辆租赁状态Label), + status: mapStatus(row.车辆租赁状态Label, row.车辆租赁状态), + operationStatus: row.车辆租赁状态Label, + ownership: mapOwnership(row.车辆归属状态Label), rentCompany: row.租赁公司 || '', contractNo: row.合同编码, customerName: row.客户名称, @@ -318,7 +369,7 @@ async function getVehiclesForUser(c: Context): Promise { return maskCustomerNames(list); } -// 归属公司筛选(所属公司 = tab_truck.org_id → org_name, 即 Vehicle.subjectOrg) +// 归属公司筛选(所属公司 = vehicle_info.registered_ownership, 即 Vehicle.subjectOrg) function getSubjectParam(c: Context): string | null { const raw = (c.req.query('subject') || '').trim(); return raw ? raw : null; @@ -354,24 +405,27 @@ async function getWeeklyTruckIds(): Promise { } const [[pendingRows], [deliveredRows], [returnedRows], [replacedRows]] = await Promise.all([ - pool.query(`SELECT CAST(id AS CHAR) AS truck_id FROM tab_truck WHERE is_deleted=0 AND is_operation=1 AND truck_rent_status=7`), - pool.query(`SELECT CAST(rent_truck.truck_id AS CHAR) AS truck_id FROM tab_truck_rent_take take - LEFT JOIN tab_truck_rent_task task ON task.id = take.truck_rent_task_id - LEFT JOIN tab_contract_rent_truck rent_truck ON rent_truck.id = task.contract_rent_truck_id - WHERE take.is_deleted=0 AND take.take_name IS NOT NULL - AND task.task_type=1 AND task.task_status=1 AND take.update_time IS NOT NULL - AND take.handover_date >= ${WEEK_START_SQL} AND take.handover_date < ${WEEK_END_SQL}`), - pool.query(`SELECT CAST(rent_truck.truck_id AS CHAR) AS truck_id FROM tab_truck_rent_return r - LEFT JOIN tab_truck_rent_task task ON task.id = r.truck_rent_task_id - LEFT JOIN tab_contract_rent_truck rent_truck ON rent_truck.id = task.contract_rent_truck_id - WHERE r.is_deleted=0 AND r.return_date IS NOT NULL - AND r.return_date >= ${WEEK_START_SQL} AND r.return_date < ${WEEK_END_SQL}`), - pool.query(`SELECT CAST(rent_truck.truck_id AS CHAR) AS truck_id FROM tab_truck_rent_take take - LEFT JOIN tab_truck_rent_task task ON task.id = take.truck_rent_task_id - LEFT JOIN tab_contract_rent_truck rent_truck ON rent_truck.id = task.contract_rent_truck_id - WHERE take.is_deleted=0 AND take.take_name IS NOT NULL - AND task.task_type=3 AND task.task_status=1 AND take.update_time IS NOT NULL - AND take.handover_date >= ${WEEK_START_SQL} AND take.handover_date < ${WEEK_END_SQL}`), + pool.query(`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'`), + pool.query(`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)`), + pool.query(`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)`), + pool.query(`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`), ]); const toSet = (rows: any[]) => new Set((rows as any[]).map((r) => String(r.truck_id)).filter((s) => s && s !== 'null')); @@ -419,59 +473,54 @@ interface WeeklyStats { // 交车单 SQL const DELIVERED_SQL = `SELECT - take.id, DATE(take.handover_date) AS handover_date, - CAST(truck.id AS CHAR) AS truck_id, truck.plate_number, - dic_contract_type.dic_name AS contract_type, - customer.customer_name -FROM tab_truck_rent_take take -LEFT JOIN tab_truck_rent_task task ON task.id = take.truck_rent_task_id -LEFT JOIN tab_contract_rent_truck rent_truck ON rent_truck.id = task.contract_rent_truck_id -LEFT JOIN tab_truck truck ON rent_truck.truck_id = truck.id -LEFT JOIN tab_contract contract ON task.contract_id = contract.id -LEFT JOIN tab_customer customer ON contract.customer_id = customer.id -LEFT JOIN tab_dic dic_contract_type - ON dic_contract_type.parent_code = 'dic_contract_type' - AND dic_contract_type.dic_code = contract.contract_type - AND dic_contract_type.is_deleted = 0 -WHERE take.is_deleted = 0 AND take.take_name IS NOT NULL - AND task.task_type = 1 AND task.task_status = 1 AND take.update_time IS NOT NULL`; + 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.return_date) AS handover_date, - CAST(truck.id AS CHAR) AS truck_id, truck.plate_number, - dic_contract_type.dic_name AS contract_type, - customer.customer_name -FROM tab_truck_rent_return r -LEFT JOIN tab_truck_rent_task task ON task.id = r.truck_rent_task_id -LEFT JOIN tab_contract_rent_truck rent_truck ON rent_truck.id = task.contract_rent_truck_id -LEFT JOIN tab_truck truck ON rent_truck.truck_id = truck.id -LEFT JOIN tab_contract contract ON task.contract_id = contract.id -LEFT JOIN tab_customer customer ON contract.customer_id = customer.id -LEFT JOIN tab_dic dic_contract_type - ON dic_contract_type.parent_code = 'dic_contract_type' - AND dic_contract_type.dic_code = contract.contract_type - AND dic_contract_type.is_deleted = 0 -WHERE r.is_deleted = 0 AND r.return_date IS NOT NULL`; + 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 - take.id, DATE(take.handover_date) AS handover_date, - CAST(truck.id AS CHAR) AS truck_id, truck.plate_number, - dic_contract_type.dic_name AS contract_type, - customer.customer_name -FROM tab_truck_rent_take take -LEFT JOIN tab_truck_rent_task task ON task.id = take.truck_rent_task_id -LEFT JOIN tab_contract_rent_truck rent_truck ON rent_truck.id = task.contract_rent_truck_id -LEFT JOIN tab_truck truck ON rent_truck.truck_id = truck.id -LEFT JOIN tab_contract contract ON task.contract_id = contract.id -LEFT JOIN tab_customer customer ON contract.customer_id = customer.id -LEFT JOIN tab_dic dic_contract_type - ON dic_contract_type.parent_code = 'dic_contract_type' - AND dic_contract_type.dic_code = contract.contract_type - AND dic_contract_type.is_deleted = 0 -WHERE take.is_deleted = 0 AND take.take_name IS NOT NULL - AND task.task_type = 3 AND task.task_status = 1 AND take.update_time IS NOT NULL`; + 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`; let cachedWeeklyStats: WeeklyStats | null = null; let weeklyStatsLastFetch = 0; @@ -483,23 +532,18 @@ async function getWeeklyStats(): Promise { } const [[pendingRows], [newRows], [removedRows], [deliveredRows], [returnedRows], [replacedRows]] = await Promise.all([ - pool.query(`SELECT COUNT(*) AS cnt FROM tab_truck WHERE is_deleted=0 AND is_operation=1 AND truck_rent_status=7`), - pool.query(`SELECT COUNT(*) AS cnt FROM tab_truck WHERE is_deleted=0 AND is_operation=1 AND create_time >= ${WEEK_START_SQL} AND create_time < ${WEEK_END_SQL}`), - pool.query(`SELECT COUNT(*) AS cnt FROM tab_truck WHERE (is_operation=0 OR is_deleted=1) AND update_time >= ${WEEK_START_SQL} AND update_time < ${WEEK_END_SQL}`), - pool.query(`SELECT COUNT(*) AS cnt FROM tab_truck_rent_take take - LEFT JOIN tab_truck_rent_task task ON task.id = take.truck_rent_task_id - WHERE take.is_deleted=0 AND take.take_name IS NOT NULL - AND task.task_type=1 AND task.task_status=1 AND take.update_time IS NOT NULL - AND take.handover_date >= ${WEEK_START_SQL} AND take.handover_date < ${WEEK_END_SQL}`), - pool.query(`SELECT COUNT(*) AS cnt FROM tab_truck_rent_return r - LEFT JOIN tab_truck_rent_task task ON task.id = r.truck_rent_task_id - WHERE r.is_deleted=0 AND r.return_date IS NOT NULL - AND r.return_date >= ${WEEK_START_SQL} AND r.return_date < ${WEEK_END_SQL}`), - pool.query(`SELECT COUNT(*) AS cnt FROM tab_truck_rent_take take - LEFT JOIN tab_truck_rent_task task ON task.id = take.truck_rent_task_id - WHERE take.is_deleted=0 AND take.take_name IS NOT NULL - AND task.task_type=3 AND task.task_status=1 AND take.update_time IS NOT NULL - AND take.handover_date >= ${WEEK_START_SQL} AND take.handover_date < ${WEEK_END_SQL}`), + pool.query(`SELECT COUNT(*) AS cnt FROM vehicle_status WHERE del_flag=0 AND vehicle_status='4' AND COALESCE(operation_status, '') <> '5'`), + pool.query(`SELECT COUNT(*) AS cnt FROM vehicle_info WHERE del_flag='0' AND create_time >= ${WEEK_START_SQL} AND create_time < ${WEEK_END_SQL}`), + pool.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}`), + pool.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}`), + pool.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}`), + pool.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}`), ]); cachedWeeklyStats = { @@ -520,13 +564,13 @@ app.get('/summary', async (c) => { const vehicleIds = new Set(vehicles.map(v => String(v.id))); const summary: SummaryData = { totalAssets: vehicles.length, - operating: { - total: vehicles.filter((v) => v.status === 'Operating').length, - self: vehicles.filter((v) => v.status === 'Operating' && v.ownership === 'Self').length, - leased: vehicles.filter((v) => v.status === 'Operating' && v.ownership === 'Leased').length, - public: vehicles.filter((v) => v.status === 'Operating' && v.ownership === 'Public').length, - hanging: vehicles.filter((v) => v.status === 'Operating' && v.ownership === 'Hanging').length, - }, + operating: { + total: vehicles.filter((v) => v.status === 'Operating' && (v.operationStatus === '1' || v.operationStatus === '2')).length, + self: vehicles.filter((v) => v.status === 'Operating' && v.operationStatus === '2').length, + leased: vehicles.filter((v) => v.status === 'Operating' && v.operationStatus === '1').length, + public: vehicles.filter((v) => v.status === 'Operating' && v.ownership === 'Public').length, + hanging: 0, + }, inventory: { total: vehicles.filter((v) => v.status === 'Inventory' || v.status === 'Abnormal').length, inStock: vehicles.filter((v) => v.status === 'Inventory').length, @@ -695,7 +739,8 @@ app.get('/dept-stats', async (c) => { const deptMap = new Map>(); for (const v of withManager) { - const dept = v.departmentName || '公务车'; + const isPublicServiceVehicle = v.model === '公务车/挂靠车'; + const dept = isPublicServiceVehicle ? '公务车' : (v.departmentName || '未分配部门'); const mgr = v.customerManager || '未分配'; if (EXCLUDED_MANAGERS.has(mgr)) continue; if (!deptMap.has(dept)) deptMap.set(dept, new Map()); @@ -704,29 +749,6 @@ app.get('/dept-stats', async (c) => { mgrMap.get(mgr)!.push(v); } - // 补齐:业务部门内所有在职用户,即使当前无车辆也需显示 - const deptNames = Array.from(deptMap.keys()).filter((d) => d !== '公务车'); - if (deptNames.length > 0) { - const placeholders = deptNames.map(() => '?').join(','); - const [userRows] = await pool.query( - `SELECT u.user_name, dep.dep_name - FROM tab_user u - LEFT JOIN tab_department dep ON dep.id = u.dep_id AND dep.is_deleted = 0 - WHERE u.is_deleted = 0 - AND dep.dep_name IN (${placeholders})`, - deptNames, - ); - for (const r of userRows as any[]) { - const dept = r.dep_name as string | null; - const mgr = r.user_name as string | null; - if (!dept || !mgr) continue; - if (EXCLUDED_MANAGERS.has(mgr)) continue; - const mgrMap = deptMap.get(dept); - if (!mgrMap) continue; - if (!mgrMap.has(mgr)) mgrMap.set(mgr, []); - } - } - // Compute attendance & avg mileage from realtime data const getMileageStats = (vList: Vehicle[]) => { const todayActive = vList.filter((v) => (todayMileageMap.get(v.plateNumber) || 0) > 0).length; @@ -971,7 +993,11 @@ app.get('/list', async (c) => { filtered = filtered.filter((v) => customer === '未分配客户' ? !v.customerName : v.customerName === customer); } if (department) { - filtered = filtered.filter((v) => department === '公务车' ? !v.departmentName : v.departmentName === department); + filtered = filtered.filter((v) => { + if (department === '公务车') return v.model === '公务车/挂靠车'; + if (department === '未分配部门') return v.model !== '公务车/挂靠车' && !v.departmentName; + return v.departmentName === department; + }); } if (isColdChain !== undefined) { const wantCold = isColdChain === 'true'; @@ -992,9 +1018,10 @@ app.get('/list', async (c) => { location: v.location, province: v.province, city: v.city, - status: v.status, - ownership: v.ownership, - contractNo: v.contractNo, + status: v.status, + ownership: v.ownership, + rentCompany: v.rentCompany, + contractNo: v.contractNo, customerName: v.customerName, subjectOrg: v.subjectOrg, departmentName: v.departmentName, @@ -1049,18 +1076,22 @@ app.get('/weekly-detail', async (c) => { const source = c.req.query('source'); let sql: string; if (type === 'delivered') { - sql = `${DELIVERED_SQL} AND take.handover_date >= ${WEEK_START_SQL} AND take.handover_date < ${WEEK_END_SQL} ORDER BY take.handover_date DESC`; + sql = `${DELIVERED_SQL} AND dv.delivery_time >= ${WEEK_START_SQL} AND dv.delivery_time < ${WEEK_END_SQL} ORDER BY dv.delivery_time DESC`; } else if (type === 'returned') { - sql = `${RETURNED_SQL} AND r.return_date >= ${WEEK_START_SQL} AND r.return_date < ${WEEK_END_SQL} ORDER BY r.return_date DESC`; + sql = `${RETURNED_SQL} AND r.arrival_time >= ${WEEK_START_SQL} AND r.arrival_time < ${WEEK_END_SQL} ORDER BY r.arrival_time DESC`; } else if (type === 'replaced') { - sql = `${REPLACED_SQL} AND take.handover_date >= ${WEEK_START_SQL} AND take.handover_date < ${WEEK_END_SQL} ORDER BY take.handover_date DESC`; + sql = `${REPLACED_SQL} AND vr.replace_time >= ${WEEK_START_SQL} AND vr.replace_time < ${WEEK_END_SQL} ORDER BY vr.replace_time DESC`; } else if (type === 'pending') { - sql = `SELECT CAST(truck.id AS CHAR) AS truck_id, truck.plate_number, NULL AS handover_date, NULL AS contract_type, NULL AS customer_name - FROM tab_truck truck WHERE truck.is_deleted=0 AND truck.is_operation=1 AND truck.truck_rent_status=7`; + sql = `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'`; } else if (type === 'new') { - sql = `SELECT CAST(truck.id AS CHAR) AS truck_id, truck.plate_number, truck.create_time AS handover_date, NULL AS contract_type, NULL AS customer_name - FROM tab_truck truck WHERE truck.is_deleted=0 AND truck.is_operation=1 - AND truck.create_time >= ${WEEK_START_SQL} AND truck.create_time < ${WEEK_END_SQL} ORDER BY truck.create_time DESC`; + sql = `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`; } else { return c.json([]); } @@ -1124,20 +1155,18 @@ app.get('/debug', async (c) => { ${WEEK_END_SQL} AS week_end, CURDATE() AS today, WEEKDAY(CURDATE()) AS weekday`); - const [[deliveredAll]] = await pool.query(`SELECT COUNT(*) AS cnt FROM tab_truck_rent_take take - LEFT JOIN tab_truck_rent_task task ON task.id = take.truck_rent_task_id - WHERE take.is_deleted=0 AND take.take_name IS NOT NULL AND task.task_type=1 AND task.task_status=1`); - const [[deliveredRecent]] = await pool.query(`SELECT COUNT(*) AS cnt FROM tab_truck_rent_take take - LEFT JOIN tab_truck_rent_task task ON task.id = take.truck_rent_task_id - WHERE take.is_deleted=0 AND take.take_name IS NOT NULL AND task.task_type=1 AND task.task_status=1 - AND take.handover_date >= ${WEEK_START_SQL} AND take.handover_date < ${WEEK_END_SQL}`); - const [[latestTake]] = await pool.query(`SELECT MAX(take.handover_date) AS latest FROM tab_truck_rent_take take - LEFT JOIN tab_truck_rent_task task ON task.id = take.truck_rent_task_id - WHERE take.is_deleted=0 AND take.take_name IS NOT NULL AND task.task_type=1 AND task.task_status=1`); - const [[returnedRecent]] = await pool.query(`SELECT COUNT(*) AS cnt FROM tab_truck_rent_return r - WHERE r.is_deleted=0 AND r.return_date IS NOT NULL - AND r.return_date >= ${WEEK_START_SQL} AND r.return_date < ${WEEK_END_SQL}`); - const [[latestReturn]] = await pool.query(`SELECT MAX(r.return_date) AS latest FROM tab_truck_rent_return r WHERE r.is_deleted=0 AND r.return_date IS NOT NULL`); + const [[deliveredAll]] = await pool.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 pool.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 pool.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 pool.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 pool.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 c.json({ weekRange: dateRange, diff --git a/src/server/types.ts b/src/server/types.ts index ae2d33e..a10135c 100644 --- a/src/server/types.ts +++ b/src/server/types.ts @@ -8,6 +8,7 @@ export interface VehicleRow { 租赁公司: string; 车辆归属状态Label: string | null; 车辆型号Label: string | null; + 车辆类型参数: string | null; 库存区域: string | null; 车辆租赁状态: string | null; 车辆租赁状态Label: string | null; @@ -40,6 +41,7 @@ export interface Vehicle { province: string | null; city: string | null; status: 'Operating' | 'Inventory' | 'Pending' | 'Abnormal'; + operationStatus: string | null; ownership: string; rentCompany: string; contractNo: string | null;