fix asset module database migration
All checks were successful
ci/woodpecker/manual/woodpecker Pipeline was successful

This commit is contained in:
kkfluous
2026-06-17 11:53:39 +08:00
parent c13f341d5e
commit 91202bdf71
11 changed files with 346 additions and 291 deletions

View File

@@ -5,11 +5,11 @@ services:
image: harbor.lnh2e.com/lingniu-v1/ln-bi:main-1.0.0 image: harbor.lnh2e.com/lingniu-v1/ln-bi:main-1.0.0
network_mode: host network_mode: host
environment: environment:
DB_HOST: "47.101.148.99" DB_HOST: "rm-bp179zbv481rnw3e2no.mysql.rds.aliyuncs.com"
DB_PORT: "3306" DB_PORT: "3306"
DB_USER: "root" DB_USER: "oneos_db_prod"
DB_PASSWORD: "LN#Passw0rd@2026" DB_PASSWORD: "adASHJcviqwjkbn23ngt1"
DB_NAME: "lingniu_prod" DB_NAME: "ln_asset_management"
HYDROGEN_DB_HOST: "47.99.185.173" HYDROGEN_DB_HOST: "47.99.185.173"
HYDROGEN_DB_PORT: "3306" HYDROGEN_DB_PORT: "3306"
HYDROGEN_DB_USER: "root" HYDROGEN_DB_USER: "root"

View File

@@ -106,6 +106,7 @@ export interface VehicleListItem {
city: string | null; city: string | null;
status: string; status: string;
ownership: string; ownership: string;
rentCompany?: string | null;
contractNo: string | null; contractNo: string | null;
customerName: string | null; customerName: string | null;
subjectOrg: string | null; subjectOrg: string | null;

View File

@@ -52,11 +52,15 @@ app.get('/exchange', async (c) => {
// 查询 depCode 对应的部门名称 // 查询 depCode 对应的部门名称
let depName = ''; let depName = '';
if (userInfo.depCode) { if (userInfo.depCode) {
try {
const [rows] = await pool.execute( const [rows] = await pool.execute(
'SELECT dep_name FROM tab_department WHERE dep_code = ? AND is_deleted = 0 LIMIT 1', 'SELECT dep_name FROM tab_department WHERE dep_code = ? AND is_deleted = 0 LIMIT 1',
[userInfo.depCode] [userInfo.depCode]
) as [{ dep_name: string }[], unknown]; ) as [{ dep_name: string }[], unknown];
depName = rows[0]?.dep_name || ''; depName = rows[0]?.dep_name || '';
} catch (e: any) {
if (e?.code !== 'ER_NO_SUCH_TABLE') throw e;
}
} }
const payload: JwtPayload = { const payload: JwtPayload = {

View File

@@ -149,8 +149,8 @@ async function buildPlateLookup(plates: Set<string>): Promise<Map<string, string
const placeholders = arr.map(() => '?').join(','); const placeholders = arr.map(() => '?').join(',');
const [rows] = await pool.query<RowDataPacket[]>( const [rows] = await pool.query<RowDataPacket[]>(
`SELECT plate_number, CAST(id AS CHAR) AS truck_id `SELECT plate_number, CAST(id AS CHAR) AS truck_id
FROM tab_truck FROM vehicle_info
WHERE is_deleted = 0 AND plate_number IN (${placeholders})`, WHERE del_flag = '0' AND plate_number IN (${placeholders})`,
arr, arr,
); );
const map = new Map<string, string>(); const map = new Map<string, string>();

View File

@@ -74,8 +74,8 @@ interface TargetRow {
async function fetchTargetRows(): Promise<TargetRow[]> { async function fetchTargetRows(): Promise<TargetRow[]> {
return pool.execute( return pool.execute(
`SELECT t.id, t.target_name, v.plate_number `SELECT t.id, t.target_name, v.plate_number
FROM tab_mileage_assessment_target t FROM lingniu_prod.tab_mileage_assessment_target t
JOIN tab_mileage_assessment_vehicle v ON v.target_id = t.id AND v.is_deleted = 0 JOIN lingniu_prod.tab_mileage_assessment_vehicle v ON v.target_id = t.id AND v.is_deleted = 0
WHERE t.is_deleted = 0` WHERE t.is_deleted = 0`
).then(([rows]) => rows as TargetRow[]); ).then(([rows]) => rows as TargetRow[]);
} }
@@ -102,10 +102,10 @@ function buildPlateTargetNamesMap(targetRows: TargetRow[]): Map<string, string[]
async function fetchBizTotalMileageMap(): Promise<Map<string, number>> { async function fetchBizTotalMileageMap(): Promise<Map<string, number>> {
// v_vehicle_daily_stats.total_km 对 G7S 数据源常为 NULLG7 只回传日增量), // v_vehicle_daily_stats.total_km 对 G7S 数据源常为 NULLG7 只回传日增量),
// 业务库 tab_mileage_assessment_vehicle.vehicle_total_mileage 是累加后的权威累计值, // 业务库 lingniu_prod.tab_mileage_assessment_vehicle.vehicle_total_mileage 是累加后的权威累计值,
// 用它兜底保证 totalKm 汇总完整。 // 用它兜底保证 totalKm 汇总完整。
const [rows] = await pool.execute( 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]; ) as [{ plate_number: string; vehicle_total_mileage: string | number | null }[], unknown];
const map = new Map<string, number>(); const map = new Map<string, number>();
for (const r of rows) { for (const r of rows) {

View File

@@ -10,7 +10,7 @@ const app = new Hono();
app.get('/', async (c) => { app.get('/', async (c) => {
try { try {
const [targets] = await pool.execute( 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]; ) as [any[], unknown];
const [vehicleStats] = await pool.execute(` 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_task) as current_year_target,
SUM(current_year_mileage) as current_year_completed, SUM(current_year_mileage) as current_year_completed,
MAX(current_year_assessment_end_date) as year_end_date 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 GROUP BY target_id
`) as [any[], unknown]; `) 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, 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(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 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 FROM lingniu_prod.tab_mileage_assessment_vehicle v
JOIN tab_mileage_assessment_target t ON t.id = v.target_id AND t.is_deleted = 0 JOIN lingniu_prod.tab_mileage_assessment_target t ON t.id = v.target_id AND t.is_deleted = 0
WHERE v.is_deleted = 0 WHERE v.is_deleted = 0
GROUP BY v.target_id GROUP BY v.target_id
`) as [any[], unknown]; `) 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, 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(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 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 FROM lingniu_prod.tab_mileage_assessment_vehicle v
JOIN tab_mileage_assessment_target t ON t.id = v.target_id AND t.is_deleted = 0 JOIN lingniu_prod.tab_mileage_assessment_target t ON t.id = v.target_id AND t.is_deleted = 0
JOIN ( JOIN (
SELECT 1 as year_number UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 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) ) 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_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, 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 COUNT(*) as cnt
FROM tab_mileage_assessment_vehicle v FROM lingniu_prod.tab_mileage_assessment_vehicle v
JOIN tab_mileage_assessment_target t ON t.id = v.target_id AND t.is_deleted = 0 JOIN lingniu_prod.tab_mileage_assessment_target t ON t.id = v.target_id AND t.is_deleted = 0
JOIN ( JOIN (
SELECT 1 as year_number UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 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) ) 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_start_date, '%Y-%m-%d') as start_date,
DATE_FORMAT(assessment_end_date, '%Y-%m-%d') as end_date, DATE_FORMAT(assessment_end_date, '%Y-%m-%d') as end_date,
COUNT(*) as cnt 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 GROUP BY target_id, assessment_start_date, assessment_end_date
ORDER BY target_id, assessment_start_date ORDER BY target_id, assessment_start_date
`) as [any[], unknown]; `) as [any[], unknown];
@@ -135,7 +135,7 @@ app.get('/', async (c) => {
} }
const [targetVehicleRows] = await pool.execute( 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]; ) as [{ target_id: number; plate_number: string }[], unknown];
const targetIdPlatesMap = new Map<number, string[]>(); const targetIdPlatesMap = new Map<number, string[]>();
@@ -245,7 +245,7 @@ app.get('/:id/vehicles', async (c) => {
`SELECT plate_number, today_mileage, vehicle_total_mileage, `SELECT plate_number, today_mileage, vehicle_total_mileage,
completion_rate, is_qualified, current_year_is_qualified, completion_rate, is_qualified, current_year_is_qualified,
daily_required_mileage daily_required_mileage
FROM tab_mileage_assessment_vehicle FROM lingniu_prod.tab_mileage_assessment_vehicle
WHERE target_id = ? AND is_deleted = 0 WHERE target_id = ? AND is_deleted = 0
ORDER BY today_mileage DESC`, ORDER BY today_mileage DESC`,
[targetId] [targetId]

View File

@@ -12,7 +12,7 @@ app.get('/', async (c) => {
let plates: string[] = []; let plates: string[] = [];
if (targetId) { if (targetId) {
const [vehicleRows] = await pool.execute( 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] [targetId]
) as [{ plate_number: string }[], unknown]; ) as [{ plate_number: string }[], unknown];
plates = vehicleRows.map(r => r.plate_number); plates = vehicleRows.map(r => r.plate_number);

View File

@@ -3,24 +3,42 @@ import type { VehicleInfoRow } from './types.js';
/** 车辆关联信息 SQL客户名、部门、经理、租赁状态、主体、项目 */ /** 车辆关联信息 SQL客户名、部门、经理、租赁状态、主体、项目 */
export const VEHICLE_INFO_SQL = `SELECT export const VEHICLE_INFO_SQL = `SELECT
truck.plate_number AS plate, vi.plate_number AS plate,
cus.customer_name AS customer, COALESCE(c.customer_name, vor.customer_name, ci.customer_name) AS customer,
dep.dep_name AS department, COALESCE(c.business_department_name, vor.business_dept) AS department,
u.user_name AS manager, COALESCE(c.business_manager_name, vor.business_manager) AS manager,
CAST(c.bd AS CHAR) AS manager_id, CAST(COALESCE(c.business_manager_id, vi.business_id) AS CHAR) AS manager_id,
dic_status.dic_name AS rent_status, CASE vs.operation_status
org_truck.org_name AS entity, WHEN '1' THEN '租赁'
c.project_name AS project WHEN '2' THEN '自营'
FROM tab_truck truck WHEN '3' THEN '可运营'
LEFT JOIN tab_truck_status_info si ON si.truck_id = truck.id AND si.is_deleted = 0 WHEN '4' THEN '待运营'
LEFT JOIN tab_contract c ON c.id = si.contract_id AND c.is_deleted = 0 WHEN '5' THEN '退出运营'
LEFT JOIN tab_customer cus ON cus.id = c.customer_id AND cus.is_deleted = 0 ELSE vs.operation_status
LEFT JOIN tab_user u ON u.id = c.bd AND u.is_deleted = 0 END AS rent_status,
LEFT JOIN tab_department dep ON dep.id = u.dep_id AND dep.is_deleted = 0 NULLIF(vi.registered_ownership, '') AS entity,
LEFT JOIN tab_dic dic_status ON dic_status.parent_code = 'dic_truck_rent_status' COALESCE(c.project_name, vor.project_name) AS project
AND dic_status.dic_code = truck.truck_rent_status AND dic_status.is_deleted = 0 FROM vehicle_info vi
LEFT JOIN tab_org org_truck ON org_truck.id = truck.org_id AND org_truck.is_deleted = 0 LEFT JOIN vehicle_status vs
WHERE truck.is_deleted = 0 AND truck.is_operation = 1`; 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 */ /** 查询所有车辆关联信息,返回 plate→info 的 Map */
export async function fetchVehicleInfoMap(): Promise<Map<string, VehicleInfoRow>> { export async function fetchVehicleInfoMap(): Promise<Map<string, VehicleInfoRow>> {
@@ -36,7 +54,7 @@ export async function fetchVehicleInfoMap(): Promise<Map<string, VehicleInfoRow>
export async function fetchVehicleInfoByPlates(plates: string[]): Promise<Map<string, VehicleInfoRow>> { export async function fetchVehicleInfoByPlates(plates: string[]): Promise<Map<string, VehicleInfoRow>> {
if (plates.length === 0) return new Map(); if (plates.length === 0) return new Map();
const [rows] = await pool.execute( 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 plates
) as [VehicleInfoRow[], unknown]; ) as [VehicleInfoRow[], unknown];
const map = new Map<string, VehicleInfoRow>(); const map = new Map<string, VehicleInfoRow>();

View File

@@ -28,16 +28,17 @@ function inferTypeFromTargetName(targetName: string): string {
} }
/** /**
* Classify vehicle type from dic_type.dic_name (e.g. "4.5吨冷链车", "4.5吨货车", "18吨双飞翼货车"). * Classify vehicle type from ln_asset_management.vehicle_model.
* The typeName is the full label from the dictionary, modelRaw is the numeric dic_code. * modelRaw is vehicle_model.vehicle_type, which is not the old dic_truck_type code.
*/ */
function classifyVehicleType(typeName: string, _modelRaw: string): string { function classifyVehicleType(typeName: string, _modelRaw: string): string {
const t = (typeName || '').trim(); const t = (typeName || '').trim();
if (t.includes('4.5') && t.includes('冷链')) return '4.5T冷链'; if (t.includes('4.5') && t.includes('冷链')) return '4.5T冷链';
if (t.includes('4.5')) return '4.5T普货'; if (t.includes('4.5')) return '4.5T普货';
if (t.includes('18')) return '18T'; if (t.includes('18')) return '18T';
if (t.includes('49') || t.includes('牵引')) return '49T';
if (t.includes('挂车')) return '挂车'; if (t.includes('挂车')) return '挂车';
if (t.includes('49')) return '49T';
if (t.includes('35')) return '35T';
return t || '其他'; return t || '其他';
} }
@@ -54,7 +55,7 @@ app.get('/', async (c) => {
// ---- Query 1: Assessment targets ---- // ---- Query 1: Assessment targets ----
const [targets] = await pool.execute( 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]; ) as [any[], unknown];
const targetMap = new Map<number, { targetName: string; annualMileage: number }>(); const targetMap = new Map<number, { targetName: string; annualMileage: number }>();
@@ -71,21 +72,20 @@ app.get('/', async (c) => {
current_mileage, current_year_mileage, current_year_mileage_task, current_mileage, current_year_mileage, current_year_mileage_task,
completion_rate, is_qualified, current_year_is_qualified, completion_rate, is_qualified, current_year_is_qualified,
daily_required_mileage, current_year_assessment_end_date 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]; `) as [any[], unknown];
// ---- Query 3: Vehicle info (customer, dept, manager) ---- // ---- Query 3: Vehicle info (customer, dept, manager) ----
const vehicleInfoMap = await fetchVehicleInfoMap(); const vehicleInfoMap = await fetchVehicleInfoMap();
// ---- Query 4: Vehicle types from tab_truck ---- // ---- Query 4: Vehicle types from vehicle_info ----
// 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.
const [truckTypeRows] = await pool.execute(` const [truckTypeRows] = await pool.execute(`
SELECT truck.plate_number, dic_type.dic_name AS type_name, truck.model AS model_raw SELECT vi.plate_number, vm.model AS type_name, vm.vehicle_type AS model_raw
FROM tab_truck truck FROM vehicle_info vi
LEFT JOIN tab_dic dic_type ON dic_type.parent_code = 'dic_truck_type' LEFT JOIN vehicle_status vs ON vs.vehicle_id = vi.id AND vs.del_flag = 0
AND dic_type.dic_code = truck.model AND dic_type.is_deleted = 0 LEFT JOIN vehicle_model vm ON vm.id = vi.vehicle_model_id AND vm.del_flag = '0'
WHERE truck.is_operation = 1 WHERE vi.del_flag = '0'
AND COALESCE(vs.operation_status, '') <> '5'
`) as [any[], unknown]; `) as [any[], unknown];
const truckTypeMap = new Map<string, { typeName: string; modelRaw: string }>(); const truckTypeMap = new Map<string, { typeName: string; modelRaw: string }>();
@@ -161,12 +161,13 @@ app.get('/', async (c) => {
// ---- Query 7: Inventory vehicles (rent_status = 0) ---- // ---- Query 7: Inventory vehicles (rent_status = 0) ----
const [inventoryTruckRows] = await pool.execute(` const [inventoryTruckRows] = await pool.execute(`
SELECT truck.plate_number, dic_type.dic_name AS type_name, truck.model AS model_raw SELECT vi.plate_number, vm.model AS type_name, vm.vehicle_type AS model_raw
FROM tab_truck truck FROM vehicle_info vi
LEFT JOIN tab_dic dic_type ON dic_type.parent_code = 'dic_truck_type' LEFT JOIN vehicle_status vs ON vs.vehicle_id = vi.id AND vs.del_flag = 0
AND dic_type.dic_code = truck.model AND dic_type.is_deleted = 0 LEFT JOIN vehicle_model vm ON vm.id = vi.vehicle_model_id AND vm.del_flag = '0'
WHERE truck.is_deleted = 0 AND truck.is_operation = 1 WHERE vi.del_flag = '0'
AND truck.truck_rent_status = 0 AND COALESCE(vs.operation_status, '') IN ('3','4')
AND COALESCE(vs.vehicle_status, '') <> '4'
`) as [any[], unknown]; `) as [any[], unknown];
// ---- Build assessment vehicle lookup for inventory cross-reference ---- // ---- Build assessment vehicle lookup for inventory cross-reference ----

View File

@@ -17,75 +17,100 @@ import type { Context } from 'hono';
const app = new Hono(); const app = new Hono();
const MAIN_SQL = `SELECT const MAIN_SQL = `SELECT
CAST(truck.id AS CHAR) AS id, CAST(vi.id AS CHAR) AS id,
truck.plate_number AS 车牌号, vi.plate_number AS 车牌号,
truck.vin AS vin, vi.vin AS vin,
truck.brand AS 车辆品牌, vm.brand AS 车辆品牌,
truck.model AS 车辆型号, vm.model AS 车辆型号,
truck.color AS 车辆颜色, vi.body_color AS 车辆颜色,
truck.rent_from_company AS 租赁公司, vi.rental_company AS 租赁公司,
dic_ascription_status.dic_name AS 车辆归属状态Label, CASE vi.vehicle_source
dic_type.dic_name AS 车辆型号Label, WHEN '0' THEN '自有'
truck.stock_area AS 库存区域, WHEN '1' THEN '外租'
truck.truck_rent_status AS 车辆租赁状态, WHEN '2' THEN '挂靠'
dic_status.dic_name AS 车辆租赁状态Label, ELSE vi.actual_ownership
truck.is_operation AS 是否营运, END AS 车辆归属状态Label,
info.province AS 省, vm.model AS 车辆型号Label,
info.city AS 市, 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.lat AS 纬度,
info.lng AS 经度, info.lng AS 经度,
dic_brand.dic_name AS 车辆品牌Label, CASE vm.brand
si.contract_id AS 合同ID, WHEN 'hyundai' THEN CASE WHEN vm.model LIKE '%帕力安%' OR vm.model LIKE '%冷链%' OR vm.model LIKE '%双飞翼%' THEN '帕力安牌' ELSE '现代' END
COALESCE(c.contract_no, si.contract_no) AS 合同编码, WHEN 'yuejin' THEN '跃进'
cus.customer_name AS 客户名称, WHEN 'feichi' THEN '飞驰'
org.org_name AS 合同归属公司, WHEN 'sulong' THEN '苏龙'
dep.dep_name AS 合同归属部门, WHEN 'higer' THEN '海格'
org_truck.org_name AS 主体, WHEN 'dongfeng' THEN '东风'
c.project_name AS 项目名称, WHEN 'yutong' THEN '宇通'
u.user_name AS 客户经理, WHEN 'chufeng' THEN '楚风'
CAST(c.bd AS CHAR) AS 经理ID WHEN 'tonghua' THEN '通华'
FROM tab_truck truck 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 LEFT JOIN tab_truck_remote_sync_realtime_info info
ON info.id = truck.id ON info.plate_number = vi.plate_number
LEFT JOIN tab_dic dic_type AND info.is_deleted = 0
ON dic_type.parent_code = 'dic_truck_type' LEFT JOIN common_district info_province
AND dic_type.dic_code = truck.model ON info_province.CODE = info.province COLLATE utf8mb4_unicode_ci
AND dic_type.is_deleted = 0 AND info_province.STATUS = 'VALID'
LEFT JOIN tab_dic dic_status LEFT JOIN common_district info_city
ON dic_status.parent_code = 'dic_truck_rent_status' ON info_city.CODE = info.city COLLATE utf8mb4_unicode_ci
AND dic_status.dic_code = truck.truck_rent_status AND info_city.STATUS = 'VALID'
AND dic_status.is_deleted = 0 LEFT JOIN common_district vi_province
LEFT JOIN tab_dic dic_brand ON vi_province.CODE = vi.province COLLATE utf8mb4_unicode_ci
ON dic_brand.parent_code = 'dic_vehicle_brand' AND vi_province.STATUS = 'VALID'
AND dic_brand.dic_code = truck.brand LEFT JOIN common_district vi_city
AND dic_brand.is_deleted = 0 ON vi_city.CODE = vi.city COLLATE utf8mb4_unicode_ci
LEFT JOIN tab_truck_status_info si AND vi_city.STATUS = 'VALID'
ON si.truck_id = truck.id LEFT JOIN common_district vi_operation_city
AND si.is_deleted = 0 ON vi_operation_city.CODE = vi.operation_city COLLATE utf8mb4_unicode_ci
LEFT JOIN tab_contract c AND vi_operation_city.STATUS = 'VALID'
ON c.id = si.contract_id LEFT JOIN vehicle_lease_order_record vor
AND c.is_deleted = 0 ON vor.vehicle_id = vi.id
LEFT JOIN tab_customer cus AND vor.del_flag = '0'
ON cus.id = c.customer_id AND vor.id = (
AND cus.is_deleted = 0 SELECT MAX(vor2.id)
LEFT JOIN tab_org org FROM vehicle_lease_order_record vor2
ON org.id = c.org_id WHERE vor2.vehicle_id = vi.id
AND org.is_deleted = 0 AND vor2.del_flag = '0'
LEFT JOIN tab_org org_truck )
ON org_truck.id = truck.org_id LEFT JOIN vehicle_lease_contract_info c
AND org_truck.is_deleted = 0 ON c.order_id = vor.contract_id
LEFT JOIN tab_dic dic_ascription_status AND c.del_flag = '0'
ON dic_ascription_status.parent_code = 'dic_truck_ascription_status' LEFT JOIN customer_info ci
AND dic_ascription_status.dic_code = truck.ascription_status ON ci.id = vi.customer_id
AND dic_ascription_status.is_deleted = 0 AND ci.del_flag = '0'
LEFT JOIN tab_user u WHERE vi.del_flag = '0'
ON u.id = c.bd AND COALESCE(vs.operation_status, '') <> '5'`;
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`;
// Region mapping: province/city -> display region // Region mapping: province/city -> display region
const REGIONS = ['嘉兴', '广东', '北京', '新疆', '其他'] as const; const REGIONS = ['嘉兴', '广东', '北京', '新疆', '其他'] as const;
@@ -148,23 +173,32 @@ function countByType(vehicles: Vehicle[]): VehicleTypeCounts {
return counts; return counts;
} }
// Map rental status to frontend status // Map operation status to frontend status.
// Actual DB values: 在库(0), 自营(1), 租赁(2), 待交车(7), 挂靠(8), 异动(12) // ln_asset_management.vehicle_status.operation_status:
function mapStatus(rentStatus: string | null): 'Operating' | 'Inventory' | 'Pending' | 'Abnormal' { // 1=租赁, 2=自营, 3=可运营, 4=待运营, 5=退出运营.
if (!rentStatus) return 'Inventory'; function mapStatus(operationStatus: string | null, vehicleStatus: string | null): 'Operating' | 'Inventory' | 'Pending' | 'Abnormal' {
const s = rentStatus.trim(); const op = (operationStatus || '').trim();
if (s === '租赁' || s === '自营' || s === '挂靠') return 'Operating'; const vehicle = (vehicleStatus || '').trim();
if (s === '在库') return 'Inventory'; if (vehicle === '4') return 'Pending';
if (s === '待交车') return 'Pending'; if (vehicle === '14') return 'Abnormal';
if (s === '异动') 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'; return 'Inventory';
} }
// Map ownership from truck_rent_status (rentStatusLabel) // Map ownership from vehicle_info.vehicle_source.
// DB values: 自营(1), 租赁(2), 挂靠(8) → these are the operating subtypes // DB values: 0/自有, 1/外租, 2/挂靠.
function mapOwnership(rentStatusLabel: string | null): string { function mapOwnership(rentStatusLabel: string | null): string {
if (!rentStatusLabel) return 'Unknown'; if (!rentStatusLabel) return 'Unknown';
const s = rentStatusLabel.trim(); 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 'Self';
if (s === '租赁') return 'Leased'; if (s === '租赁') return 'Leased';
if (s === '挂靠') return 'Hanging'; if (s === '挂靠') return 'Hanging';
@@ -179,26 +213,38 @@ function resolveCity(city: string | null, province: string | null): string {
return p || '其他'; return p || '其他';
} }
// Derive vehicle type category from model label // Derive page category from ln_asset_management.vehicle_model.
// Actual DB values: 4.5吨冷链车, 4.5吨货车, 18吨双飞翼货车, 18吨厢式货车, 49吨牵引车头, 35吨牵引车头, // vehicle_type is a new-system category code, not the old lingniu_prod.dic_truck_type code:
// 重型集装箱半挂车, 重型平板半挂车, 氢能叉车, SJ型蓄电池观光车, 公务用车/小客车, 挂靠油车 // 1 = 4.5T, 2 = 18T / other truck-like models, 3 = tractor head, 5/6 = trailers.
function deriveType(modelLabel: string | null, brandLabel: string | null): string { function deriveType(modelLabel: string | null, vehicleTypeCode: string | null): string {
const label = (modelLabel || '').trim(); 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('4.5吨')) return '4.5T';
if (label.includes('18吨')) return '18T'; if (label.includes('18吨')) return '18T';
if (label.includes('49吨')) return '49T'; if (label.includes('49吨')) return '49T';
if (label.includes('35吨')) return '35T'; 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 '叉车';
if (label.includes('半挂车')) return '挂车';
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 → alias mapping with sort order
// tag is generated as: brand-modelLabel-color[+rentCompany if 外租] // tag is generated as: brand-modelLabel-color[+rentCompany if 外租]
// Some tags are merged (e.g. 嘉氢 red + 嘉氢 blue/green → one alias) // Some tags are merged (e.g. 嘉氢 red + 嘉氢 blue/green → one alias)
const MODEL_ALIAS_MAP: Record<string, { alias: string; order: number }> = { const MODEL_ALIAS_MAP: Record<string, { alias: string; order: number }> = {
// 4.5T 普货 // 4.5T 普货
'现代-4.5吨货车-白色广州开发区交投氢能运营管理有限公司': { alias: '现代4.5T普货(交投)', order: 101 }, '现代-4.5吨货车-白色广州开发区交投氢能运营管理有限公司': { alias: '现代4.5T普货(交投)', order: 101 },
'现代-4.5吨货车-白色': { alias: '现代4.5T普货', order: 101 },
'现代-4.5吨货车-白': { alias: '现代4.5T普货(恒运)', order: 102 }, '现代-4.5吨货车-白': { alias: '现代4.5T普货(恒运)', order: 102 },
// 4.5T 冷链 // 4.5T 冷链
'帕力安牌-4.5吨冷链车-白色广州开发区交投氢能运营管理有限公司': { alias: '现代4.5T冷链(交投)', order: 201 }, '帕力安牌-4.5吨冷链车-白色广州开发区交投氢能运营管理有限公司': { alias: '现代4.5T冷链(交投)', order: 201 },
@@ -216,9 +262,11 @@ const MODEL_ALIAS_MAP: Record<string, { alias: string; order: number }> = {
'宇通-49吨牵引车头-白': { alias: '49T宇通', order: 401 }, '宇通-49吨牵引车头-白': { alias: '49T宇通', order: 401 },
'飞驰-49吨牵引车头-白/蓝/绿': { alias: '49T飞驰', order: 402 }, '飞驰-49吨牵引车头-白/蓝/绿': { alias: '49T飞驰', order: 402 },
'飞驰-49吨牵引车头-白/蓝/绿嘉兴氢能产业发展股份有限公司': { alias: '49T飞驰嘉氢', order: 403 }, '飞驰-49吨牵引车头-白/蓝/绿嘉兴氢能产业发展股份有限公司': { alias: '49T飞驰嘉氢', order: 403 },
'飞驰-49吨牵引车头-红': { alias: '49T飞驰', order: 402 },
'飞驰-49吨牵引车头-红嘉兴氢能产业发展股份有限公司': { alias: '49T飞驰嘉氢', order: 403 }, // merge with above '飞驰-49吨牵引车头-红嘉兴氢能产业发展股份有限公司': { alias: '49T飞驰嘉氢', order: 403 }, // merge with above
'飞驰-49吨牵引车头-红浙江氢能产业发展有限公司': { alias: '49T飞驰浙氢-红)', order: 404 }, '飞驰-49吨牵引车头-红浙江氢能产业发展有限公司': { alias: '49T飞驰浙氢-红)', order: 404 },
'飞驰-49吨牵引车头-白/蓝/绿浙江氢能产业发展有限公司': { alias: '49T飞驰浙氢-蓝白绿)', order: 405 }, '飞驰-49吨牵引车头-白/蓝/绿浙江氢能产业发展有限公司': { alias: '49T飞驰浙氢-蓝白绿)', order: 405 },
'楚风-49吨牵引车头-蓝/黑': { alias: '49T楚风', order: 406 },
'楚风-49吨牵引车头-蓝/黑海珀特科技(北京)有限公司': { alias: '49T楚风海珀特', order: 406 }, '楚风-49吨牵引车头-蓝/黑海珀特科技(北京)有限公司': { alias: '49T楚风海珀特', order: 406 },
// 其他 // 其他
'红岩-35吨牵引车头-红色': { alias: '35T油车', order: 501 }, '红岩-35吨牵引车头-红色': { alias: '35T油车', order: 501 },
@@ -232,9 +280,11 @@ const MODEL_ALIAS_MAP: Record<string, { alias: string; order: number }> = {
'万风-重型平板半挂车-红': { alias: '挂车', order: 503 }, '万风-重型平板半挂车-红': { alias: '挂车', order: 503 },
'舒捷-SJ型蓄电池观光车-蓝白': { alias: '观光车', order: 504 }, '舒捷-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 }, '其他-公务用车/小客车-蓝色': { 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, rentCompany: string | null,
): string { ): string {
const brand = (brandLabel || '').trim(); const brand = (brandLabel || '').trim();
const model = (modelLabel || '').trim(); const model = (normalizeModelLabel(modelLabel) || '').trim();
const c = (color || '').trim(); const c = (color || '').trim();
const isRented = ownershipLabel?.trim() === '外租'; const isRented = ownershipLabel?.trim() === '外租';
const company = isRented ? (rentCompany || '').trim() : ''; const company = isRented ? (rentCompany || '').trim() : '';
@@ -272,15 +322,16 @@ function transformRow(row: VehicleRow): Vehicle {
id: row.id, id: row.id,
plateNumber: row.车牌号 || '', plateNumber: row.车牌号 || '',
vin: row.vin || '', vin: row.vin || '',
type: deriveType(row.Label, row.Label), type: deriveType(row.Label, row.),
model: deriveModelTag(row.Label, row.Label, row., row.Label, row.), model: deriveModelTag(row.Label, row.Label, row., row.Label, row.),
color: row.车辆颜色 || '', color: row.车辆颜色 || '',
location: region, location: region,
region, region,
province: row.省, province: row.省,
city: row.市, city: row.市,
status: mapStatus(row.Label), status: mapStatus(row.Label, row.),
ownership: mapOwnership(row.Label), operationStatus: row.车辆租赁状态Label,
ownership: mapOwnership(row.Label),
rentCompany: row.租赁公司 || '', rentCompany: row.租赁公司 || '',
contractNo: row.合同编码, contractNo: row.合同编码,
customerName: row.客户名称, customerName: row.客户名称,
@@ -318,7 +369,7 @@ async function getVehiclesForUser(c: Context): Promise<Vehicle[]> {
return maskCustomerNames(list); return maskCustomerNames(list);
} }
// 归属公司筛选(所属公司 = tab_truck.org_id → org_name, 即 Vehicle.subjectOrg // 归属公司筛选(所属公司 = vehicle_info.registered_ownership, 即 Vehicle.subjectOrg
function getSubjectParam(c: Context): string | null { function getSubjectParam(c: Context): string | null {
const raw = (c.req.query('subject') || '').trim(); const raw = (c.req.query('subject') || '').trim();
return raw ? raw : null; return raw ? raw : null;
@@ -354,24 +405,27 @@ async function getWeeklyTruckIds(): Promise<WeeklyTruckIds> {
} }
const [[pendingRows], [deliveredRows], [returnedRows], [replacedRows]] = await Promise.all([ const [[pendingRows], [deliveredRows], [returnedRows], [replacedRows]] = await Promise.all([
pool.query<any[]>(`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<any[]>(`SELECT CAST(vehicle_id AS CHAR) AS truck_id
pool.query<any[]>(`SELECT CAST(rent_truck.truck_id AS CHAR) AS truck_id FROM tab_truck_rent_take take FROM vehicle_status
LEFT JOIN tab_truck_rent_task task ON task.id = take.truck_rent_task_id WHERE del_flag=0 AND vehicle_status='4' AND COALESCE(operation_status, '') <> '5'`),
LEFT JOIN tab_contract_rent_truck rent_truck ON rent_truck.id = task.contract_rent_truck_id pool.query<any[]>(`SELECT CAST(vehicle_id AS CHAR) AS truck_id
WHERE take.is_deleted=0 AND take.take_name IS NOT NULL FROM delivery_vehicle
AND task.task_type=1 AND task.task_status=1 AND take.update_time IS NOT NULL WHERE del_flag='0'
AND take.handover_date >= ${WEEK_START_SQL} AND take.handover_date < ${WEEK_END_SQL}`), AND vehicle_id IS NOT NULL
pool.query<any[]>(`SELECT CAST(rent_truck.truck_id AS CHAR) AS truck_id FROM tab_truck_rent_return r AND delivery_time >= ${WEEK_START_SQL} AND delivery_time < ${WEEK_END_SQL}
LEFT JOIN tab_truck_rent_task task ON task.id = r.truck_rent_task_id AND delivery_status IN (2,3,5)`),
LEFT JOIN tab_contract_rent_truck rent_truck ON rent_truck.id = task.contract_rent_truck_id pool.query<any[]>(`SELECT CAST(vehicle_id AS CHAR) AS truck_id
WHERE r.is_deleted=0 AND r.return_date IS NOT NULL FROM return_vehicle_task
AND r.return_date >= ${WEEK_START_SQL} AND r.return_date < ${WEEK_END_SQL}`), WHERE del_flag='0'
pool.query<any[]>(`SELECT CAST(rent_truck.truck_id AS CHAR) AS truck_id FROM tab_truck_rent_take take AND vehicle_id IS NOT NULL
LEFT JOIN tab_truck_rent_task task ON task.id = take.truck_rent_task_id AND arrival_time >= ${WEEK_START_SQL} AND arrival_time < ${WEEK_END_SQL}
LEFT JOIN tab_contract_rent_truck rent_truck ON rent_truck.id = task.contract_rent_truck_id AND status IN (2,3,5)`),
WHERE take.is_deleted=0 AND take.take_name IS NOT NULL pool.query<any[]>(`SELECT CAST(new_vehicle_id AS CHAR) AS truck_id
AND task.task_type=3 AND task.task_status=1 AND take.update_time IS NOT NULL FROM vehicle_replacement
AND take.handover_date >= ${WEEK_START_SQL} AND take.handover_date < ${WEEK_END_SQL}`), 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')); 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 // 交车单 SQL
const DELIVERED_SQL = `SELECT const DELIVERED_SQL = `SELECT
take.id, DATE(take.handover_date) AS handover_date, dv.id, DATE(dv.delivery_time) AS handover_date,
CAST(truck.id AS CHAR) AS truck_id, truck.plate_number, CAST(dv.vehicle_id AS CHAR) AS truck_id, dv.plate_number,
dic_contract_type.dic_name AS contract_type, c.contract_type,
customer.customer_name COALESCE(dts.customer_name, c.customer_name) AS customer_name
FROM tab_truck_rent_take take FROM delivery_vehicle dv
LEFT JOIN tab_truck_rent_task task ON task.id = take.truck_rent_task_id LEFT JOIN delivery_task_subject dts
LEFT JOIN tab_contract_rent_truck rent_truck ON rent_truck.id = task.contract_rent_truck_id ON dts.id = dv.delivery_task_subject_id
LEFT JOIN tab_truck truck ON rent_truck.truck_id = truck.id AND dts.del_flag = '0'
LEFT JOIN tab_contract contract ON task.contract_id = contract.id LEFT JOIN vehicle_lease_contract_info c
LEFT JOIN tab_customer customer ON contract.customer_id = customer.id ON c.order_id = dv.contract_id
LEFT JOIN tab_dic dic_contract_type AND c.del_flag = '0'
ON dic_contract_type.parent_code = 'dic_contract_type' WHERE dv.del_flag = '0'
AND dic_contract_type.dic_code = contract.contract_type AND dv.vehicle_id IS NOT NULL
AND dic_contract_type.is_deleted = 0 AND dv.delivery_time IS NOT NULL
WHERE take.is_deleted = 0 AND take.take_name IS NOT NULL AND dv.delivery_status IN (2,3,5)`;
AND task.task_type = 1 AND task.task_status = 1 AND take.update_time IS NOT NULL`;
// 还车单 SQL // 还车单 SQL
const RETURNED_SQL = `SELECT const RETURNED_SQL = `SELECT
r.id, DATE(r.return_date) AS handover_date, r.id, DATE(r.arrival_time) AS handover_date,
CAST(truck.id AS CHAR) AS truck_id, truck.plate_number, CAST(r.vehicle_id AS CHAR) AS truck_id, r.plate_number,
dic_contract_type.dic_name AS contract_type, c.contract_type,
customer.customer_name COALESCE(dts.customer_name, c.customer_name) AS customer_name
FROM tab_truck_rent_return r FROM return_vehicle_task r
LEFT JOIN tab_truck_rent_task task ON task.id = r.truck_rent_task_id LEFT JOIN delivery_task_subject dts
LEFT JOIN tab_contract_rent_truck rent_truck ON rent_truck.id = task.contract_rent_truck_id ON dts.id = r.delivery_task_subject_id
LEFT JOIN tab_truck truck ON rent_truck.truck_id = truck.id AND dts.del_flag = '0'
LEFT JOIN tab_contract contract ON task.contract_id = contract.id LEFT JOIN vehicle_lease_contract_info c
LEFT JOIN tab_customer customer ON contract.customer_id = customer.id ON c.order_id = r.contract_id
LEFT JOIN tab_dic dic_contract_type AND c.del_flag = '0'
ON dic_contract_type.parent_code = 'dic_contract_type' WHERE r.del_flag = '0'
AND dic_contract_type.dic_code = contract.contract_type AND r.vehicle_id IS NOT NULL
AND dic_contract_type.is_deleted = 0 AND r.arrival_time IS NOT NULL
WHERE r.is_deleted = 0 AND r.return_date IS NOT NULL`; AND r.status IN (2,3,5)`;
// 替换车单 SQL // 替换车单 SQL
const REPLACED_SQL = `SELECT const REPLACED_SQL = `SELECT
take.id, DATE(take.handover_date) AS handover_date, vr.id, DATE(vr.replace_time) AS handover_date,
CAST(truck.id AS CHAR) AS truck_id, truck.plate_number, CAST(vr.new_vehicle_id AS CHAR) AS truck_id, vr.new_vehicle_plate AS plate_number,
dic_contract_type.dic_name AS contract_type, c.contract_type,
customer.customer_name c.customer_name
FROM tab_truck_rent_take take FROM vehicle_replacement vr
LEFT JOIN tab_truck_rent_task task ON task.id = take.truck_rent_task_id LEFT JOIN vehicle_lease_contract_info c
LEFT JOIN tab_contract_rent_truck rent_truck ON rent_truck.id = task.contract_rent_truck_id ON c.id = vr.contract_id
LEFT JOIN tab_truck truck ON rent_truck.truck_id = truck.id AND c.del_flag = '0'
LEFT JOIN tab_contract contract ON task.contract_id = contract.id WHERE vr.del_flag = '0'
LEFT JOIN tab_customer customer ON contract.customer_id = customer.id AND vr.new_vehicle_id IS NOT NULL
LEFT JOIN tab_dic dic_contract_type AND vr.replace_time IS NOT NULL
ON dic_contract_type.parent_code = 'dic_contract_type' AND vr.status = 20`;
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`;
let cachedWeeklyStats: WeeklyStats | null = null; let cachedWeeklyStats: WeeklyStats | null = null;
let weeklyStatsLastFetch = 0; let weeklyStatsLastFetch = 0;
@@ -483,23 +532,18 @@ async function getWeeklyStats(): Promise<WeeklyStats> {
} }
const [[pendingRows], [newRows], [removedRows], [deliveredRows], [returnedRows], [replacedRows]] = await Promise.all([ const [[pendingRows], [newRows], [removedRows], [deliveredRows], [returnedRows], [replacedRows]] = await Promise.all([
pool.query<any[]>(`SELECT COUNT(*) AS cnt FROM tab_truck WHERE is_deleted=0 AND is_operation=1 AND truck_rent_status=7`), pool.query<any[]>(`SELECT COUNT(*) AS cnt FROM vehicle_status WHERE del_flag=0 AND vehicle_status='4' AND COALESCE(operation_status, '') <> '5'`),
pool.query<any[]>(`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<any[]>(`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<any[]>(`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<any[]>(`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<any[]>(`SELECT COUNT(*) AS cnt FROM tab_truck_rent_take take pool.query<any[]>(`SELECT COUNT(*) AS cnt FROM delivery_vehicle
LEFT JOIN tab_truck_rent_task task ON task.id = take.truck_rent_task_id WHERE del_flag='0' AND delivery_time IS NOT NULL AND delivery_status IN (2,3,5)
WHERE take.is_deleted=0 AND take.take_name IS NOT NULL AND delivery_time >= ${WEEK_START_SQL} AND delivery_time < ${WEEK_END_SQL}`),
AND task.task_type=1 AND task.task_status=1 AND take.update_time IS NOT NULL pool.query<any[]>(`SELECT COUNT(*) AS cnt FROM return_vehicle_task
AND take.handover_date >= ${WEEK_START_SQL} AND take.handover_date < ${WEEK_END_SQL}`), WHERE del_flag='0' AND arrival_time IS NOT NULL AND status IN (2,3,5)
pool.query<any[]>(`SELECT COUNT(*) AS cnt FROM tab_truck_rent_return r AND arrival_time >= ${WEEK_START_SQL} AND arrival_time < ${WEEK_END_SQL}`),
LEFT JOIN tab_truck_rent_task task ON task.id = r.truck_rent_task_id pool.query<any[]>(`SELECT COUNT(*) AS cnt FROM vehicle_replacement
WHERE r.is_deleted=0 AND r.return_date IS NOT NULL WHERE del_flag='0' AND replace_time IS NOT NULL AND status=20
AND r.return_date >= ${WEEK_START_SQL} AND r.return_date < ${WEEK_END_SQL}`), AND replace_time >= ${WEEK_START_SQL} AND replace_time < ${WEEK_END_SQL}`),
pool.query<any[]>(`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}`),
]); ]);
cachedWeeklyStats = { cachedWeeklyStats = {
@@ -521,11 +565,11 @@ app.get('/summary', async (c) => {
const summary: SummaryData = { const summary: SummaryData = {
totalAssets: vehicles.length, totalAssets: vehicles.length,
operating: { operating: {
total: vehicles.filter((v) => v.status === 'Operating').length, total: vehicles.filter((v) => v.status === 'Operating' && (v.operationStatus === '1' || v.operationStatus === '2')).length,
self: vehicles.filter((v) => v.status === 'Operating' && v.ownership === 'Self').length, self: vehicles.filter((v) => v.status === 'Operating' && v.operationStatus === '2').length,
leased: vehicles.filter((v) => v.status === 'Operating' && v.ownership === 'Leased').length, leased: vehicles.filter((v) => v.status === 'Operating' && v.operationStatus === '1').length,
public: vehicles.filter((v) => v.status === 'Operating' && v.ownership === 'Public').length, public: vehicles.filter((v) => v.status === 'Operating' && v.ownership === 'Public').length,
hanging: vehicles.filter((v) => v.status === 'Operating' && v.ownership === 'Hanging').length, hanging: 0,
}, },
inventory: { inventory: {
total: vehicles.filter((v) => v.status === 'Inventory' || v.status === 'Abnormal').length, total: vehicles.filter((v) => v.status === 'Inventory' || v.status === 'Abnormal').length,
@@ -695,7 +739,8 @@ app.get('/dept-stats', async (c) => {
const deptMap = new Map<string, Map<string, Vehicle[]>>(); const deptMap = new Map<string, Map<string, Vehicle[]>>();
for (const v of withManager) { for (const v of withManager) {
const dept = v.departmentName || '公务车'; const isPublicServiceVehicle = v.model === '公务车/挂靠车';
const dept = isPublicServiceVehicle ? '公务车' : (v.departmentName || '未分配部门');
const mgr = v.customerManager || '未分配'; const mgr = v.customerManager || '未分配';
if (EXCLUDED_MANAGERS.has(mgr)) continue; if (EXCLUDED_MANAGERS.has(mgr)) continue;
if (!deptMap.has(dept)) deptMap.set(dept, new Map()); if (!deptMap.has(dept)) deptMap.set(dept, new Map());
@@ -704,29 +749,6 @@ app.get('/dept-stats', async (c) => {
mgrMap.get(mgr)!.push(v); 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<any[]>(
`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 // Compute attendance & avg mileage from realtime data
const getMileageStats = (vList: Vehicle[]) => { const getMileageStats = (vList: Vehicle[]) => {
const todayActive = vList.filter((v) => (todayMileageMap.get(v.plateNumber) || 0) > 0).length; 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); filtered = filtered.filter((v) => customer === '未分配客户' ? !v.customerName : v.customerName === customer);
} }
if (department) { 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) { if (isColdChain !== undefined) {
const wantCold = isColdChain === 'true'; const wantCold = isColdChain === 'true';
@@ -994,6 +1020,7 @@ app.get('/list', async (c) => {
city: v.city, city: v.city,
status: v.status, status: v.status,
ownership: v.ownership, ownership: v.ownership,
rentCompany: v.rentCompany,
contractNo: v.contractNo, contractNo: v.contractNo,
customerName: v.customerName, customerName: v.customerName,
subjectOrg: v.subjectOrg, subjectOrg: v.subjectOrg,
@@ -1049,18 +1076,22 @@ app.get('/weekly-detail', async (c) => {
const source = c.req.query('source'); const source = c.req.query('source');
let sql: string; let sql: string;
if (type === 'delivered') { 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') { } 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') { } 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') { } 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 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 tab_truck truck WHERE truck.is_deleted=0 AND truck.is_operation=1 AND truck.truck_rent_status=7`; 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') { } 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 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 tab_truck truck WHERE truck.is_deleted=0 AND truck.is_operation=1 FROM vehicle_info vi
AND truck.create_time >= ${WEEK_START_SQL} AND truck.create_time < ${WEEK_END_SQL} ORDER BY truck.create_time DESC`; 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 { } else {
return c.json([]); return c.json([]);
} }
@@ -1124,20 +1155,18 @@ app.get('/debug', async (c) => {
${WEEK_END_SQL} AS week_end, ${WEEK_END_SQL} AS week_end,
CURDATE() AS today, CURDATE() AS today,
WEEKDAY(CURDATE()) AS weekday`); WEEKDAY(CURDATE()) AS weekday`);
const [[deliveredAll]] = await pool.query<any[]>(`SELECT COUNT(*) AS cnt FROM tab_truck_rent_take take const [[deliveredAll]] = await pool.query<any[]>(`SELECT COUNT(*) AS cnt FROM delivery_vehicle
LEFT JOIN tab_truck_rent_task task ON task.id = take.truck_rent_task_id WHERE del_flag='0' AND delivery_time IS NOT NULL AND delivery_status IN (2,3,5)`);
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<any[]>(`SELECT COUNT(*) AS cnt FROM delivery_vehicle
const [[deliveredRecent]] = await pool.query<any[]>(`SELECT COUNT(*) AS cnt FROM tab_truck_rent_take take WHERE del_flag='0' AND delivery_time IS NOT NULL AND delivery_status IN (2,3,5)
LEFT JOIN tab_truck_rent_task task ON task.id = take.truck_rent_task_id AND delivery_time >= ${WEEK_START_SQL} AND delivery_time < ${WEEK_END_SQL}`);
WHERE take.is_deleted=0 AND take.take_name IS NOT NULL AND task.task_type=1 AND task.task_status=1 const [[latestTake]] = await pool.query<any[]>(`SELECT MAX(delivery_time) AS latest FROM delivery_vehicle
AND take.handover_date >= ${WEEK_START_SQL} AND take.handover_date < ${WEEK_END_SQL}`); WHERE del_flag='0' AND delivery_time IS NOT NULL AND delivery_status IN (2,3,5)`);
const [[latestTake]] = await pool.query<any[]>(`SELECT MAX(take.handover_date) AS latest FROM tab_truck_rent_take take const [[returnedRecent]] = await pool.query<any[]>(`SELECT COUNT(*) AS cnt FROM return_vehicle_task
LEFT JOIN tab_truck_rent_task task ON task.id = take.truck_rent_task_id WHERE del_flag='0' AND arrival_time IS NOT NULL AND status IN (2,3,5)
WHERE take.is_deleted=0 AND take.take_name IS NOT NULL AND task.task_type=1 AND task.task_status=1`); AND arrival_time >= ${WEEK_START_SQL} AND arrival_time < ${WEEK_END_SQL}`);
const [[returnedRecent]] = await pool.query<any[]>(`SELECT COUNT(*) AS cnt FROM tab_truck_rent_return r const [[latestReturn]] = await pool.query<any[]>(`SELECT MAX(arrival_time) AS latest FROM return_vehicle_task
WHERE r.is_deleted=0 AND r.return_date IS NOT NULL WHERE del_flag='0' AND arrival_time IS NOT NULL AND status IN (2,3,5)`);
AND r.return_date >= ${WEEK_START_SQL} AND r.return_date < ${WEEK_END_SQL}`);
const [[latestReturn]] = await pool.query<any[]>(`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`);
return c.json({ return c.json({
weekRange: dateRange, weekRange: dateRange,

View File

@@ -8,6 +8,7 @@ export interface VehicleRow {
租赁公司: string; 租赁公司: string;
车辆归属状态Label: string | null; 车辆归属状态Label: string | null;
车辆型号Label: string | null; 车辆型号Label: string | null;
车辆类型参数: string | null;
库存区域: string | null; 库存区域: string | null;
车辆租赁状态: string | null; 车辆租赁状态: string | null;
车辆租赁状态Label: string | null; 车辆租赁状态Label: string | null;
@@ -40,6 +41,7 @@ export interface Vehicle {
province: string | null; province: string | null;
city: string | null; city: string | null;
status: 'Operating' | 'Inventory' | 'Pending' | 'Abnormal'; status: 'Operating' | 'Inventory' | 'Pending' | 'Abnormal';
operationStatus: string | null;
ownership: string; ownership: string;
rentCompany: string; rentCompany: string;
contractNo: string | null; contractNo: string | null;