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 有契约测试 |
|
| `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/参数/响应体已做等价性验证 |
|
| `ele/` | `routes.ts` `repository.ts` `model.ts`(+ `model.test.ts` `routes.test.ts`) | ✅ 完整分层;改造前后 SQL/参数/响应体已做等价性验证 |
|
||||||
| `feedback/` | `routes.ts` `repository.ts` `oss.ts`(+ `routes.test.ts`) | ✅ 完整分层;同上 |
|
| `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 |
|
| `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)显式注入 |
|
| `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` 片段与参数顺序已锁定 |
|
| `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(定位点)两个库 |
|
| `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**。
|
由架构测试守护:必须存在 `repository.ts`,且该域**其他任何非测试文件都不得含 SQL**。
|
||||||
其余域(`energy` / `mileage`)尚未拆出 repository——拆分时**不要改变 SQL 与参数顺序**,
|
其余域(`energy` / `mileage`)尚未拆出 repository——拆分时**不要改变 SQL 与参数顺序**,
|
||||||
请按 `ele/routes.test.ts` 的配方先补契约测试,并用"改造前后同一批请求对比落库 SQL 与响应体"做等价性验证。
|
请按 `ele/routes.test.ts` 的配方先补契约测试,并用"改造前后同一批请求对比落库 SQL 与响应体"做等价性验证。
|
||||||
@@ -139,10 +139,9 @@ cors → read-only → /api/auth(公开) → authMiddleware → 各业务域
|
|||||||
|
|
||||||
诚实记录,避免后来者以为已经做完:
|
诚实记录,避免后来者以为已经做完:
|
||||||
|
|
||||||
- **后端仍有 1 个域没拆出 repository**:`mileage` 的 SQL 仍分散在 `cache.ts` /
|
- **后端八个域都已拆出 repository**,SQL 只允许出现在各域的 `repository.ts`(以及共享片段模块
|
||||||
`daily-report-store.ts` / `targets.ts` / `monitoring.ts` / `trend.ts` / `vehicle-recent.ts` 等文件里。
|
`energy/constants.ts`)中,由架构测试守护。新增查询请沿用同一形状:路由只做校验与组装,
|
||||||
已完成的有 7 个域,可作为模板:路由只做校验与组装,SQL 进 `repository.ts`,
|
SQL 进 `repository.ts`,纯逻辑进 `model.ts`,并用 mock pool 的契约测试锁定 SQL 与参数。
|
||||||
纯逻辑进 `model.ts`,并用 mock pool 的契约测试锁定 SQL 与参数。
|
|
||||||
守卫用的是"语句形状"正则(如 `update <table> set`)而不是裸关键字,避免把
|
守卫用的是"语句形状"正则(如 `update <table> set`)而不是裸关键字,避免把
|
||||||
`UpdateNotification` 或日志里的 "update error" 误判为 SQL;小写 SQL 同样能被抓到。
|
`UpdateNotification` 或日志里的 "update error" 误判为 SQL;小写 SQL 同样能被抓到。
|
||||||
- **运行时建表已集中到 `server/db/schema/`**,并在 `DB_READ_ONLY=1` 时整体跳过(由架构测试守护,
|
- **运行时建表已集中到 `server/db/schema/`**,并在 `DB_READ_ONLY=1` 时整体跳过(由架构测试守护,
|
||||||
|
|||||||
@@ -180,6 +180,7 @@ test("已完整分层的域:repository.ts 存在,且 SQL 只允许出现在
|
|||||||
"server/routes/hydrogen-heatmap",
|
"server/routes/hydrogen-heatmap",
|
||||||
"server/routes/scheduling",
|
"server/routes/scheduling",
|
||||||
"server/routes/energy",
|
"server/routes/energy",
|
||||||
|
"server/routes/mileage",
|
||||||
];
|
];
|
||||||
// 用"语句形状"而不是裸关键字:UpdateNotification 或 "update error" 这类标识符/日志
|
// 用"语句形状"而不是裸关键字:UpdateNotification 或 "update error" 这类标识符/日志
|
||||||
// 不应误判,而小写 SQL 也依然能被抓到。
|
// 不应误判,而小写 SQL 也依然能被抓到。
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { loadMonitoringTargetRows } from './repository.js';
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { dirname, join } from 'node:path';
|
import { dirname, join } from 'node:path';
|
||||||
@@ -51,12 +52,7 @@ export interface RangeMileageResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchTargetRows(): Promise<TargetRow[]> {
|
async function fetchTargetRows(): Promise<TargetRow[]> {
|
||||||
return pool.execute(
|
return loadMonitoringTargetRows(pool).then(([rows]) => rows as TargetRow[]);
|
||||||
`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[]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function refreshMonitoringCache(): Promise<void> {
|
export async function refreshMonitoringCache(): Promise<void> {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { loadReportAssessmentRows } from './repository.js';
|
||||||
import type { RowDataPacket } from 'mysql2';
|
import type { RowDataPacket } from 'mysql2';
|
||||||
import pool from '../../db/mysql.js';
|
import pool from '../../db/mysql.js';
|
||||||
import type {
|
import type {
|
||||||
@@ -123,30 +124,7 @@ function shanghaiIsoNow(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchAssessmentRows(): Promise<AssessmentRow[]> {
|
async function fetchAssessmentRows(): Promise<AssessmentRow[]> {
|
||||||
const [rows] = await pool.query<AssessmentRow[]>(`
|
const rows = await loadReportAssessmentRows(pool) as unknown as 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
|
|
||||||
`);
|
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
import {
|
||||||
|
dailyReportSnapshotExists,
|
||||||
|
insertDailyReportSnapshot,
|
||||||
|
listDailyReportHistoryRows,
|
||||||
|
loadDailyReportPayload,
|
||||||
|
updateDailyReportPayload,
|
||||||
|
} from './repository.js';
|
||||||
import type { ResultSetHeader, RowDataPacket } from 'mysql2';
|
import type { ResultSetHeader, RowDataPacket } from 'mysql2';
|
||||||
import pool from '../../db/mysql.js';
|
import pool from '../../db/mysql.js';
|
||||||
import { ensureDailyReportTable, DAILY_REPORT_TABLE as TABLE } from '../../db/schema/mileage-report.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> {
|
export async function getDailyReportSnapshot(reportDate: string): Promise<DailyMileageReport | null> {
|
||||||
await ensureDailyReportTable();
|
await ensureDailyReportTable();
|
||||||
const [rows] = await pool.query<RowDataPacket[]>(
|
const rows = await loadDailyReportPayload(pool, { reportDate });
|
||||||
`SELECT report_payload FROM ${TABLE} WHERE report_date = ? LIMIT 1`,
|
|
||||||
[reportDate],
|
|
||||||
);
|
|
||||||
return parsePayload(rows[0]?.report_payload);
|
return parsePayload(rows[0]?.report_payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function hasDailyReportSnapshot(reportDate: string): Promise<boolean> {
|
export async function hasDailyReportSnapshot(reportDate: string): Promise<boolean> {
|
||||||
await ensureDailyReportTable();
|
await ensureDailyReportTable();
|
||||||
const [rows] = await pool.query<RowDataPacket[]>(
|
const rows = await dailyReportSnapshotExists(pool, { reportDate });
|
||||||
`SELECT 1 AS found FROM ${TABLE} WHERE report_date = ? LIMIT 1`,
|
|
||||||
[reportDate],
|
|
||||||
);
|
|
||||||
return rows.length > 0;
|
return rows.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,41 +58,20 @@ export async function saveDailyReportSnapshot(
|
|||||||
archived.generatedAt.replace('T', ' ').replace(/Z$/, '').slice(0, 19),
|
archived.generatedAt.replace('T', ' ').replace(/Z$/, '').slice(0, 19),
|
||||||
];
|
];
|
||||||
|
|
||||||
const insert = options.replace ? 'REPLACE' : 'INSERT IGNORE';
|
const affected = await insertDailyReportSnapshot(pool, { replace: Boolean(options.replace), values });
|
||||||
const [result] = await pool.query<ResultSetHeader>(
|
return affected > 0;
|
||||||
`${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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateDailyReportSnapshotPayload(report: DailyMileageReport): Promise<boolean> {
|
export async function updateDailyReportSnapshotPayload(report: DailyMileageReport): Promise<boolean> {
|
||||||
await ensureDailyReportTable();
|
await ensureDailyReportTable();
|
||||||
const [result] = await pool.query<ResultSetHeader>(
|
const affected = await updateDailyReportPayload(pool, { report: JSON.stringify(report), reportDate: report.reportDate });
|
||||||
`UPDATE ${TABLE}
|
return affected > 0;
|
||||||
SET report_payload = CAST(? AS JSON)
|
|
||||||
WHERE report_date = ?`,
|
|
||||||
[JSON.stringify(report), report.reportDate],
|
|
||||||
);
|
|
||||||
return result.affectedRows > 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listDailyReportHistory(limit = 90): Promise<MileageReportHistoryResponse> {
|
export async function listDailyReportHistory(limit = 90): Promise<MileageReportHistoryResponse> {
|
||||||
await ensureDailyReportTable();
|
await ensureDailyReportTable();
|
||||||
const safeLimit = Math.min(366, Math.max(1, limit));
|
const safeLimit = Math.min(366, Math.max(1, limit));
|
||||||
const [rows] = await pool.query<RowDataPacket[]>(
|
const rows = await listDailyReportHistoryRows(pool, { safeLimit });
|
||||||
`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 items: MileageReportHistoryItem[] = rows.map(row => ({
|
const items: MileageReportHistoryItem[] = rows.map(row => ({
|
||||||
reportDate: row.report_date,
|
reportDate: row.report_date,
|
||||||
status: row.status,
|
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 { Hono } from 'hono';
|
||||||
import pool from '../../db/mysql.js';
|
import pool from '../../db/mysql.js';
|
||||||
import { getCache } from './cache.js';
|
import { getCache } from './cache.js';
|
||||||
@@ -9,73 +19,19 @@ const app = new Hono();
|
|||||||
|
|
||||||
app.get('/', async (c) => {
|
app.get('/', async (c) => {
|
||||||
try {
|
try {
|
||||||
const [targets] = await pool.execute(
|
const targets = await loadAssessmentTargetRows(pool);
|
||||||
'SELECT * FROM lingniu_prod.tab_mileage_assessment_target WHERE is_deleted = 0 ORDER BY id'
|
|
||||||
) as [any[], unknown];
|
|
||||||
|
|
||||||
const [vehicleStats] = await pool.execute(`
|
const vehicleStats = await loadAssessmentVehicleStats(pool);
|
||||||
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 statsMap = new Map<number, any>();
|
const statsMap = new Map<number, any>();
|
||||||
for (const s of vehicleStats) statsMap.set(s.target_id, s);
|
for (const s of vehicleStats) statsMap.set(s.target_id, s);
|
||||||
|
|
||||||
const [firstYearRows] = await pool.execute(`
|
const firstYearRows = await loadAssessmentFirstYearStats(pool);
|
||||||
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 firstYearMap = new Map<number, any>();
|
const firstYearMap = new Map<number, any>();
|
||||||
for (const s of firstYearRows) firstYearMap.set(s.target_id, s);
|
for (const s of firstYearRows) firstYearMap.set(s.target_id, s);
|
||||||
|
|
||||||
const [yearlyRows] = await pool.execute(`
|
const yearlyRows = await loadAssessmentYearlyStats(pool);
|
||||||
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 yearlyMap = new Map<number, any[]>();
|
const yearlyMap = new Map<number, any[]>();
|
||||||
for (const row of yearlyRows) {
|
for (const row of yearlyRows) {
|
||||||
@@ -84,22 +40,7 @@ app.get('/', async (c) => {
|
|||||||
yearlyMap.set(row.target_id, list);
|
yearlyMap.set(row.target_id, list);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [yearlyPeriodRows] = await pool.execute(`
|
const yearlyPeriodRows = await loadAssessmentYearlyPeriodStats(pool);
|
||||||
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 yearlyPeriodsMap = new Map<string, string[]>();
|
const yearlyPeriodsMap = new Map<string, string[]>();
|
||||||
for (const row of yearlyPeriodRows) {
|
for (const row of yearlyPeriodRows) {
|
||||||
@@ -109,15 +50,7 @@ app.get('/', async (c) => {
|
|||||||
yearlyPeriodsMap.set(key, list);
|
yearlyPeriodsMap.set(key, list);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [periodRows] = await pool.execute(`
|
const periodRows = await loadAssessmentPeriodStats(pool);
|
||||||
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 periodsMap = new Map<number, string[]>();
|
const periodsMap = new Map<number, string[]>();
|
||||||
for (const p of periodRows) {
|
for (const p of periodRows) {
|
||||||
@@ -134,9 +67,7 @@ app.get('/', async (c) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [targetVehicleRows] = await pool.execute(
|
const targetVehicleRows = await loadTargetVehicleRows(pool);
|
||||||
'SELECT target_id, plate_number FROM lingniu_prod.tab_mileage_assessment_vehicle WHERE is_deleted = 0'
|
|
||||||
) as [{ target_id: number; plate_number: string }[], unknown];
|
|
||||||
|
|
||||||
const targetIdPlatesMap = new Map<number, string[]>();
|
const targetIdPlatesMap = new Map<number, string[]>();
|
||||||
for (const r of targetVehicleRows) {
|
for (const r of targetVehicleRows) {
|
||||||
@@ -241,15 +172,7 @@ app.get('/:id/vehicles', async (c) => {
|
|||||||
const date = c.req.query('date') || '';
|
const date = c.req.query('date') || '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [rows] = await pool.execute(
|
const rows = await loadYearlyAssessments(pool, { targetId });
|
||||||
`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 plates: string[] = rows.map((r: any) => r.plate_number);
|
const plates: string[] = rows.map((r: any) => r.plate_number);
|
||||||
const infoMap = await fetchVehicleInfoByPlates(plates);
|
const infoMap = await fetchVehicleInfoByPlates(plates);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { loadTargetVehiclePlates } from './repository.js';
|
||||||
import { Hono } from 'hono';
|
import { Hono } from 'hono';
|
||||||
import pool from '../../db/mysql.js';
|
import pool from '../../db/mysql.js';
|
||||||
import { fetchOneOsMileageDates } from './oneos-api.js';
|
import { fetchOneOsMileageDates } from './oneos-api.js';
|
||||||
@@ -11,10 +12,7 @@ app.get('/', async (c) => {
|
|||||||
try {
|
try {
|
||||||
let plates: string[] = [];
|
let plates: string[] = [];
|
||||||
if (targetId) {
|
if (targetId) {
|
||||||
const [vehicleRows] = await pool.execute(
|
const vehicleRows = await loadTargetVehiclePlates(pool, { targetId });
|
||||||
'SELECT plate_number FROM lingniu_prod.tab_mileage_assessment_vehicle WHERE target_id = ? AND is_deleted = 0',
|
|
||||||
[targetId]
|
|
||||||
) as [{ plate_number: string }[], unknown];
|
|
||||||
plates = vehicleRows.map(r => r.plate_number);
|
plates = vehicleRows.map(r => r.plate_number);
|
||||||
if (plates.length === 0) return c.json([]);
|
if (plates.length === 0) return c.json([]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,76 +1,10 @@
|
|||||||
|
import { loadVehicleInfoByPlates, loadVehicleInfoMapRows } from './repository.js';
|
||||||
import pool from '../../db/mysql.js';
|
import pool from '../../db/mysql.js';
|
||||||
import type { VehicleInfoRow } from './types.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 */
|
/** 查询所有车辆关联信息,返回 plate→info 的 Map */
|
||||||
export async function fetchVehicleInfoMap(): Promise<Map<string, VehicleInfoRow>> {
|
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>();
|
const map = new Map<string, VehicleInfoRow>();
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
map.set(row.plate, row);
|
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>> {
|
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 loadVehicleInfoByPlates(pool, { plates }) as unknown as VehicleInfoRow[];
|
||||||
`${VEHICLE_INFO_SQL} AND vi.plate_number IN (${plates.map(() => '?').join(',')})`,
|
|
||||||
plates
|
|
||||||
) as [VehicleInfoRow[], unknown];
|
|
||||||
const map = new Map<string, VehicleInfoRow>();
|
const map = new Map<string, VehicleInfoRow>();
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
map.set(row.plate, row);
|
map.set(row.plate, row);
|
||||||
|
|||||||
Reference in New Issue
Block a user