改动 - hydrogen-bi-v2.ts 的 14 条查询移入 energy/repository.ts(SQL 与参数表达式逐条对应)。 - 片段组装也随之搬走,因为它们本质是 SQL 构造,留在路由里会让"路由不含 SQL"形同虚设: resolvedWhere / stationMasterRegionSql / buildFilterClauses / buildGroupSelect / buildGroupExpression / buildScopedWhere。 - 承担方映射片段(CUSTOMER/COMPANY/OTHER_BEARING_ORDER)移入 constants.ts 供多处共享。 - 结果形状从 mysql2 的 [rows, fields] 改为直接返回 rows,路由侧 13 处 [0] 索引同步调整。 - 架构守护的"已完整分层"清单加入 energy(现为 7 个域)。 提取方式与一次失误 - 查询文本用"反引号定界 + 括号配对"扫描提取。第一次用纯括号配对扫描时, 因为把 SQL 文本里的括号也算了进去,导致调用点跨度越界、文件被改坏; 已从 git 恢复并改用反引号定界(并对跨度做了"严格递增且不重叠"的断言)后一次通过。 - 中途还有一次变量写错文件(把 repository 内容写进了路由文件), 被随后的正常写入覆盖,未造成实际损坏;已核对两个文件的首行与关键符号确认无误。 验证 - energy/routes.test.ts 的 13 个用例(含氢能 v2 的口径、利润、账单、路由顺序、 SQL 与参数指纹)在提取前后全部通过。 - 全量 lint / test(191) / build 全绿,可达性 0 未引用文件。
772 lines
41 KiB
TypeScript
772 lines
41 KiB
TypeScript
import type { RowDataPacket } from 'mysql2';
|
||
import { dateRangeClause, type CustomerKind } from './query-model.js';
|
||
import {
|
||
COMPANY_BEARING_ORDER,
|
||
CUSTOMER_BEARING_ORDER,
|
||
EXTERNAL_CUSTOMER_MAP_CTE,
|
||
OTHER_BEARING_ORDER,
|
||
HYDROGEN_BASE_WHERE,
|
||
HYDROGEN_BASE_WHERE_B,
|
||
HYDROGEN_FUEL_ONLY_WHERE,
|
||
HYDROGEN_FUEL_ONLY_WHERE_B,
|
||
HYDROGEN_LOCAL,
|
||
HYDROGEN_MIN_DATE,
|
||
HYDROGEN_TABLE,
|
||
HYDROGEN_TENANT_ID,
|
||
MANUAL_RECHARGE_SOURCE,
|
||
NORMALIZED_PLATE,
|
||
NORMALIZED_PLATE_UNICODE,
|
||
} from './constants.js';
|
||
|
||
/**
|
||
* 能源域的数据访问。
|
||
*
|
||
* 电能 / ETC / 单站日报 / 氢能 v2 的 SQL 都集中在这里;路由只做校验、编排与口径计算。
|
||
* SQL 文本与参数顺序由 routes.test.ts 的契约测试锁定(该文件对多处查询做了指纹断言)。
|
||
*/
|
||
|
||
/** 只需要 query 能力的连接(主库或氢能库)。 */
|
||
export interface Database {
|
||
query<T = any>(sql: string, values?: any[]): Promise<[T, ...any[]]>;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// ETC
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/** ETC 台账与账单汇总;空表返回全 0,由路由判定 hasData。 */
|
||
export async function loadEtcOverview(db: Database): Promise<Record<string, unknown>> {
|
||
const [rows] = await db.query<RowDataPacket[]>(
|
||
`SELECT
|
||
(SELECT COUNT(*) FROM etc_toll_record WHERE del_flag = '0') AS tollRecordCount,
|
||
(SELECT COUNT(DISTINCT plate_number) FROM etc_toll_record WHERE del_flag = '0') AS vehicleCount,
|
||
(SELECT ROUND(SUM(total_amount), 2) FROM etc_toll_record WHERE del_flag = '0') AS totalAmount,
|
||
(SELECT DATE_FORMAT(MAX(trans_time), '%Y-%m-%d %H:%i:%s') FROM etc_toll_record WHERE del_flag = '0') AS latestTollTime,
|
||
(SELECT COUNT(*) FROM energy_etc_bill WHERE del_flag = '0') AS billCount,
|
||
(SELECT ROUND(SUM(receivable_amount), 2) FROM energy_etc_bill WHERE del_flag = '0') AS receivableAmount,
|
||
(SELECT ROUND(SUM(paid_amount), 2) FROM energy_etc_bill WHERE del_flag = '0') AS paidAmount`,
|
||
);
|
||
return (rows[0] ?? {}) as Record<string, unknown>;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 电能
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/** 电能总览 KPI(全量 + 本月 + 本日)。 */
|
||
export async function loadElectricKpi(db: Database): Promise<Record<string, unknown>> {
|
||
const [rows] = await db.query<RowDataPacket[]>(
|
||
`SELECT
|
||
SUM(kwh) AS totalKwh,
|
||
SUM(fee) AS totalFee,
|
||
SUM(CASE WHEN DATE_FORMAT(start_time, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m')
|
||
THEN kwh ELSE 0 END) AS monthKwh,
|
||
SUM(CASE WHEN DATE_FORMAT(start_time, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m')
|
||
THEN fee ELSE 0 END) AS monthFee,
|
||
SUM(CASE WHEN DATE(start_time) = CURDATE() THEN kwh ELSE 0 END) AS todayKwh,
|
||
SUM(CASE WHEN DATE(start_time) = CURDATE() THEN fee ELSE 0 END) AS todayFee,
|
||
DATE_FORMAT(MAX(start_time), '%Y-%m-%d %H:%i:%s') AS latestChargeTime
|
||
FROM bi_ele_charge_record`,
|
||
);
|
||
return (rows[0] ?? {}) as Record<string, unknown>;
|
||
}
|
||
|
||
/** 本月逐日电量/费用(柱图)。 */
|
||
export async function loadElectricCurrentMonthTrend(db: Database): Promise<RowDataPacket[]> {
|
||
const [rows] = await db.query<RowDataPacket[]>(
|
||
`SELECT DATE_FORMAT(start_time, '%Y-%m-%d') AS date,
|
||
SUM(kwh) AS kwh,
|
||
SUM(fee) AS fee
|
||
FROM bi_ele_charge_record
|
||
WHERE DATE_FORMAT(start_time, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m')
|
||
GROUP BY date
|
||
ORDER BY date ASC`,
|
||
);
|
||
return rows;
|
||
}
|
||
|
||
/** 本月无数据时的降级:最近一个有数据的自然月。 */
|
||
export async function loadElectricLatestMonthTrend(db: Database): Promise<RowDataPacket[]> {
|
||
const [rows] = await db.query<RowDataPacket[]>(
|
||
`SELECT DATE_FORMAT(start_time, '%Y-%m-%d') AS date,
|
||
SUM(kwh) AS kwh,
|
||
SUM(fee) AS fee
|
||
FROM bi_ele_charge_record
|
||
WHERE DATE_FORMAT(start_time, '%Y-%m') = (
|
||
SELECT DATE_FORMAT(MAX(start_time), '%Y-%m') FROM bi_ele_charge_record
|
||
)
|
||
GROUP BY date
|
||
ORDER BY date ASC`,
|
||
);
|
||
return rows;
|
||
}
|
||
|
||
/** 昨日电量(用于本日环比)。 */
|
||
export async function loadElectricPreviousDayKwh(db: Database): Promise<number> {
|
||
const [rows] = await db.query<RowDataPacket[]>(
|
||
`SELECT SUM(kwh) AS kwh
|
||
FROM bi_ele_charge_record
|
||
WHERE DATE(start_time) = DATE_SUB(CURDATE(), INTERVAL 1 DAY)`,
|
||
);
|
||
return Number(rows[0]?.kwh) || 0;
|
||
}
|
||
|
||
/** 车辆归属筛选片段。internal=我司,external=外部;其余不过滤。 */
|
||
export function electricKindClause(customer: CustomerKind): string {
|
||
if (customer === 'lingniu') return `vehicle_kind = 'internal'`;
|
||
if (customer === 'external') return `vehicle_kind = 'external'`;
|
||
return '1=1';
|
||
}
|
||
|
||
/** 按日电量/费用(区间)。 */
|
||
export async function loadElectricDailyRows(
|
||
db: Database,
|
||
args: { customer: CustomerKind; start: string; end: string },
|
||
): Promise<RowDataPacket[]> {
|
||
const [rows] = await db.query<RowDataPacket[]>(
|
||
`SELECT DATE_FORMAT(start_time, '%Y-%m-%d') AS date,
|
||
SUM(kwh) AS kwh,
|
||
SUM(fee) AS fee
|
||
FROM bi_ele_charge_record
|
||
WHERE ${electricKindClause(args.customer)}
|
||
AND ${dateRangeClause('start_time')}
|
||
GROUP BY date`,
|
||
[args.start, args.end],
|
||
);
|
||
return rows;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 氢能单站日报(/hydrogen/station-board)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
|
||
/** 区间内有有效加氢记录的站点列表。 */
|
||
export async function loadStationList(db: Database, args: { start: string; end: string }): Promise<RowDataPacket[]> {
|
||
const { start, end } = args;
|
||
const [rows] = await db.query<RowDataPacket[]>(`SELECT s.id,
|
||
COALESCE(NULLIF(s.station_short_name, ''), NULLIF(s.station_name, ''), CONCAT('未知站点 #', s.id)) AS name,
|
||
COALESCE(NULLIF(dp.NAME, ''), NULLIF(s.province, ''), '未归属') AS province,
|
||
COALESCE(NULLIF(dc.NAME, ''), NULLIF(s.city, ''), '未归属') AS city,
|
||
ROUND(COALESCE(SUM(b.amount_kg), 0), 2) AS kg,
|
||
ROUND(COALESCE(SUM(b.cost_total), 0), 2) AS fee,
|
||
COUNT(b.id) AS recordCount,
|
||
DATE_FORMAT(MAX(b.${HYDROGEN_LOCAL}), '%Y-%m-%d %H:%i:%s') AS latestLedgerTime
|
||
FROM hydrogen_station s
|
||
LEFT JOIN ${HYDROGEN_TABLE} b
|
||
ON b.station_id = s.id
|
||
AND ${HYDROGEN_BASE_WHERE_B}
|
||
AND b.tenant_id = ?
|
||
AND COALESCE(b.record_source, '') <> ?
|
||
AND ${dateRangeClause(`b.${HYDROGEN_LOCAL}`)}
|
||
LEFT JOIN common_district dp
|
||
ON CONVERT(dp.CODE USING utf8mb4) COLLATE utf8mb4_unicode_ci
|
||
= CONVERT(s.province USING utf8mb4) COLLATE utf8mb4_unicode_ci
|
||
AND dp.STATUS = 'VALID'
|
||
LEFT JOIN common_district dc
|
||
ON CONVERT(dc.CODE USING utf8mb4) COLLATE utf8mb4_unicode_ci
|
||
= CONVERT(s.city USING utf8mb4) COLLATE utf8mb4_unicode_ci
|
||
AND dc.STATUS = 'VALID'
|
||
WHERE s.del_flag = '0'
|
||
GROUP BY s.id, s.station_short_name, s.station_name, dp.NAME, s.province, dc.NAME, s.city
|
||
ORDER BY kg DESC, name ASC`, [HYDROGEN_TENANT_ID, MANUAL_RECHARGE_SOURCE, start, end]);
|
||
return rows;
|
||
}
|
||
|
||
|
||
/** 区间内现结流水(按站点汇总)。 */
|
||
export async function loadRangePayments(db: Database, args: { start: string; end: string }): Promise<RowDataPacket[]> {
|
||
const { start, end } = args;
|
||
const [rows] = await db.query<RowDataPacket[]>(`SELECT station_id AS stationId,
|
||
ROUND(SUM(amount), 2) AS amount,
|
||
COUNT(*) AS paymentCount,
|
||
DATE_FORMAT(MAX(payment_date), '%Y-%m-%d') AS latestPaymentDate
|
||
FROM hydrogen_station_payment
|
||
WHERE del_flag = '0'
|
||
AND payment_date >= ?
|
||
AND payment_date <= ?
|
||
GROUP BY station_id`, [start, end]);
|
||
return rows;
|
||
}
|
||
|
||
|
||
/** 全部站点逐日汇总。 */
|
||
export async function loadSummaryDaily(db: Database, args: { start: string; end: string }): Promise<RowDataPacket[]> {
|
||
const { start, end } = args;
|
||
const [rows] = await db.query<RowDataPacket[]>(`SELECT DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m-%d') AS date,
|
||
ROUND(SUM(b.amount_kg), 2) AS kg,
|
||
ROUND(SUM(b.cost_total), 2) AS fee,
|
||
COUNT(*) AS recordCount
|
||
FROM ${HYDROGEN_TABLE} b
|
||
WHERE ${HYDROGEN_BASE_WHERE_B}
|
||
AND b.tenant_id = ?
|
||
AND COALESCE(b.record_source, '') <> ?
|
||
AND ${dateRangeClause(`b.${HYDROGEN_LOCAL}`)}
|
||
GROUP BY DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m-%d')
|
||
ORDER BY date ASC`, [HYDROGEN_TENANT_ID, MANUAL_RECHARGE_SOURCE, start, end]);
|
||
return rows;
|
||
}
|
||
|
||
|
||
/** 站点逐日汇总。 */
|
||
export async function loadStationDaily(db: Database, args: { start: string; end: string }): Promise<RowDataPacket[]> {
|
||
const { start, end } = args;
|
||
const [rows] = await db.query<RowDataPacket[]>(`SELECT b.station_id AS stationId,
|
||
DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m-%d') AS date,
|
||
ROUND(SUM(b.amount_kg), 2) AS kg
|
||
FROM ${HYDROGEN_TABLE} b
|
||
WHERE ${HYDROGEN_BASE_WHERE_B}
|
||
AND b.tenant_id = ?
|
||
AND COALESCE(b.record_source, '') <> ?
|
||
AND ${dateRangeClause(`b.${HYDROGEN_LOCAL}`)}
|
||
GROUP BY b.station_id, DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m-%d')
|
||
ORDER BY stationId ASC, date ASC`, [HYDROGEN_TENANT_ID, MANUAL_RECHARGE_SOURCE, start, end]);
|
||
return rows;
|
||
}
|
||
|
||
|
||
/** 全部站点逐日现结。 */
|
||
export async function loadSummaryPaymentDaily(db: Database, args: { start: string; end: string }): Promise<RowDataPacket[]> {
|
||
const { start, end } = args;
|
||
const [rows] = await db.query<RowDataPacket[]>(`SELECT DATE_FORMAT(payment_date, '%Y-%m-%d') AS date,
|
||
ROUND(SUM(amount), 2) AS amount,
|
||
COUNT(*) AS paymentCount
|
||
FROM hydrogen_station_payment
|
||
WHERE del_flag = '0'
|
||
AND payment_date >= ?
|
||
AND payment_date <= ?
|
||
GROUP BY DATE_FORMAT(payment_date, '%Y-%m-%d')
|
||
ORDER BY date ASC`, [start, end]);
|
||
return rows;
|
||
}
|
||
|
||
|
||
/** 指定站点逐日加氢。 */
|
||
export async function loadStationDailyForStation(db: Database, args: { stationId: number; start: string; end: string }): Promise<RowDataPacket[]> {
|
||
const { stationId, start, end } = args;
|
||
const [rows] = await db.query<RowDataPacket[]>(`SELECT DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m-%d') AS date,
|
||
ROUND(SUM(b.amount_kg), 2) AS kg,
|
||
ROUND(SUM(b.cost_total), 2) AS fee,
|
||
ROUND(AVG(NULLIF(b.cost_price, 0)), 2) AS avgPrice,
|
||
COUNT(*) AS recordCount
|
||
FROM ${HYDROGEN_TABLE} b
|
||
WHERE ${HYDROGEN_BASE_WHERE_B}
|
||
AND b.tenant_id = ?
|
||
AND COALESCE(b.record_source, '') <> ?
|
||
AND b.station_id = ?
|
||
AND ${dateRangeClause(`b.${HYDROGEN_LOCAL}`)}
|
||
GROUP BY DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m-%d')
|
||
ORDER BY date ASC`, [HYDROGEN_TENANT_ID, MANUAL_RECHARGE_SOURCE, stationId, start, end]);
|
||
return rows;
|
||
}
|
||
|
||
|
||
/** 指定站点逐日现结。 */
|
||
export async function loadStationPaymentDaily(db: Database, args: { stationId: number; start: string; end: string }): Promise<RowDataPacket[]> {
|
||
const { stationId, start, end } = args;
|
||
const [rows] = await db.query<RowDataPacket[]>(`SELECT DATE_FORMAT(payment_date, '%Y-%m-%d') AS date,
|
||
ROUND(SUM(amount), 2) AS amount,
|
||
COUNT(*) AS paymentCount
|
||
FROM hydrogen_station_payment
|
||
WHERE del_flag = '0'
|
||
AND station_id = ?
|
||
AND payment_date >= ?
|
||
AND payment_date <= ?
|
||
GROUP BY DATE_FORMAT(payment_date, '%Y-%m-%d')
|
||
ORDER BY date ASC`, [stationId, start, end]);
|
||
return rows;
|
||
}
|
||
|
||
|
||
/** 指定站点的客户月度汇总。 */
|
||
export async function loadCustomerMonth(db: Database, args: { stationId: number; end: string }): Promise<RowDataPacket[]> {
|
||
const { stationId, end } = args;
|
||
const [rows] = await db.query<RowDataPacket[]>(`${EXTERNAL_CUSTOMER_MAP_CTE}
|
||
SELECT DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m') AS month,
|
||
COALESCE(m.customerName, NULLIF(TRIM(b.customer_name), ''), NULLIF(TRIM(b.system_customer_name), ''), '未关联客户') AS customerName,
|
||
ROUND(SUM(b.amount_kg), 2) AS kg,
|
||
ROUND(SUM(b.cost_total), 2) AS fee,
|
||
COUNT(*) AS recordCount
|
||
FROM ${HYDROGEN_TABLE} b
|
||
LEFT JOIN external_vehicle_map m
|
||
ON CONVERT(${NORMALIZED_PLATE('b.license_plate')} USING utf8mb4) COLLATE utf8mb4_unicode_ci
|
||
= CONVERT(m.normalizedPlate USING utf8mb4) COLLATE utf8mb4_unicode_ci
|
||
AND (b.vehicle_id IS NULL OR b.vehicle_id = 0)
|
||
WHERE ${HYDROGEN_BASE_WHERE_B}
|
||
AND b.tenant_id = ?
|
||
AND COALESCE(b.record_source, '') <> ?
|
||
AND b.station_id = ?
|
||
AND b.${HYDROGEN_LOCAL} >= DATE_SUB(DATE_FORMAT(?, '%Y-%m-01'), INTERVAL 11 MONTH)
|
||
AND b.${HYDROGEN_LOCAL} < DATE_ADD(?, INTERVAL 1 DAY)
|
||
GROUP BY DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m'),
|
||
COALESCE(m.customerName, NULLIF(TRIM(b.customer_name), ''), NULLIF(TRIM(b.system_customer_name), ''), '未关联客户')
|
||
ORDER BY month ASC, kg DESC`, [HYDROGEN_TENANT_ID, MANUAL_RECHARGE_SOURCE, stationId, end, end]);
|
||
return rows;
|
||
}
|
||
|
||
|
||
/** 指定站点的外部客户月度汇总(含映射 CTE)。 */
|
||
export async function loadExternalCustomerMonth(db: Database, args: { stationId: number; end: string }): Promise<RowDataPacket[]> {
|
||
const { stationId, end } = args;
|
||
const [rows] = await db.query<RowDataPacket[]>(`${EXTERNAL_CUSTOMER_MAP_CTE}
|
||
SELECT DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m') AS month,
|
||
m.customerName,
|
||
ROUND(SUM(b.amount_kg), 2) AS kg,
|
||
ROUND(SUM(COALESCE(b.fee_total, b.cost_total)), 2) AS fee,
|
||
COUNT(*) AS recordCount
|
||
FROM ${HYDROGEN_TABLE} b
|
||
INNER JOIN external_vehicle_map m
|
||
ON ${NORMALIZED_PLATE_UNICODE('b.license_plate')}
|
||
= m.normalizedPlate
|
||
WHERE ${HYDROGEN_BASE_WHERE_B}
|
||
AND b.tenant_id = ?
|
||
AND COALESCE(b.record_source, '') <> ?
|
||
AND b.station_id = ?
|
||
AND (b.vehicle_id IS NULL OR b.vehicle_id = 0)
|
||
AND b.${HYDROGEN_LOCAL} >= DATE_SUB(DATE_FORMAT(?, '%Y-%m-01'), INTERVAL 11 MONTH)
|
||
AND b.${HYDROGEN_LOCAL} < DATE_ADD(?, INTERVAL 1 DAY)
|
||
GROUP BY DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m'), m.customerName
|
||
ORDER BY month ASC, kg DESC, m.customerName ASC`, [HYDROGEN_TENANT_ID, MANUAL_RECHARGE_SOURCE, stationId, end, end]);
|
||
return rows;
|
||
}
|
||
|
||
|
||
/** 外部客户进账(自动 + 手工充值合并)。 */
|
||
export async function loadExternalReceipts(db: Database, args: { start: string; end: string }): Promise<RowDataPacket[]> {
|
||
const { start, end } = args;
|
||
const [rows] = await db.query<RowDataPacket[]>(`SELECT * FROM (
|
||
SELECT CONCAT('auto:', CAST(id AS CHAR)) AS id,
|
||
DATE_FORMAT(summary_date, '%Y-%m-%d') AS date,
|
||
customer_name AS customerName,
|
||
amount,
|
||
pay_method AS payMethod,
|
||
source,
|
||
spot_record_count AS sourceRecordCount,
|
||
DATE_FORMAT(COALESCE(refresh_time, update_time), '%Y-%m-%d %H:%i:%s') AS updatedAt
|
||
FROM hydrogen_external_recharge_daily_summary
|
||
WHERE tenant_id = ?
|
||
AND summary_date >= ?
|
||
AND summary_date <= ?
|
||
UNION ALL
|
||
SELECT CONCAT('manual:', CAST(id AS CHAR)) AS id,
|
||
DATE_FORMAT(${HYDROGEN_LOCAL}, '%Y-%m-%d') AS date,
|
||
COALESCE(NULLIF(customer_name, ''), '未关联客户') AS customerName,
|
||
fee_total AS amount,
|
||
recharge_pay_method AS payMethod,
|
||
record_source AS source,
|
||
1 AS sourceRecordCount,
|
||
DATE_FORMAT(update_time, '%Y-%m-%d %H:%i:%s') AS updatedAt
|
||
FROM ${HYDROGEN_TABLE}
|
||
WHERE ${HYDROGEN_BASE_WHERE}
|
||
AND tenant_id = ?
|
||
AND record_source = ?
|
||
AND ${HYDROGEN_LOCAL} >= ?
|
||
AND ${HYDROGEN_LOCAL} < DATE_ADD(?, INTERVAL 1 DAY)
|
||
) externalReceipts
|
||
ORDER BY date DESC, customerName ASC, id ASC`, [HYDROGEN_TENANT_ID, start, end, HYDROGEN_TENANT_ID, MANUAL_RECHARGE_SOURCE, start, end]);
|
||
return rows;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 氢能 v2(/h2/v2/*)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
export type RegionGranularity = 'province' | 'city';
|
||
|
||
/** 区域筛选所需的最小输入(与路由的 Filter 结构兼容)。 */
|
||
export interface RegionFilter {
|
||
clauses: string[];
|
||
params: unknown[];
|
||
region: string | null;
|
||
regionGranularity: RegionGranularity;
|
||
}
|
||
|
||
export function stationMasterRegionSql(granularity: RegionGranularity) {
|
||
const districtColumn = granularity === "province" ? "rs.province" : "rs.city";
|
||
const stationName = "COALESCE(NULLIF(rs.site_short_name, ''), NULLIF(rs.site_name, ''), '')";
|
||
const fallback =
|
||
granularity === "province"
|
||
? `CASE WHEN ${stationName} LIKE '%嘉兴%' OR ${stationName} LIKE '%平湖%' THEN '浙江省'
|
||
WHEN ${stationName} LIKE '%广州%' OR ${stationName} LIKE '%佛山%' THEN '广东省'
|
||
WHEN ${stationName} LIKE '%成都%' THEN '四川省'
|
||
WHEN ${stationName} LIKE '%昆山%' THEN '江苏省'
|
||
ELSE '未归属省份' END`
|
||
: `CASE WHEN ${stationName} LIKE '%嘉兴%' OR ${stationName} LIKE '%平湖%' THEN '嘉兴市'
|
||
WHEN ${stationName} LIKE '%广州%' THEN '广州市'
|
||
WHEN ${stationName} LIKE '%佛山%' THEN '佛山市'
|
||
WHEN ${stationName} LIKE '%成都%' THEN '成都市'
|
||
WHEN ${stationName} LIKE '%昆山%' THEN '昆山市'
|
||
ELSE '未归属区域' END`;
|
||
return `COALESCE(NULLIF(rd.NAME, ''), ${fallback})`;
|
||
}
|
||
|
||
export async function resolvedWhere(
|
||
db: Database,
|
||
filter: RegionFilter,
|
||
) {
|
||
const clauses = [...filter.clauses];
|
||
const params = [...filter.params];
|
||
if (filter.region) {
|
||
// Resolve the small station master once. The old implementation ran two
|
||
// correlated station/district subqueries for every ledger row and repeated
|
||
// that work in summary, grouping and record queries.
|
||
const districtColumn =
|
||
filter.regionGranularity === "province" ? "rs.province" : "rs.city";
|
||
const [rows] = await db.query<RowDataPacket[]>(
|
||
`SELECT CAST(rs.id AS CHAR) AS id
|
||
FROM new_hydrogen_site rs
|
||
LEFT JOIN common_district rd
|
||
ON CONVERT(rd.CODE USING utf8mb4) COLLATE utf8mb4_unicode_ci = CONVERT(${districtColumn} USING utf8mb4) COLLATE utf8mb4_unicode_ci
|
||
AND rd.STATUS = 'VALID'
|
||
WHERE rs.del_flag = '0'
|
||
AND ${stationMasterRegionSql(filter.regionGranularity)} = ?`,
|
||
[filter.region],
|
||
);
|
||
const stationIds = [...new Set(rows.map((row) => String(row.id)))];
|
||
if (stationIds.length === 0) {
|
||
clauses.push("1 = 0");
|
||
} else {
|
||
clauses.push(`b.station_id IN (${stationIds.map(() => "?").join(", ")})`);
|
||
params.push(...stationIds);
|
||
}
|
||
}
|
||
return { sql: clauses.join(" AND "), params };
|
||
}
|
||
|
||
|
||
|
||
/** 氢能 v2 查询(SQL 与参数顺序由 routes.test.ts 的指纹断言锁定)。 */
|
||
export async function loadMetaYears(db: Database): Promise<RowDataPacket[]> {
|
||
const [rows] = await db.query<RowDataPacket[]>(`SELECT YEAR(${HYDROGEN_LOCAL}) AS value,
|
||
DATE_FORMAT(MIN(${HYDROGEN_LOCAL}), '%Y-%m-%d') AS startDate,
|
||
DATE_FORMAT(MAX(${HYDROGEN_LOCAL}), '%Y-%m-%d') AS endDate
|
||
FROM ${HYDROGEN_TABLE}
|
||
WHERE ${HYDROGEN_FUEL_ONLY_WHERE} AND ${HYDROGEN_LOCAL} >= ?
|
||
GROUP BY YEAR(${HYDROGEN_LOCAL})
|
||
ORDER BY value DESC`, [HYDROGEN_MIN_DATE]);
|
||
return rows;
|
||
}
|
||
|
||
|
||
/** 氢能 v2 查询(SQL 与参数顺序由 routes.test.ts 的指纹断言锁定)。 */
|
||
export async function loadMetaStations(db: Database): Promise<RowDataPacket[]> {
|
||
const [rows] = await db.query<RowDataPacket[]>(`SELECT CAST(s.id AS CHAR) AS id,
|
||
COALESCE(NULLIF(s.site_short_name, ''), NULLIF(s.site_name, ''), CONCAT('站点 #', s.id)) AS name,
|
||
COALESCE(NULLIF(p.NAME, ''), '未归属') AS province,
|
||
COALESCE(NULLIF(ct.NAME, ''), '未归属') AS city
|
||
FROM new_hydrogen_site s
|
||
LEFT JOIN common_district p ON CONVERT(p.CODE USING utf8mb4) COLLATE utf8mb4_unicode_ci = CONVERT(s.province USING utf8mb4) COLLATE utf8mb4_unicode_ci AND p.STATUS = 'VALID'
|
||
LEFT JOIN common_district ct ON CONVERT(ct.CODE USING utf8mb4) COLLATE utf8mb4_unicode_ci = CONVERT(s.city USING utf8mb4) COLLATE utf8mb4_unicode_ci AND ct.STATUS = 'VALID'
|
||
WHERE s.del_flag = '0'
|
||
AND EXISTS (
|
||
SELECT 1
|
||
FROM ${HYDROGEN_TABLE} b
|
||
WHERE ${HYDROGEN_FUEL_ONLY_WHERE_B}
|
||
AND b.station_id = s.id
|
||
AND COALESCE(b.amount_kg, 0) > 0
|
||
)
|
||
ORDER BY name`, []);
|
||
return rows;
|
||
}
|
||
|
||
|
||
/** 氢能 v2 查询(SQL 与参数顺序由 routes.test.ts 的指纹断言锁定)。 */
|
||
export async function loadMetaWatermark(db: Database): Promise<RowDataPacket[]> {
|
||
const [rows] = await db.query<RowDataPacket[]>(`SELECT DATE_FORMAT(MAX(${HYDROGEN_LOCAL}), '%Y-%m-%d %H:%i:%s') AS ledgerAt
|
||
FROM ${HYDROGEN_TABLE}
|
||
WHERE ${HYDROGEN_FUEL_ONLY_WHERE} AND ${HYDROGEN_LOCAL} >= ?`, [HYDROGEN_MIN_DATE]);
|
||
return rows;
|
||
}
|
||
|
||
|
||
/** 氢能 v2 查询(SQL 与参数顺序由 routes.test.ts 的指纹断言锁定)。 */
|
||
export async function loadOverviewSummary(db: Database, args: { sqlWhere: string; params: unknown[]; month: string; endDate: string }): Promise<RowDataPacket[]> {
|
||
const { sqlWhere, params, month, endDate } = args;
|
||
const [rows] = await db.query<RowDataPacket[]>(`SELECT DATE_FORMAT(MAX(b.${HYDROGEN_LOCAL}), '%Y-%m-%d %H:%i:%s') AS ledgerAt,
|
||
ROUND(COALESCE(SUM(b.amount_kg), 0), 3) AS totalKg,
|
||
ROUND(COALESCE(SUM(b.cost_total), 0), 2) AS totalCost,
|
||
ROUND(COALESCE(SUM(b.fee_total), 0), 2) AS totalRevenue,
|
||
ROUND(COALESCE(SUM(CASE WHEN ${CUSTOMER_BEARING_ORDER} THEN b.amount_kg ELSE 0 END), 0), 3) AS customerBearingKg,
|
||
ROUND(COALESCE(SUM(CASE WHEN ${COMPANY_BEARING_ORDER} THEN b.amount_kg ELSE 0 END), 0), 3) AS companyBearingKg,
|
||
ROUND(COALESCE(SUM(CASE WHEN ${OTHER_BEARING_ORDER} THEN b.amount_kg ELSE 0 END), 0), 3) AS otherBearingKg,
|
||
ROUND(COALESCE(SUM(CASE WHEN ${CUSTOMER_BEARING_ORDER} THEN b.fee_total ELSE 0 END), 0), 2) AS customerRevenue,
|
||
ROUND(COALESCE(SUM(CASE WHEN ${CUSTOMER_BEARING_ORDER} THEN b.cost_total ELSE 0 END), 0), 2) AS customerCost,
|
||
ROUND(COALESCE(SUM(CASE WHEN ${COMPANY_BEARING_ORDER} THEN b.cost_total ELSE 0 END), 0), 2) AS companyCost,
|
||
ROUND(COALESCE(SUM(CASE WHEN ${OTHER_BEARING_ORDER} THEN b.cost_total ELSE 0 END), 0), 2) AS otherCost,
|
||
ROUND(COALESCE(SUM(CASE WHEN DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m') = ? THEN b.amount_kg ELSE 0 END), 0), 3) AS monthKg,
|
||
ROUND(COALESCE(SUM(CASE WHEN DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m') = ? THEN b.cost_total ELSE 0 END), 0), 2) AS monthCost,
|
||
ROUND(COALESCE(SUM(CASE WHEN DATE(b.${HYDROGEN_LOCAL}) = ? THEN b.amount_kg ELSE 0 END), 0), 3) AS todayKg,
|
||
ROUND(COALESCE(SUM(CASE WHEN DATE(b.${HYDROGEN_LOCAL}) = ? THEN b.cost_total ELSE 0 END), 0), 2) AS todayCost,
|
||
COUNT(*) AS recordCount,
|
||
COUNT(DISTINCT COALESCE(b.station_id, 0)) AS stationCount
|
||
FROM ${HYDROGEN_TABLE} b
|
||
WHERE ${sqlWhere}`, [month, month, endDate, endDate, ...params]);
|
||
return rows;
|
||
}
|
||
|
||
|
||
/** 氢能 v2 查询(SQL 与参数顺序由 routes.test.ts 的指纹断言锁定)。 */
|
||
export async function loadOverviewMonthly(db: Database, args: { sqlWhere: string; params: unknown[] }): Promise<RowDataPacket[]> {
|
||
const { sqlWhere, params } = args;
|
||
const [rows] = await db.query<RowDataPacket[]>(`SELECT DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m') AS month,
|
||
ROUND(COALESCE(SUM(b.amount_kg), 0), 3) AS totalKg,
|
||
ROUND(COALESCE(SUM(CASE WHEN b.vehicle_id IS NOT NULL THEN b.amount_kg ELSE 0 END), 0), 3) AS lingniuKg,
|
||
ROUND(COALESCE(SUM(CASE WHEN b.vehicle_id IS NULL THEN b.amount_kg ELSE 0 END), 0), 3) AS externalKg,
|
||
ROUND(COALESCE(SUM(b.cost_total), 0), 2) AS cost,
|
||
ROUND(COALESCE(SUM(b.fee_total), 0), 2) AS revenue,
|
||
ROUND(COALESCE(SUM(CASE WHEN ${CUSTOMER_BEARING_ORDER} THEN b.fee_total ELSE 0 END), 0), 2) AS customerRevenue,
|
||
ROUND(COALESCE(SUM(CASE WHEN ${CUSTOMER_BEARING_ORDER} THEN b.cost_total ELSE 0 END), 0), 2) AS customerCost,
|
||
ROUND(COALESCE(SUM(CASE WHEN ${COMPANY_BEARING_ORDER} THEN b.cost_total ELSE 0 END), 0), 2) AS companyCost,
|
||
ROUND(COALESCE(SUM(CASE WHEN ${OTHER_BEARING_ORDER} THEN b.cost_total ELSE 0 END), 0), 2) AS otherCost
|
||
FROM ${HYDROGEN_TABLE} b
|
||
WHERE ${sqlWhere}
|
||
GROUP BY DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m')
|
||
ORDER BY month`, params);
|
||
return rows;
|
||
}
|
||
|
||
|
||
/** 氢能 v2 查询(SQL 与参数顺序由 routes.test.ts 的指纹断言锁定)。 */
|
||
export async function loadOverviewStations(db: Database, args: { sqlWhere: string; params: unknown[] }): Promise<RowDataPacket[]> {
|
||
const { sqlWhere, params } = args;
|
||
const [rows] = await db.query<RowDataPacket[]>(`SELECT CAST(MAX(COALESCE(b.station_id, 0)) AS CHAR) AS id,
|
||
COALESCE(MAX(NULLIF(s.site_short_name, '')), MAX(NULLIF(s.site_name, '')), MAX(NULLIF(b.station_name, '')), '未关联站点') AS name,
|
||
COALESCE(MAX(NULLIF(p.NAME, '')), '未归属') AS province,
|
||
COALESCE(MAX(NULLIF(ct.NAME, '')), '未归属') AS city,
|
||
ROUND(COALESCE(SUM(b.amount_kg), 0), 3) AS kg,
|
||
ROUND(COALESCE(SUM(CASE WHEN b.vehicle_id IS NOT NULL THEN b.amount_kg ELSE 0 END), 0), 3) AS lingniuKg,
|
||
ROUND(COALESCE(SUM(CASE WHEN b.vehicle_id IS NULL THEN b.amount_kg ELSE 0 END), 0), 3) AS externalKg,
|
||
ROUND(COALESCE(SUM(b.cost_total), 0), 2) AS cost,
|
||
ROUND(COALESCE(SUM(b.fee_total), 0), 2) AS revenue,
|
||
ROUND(COALESCE(SUM(CASE WHEN ${CUSTOMER_BEARING_ORDER} THEN b.fee_total ELSE 0 END), 0), 2) AS customerRevenue,
|
||
ROUND(COALESCE(SUM(CASE WHEN ${CUSTOMER_BEARING_ORDER} THEN b.cost_total ELSE 0 END), 0), 2) AS customerCost,
|
||
ROUND(COALESCE(SUM(CASE WHEN ${COMPANY_BEARING_ORDER} THEN b.cost_total ELSE 0 END), 0), 2) AS companyCost,
|
||
ROUND(COALESCE(SUM(CASE WHEN ${OTHER_BEARING_ORDER} THEN b.cost_total ELSE 0 END), 0), 2) AS otherCost,
|
||
COUNT(*) AS recordCount,
|
||
COUNT(DISTINCT COALESCE(b.system_customer_id, b.customer_id, 0)) AS customerCount
|
||
FROM ${HYDROGEN_TABLE} b
|
||
LEFT JOIN new_hydrogen_site s ON s.id = b.station_id AND s.del_flag = '0'
|
||
LEFT JOIN common_district p ON CONVERT(p.CODE USING utf8mb4) COLLATE utf8mb4_unicode_ci = CONVERT(s.province USING utf8mb4) COLLATE utf8mb4_unicode_ci AND p.STATUS = 'VALID'
|
||
LEFT JOIN common_district ct ON CONVERT(ct.CODE USING utf8mb4) COLLATE utf8mb4_unicode_ci = CONVERT(s.city USING utf8mb4) COLLATE utf8mb4_unicode_ci AND ct.STATUS = 'VALID'
|
||
WHERE ${sqlWhere}
|
||
GROUP BY COALESCE(b.station_id, 0)
|
||
HAVING SUM(COALESCE(b.amount_kg, 0)) > 0
|
||
ORDER BY kg DESC`, params);
|
||
return rows;
|
||
}
|
||
|
||
|
||
/** 氢能 v2 查询(SQL 与参数顺序由 routes.test.ts 的指纹断言锁定)。 */
|
||
export async function loadOverviewCustomers(db: Database, args: { sqlWhere: string; params: unknown[] }): Promise<RowDataPacket[]> {
|
||
const { sqlWhere, params } = args;
|
||
const [rows] = await db.query<RowDataPacket[]>(`SELECT COALESCE(b.system_customer_id, b.customer_id, 0) AS id,
|
||
COALESCE(NULLIF(b.system_customer_name, ''), NULLIF(b.customer_name, ''), '未关联客户') AS name,
|
||
ROUND(COALESCE(SUM(b.amount_kg), 0), 3) AS kg,
|
||
ROUND(COALESCE(SUM(CASE WHEN ${CUSTOMER_BEARING_ORDER} THEN b.amount_kg ELSE 0 END), 0), 3) AS customerBearingKg,
|
||
ROUND(COALESCE(SUM(CASE WHEN ${COMPANY_BEARING_ORDER} THEN b.amount_kg ELSE 0 END), 0), 3) AS companyBearingKg,
|
||
ROUND(COALESCE(SUM(CASE WHEN ${OTHER_BEARING_ORDER} THEN b.amount_kg ELSE 0 END), 0), 3) AS otherBearingKg,
|
||
ROUND(COALESCE(SUM(b.cost_total), 0), 2) AS cost,
|
||
ROUND(COALESCE(SUM(b.fee_total), 0), 2) AS revenue,
|
||
ROUND(COALESCE(SUM(CASE WHEN ${CUSTOMER_BEARING_ORDER} THEN b.fee_total ELSE 0 END), 0), 2) AS customerRevenue,
|
||
ROUND(COALESCE(SUM(CASE WHEN ${CUSTOMER_BEARING_ORDER} THEN b.cost_total ELSE 0 END), 0), 2) AS customerCost,
|
||
ROUND(COALESCE(SUM(CASE WHEN ${COMPANY_BEARING_ORDER} THEN b.cost_total ELSE 0 END), 0), 2) AS companyCost,
|
||
ROUND(COALESCE(SUM(CASE WHEN ${OTHER_BEARING_ORDER} THEN b.cost_total ELSE 0 END), 0), 2) AS otherCost,
|
||
COUNT(*) AS recordCount
|
||
FROM ${HYDROGEN_TABLE} b
|
||
WHERE ${sqlWhere}
|
||
GROUP BY COALESCE(b.system_customer_id, b.customer_id, 0), COALESCE(NULLIF(b.system_customer_name, ''), NULLIF(b.customer_name, ''), '未关联客户')
|
||
ORDER BY kg DESC
|
||
LIMIT 200`, params);
|
||
return rows;
|
||
}
|
||
|
||
|
||
/** 氢能 v2 查询(SQL 与参数顺序由 routes.test.ts 的指纹断言锁定)。 */
|
||
export async function loadDailyRows(db: Database, args: { sqlWhere: string; params: unknown[] }): Promise<RowDataPacket[]> {
|
||
const { sqlWhere, params } = args;
|
||
const [rows] = await db.query<RowDataPacket[]>(`SELECT DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m-%d') AS date,
|
||
ROUND(COALESCE(SUM(b.amount_kg), 0), 3) AS kg,
|
||
ROUND(COALESCE(SUM(CASE WHEN b.vehicle_id IS NOT NULL THEN b.amount_kg ELSE 0 END), 0), 3) AS lingniuKg,
|
||
ROUND(COALESCE(SUM(CASE WHEN b.vehicle_id IS NULL THEN b.amount_kg ELSE 0 END), 0), 3) AS externalKg,
|
||
ROUND(COALESCE(SUM(b.cost_total), 0), 2) AS cost,
|
||
COUNT(*) AS recordCount,
|
||
COUNT(DISTINCT COALESCE(b.station_id, 0)) AS stationCount
|
||
FROM ${HYDROGEN_TABLE} b
|
||
WHERE ${sqlWhere}
|
||
GROUP BY DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m-%d')
|
||
ORDER BY date`, params);
|
||
return rows;
|
||
}
|
||
|
||
|
||
/** 氢能 v2 查询(SQL 与参数顺序由 routes.test.ts 的指纹断言锁定)。 */
|
||
export async function loadDailyWatermark(db: Database, args: { sqlWhere: string; params: unknown[] }): Promise<RowDataPacket[]> {
|
||
const { sqlWhere, params } = args;
|
||
const [rows] = await db.query<RowDataPacket[]>(`SELECT DATE_FORMAT(MAX(b.${HYDROGEN_LOCAL}), '%Y-%m-%d %H:%i:%s') AS ledgerAt
|
||
FROM ${HYDROGEN_TABLE} b WHERE ${sqlWhere}`, params);
|
||
return rows;
|
||
}
|
||
|
||
|
||
/** 氢能 v2 查询(SQL 与参数顺序由 routes.test.ts 的指纹断言锁定)。 */
|
||
export async function loadDailyTreeStations(db: Database, args: { sqlWhere: string; params: unknown[] }): Promise<RowDataPacket[]> {
|
||
const { sqlWhere, params } = args;
|
||
const [rows] = await db.query<RowDataPacket[]>(`SELECT CAST(MAX(COALESCE(b.station_id, 0)) AS CHAR) AS id,
|
||
COALESCE(MAX(NULLIF(s.site_short_name, '')), MAX(NULLIF(s.site_name, '')), MAX(NULLIF(b.station_name, '')), '未关联站点') AS name,
|
||
ROUND(COALESCE(SUM(b.amount_kg), 0), 3) AS kg,
|
||
ROUND(COALESCE(SUM(b.cost_total), 0), 2) AS cost,
|
||
COUNT(*) AS recordCount
|
||
FROM ${HYDROGEN_TABLE} b
|
||
LEFT JOIN new_hydrogen_site s ON s.id = b.station_id AND s.del_flag = '0'
|
||
WHERE ${sqlWhere}
|
||
GROUP BY COALESCE(b.station_id, 0)
|
||
ORDER BY kg DESC`, params);
|
||
return rows;
|
||
}
|
||
|
||
|
||
/** 氢能 v2 查询(SQL 与参数顺序由 routes.test.ts 的指纹断言锁定)。 */
|
||
export async function loadDailyTreeCustomers(db: Database, args: { sqlWhere: string; params: unknown[] }): Promise<RowDataPacket[]> {
|
||
const { sqlWhere, params } = args;
|
||
const [rows] = await db.query<RowDataPacket[]>(`SELECT CAST(MAX(COALESCE(b.station_id, 0)) AS CHAR) AS stationId,
|
||
COALESCE(b.system_customer_id, b.customer_id, 0) AS id,
|
||
COALESCE(NULLIF(b.system_customer_name, ''), NULLIF(b.customer_name, ''), '未关联客户') AS name,
|
||
ROUND(COALESCE(SUM(b.amount_kg), 0), 3) AS kg,
|
||
ROUND(COALESCE(SUM(b.cost_total), 0), 2) AS cost,
|
||
COUNT(*) AS recordCount
|
||
FROM ${HYDROGEN_TABLE} b
|
||
WHERE ${sqlWhere}
|
||
GROUP BY COALESCE(b.station_id, 0), COALESCE(b.system_customer_id, b.customer_id, 0), COALESCE(NULLIF(b.system_customer_name, ''), NULLIF(b.customer_name, ''), '未关联客户')
|
||
ORDER BY stationId, kg DESC`, params);
|
||
return rows;
|
||
}
|
||
|
||
|
||
/** 氢能 v2 查询(SQL 与参数顺序由 routes.test.ts 的指纹断言锁定)。 */
|
||
export async function loadDrillSummary(db: Database, args: { scopedWhere: string; params: unknown[] }): Promise<RowDataPacket[]> {
|
||
const { scopedWhere, params } = args;
|
||
const [rows] = await db.query<RowDataPacket[]>(`SELECT COUNT(*) AS recordCount,
|
||
COUNT(DISTINCT COALESCE(b.station_id, 0)) AS stationCount,
|
||
COUNT(DISTINCT COALESCE(b.system_customer_id, b.customer_id, 0)) AS customerCount,
|
||
SUM(CASE WHEN NULLIF(b.record_source, '') IS NOT NULL OR b.source IS NOT NULL THEN 1 ELSE 0 END) AS traceableRecordCount,
|
||
ROUND(COALESCE(SUM(b.amount_kg), 0), 3) AS kg,
|
||
ROUND(COALESCE(SUM(b.cost_total), 0), 2) AS cost, ROUND(COALESCE(SUM(b.fee_total), 0), 2) AS revenue
|
||
FROM ${HYDROGEN_TABLE} b WHERE ${scopedWhere}`, params);
|
||
return rows;
|
||
}
|
||
|
||
|
||
/** 氢能 v2 查询(SQL 与参数顺序由 routes.test.ts 的指纹断言锁定)。 */
|
||
export async function loadDrillGroups(db: Database, args: { scopedWhere: string; params: unknown[]; groupSelect: string; groupExpression: string; groupHaving: string; groupOrder: string; pageSize: number; offset: number }): Promise<RowDataPacket[]> {
|
||
const { scopedWhere, params, groupSelect, groupExpression, groupHaving, groupOrder, pageSize, offset } = args;
|
||
const [rows] = await db.query<RowDataPacket[]>(`SELECT ${groupSelect},
|
||
GROUP_CONCAT(DISTINCT COALESCE(CAST(b.settlement_type AS CHAR), 'unknown') ORDER BY COALESCE(CAST(b.settlement_type AS CHAR), 'unknown')) AS settlementTypes,
|
||
COUNT(*) AS recordCount, COUNT(DISTINCT COALESCE(b.station_id, 0)) AS stationCount,
|
||
COUNT(DISTINCT COALESCE(b.system_customer_id, b.customer_id, 0)) AS customerCount,
|
||
ROUND(COALESCE(SUM(b.amount_kg), 0), 3) AS kg, ROUND(COALESCE(SUM(b.cost_total), 0), 2) AS cost, ROUND(COALESCE(SUM(b.fee_total), 0), 2) AS revenue,
|
||
ROUND(COALESCE(SUM(CASE WHEN b.vehicle_id IS NOT NULL THEN b.amount_kg ELSE 0 END), 0), 3) AS lingniuKg,
|
||
ROUND(COALESCE(SUM(CASE WHEN b.vehicle_id IS NULL THEN b.amount_kg ELSE 0 END), 0), 3) AS externalKg
|
||
FROM ${HYDROGEN_TABLE} b
|
||
LEFT JOIN new_hydrogen_site s ON s.id = b.station_id AND s.del_flag = '0'
|
||
LEFT JOIN common_district p ON CONVERT(p.CODE USING utf8mb4) COLLATE utf8mb4_unicode_ci = CONVERT(s.province USING utf8mb4) COLLATE utf8mb4_unicode_ci AND p.STATUS = 'VALID'
|
||
LEFT JOIN common_district ct ON CONVERT(ct.CODE USING utf8mb4) COLLATE utf8mb4_unicode_ci = CONVERT(s.city USING utf8mb4) COLLATE utf8mb4_unicode_ci AND ct.STATUS = 'VALID'
|
||
WHERE ${scopedWhere}
|
||
GROUP BY ${groupExpression}
|
||
${groupHaving}
|
||
ORDER BY ${groupOrder} LIMIT ? OFFSET ?`, [...params, pageSize, offset]);
|
||
return rows;
|
||
}
|
||
|
||
|
||
/** 氢能 v2 查询(SQL 与参数顺序由 routes.test.ts 的指纹断言锁定)。 */
|
||
export async function loadDrillRecords(db: Database, args: { scopedWhere: string; params: unknown[]; pageSize: number; offset: number }): Promise<RowDataPacket[]> {
|
||
const { scopedWhere, params, pageSize, offset } = args;
|
||
const [rows] = await db.query<RowDataPacket[]>(`SELECT b.id, b.settlement_type AS settlementType, DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m-%d %H:%i:%s') AS time, b.order_no AS orderNo,
|
||
CAST(COALESCE(b.station_id, 0) AS CHAR) AS stationId, COALESCE(NULLIF(b.station_name, ''), '未关联站点') AS stationName,
|
||
COALESCE(b.system_customer_id, b.customer_id, 0) AS customerId, COALESCE(NULLIF(b.system_customer_name, ''), NULLIF(b.customer_name, ''), '未关联客户') AS customerName,
|
||
COALESCE(NULLIF(b.license_plate, ''), '无车牌') AS plateNo, COALESCE(NULLIF(b.record_source, ''), CAST(b.source AS CHAR), '未知来源') AS source,
|
||
COALESCE(NULLIF(b.verify_status, ''), 'UNVERIFIED') AS verifyStatus, b.vehicle_id AS vehicleId,
|
||
ROUND(COALESCE(b.amount_kg, 0), 3) AS kg, ROUND(COALESCE(b.cost_price, 0), 2) AS unitPrice,
|
||
ROUND(COALESCE(b.cost_total, 0), 2) AS cost, ROUND(COALESCE(b.fee_total, 0), 2) AS revenue
|
||
FROM ${HYDROGEN_TABLE} b WHERE ${scopedWhere}
|
||
ORDER BY b.${HYDROGEN_LOCAL} DESC, b.id DESC LIMIT ? OFFSET ?`, [...params, pageSize, offset]);
|
||
return rows;
|
||
}
|
||
|
||
/** 下钻的金额口径片段:在基础 WHERE 之上追加承担方条件。 */
|
||
export function buildScopedWhere(sqlWhere: string, amountScope: string): string {
|
||
if (amountScope === 'customer') return `${sqlWhere} AND ${CUSTOMER_BEARING_ORDER}`;
|
||
if (amountScope === 'company') return `${sqlWhere} AND ${COMPANY_BEARING_ORDER}`;
|
||
if (amountScope === 'other') return `${sqlWhere} AND ${OTHER_BEARING_ORDER}`;
|
||
return sqlWhere;
|
||
}
|
||
|
||
/** 下钻分组列(站点 / 客户 / 日期 / 车牌)。 */
|
||
export function buildGroupSelect(groupBy: string): string {
|
||
if (groupBy === 'station') return "CAST(MAX(COALESCE(b.station_id, 0)) AS CHAR) AS id, COALESCE(MAX(NULLIF(s.site_short_name, '')), MAX(NULLIF(s.site_name, '')), MAX(NULLIF(b.station_name, '')), '未关联站点') AS name, COALESCE(MAX(NULLIF(p.NAME, '')), '未归属') AS province, COALESCE(MAX(NULLIF(ct.NAME, '')), '未归属') AS city";
|
||
if (groupBy === 'customer') return "COALESCE(b.system_customer_id, b.customer_id, 0) AS id, COALESCE(NULLIF(b.system_customer_name, ''), NULLIF(b.customer_name, ''), '未关联客户') AS name, NULL AS province, NULL AS city";
|
||
if (groupBy === 'date') return `DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m-%d') AS id, DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m-%d') AS name, NULL AS province, NULL AS city`;
|
||
return "COALESCE(NULLIF(b.license_plate, ''), '无车牌') AS id, COALESCE(NULLIF(b.license_plate, ''), '无车牌') AS name, NULL AS province, NULL AS city";
|
||
}
|
||
|
||
/** 下钻分组表达式(与 buildGroupSelect 必须成对使用)。 */
|
||
export function buildGroupExpression(groupBy: string): string {
|
||
if (groupBy === 'station') return 'COALESCE(b.station_id, 0)';
|
||
if (groupBy === 'customer') return "COALESCE(b.system_customer_id, b.customer_id, 0), COALESCE(NULLIF(b.system_customer_name, ''), NULLIF(b.customer_name, ''), '未关联客户')";
|
||
if (groupBy === 'date') return `DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m-%d')`;
|
||
return "COALESCE(NULLIF(b.license_plate, ''), '无车牌')";
|
||
}
|
||
|
||
/** 由筛选条件构建 WHERE 片段与参数(顺序即 SQL 参数顺序,由契约测试锁定)。 */
|
||
export function buildFilterClauses(input: {
|
||
startDate: string;
|
||
endDate: string;
|
||
stationId: string | null;
|
||
customerId: number | null;
|
||
customerName: string | null;
|
||
plateNo: string | null;
|
||
vehicleScope: string;
|
||
verifyScope: string;
|
||
}): { clauses: string[]; params: unknown[] } {
|
||
const { startDate: safeStart, endDate: safeEnd, stationId, customerId, customerName, plateNo, vehicleScope, verifyScope } = input;
|
||
const clauses = [
|
||
HYDROGEN_FUEL_ONLY_WHERE_B,
|
||
`b.${HYDROGEN_LOCAL} >= ?`,
|
||
`b.${HYDROGEN_LOCAL} < DATE_ADD(?, INTERVAL 1 DAY)`,
|
||
];
|
||
const params: unknown[] = [
|
||
safeStart < HYDROGEN_MIN_DATE ? HYDROGEN_MIN_DATE : safeStart,
|
||
safeEnd,
|
||
];
|
||
if (stationId !== null) {
|
||
clauses.push("COALESCE(b.station_id, 0) = ?");
|
||
params.push(stationId);
|
||
}
|
||
if (customerId !== null) {
|
||
clauses.push("COALESCE(b.system_customer_id, b.customer_id, 0) = ?");
|
||
params.push(customerId);
|
||
}
|
||
if (customerName) {
|
||
clauses.push(
|
||
"COALESCE(NULLIF(b.system_customer_name, ''), NULLIF(b.customer_name, ''), '未关联客户') = ?",
|
||
);
|
||
params.push(customerName);
|
||
}
|
||
if (plateNo) {
|
||
clauses.push("COALESCE(NULLIF(b.license_plate, ''), '无车牌') = ?");
|
||
params.push(plateNo);
|
||
}
|
||
if (vehicleScope === "lingniu") clauses.push("b.vehicle_id IS NOT NULL");
|
||
if (vehicleScope === "external") clauses.push("b.vehicle_id IS NULL");
|
||
if (verifyScope === "verified")
|
||
clauses.push(
|
||
"LOWER(COALESCE(NULLIF(TRIM(b.verify_status), ''), 'unverified')) = 'verified'",
|
||
);
|
||
if (verifyScope === "unverified")
|
||
clauses.push(
|
||
"LOWER(COALESCE(NULLIF(TRIM(b.verify_status), ''), 'unverified')) <> 'verified'",
|
||
"b.refuel_time >= '2026-05-01'",
|
||
);
|
||
return { clauses, params };
|
||
}
|