refactor(stage16): 里程域移入 repository,后端分层全部完成
改动
- 新增 mileage/repository.ts:18 个查询函数,覆盖
vehicle-info(含搬走的 VEHICLE_INFO_SQL 共享常量)、trend、监控缓存、targets(8 条)、
daily-report-store(5 条)、daily-report-service。
- 6 个含 SQL 的文件改为调用 repository;路由/服务层不再出现 SQL 文本。
- 日报写入的两个函数改为返回 affectedRows(调用方本来只用它),而不是行数组。
- 架构守护的"已完整分层"清单加入 mileage —— 至此八个域全部在守护范围内。
SQL 保持性验证
- 逐字比对:改造前 6 个文件里的 19 条 SQL 字面量,除"动态 INSERT 关键字"一处外,
全部在 repository.ts 中原样出现。
- 那一处是 `${insert} INTO ${TABLE}`(insert 为 'REPLACE' | 'INSERT IGNORE'),
改为 `${replace ? "REPLACE" : "INSERT IGNORE"}`;已渲染两个分支与原文本逐字比对,均为 identical。
验证
- 全量 lint / test(191) / build 全绿,可达性 0 未引用文件。
- 里程域的模型层测试(cache-model / oneos-model / daily-report-model)以及依赖里程的
调度、车辆热力图契约测试全部通过。
This commit is contained in:
@@ -94,13 +94,13 @@ server/
|
||||
| `vehicles/` | `routes.ts` `repository.ts` `model.ts` `utils.ts` `types.ts` | ✅ 完整分层;SQL 有契约测试 |
|
||||
| `ele/` | `routes.ts` `repository.ts` `model.ts`(+ `model.test.ts` `routes.test.ts`) | ✅ 完整分层;改造前后 SQL/参数/响应体已做等价性验证 |
|
||||
| `feedback/` | `routes.ts` `repository.ts` `oss.ts`(+ `routes.test.ts`) | ✅ 完整分层;同上 |
|
||||
| `mileage/` | `index.ts`(聚合)+ `monitoring.ts` `targets.ts` `trend.ts` `daily-report.ts` `vehicle-recent.ts` + `*-model.ts` + `cache.ts` `oneos-api.ts` `daily-report-{service,store,scheduler}.ts` | 路由 / 模型 / 服务已分开,**SQL 仍在各路由文件内** |
|
||||
| `mileage/` | `index.ts`(聚合)+ `monitoring.ts` `targets.ts` `trend.ts` `daily-report.ts` `vehicle-recent.ts` + `repository.ts` + `*-model.ts` + `cache.ts` `oneos-api.ts` `daily-report-{service,store,scheduler}.ts` | ✅ 完整分层;车辆关联信息 SQL 与 18 个查询集中在此 |
|
||||
| `energy/` | `index.ts`(聚合)+ `electric.ts` `etc.ts` `hydrogen-station-board.ts` `hydrogen-bi-v2.ts` + `repository.ts` + `query-model.ts` `cache.ts` `constants.ts` | ✅ 完整分层;WHERE 片段组装(`buildFilterClauses` / `buildGroupSelect` / `buildScopedWhere` / `resolvedWhere`)与共享 SQL 片段也都在 repository / constants |
|
||||
| `scheduling/` | `index.ts`(聚合)+ `suggestions.ts` `notify.ts` `repository.ts` + `algorithm.ts` `notification-model.ts` | ✅ 完整分层;跨域数据源(里程车辆信息 / OneOS)显式注入 |
|
||||
| `hydrogen-heatmap/` | `routes.ts` `repository.ts` `model.ts`(+ `routes.test.ts`) | ✅ 完整分层;`buildWhere` 片段与参数顺序已锁定 |
|
||||
| `vehicle-heatmap/` | `routes.ts` `repository.ts` `model.ts`(+ `routes.test.ts`) | ✅ 完整分层;同时覆盖 MySQL(考核批次)与 PG(定位点)两个库 |
|
||||
|
||||
已完整分层的七个域(`vehicles` / `ele` / `feedback` / `vehicle-heatmap` / `hydrogen-heatmap` / `scheduling` / `energy`)
|
||||
**全部八个域**(`vehicles` / `ele` / `feedback` / `vehicle-heatmap` / `hydrogen-heatmap` / `scheduling` / `energy` / `mileage`)
|
||||
由架构测试守护:必须存在 `repository.ts`,且该域**其他任何非测试文件都不得含 SQL**。
|
||||
其余域(`energy` / `mileage`)尚未拆出 repository——拆分时**不要改变 SQL 与参数顺序**,
|
||||
请按 `ele/routes.test.ts` 的配方先补契约测试,并用"改造前后同一批请求对比落库 SQL 与响应体"做等价性验证。
|
||||
@@ -139,10 +139,9 @@ cors → read-only → /api/auth(公开) → authMiddleware → 各业务域
|
||||
|
||||
诚实记录,避免后来者以为已经做完:
|
||||
|
||||
- **后端仍有 1 个域没拆出 repository**:`mileage` 的 SQL 仍分散在 `cache.ts` /
|
||||
`daily-report-store.ts` / `targets.ts` / `monitoring.ts` / `trend.ts` / `vehicle-recent.ts` 等文件里。
|
||||
已完成的有 7 个域,可作为模板:路由只做校验与组装,SQL 进 `repository.ts`,
|
||||
纯逻辑进 `model.ts`,并用 mock pool 的契约测试锁定 SQL 与参数。
|
||||
- **后端八个域都已拆出 repository**,SQL 只允许出现在各域的 `repository.ts`(以及共享片段模块
|
||||
`energy/constants.ts`)中,由架构测试守护。新增查询请沿用同一形状:路由只做校验与组装,
|
||||
SQL 进 `repository.ts`,纯逻辑进 `model.ts`,并用 mock pool 的契约测试锁定 SQL 与参数。
|
||||
守卫用的是"语句形状"正则(如 `update <table> set`)而不是裸关键字,避免把
|
||||
`UpdateNotification` 或日志里的 "update error" 误判为 SQL;小写 SQL 同样能被抓到。
|
||||
- **运行时建表已集中到 `server/db/schema/`**,并在 `DB_READ_ONLY=1` 时整体跳过(由架构测试守护,
|
||||
|
||||
@@ -180,6 +180,7 @@ test("已完整分层的域:repository.ts 存在,且 SQL 只允许出现在
|
||||
"server/routes/hydrogen-heatmap",
|
||||
"server/routes/scheduling",
|
||||
"server/routes/energy",
|
||||
"server/routes/mileage",
|
||||
];
|
||||
// 用"语句形状"而不是裸关键字:UpdateNotification 或 "update error" 这类标识符/日志
|
||||
// 不应误判,而小写 SQL 也依然能被抓到。
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { loadMonitoringTargetRows } from './repository.js';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
@@ -51,12 +52,7 @@ export interface RangeMileageResult {
|
||||
}
|
||||
|
||||
async function fetchTargetRows(): Promise<TargetRow[]> {
|
||||
return pool.execute(
|
||||
`SELECT t.id, t.target_name, v.plate_number
|
||||
FROM lingniu_prod.tab_mileage_assessment_target t
|
||||
JOIN lingniu_prod.tab_mileage_assessment_vehicle v ON v.target_id = t.id AND v.is_deleted = 0
|
||||
WHERE t.is_deleted = 0`
|
||||
).then(([rows]) => rows as TargetRow[]);
|
||||
return loadMonitoringTargetRows(pool).then(([rows]) => rows as TargetRow[]);
|
||||
}
|
||||
|
||||
export async function refreshMonitoringCache(): Promise<void> {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { loadReportAssessmentRows } from './repository.js';
|
||||
import type { RowDataPacket } from 'mysql2';
|
||||
import pool from '../../db/mysql.js';
|
||||
import type {
|
||||
@@ -123,30 +124,7 @@ function shanghaiIsoNow(): string {
|
||||
}
|
||||
|
||||
async function fetchAssessmentRows(): Promise<AssessmentRow[]> {
|
||||
const [rows] = await pool.query<AssessmentRow[]>(`
|
||||
SELECT
|
||||
t.id AS target_id,
|
||||
t.target_name,
|
||||
t.annual_mileage_per_vehicle,
|
||||
v.id AS assessment_vehicle_id,
|
||||
v.plate_number,
|
||||
DATE_FORMAT(v.assessment_start_date, '%Y-%m-%d') AS assessment_start_date,
|
||||
DATE_FORMAT(v.assessment_end_date, '%Y-%m-%d') AS assessment_end_date,
|
||||
v.total_mileage_task,
|
||||
v.current_mileage,
|
||||
v.completion_rate,
|
||||
v.current_year_mileage_task,
|
||||
v.current_year_mileage,
|
||||
v.current_year_completion_rate,
|
||||
v.current_year_is_qualified,
|
||||
v.daily_required_mileage,
|
||||
DATE_FORMAT(v.current_year_assessment_end_date, '%Y-%m-%d') AS current_year_assessment_end_date
|
||||
FROM lingniu_prod.tab_mileage_assessment_target t
|
||||
JOIN lingniu_prod.tab_mileage_assessment_vehicle v
|
||||
ON v.target_id = t.id AND v.is_deleted = 0
|
||||
WHERE t.is_deleted = 0
|
||||
ORDER BY t.id, v.plate_number
|
||||
`);
|
||||
const rows = await loadReportAssessmentRows(pool) as unknown as AssessmentRow[];
|
||||
return rows;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
import {
|
||||
dailyReportSnapshotExists,
|
||||
insertDailyReportSnapshot,
|
||||
listDailyReportHistoryRows,
|
||||
loadDailyReportPayload,
|
||||
updateDailyReportPayload,
|
||||
} from './repository.js';
|
||||
import type { ResultSetHeader, RowDataPacket } from 'mysql2';
|
||||
import pool from '../../db/mysql.js';
|
||||
import { ensureDailyReportTable, DAILY_REPORT_TABLE as TABLE } from '../../db/schema/mileage-report.js';
|
||||
@@ -21,19 +28,13 @@ function parsePayload(value: unknown): DailyMileageReport | null {
|
||||
|
||||
export async function getDailyReportSnapshot(reportDate: string): Promise<DailyMileageReport | null> {
|
||||
await ensureDailyReportTable();
|
||||
const [rows] = await pool.query<RowDataPacket[]>(
|
||||
`SELECT report_payload FROM ${TABLE} WHERE report_date = ? LIMIT 1`,
|
||||
[reportDate],
|
||||
);
|
||||
const rows = await loadDailyReportPayload(pool, { reportDate });
|
||||
return parsePayload(rows[0]?.report_payload);
|
||||
}
|
||||
|
||||
export async function hasDailyReportSnapshot(reportDate: string): Promise<boolean> {
|
||||
await ensureDailyReportTable();
|
||||
const [rows] = await pool.query<RowDataPacket[]>(
|
||||
`SELECT 1 AS found FROM ${TABLE} WHERE report_date = ? LIMIT 1`,
|
||||
[reportDate],
|
||||
);
|
||||
const rows = await dailyReportSnapshotExists(pool, { reportDate });
|
||||
return rows.length > 0;
|
||||
}
|
||||
|
||||
@@ -57,41 +58,20 @@ export async function saveDailyReportSnapshot(
|
||||
archived.generatedAt.replace('T', ' ').replace(/Z$/, '').slice(0, 19),
|
||||
];
|
||||
|
||||
const insert = options.replace ? 'REPLACE' : 'INSERT IGNORE';
|
||||
const [result] = await pool.query<ResultSetHeader>(
|
||||
`${insert} INTO ${TABLE}
|
||||
(report_date, status, source, vehicle_count, operating_count, inventory_count,
|
||||
daily_mileage, day_over_day_rate, report_payload, generated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, CAST(? AS JSON), ?)`,
|
||||
values,
|
||||
);
|
||||
return result.affectedRows > 0;
|
||||
const affected = await insertDailyReportSnapshot(pool, { replace: Boolean(options.replace), values });
|
||||
return affected > 0;
|
||||
}
|
||||
|
||||
export async function updateDailyReportSnapshotPayload(report: DailyMileageReport): Promise<boolean> {
|
||||
await ensureDailyReportTable();
|
||||
const [result] = await pool.query<ResultSetHeader>(
|
||||
`UPDATE ${TABLE}
|
||||
SET report_payload = CAST(? AS JSON)
|
||||
WHERE report_date = ?`,
|
||||
[JSON.stringify(report), report.reportDate],
|
||||
);
|
||||
return result.affectedRows > 0;
|
||||
const affected = await updateDailyReportPayload(pool, { report: JSON.stringify(report), reportDate: report.reportDate });
|
||||
return affected > 0;
|
||||
}
|
||||
|
||||
export async function listDailyReportHistory(limit = 90): Promise<MileageReportHistoryResponse> {
|
||||
await ensureDailyReportTable();
|
||||
const safeLimit = Math.min(366, Math.max(1, limit));
|
||||
const [rows] = await pool.query<RowDataPacket[]>(
|
||||
`SELECT DATE_FORMAT(report_date, '%Y-%m-%d') AS report_date,
|
||||
status, source, vehicle_count, operating_count, inventory_count,
|
||||
daily_mileage, day_over_day_rate,
|
||||
DATE_FORMAT(generated_at, '%Y-%m-%dT%H:%i:%s+08:00') AS generated_at
|
||||
FROM ${TABLE}
|
||||
ORDER BY report_date DESC
|
||||
LIMIT ?`,
|
||||
[safeLimit],
|
||||
);
|
||||
const rows = await listDailyReportHistoryRows(pool, { safeLimit });
|
||||
const items: MileageReportHistoryItem[] = rows.map(row => ({
|
||||
reportDate: row.report_date,
|
||||
status: row.status,
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
import type { RowDataPacket } from 'mysql2';
|
||||
import { DAILY_REPORT_TABLE as TABLE } from '../../db/schema/mileage-report.js';
|
||||
|
||||
/**
|
||||
* 里程域的数据访问。
|
||||
* SQL 集中在此;缓存/口径计算仍在 cache.ts、daily-report-* 等模块里。
|
||||
*/
|
||||
|
||||
/** 只需要 query 能力的连接。 */
|
||||
export interface Database {
|
||||
query<T = any>(sql: string, values?: any[]): Promise<[T, ...any[]]>;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 里程域
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** 车辆关联信息(客户 / 部门 / 经理 / 租赁状态 / 主体 / 项目 / 品牌)。 */
|
||||
export const VEHICLE_INFO_SQL = `SELECT
|
||||
vi.plate_number AS plate,
|
||||
vi.vin AS vin,
|
||||
COALESCE(c.customer_name, vor.customer_name, ci.customer_name) AS customer,
|
||||
COALESCE(c.business_department_name, vor.business_dept) AS department,
|
||||
COALESCE(c.business_manager_name, vor.business_manager) AS manager,
|
||||
CAST(COALESCE(c.business_manager_id, vi.business_id) AS CHAR) AS manager_id,
|
||||
CASE vs.operation_status
|
||||
WHEN '1' THEN '租赁'
|
||||
WHEN '2' THEN '自营'
|
||||
WHEN '3' THEN '可运营'
|
||||
WHEN '4' THEN '待运营'
|
||||
WHEN '5' THEN '退出运营'
|
||||
ELSE vs.operation_status
|
||||
END AS rent_status,
|
||||
NULLIF(vi.registered_ownership, '') AS entity,
|
||||
COALESCE(c.project_name, vor.project_name) AS project,
|
||||
COALESCE(vi_operation_city.NAME, NULLIF(vi.operation_city, '')) AS inventory_location,
|
||||
CASE vm.brand
|
||||
WHEN 'hyundai' THEN CASE WHEN vm.model LIKE '%帕力安%' OR vm.model LIKE '%冷链%' OR vm.model LIKE '%双飞翼%' THEN '帕力安牌' ELSE '现代' END
|
||||
WHEN 'yuejin' THEN '跃进'
|
||||
WHEN 'feichi' THEN '飞驰'
|
||||
WHEN 'sulong' THEN '苏龙'
|
||||
WHEN 'higer' THEN '海格'
|
||||
WHEN 'dongfeng' THEN '东风'
|
||||
WHEN 'yutong' THEN '宇通'
|
||||
WHEN 'chufeng' THEN '楚风'
|
||||
WHEN 'tonghua' THEN '通华'
|
||||
WHEN 'maxus' THEN '大通'
|
||||
WHEN 'mingwei' THEN '明威'
|
||||
WHEN 'wanfeng' THEN '万风'
|
||||
WHEN 'shujie' THEN '舒捷'
|
||||
WHEN 'denza' THEN '腾势'
|
||||
WHEN 'hongyan' THEN '红岩'
|
||||
WHEN 'yuanchang brand' THEN '远程牌'
|
||||
WHEN 'others' THEN '其他'
|
||||
ELSE vm.brand
|
||||
END AS brand_label
|
||||
FROM vehicle_info vi
|
||||
LEFT JOIN vehicle_status vs
|
||||
ON vs.vehicle_id = vi.id
|
||||
AND vs.del_flag = 0
|
||||
LEFT JOIN vehicle_model vm
|
||||
ON vm.id = vi.vehicle_model_id
|
||||
AND vm.del_flag = '0'
|
||||
LEFT JOIN common_district vi_operation_city
|
||||
ON vi_operation_city.CODE = vi.operation_city COLLATE utf8mb4_unicode_ci
|
||||
AND vi_operation_city.STATUS = 'VALID'
|
||||
LEFT JOIN vehicle_lease_order_record vor
|
||||
ON vor.vehicle_id = vi.id
|
||||
AND vor.del_flag = '0'
|
||||
AND vor.id = (
|
||||
SELECT MAX(vor2.id)
|
||||
FROM vehicle_lease_order_record vor2
|
||||
WHERE vor2.vehicle_id = vi.id
|
||||
AND vor2.del_flag = '0'
|
||||
)
|
||||
LEFT JOIN vehicle_lease_contract_info c
|
||||
ON c.order_id = vor.contract_id
|
||||
AND c.del_flag = '0'
|
||||
LEFT JOIN customer_info ci
|
||||
ON ci.id = vi.customer_id
|
||||
AND ci.del_flag = '0'
|
||||
WHERE vi.del_flag = '0'
|
||||
AND COALESCE(vs.operation_status, '') <> '5'`;
|
||||
|
||||
|
||||
/** 按车牌批量查询车辆关联信息。 */
|
||||
export async function loadVehicleInfoByPlates(db: Database, args: { plates: string[] }): Promise<RowDataPacket[]> {
|
||||
const { plates } = args;
|
||||
const [rows] = await db.query<RowDataPacket[]>(`${VEHICLE_INFO_SQL} AND vi.plate_number IN (${plates.map(() => '?').join(',')})`, plates);
|
||||
return rows;
|
||||
}
|
||||
|
||||
|
||||
/** 考核目标下的车牌。 */
|
||||
export async function loadTargetVehiclePlates(db: Database, args: { targetId: string }): Promise<RowDataPacket[]> {
|
||||
const { targetId } = args;
|
||||
const [rows] = await db.query<RowDataPacket[]>(`SELECT plate_number FROM lingniu_prod.tab_mileage_assessment_vehicle WHERE target_id = ? AND is_deleted = 0`, [targetId]);
|
||||
return rows;
|
||||
}
|
||||
|
||||
|
||||
/** 监控缓存的考核目标 × 车牌(全量)。 */
|
||||
export async function loadMonitoringTargetRows(db: Database): Promise<RowDataPacket[]> {
|
||||
const [rows] = await db.query<RowDataPacket[]>(`SELECT t.id, t.target_name, v.plate_number
|
||||
FROM lingniu_prod.tab_mileage_assessment_target t
|
||||
JOIN lingniu_prod.tab_mileage_assessment_vehicle v ON v.target_id = t.id AND v.is_deleted = 0
|
||||
WHERE t.is_deleted = 0`, []);
|
||||
return rows;
|
||||
}
|
||||
|
||||
|
||||
/** 考核目标列表。 */
|
||||
export async function loadAssessmentTargetRows(db: Database): Promise<RowDataPacket[]> {
|
||||
const [rows] = await db.query<RowDataPacket[]>(`SELECT * FROM lingniu_prod.tab_mileage_assessment_target WHERE is_deleted = 0 ORDER BY id`, []);
|
||||
return rows;
|
||||
}
|
||||
|
||||
|
||||
/** 各目标的车辆统计。 */
|
||||
export async function loadAssessmentVehicleStats(db: Database): Promise<RowDataPacket[]> {
|
||||
const [rows] = await db.query<RowDataPacket[]>(`SELECT
|
||||
target_id, COUNT(*) as total,
|
||||
SUM(today_mileage) as today_total,
|
||||
SUM(current_mileage) as cumulative_total,
|
||||
AVG(current_year_completion_rate) as avg_completion,
|
||||
SUM(CASE WHEN is_qualified = 1 THEN 1 ELSE 0 END) as qualified_count,
|
||||
SUM(CASE WHEN current_year_is_qualified = 1 THEN 1 ELSE 0 END) as year_qualified_count,
|
||||
SUM(CASE WHEN current_year_completion_rate >= 0.5 THEN 1 ELSE 0 END) as half_qualified_count,
|
||||
SUM(current_year_mileage_task) as current_year_target,
|
||||
SUM(current_year_mileage) as current_year_completed,
|
||||
MAX(current_year_assessment_end_date) as year_end_date
|
||||
FROM lingniu_prod.tab_mileage_assessment_vehicle WHERE is_deleted = 0
|
||||
GROUP BY target_id`, []);
|
||||
return rows;
|
||||
}
|
||||
|
||||
|
||||
/** 首年目标统计。 */
|
||||
export async function loadAssessmentFirstYearStats(db: Database): Promise<RowDataPacket[]> {
|
||||
const [rows] = await db.query<RowDataPacket[]>(`SELECT
|
||||
v.target_id,
|
||||
COUNT(*) as first_year_total,
|
||||
SUM(t.annual_mileage_per_vehicle) as first_year_target,
|
||||
SUM(LEAST(v.current_mileage, t.annual_mileage_per_vehicle)) as first_year_completed,
|
||||
SUM(GREATEST(t.annual_mileage_per_vehicle - v.current_mileage, 0)) as first_year_remaining,
|
||||
SUM(LEAST(v.current_mileage, t.annual_mileage_per_vehicle)) / NULLIF(SUM(t.annual_mileage_per_vehicle), 0) as first_year_completion_rate,
|
||||
SUM(CASE WHEN v.current_mileage >= t.annual_mileage_per_vehicle THEN 1 ELSE 0 END) as first_year_qualified_count,
|
||||
SUM(CASE WHEN v.current_mileage >= t.annual_mileage_per_vehicle * 0.5 THEN 1 ELSE 0 END) as first_year_half_qualified_count,
|
||||
DATE_FORMAT(MIN(v.assessment_start_date), '%Y-%m-%d') as first_year_start_date,
|
||||
DATE_FORMAT(MAX(DATE_SUB(DATE_ADD(v.assessment_start_date, INTERVAL 1 YEAR), INTERVAL 1 DAY)), '%Y-%m-%d') as first_year_end_date
|
||||
FROM lingniu_prod.tab_mileage_assessment_vehicle v
|
||||
JOIN lingniu_prod.tab_mileage_assessment_target t ON t.id = v.target_id AND t.is_deleted = 0
|
||||
WHERE v.is_deleted = 0
|
||||
GROUP BY v.target_id`, []);
|
||||
return rows;
|
||||
}
|
||||
|
||||
|
||||
/** 年度考核统计。 */
|
||||
export async function loadAssessmentYearlyStats(db: Database): Promise<RowDataPacket[]> {
|
||||
const [rows] = await db.query<RowDataPacket[]>(`SELECT
|
||||
v.target_id,
|
||||
y.year_number,
|
||||
COUNT(*) as vehicle_count,
|
||||
SUM(t.annual_mileage_per_vehicle * y.year_number) as target_mileage,
|
||||
SUM(LEAST(v.current_mileage, t.annual_mileage_per_vehicle * y.year_number)) as completed_mileage,
|
||||
SUM(GREATEST(t.annual_mileage_per_vehicle * y.year_number - v.current_mileage, 0)) as remaining_mileage,
|
||||
SUM(LEAST(v.current_mileage, t.annual_mileage_per_vehicle * y.year_number))
|
||||
/ NULLIF(SUM(t.annual_mileage_per_vehicle * y.year_number), 0) as completion_rate,
|
||||
SUM(CASE WHEN v.current_mileage >= t.annual_mileage_per_vehicle * y.year_number THEN 1 ELSE 0 END) as qualified_count,
|
||||
SUM(CASE WHEN v.current_mileage >= t.annual_mileage_per_vehicle * y.year_number * 0.5 THEN 1 ELSE 0 END) as half_qualified_count,
|
||||
DATE_FORMAT(MIN(DATE_ADD(v.assessment_start_date, INTERVAL y.year_number - 1 YEAR)), '%Y-%m-%d') as start_date,
|
||||
DATE_FORMAT(MAX(DATE_SUB(DATE_ADD(v.assessment_start_date, INTERVAL y.year_number YEAR), INTERVAL 1 DAY)), '%Y-%m-%d') as end_date
|
||||
FROM lingniu_prod.tab_mileage_assessment_vehicle v
|
||||
JOIN lingniu_prod.tab_mileage_assessment_target t ON t.id = v.target_id AND t.is_deleted = 0
|
||||
JOIN (
|
||||
SELECT 1 as year_number UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5
|
||||
) y ON y.year_number <= LEAST(t.assessment_years, v.current_year_number)
|
||||
WHERE v.is_deleted = 0
|
||||
GROUP BY v.target_id, y.year_number
|
||||
ORDER BY v.target_id, y.year_number`, []);
|
||||
return rows;
|
||||
}
|
||||
|
||||
|
||||
/** 年度分期统计。 */
|
||||
export async function loadAssessmentYearlyPeriodStats(db: Database): Promise<RowDataPacket[]> {
|
||||
const [rows] = await db.query<RowDataPacket[]>(`SELECT
|
||||
v.target_id,
|
||||
y.year_number,
|
||||
DATE_FORMAT(DATE_ADD(v.assessment_start_date, INTERVAL y.year_number - 1 YEAR), '%Y-%m-%d') as start_date,
|
||||
DATE_FORMAT(DATE_SUB(DATE_ADD(v.assessment_start_date, INTERVAL y.year_number YEAR), INTERVAL 1 DAY), '%Y-%m-%d') as end_date,
|
||||
COUNT(*) as cnt
|
||||
FROM lingniu_prod.tab_mileage_assessment_vehicle v
|
||||
JOIN lingniu_prod.tab_mileage_assessment_target t ON t.id = v.target_id AND t.is_deleted = 0
|
||||
JOIN (
|
||||
SELECT 1 as year_number UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5
|
||||
) y ON y.year_number <= LEAST(t.assessment_years, v.current_year_number)
|
||||
WHERE v.is_deleted = 0
|
||||
GROUP BY v.target_id, y.year_number, v.assessment_start_date
|
||||
ORDER BY v.target_id, y.year_number, v.assessment_start_date`, []);
|
||||
return rows;
|
||||
}
|
||||
|
||||
|
||||
/** 分期统计。 */
|
||||
export async function loadAssessmentPeriodStats(db: Database): Promise<RowDataPacket[]> {
|
||||
const [rows] = await db.query<RowDataPacket[]>(`SELECT target_id,
|
||||
DATE_FORMAT(assessment_start_date, '%Y-%m-%d') as start_date,
|
||||
DATE_FORMAT(assessment_end_date, '%Y-%m-%d') as end_date,
|
||||
COUNT(*) as cnt
|
||||
FROM lingniu_prod.tab_mileage_assessment_vehicle WHERE is_deleted = 0
|
||||
GROUP BY target_id, assessment_start_date, assessment_end_date
|
||||
ORDER BY target_id, assessment_start_date`, []);
|
||||
return rows;
|
||||
}
|
||||
|
||||
|
||||
/** 考核车辆明细(全量)。 */
|
||||
export async function loadTargetVehicleRows(db: Database): Promise<RowDataPacket[]> {
|
||||
const [rows] = await db.query<RowDataPacket[]>(`SELECT target_id, plate_number FROM lingniu_prod.tab_mileage_assessment_vehicle WHERE is_deleted = 0`, []);
|
||||
return rows;
|
||||
}
|
||||
|
||||
|
||||
/** 单目标年度考核车辆。 */
|
||||
export async function loadYearlyAssessments(db: Database, args: { targetId: string }): Promise<RowDataPacket[]> {
|
||||
const { targetId } = args;
|
||||
const [rows] = await db.query<RowDataPacket[]>(`SELECT plate_number, today_mileage, vehicle_total_mileage,
|
||||
completion_rate, is_qualified, current_year_is_qualified,
|
||||
daily_required_mileage
|
||||
FROM lingniu_prod.tab_mileage_assessment_vehicle
|
||||
WHERE target_id = ? AND is_deleted = 0
|
||||
ORDER BY today_mileage DESC`, [targetId]);
|
||||
return rows;
|
||||
}
|
||||
|
||||
|
||||
/** 读取日报归档负载。 */
|
||||
export async function loadDailyReportPayload(db: Database, args: { reportDate: string }): Promise<RowDataPacket[]> {
|
||||
const { reportDate } = args;
|
||||
const [rows] = await db.query<RowDataPacket[]>(`SELECT report_payload FROM ${TABLE} WHERE report_date = ? LIMIT 1`, [reportDate]);
|
||||
return rows;
|
||||
}
|
||||
|
||||
|
||||
/** 日报是否已归档。 */
|
||||
export async function dailyReportSnapshotExists(db: Database, args: { reportDate: string }): Promise<RowDataPacket[]> {
|
||||
const { reportDate } = args;
|
||||
const [rows] = await db.query<RowDataPacket[]>(`SELECT 1 AS found FROM ${TABLE} WHERE report_date = ? LIMIT 1`, [reportDate]);
|
||||
return rows;
|
||||
}
|
||||
|
||||
|
||||
/** 写入日报归档(REPLACE 或 INSERT IGNORE)。 */
|
||||
export async function insertDailyReportSnapshot(db: Database, args: { replace: boolean; values: unknown[] }): Promise<number> {
|
||||
const { replace, values } = args;
|
||||
const [rows] = await db.query<any>(`${replace ? "REPLACE" : "INSERT IGNORE"} INTO ${TABLE}
|
||||
(report_date, status, source, vehicle_count, operating_count, inventory_count,
|
||||
daily_mileage, day_over_day_rate, report_payload, generated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, CAST(? AS JSON), ?)`, values);
|
||||
return rows;
|
||||
}
|
||||
|
||||
|
||||
/** 更新日报负载。 */
|
||||
export async function updateDailyReportPayload(db: Database, args: { report: unknown; reportDate: string }): Promise<number> {
|
||||
const { report, reportDate } = args;
|
||||
const [rows] = await db.query<any>(`UPDATE ${TABLE}
|
||||
SET report_payload = CAST(? AS JSON)
|
||||
WHERE report_date = ?`, [JSON.stringify(report), reportDate]);
|
||||
return rows;
|
||||
}
|
||||
|
||||
|
||||
/** 日报归档历史。 */
|
||||
export async function listDailyReportHistoryRows(db: Database, args: { safeLimit: number }): Promise<RowDataPacket[]> {
|
||||
const { safeLimit } = args;
|
||||
const [rows] = await db.query<RowDataPacket[]>(`SELECT DATE_FORMAT(report_date, '%Y-%m-%d') AS report_date,
|
||||
status, source, vehicle_count, operating_count, inventory_count,
|
||||
daily_mileage, day_over_day_rate,
|
||||
DATE_FORMAT(generated_at, '%Y-%m-%dT%H:%i:%s+08:00') AS generated_at
|
||||
FROM ${TABLE}
|
||||
ORDER BY report_date DESC
|
||||
LIMIT ?`, [safeLimit]);
|
||||
return rows;
|
||||
}
|
||||
|
||||
|
||||
/** 日报所需的考核车辆行。 */
|
||||
export async function loadReportAssessmentRows(db: Database): Promise<RowDataPacket[]> {
|
||||
const [rows] = await db.query<RowDataPacket[]>(`SELECT
|
||||
t.id AS target_id,
|
||||
t.target_name,
|
||||
t.annual_mileage_per_vehicle,
|
||||
v.id AS assessment_vehicle_id,
|
||||
v.plate_number,
|
||||
DATE_FORMAT(v.assessment_start_date, '%Y-%m-%d') AS assessment_start_date,
|
||||
DATE_FORMAT(v.assessment_end_date, '%Y-%m-%d') AS assessment_end_date,
|
||||
v.total_mileage_task,
|
||||
v.current_mileage,
|
||||
v.completion_rate,
|
||||
v.current_year_mileage_task,
|
||||
v.current_year_mileage,
|
||||
v.current_year_completion_rate,
|
||||
v.current_year_is_qualified,
|
||||
v.daily_required_mileage,
|
||||
DATE_FORMAT(v.current_year_assessment_end_date, '%Y-%m-%d') AS current_year_assessment_end_date
|
||||
FROM lingniu_prod.tab_mileage_assessment_target t
|
||||
JOIN lingniu_prod.tab_mileage_assessment_vehicle v
|
||||
ON v.target_id = t.id AND v.is_deleted = 0
|
||||
WHERE t.is_deleted = 0
|
||||
ORDER BY t.id, v.plate_number`, []);
|
||||
return rows;
|
||||
}
|
||||
|
||||
|
||||
/** 全量车辆关联信息。 */
|
||||
export async function loadVehicleInfoMapRows(db: Database): Promise<RowDataPacket[]> {
|
||||
const [rows] = await db.query<RowDataPacket[]>(VEHICLE_INFO_SQL);
|
||||
return rows;
|
||||
}
|
||||
@@ -1,3 +1,13 @@
|
||||
import {
|
||||
loadAssessmentFirstYearStats,
|
||||
loadAssessmentPeriodStats,
|
||||
loadAssessmentTargetRows,
|
||||
loadAssessmentVehicleStats,
|
||||
loadAssessmentYearlyPeriodStats,
|
||||
loadAssessmentYearlyStats,
|
||||
loadTargetVehicleRows,
|
||||
loadYearlyAssessments,
|
||||
} from './repository.js';
|
||||
import { Hono } from 'hono';
|
||||
import pool from '../../db/mysql.js';
|
||||
import { getCache } from './cache.js';
|
||||
@@ -9,73 +19,19 @@ const app = new Hono();
|
||||
|
||||
app.get('/', async (c) => {
|
||||
try {
|
||||
const [targets] = await pool.execute(
|
||||
'SELECT * FROM lingniu_prod.tab_mileage_assessment_target WHERE is_deleted = 0 ORDER BY id'
|
||||
) as [any[], unknown];
|
||||
const targets = await loadAssessmentTargetRows(pool);
|
||||
|
||||
const [vehicleStats] = await pool.execute(`
|
||||
SELECT
|
||||
target_id, COUNT(*) as total,
|
||||
SUM(today_mileage) as today_total,
|
||||
SUM(current_mileage) as cumulative_total,
|
||||
AVG(current_year_completion_rate) as avg_completion,
|
||||
SUM(CASE WHEN is_qualified = 1 THEN 1 ELSE 0 END) as qualified_count,
|
||||
SUM(CASE WHEN current_year_is_qualified = 1 THEN 1 ELSE 0 END) as year_qualified_count,
|
||||
SUM(CASE WHEN current_year_completion_rate >= 0.5 THEN 1 ELSE 0 END) as half_qualified_count,
|
||||
SUM(current_year_mileage_task) as current_year_target,
|
||||
SUM(current_year_mileage) as current_year_completed,
|
||||
MAX(current_year_assessment_end_date) as year_end_date
|
||||
FROM lingniu_prod.tab_mileage_assessment_vehicle WHERE is_deleted = 0
|
||||
GROUP BY target_id
|
||||
`) as [any[], unknown];
|
||||
const vehicleStats = await loadAssessmentVehicleStats(pool);
|
||||
|
||||
const statsMap = new Map<number, any>();
|
||||
for (const s of vehicleStats) statsMap.set(s.target_id, s);
|
||||
|
||||
const [firstYearRows] = await pool.execute(`
|
||||
SELECT
|
||||
v.target_id,
|
||||
COUNT(*) as first_year_total,
|
||||
SUM(t.annual_mileage_per_vehicle) as first_year_target,
|
||||
SUM(LEAST(v.current_mileage, t.annual_mileage_per_vehicle)) as first_year_completed,
|
||||
SUM(GREATEST(t.annual_mileage_per_vehicle - v.current_mileage, 0)) as first_year_remaining,
|
||||
SUM(LEAST(v.current_mileage, t.annual_mileage_per_vehicle)) / NULLIF(SUM(t.annual_mileage_per_vehicle), 0) as first_year_completion_rate,
|
||||
SUM(CASE WHEN v.current_mileage >= t.annual_mileage_per_vehicle THEN 1 ELSE 0 END) as first_year_qualified_count,
|
||||
SUM(CASE WHEN v.current_mileage >= t.annual_mileage_per_vehicle * 0.5 THEN 1 ELSE 0 END) as first_year_half_qualified_count,
|
||||
DATE_FORMAT(MIN(v.assessment_start_date), '%Y-%m-%d') as first_year_start_date,
|
||||
DATE_FORMAT(MAX(DATE_SUB(DATE_ADD(v.assessment_start_date, INTERVAL 1 YEAR), INTERVAL 1 DAY)), '%Y-%m-%d') as first_year_end_date
|
||||
FROM lingniu_prod.tab_mileage_assessment_vehicle v
|
||||
JOIN lingniu_prod.tab_mileage_assessment_target t ON t.id = v.target_id AND t.is_deleted = 0
|
||||
WHERE v.is_deleted = 0
|
||||
GROUP BY v.target_id
|
||||
`) as [any[], unknown];
|
||||
const firstYearRows = await loadAssessmentFirstYearStats(pool);
|
||||
|
||||
const firstYearMap = new Map<number, any>();
|
||||
for (const s of firstYearRows) firstYearMap.set(s.target_id, s);
|
||||
|
||||
const [yearlyRows] = await pool.execute(`
|
||||
SELECT
|
||||
v.target_id,
|
||||
y.year_number,
|
||||
COUNT(*) as vehicle_count,
|
||||
SUM(t.annual_mileage_per_vehicle * y.year_number) as target_mileage,
|
||||
SUM(LEAST(v.current_mileage, t.annual_mileage_per_vehicle * y.year_number)) as completed_mileage,
|
||||
SUM(GREATEST(t.annual_mileage_per_vehicle * y.year_number - v.current_mileage, 0)) as remaining_mileage,
|
||||
SUM(LEAST(v.current_mileage, t.annual_mileage_per_vehicle * y.year_number))
|
||||
/ NULLIF(SUM(t.annual_mileage_per_vehicle * y.year_number), 0) as completion_rate,
|
||||
SUM(CASE WHEN v.current_mileage >= t.annual_mileage_per_vehicle * y.year_number THEN 1 ELSE 0 END) as qualified_count,
|
||||
SUM(CASE WHEN v.current_mileage >= t.annual_mileage_per_vehicle * y.year_number * 0.5 THEN 1 ELSE 0 END) as half_qualified_count,
|
||||
DATE_FORMAT(MIN(DATE_ADD(v.assessment_start_date, INTERVAL y.year_number - 1 YEAR)), '%Y-%m-%d') as start_date,
|
||||
DATE_FORMAT(MAX(DATE_SUB(DATE_ADD(v.assessment_start_date, INTERVAL y.year_number YEAR), INTERVAL 1 DAY)), '%Y-%m-%d') as end_date
|
||||
FROM lingniu_prod.tab_mileage_assessment_vehicle v
|
||||
JOIN lingniu_prod.tab_mileage_assessment_target t ON t.id = v.target_id AND t.is_deleted = 0
|
||||
JOIN (
|
||||
SELECT 1 as year_number UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5
|
||||
) y ON y.year_number <= LEAST(t.assessment_years, v.current_year_number)
|
||||
WHERE v.is_deleted = 0
|
||||
GROUP BY v.target_id, y.year_number
|
||||
ORDER BY v.target_id, y.year_number
|
||||
`) as [any[], unknown];
|
||||
const yearlyRows = await loadAssessmentYearlyStats(pool);
|
||||
|
||||
const yearlyMap = new Map<number, any[]>();
|
||||
for (const row of yearlyRows) {
|
||||
@@ -84,22 +40,7 @@ app.get('/', async (c) => {
|
||||
yearlyMap.set(row.target_id, list);
|
||||
}
|
||||
|
||||
const [yearlyPeriodRows] = await pool.execute(`
|
||||
SELECT
|
||||
v.target_id,
|
||||
y.year_number,
|
||||
DATE_FORMAT(DATE_ADD(v.assessment_start_date, INTERVAL y.year_number - 1 YEAR), '%Y-%m-%d') as start_date,
|
||||
DATE_FORMAT(DATE_SUB(DATE_ADD(v.assessment_start_date, INTERVAL y.year_number YEAR), INTERVAL 1 DAY), '%Y-%m-%d') as end_date,
|
||||
COUNT(*) as cnt
|
||||
FROM lingniu_prod.tab_mileage_assessment_vehicle v
|
||||
JOIN lingniu_prod.tab_mileage_assessment_target t ON t.id = v.target_id AND t.is_deleted = 0
|
||||
JOIN (
|
||||
SELECT 1 as year_number UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5
|
||||
) y ON y.year_number <= LEAST(t.assessment_years, v.current_year_number)
|
||||
WHERE v.is_deleted = 0
|
||||
GROUP BY v.target_id, y.year_number, v.assessment_start_date
|
||||
ORDER BY v.target_id, y.year_number, v.assessment_start_date
|
||||
`) as [any[], unknown];
|
||||
const yearlyPeriodRows = await loadAssessmentYearlyPeriodStats(pool);
|
||||
|
||||
const yearlyPeriodsMap = new Map<string, string[]>();
|
||||
for (const row of yearlyPeriodRows) {
|
||||
@@ -109,15 +50,7 @@ app.get('/', async (c) => {
|
||||
yearlyPeriodsMap.set(key, list);
|
||||
}
|
||||
|
||||
const [periodRows] = await pool.execute(`
|
||||
SELECT target_id,
|
||||
DATE_FORMAT(assessment_start_date, '%Y-%m-%d') as start_date,
|
||||
DATE_FORMAT(assessment_end_date, '%Y-%m-%d') as end_date,
|
||||
COUNT(*) as cnt
|
||||
FROM lingniu_prod.tab_mileage_assessment_vehicle WHERE is_deleted = 0
|
||||
GROUP BY target_id, assessment_start_date, assessment_end_date
|
||||
ORDER BY target_id, assessment_start_date
|
||||
`) as [any[], unknown];
|
||||
const periodRows = await loadAssessmentPeriodStats(pool);
|
||||
|
||||
const periodsMap = new Map<number, string[]>();
|
||||
for (const p of periodRows) {
|
||||
@@ -134,9 +67,7 @@ app.get('/', async (c) => {
|
||||
}
|
||||
}
|
||||
|
||||
const [targetVehicleRows] = await pool.execute(
|
||||
'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 targetVehicleRows = await loadTargetVehicleRows(pool);
|
||||
|
||||
const targetIdPlatesMap = new Map<number, string[]>();
|
||||
for (const r of targetVehicleRows) {
|
||||
@@ -241,15 +172,7 @@ app.get('/:id/vehicles', async (c) => {
|
||||
const date = c.req.query('date') || '';
|
||||
|
||||
try {
|
||||
const [rows] = await pool.execute(
|
||||
`SELECT plate_number, today_mileage, vehicle_total_mileage,
|
||||
completion_rate, is_qualified, current_year_is_qualified,
|
||||
daily_required_mileage
|
||||
FROM lingniu_prod.tab_mileage_assessment_vehicle
|
||||
WHERE target_id = ? AND is_deleted = 0
|
||||
ORDER BY today_mileage DESC`,
|
||||
[targetId]
|
||||
) as [any[], unknown];
|
||||
const rows = await loadYearlyAssessments(pool, { targetId });
|
||||
|
||||
const plates: string[] = rows.map((r: any) => r.plate_number);
|
||||
const infoMap = await fetchVehicleInfoByPlates(plates);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { loadTargetVehiclePlates } from './repository.js';
|
||||
import { Hono } from 'hono';
|
||||
import pool from '../../db/mysql.js';
|
||||
import { fetchOneOsMileageDates } from './oneos-api.js';
|
||||
@@ -11,10 +12,7 @@ app.get('/', async (c) => {
|
||||
try {
|
||||
let plates: string[] = [];
|
||||
if (targetId) {
|
||||
const [vehicleRows] = await pool.execute(
|
||||
'SELECT plate_number FROM lingniu_prod.tab_mileage_assessment_vehicle WHERE target_id = ? AND is_deleted = 0',
|
||||
[targetId]
|
||||
) as [{ plate_number: string }[], unknown];
|
||||
const vehicleRows = await loadTargetVehiclePlates(pool, { targetId });
|
||||
plates = vehicleRows.map(r => r.plate_number);
|
||||
if (plates.length === 0) return c.json([]);
|
||||
}
|
||||
|
||||
@@ -1,76 +1,10 @@
|
||||
import { loadVehicleInfoByPlates, loadVehicleInfoMapRows } from './repository.js';
|
||||
import pool from '../../db/mysql.js';
|
||||
import type { VehicleInfoRow } from './types.js';
|
||||
|
||||
/** 车辆关联信息 SQL(客户名、部门、经理、租赁状态、主体、项目、品牌) */
|
||||
export const VEHICLE_INFO_SQL = `SELECT
|
||||
vi.plate_number AS plate,
|
||||
vi.vin AS vin,
|
||||
COALESCE(c.customer_name, vor.customer_name, ci.customer_name) AS customer,
|
||||
COALESCE(c.business_department_name, vor.business_dept) AS department,
|
||||
COALESCE(c.business_manager_name, vor.business_manager) AS manager,
|
||||
CAST(COALESCE(c.business_manager_id, vi.business_id) AS CHAR) AS manager_id,
|
||||
CASE vs.operation_status
|
||||
WHEN '1' THEN '租赁'
|
||||
WHEN '2' THEN '自营'
|
||||
WHEN '3' THEN '可运营'
|
||||
WHEN '4' THEN '待运营'
|
||||
WHEN '5' THEN '退出运营'
|
||||
ELSE vs.operation_status
|
||||
END AS rent_status,
|
||||
NULLIF(vi.registered_ownership, '') AS entity,
|
||||
COALESCE(c.project_name, vor.project_name) AS project,
|
||||
COALESCE(vi_operation_city.NAME, NULLIF(vi.operation_city, '')) AS inventory_location,
|
||||
CASE vm.brand
|
||||
WHEN 'hyundai' THEN CASE WHEN vm.model LIKE '%帕力安%' OR vm.model LIKE '%冷链%' OR vm.model LIKE '%双飞翼%' THEN '帕力安牌' ELSE '现代' END
|
||||
WHEN 'yuejin' THEN '跃进'
|
||||
WHEN 'feichi' THEN '飞驰'
|
||||
WHEN 'sulong' THEN '苏龙'
|
||||
WHEN 'higer' THEN '海格'
|
||||
WHEN 'dongfeng' THEN '东风'
|
||||
WHEN 'yutong' THEN '宇通'
|
||||
WHEN 'chufeng' THEN '楚风'
|
||||
WHEN 'tonghua' THEN '通华'
|
||||
WHEN 'maxus' THEN '大通'
|
||||
WHEN 'mingwei' THEN '明威'
|
||||
WHEN 'wanfeng' THEN '万风'
|
||||
WHEN 'shujie' THEN '舒捷'
|
||||
WHEN 'denza' THEN '腾势'
|
||||
WHEN 'hongyan' THEN '红岩'
|
||||
WHEN 'yuanchang brand' THEN '远程牌'
|
||||
WHEN 'others' THEN '其他'
|
||||
ELSE vm.brand
|
||||
END AS brand_label
|
||||
FROM vehicle_info vi
|
||||
LEFT JOIN vehicle_status vs
|
||||
ON vs.vehicle_id = vi.id
|
||||
AND vs.del_flag = 0
|
||||
LEFT JOIN vehicle_model vm
|
||||
ON vm.id = vi.vehicle_model_id
|
||||
AND vm.del_flag = '0'
|
||||
LEFT JOIN common_district vi_operation_city
|
||||
ON vi_operation_city.CODE = vi.operation_city COLLATE utf8mb4_unicode_ci
|
||||
AND vi_operation_city.STATUS = 'VALID'
|
||||
LEFT JOIN vehicle_lease_order_record vor
|
||||
ON vor.vehicle_id = vi.id
|
||||
AND vor.del_flag = '0'
|
||||
AND vor.id = (
|
||||
SELECT MAX(vor2.id)
|
||||
FROM vehicle_lease_order_record vor2
|
||||
WHERE vor2.vehicle_id = vi.id
|
||||
AND vor2.del_flag = '0'
|
||||
)
|
||||
LEFT JOIN vehicle_lease_contract_info c
|
||||
ON c.order_id = vor.contract_id
|
||||
AND c.del_flag = '0'
|
||||
LEFT JOIN customer_info ci
|
||||
ON ci.id = vi.customer_id
|
||||
AND ci.del_flag = '0'
|
||||
WHERE vi.del_flag = '0'
|
||||
AND COALESCE(vs.operation_status, '') <> '5'`;
|
||||
|
||||
/** 查询所有车辆关联信息,返回 plate→info 的 Map */
|
||||
export async function fetchVehicleInfoMap(): Promise<Map<string, VehicleInfoRow>> {
|
||||
const [rows] = await pool.execute(VEHICLE_INFO_SQL) as [VehicleInfoRow[], unknown];
|
||||
const rows = await loadVehicleInfoMapRows(pool) as unknown as VehicleInfoRow[];
|
||||
const map = new Map<string, VehicleInfoRow>();
|
||||
for (const row of rows) {
|
||||
map.set(row.plate, row);
|
||||
@@ -81,10 +15,7 @@ export async function fetchVehicleInfoMap(): Promise<Map<string, VehicleInfoRow>
|
||||
/** 查询指定车牌的关联信息 */
|
||||
export async function fetchVehicleInfoByPlates(plates: string[]): Promise<Map<string, VehicleInfoRow>> {
|
||||
if (plates.length === 0) return new Map();
|
||||
const [rows] = await pool.execute(
|
||||
`${VEHICLE_INFO_SQL} AND vi.plate_number IN (${plates.map(() => '?').join(',')})`,
|
||||
plates
|
||||
) as [VehicleInfoRow[], unknown];
|
||||
const rows = await loadVehicleInfoByPlates(pool, { plates }) as unknown as VehicleInfoRow[];
|
||||
const map = new Map<string, VehicleInfoRow>();
|
||||
for (const row of rows) {
|
||||
map.set(row.plate, row);
|
||||
|
||||
Reference in New Issue
Block a user