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(sql: string, values?: any[]): Promise<[T, ...any[]]>; } // --------------------------------------------------------------------------- // ETC // --------------------------------------------------------------------------- /** ETC 台账与账单汇总;空表返回全 0,由路由判定 hasData。 */ export async function loadEtcOverview(db: Database): Promise> { const [rows] = await db.query( `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; } // --------------------------------------------------------------------------- // 电能 // --------------------------------------------------------------------------- /** 电能总览 KPI(全量 + 本月 + 本日)。 */ export async function loadElectricKpi(db: Database): Promise> { const [rows] = await db.query( `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; } /** 本月逐日电量/费用(柱图)。 */ export async function loadElectricCurrentMonthTrend(db: Database): Promise { const [rows] = await db.query( `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 { const [rows] = await db.query( `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 { const [rows] = await db.query( `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 { const [rows] = await db.query( `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 { const { start, end } = args; const [rows] = await db.query(`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 { const { start, end } = args; const [rows] = await db.query(`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 { const { start, end } = args; const [rows] = await db.query(`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 { const { start, end } = args; const [rows] = await db.query(`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 { const { start, end } = args; const [rows] = await db.query(`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 { const { stationId, start, end } = args; const [rows] = await db.query(`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 { const { stationId, start, end } = args; const [rows] = await db.query(`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 { const { stationId, end } = args; const [rows] = await db.query(`${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 { const { stationId, end } = args; const [rows] = await db.query(`${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 { const { start, end } = args; const [rows] = await db.query(`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( `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 { const [rows] = await db.query(`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 { const [rows] = await db.query(`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 { const [rows] = await db.query(`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 { const { sqlWhere, params, month, endDate } = args; const [rows] = await db.query(`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 { const { sqlWhere, params } = args; const [rows] = await db.query(`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 { const { sqlWhere, params } = args; const [rows] = await db.query(`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 { const { sqlWhere, params } = args; const [rows] = await db.query(`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 { const { sqlWhere, params } = args; const [rows] = await db.query(`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 { const { sqlWhere, params } = args; const [rows] = await db.query(`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 { const { sqlWhere, params } = args; const [rows] = await db.query(`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 { const { sqlWhere, params } = args; const [rows] = await db.query(`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 { const { scopedWhere, params } = args; const [rows] = await db.query(`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 { const { scopedWhere, params, groupSelect, groupExpression, groupHaving, groupOrder, pageSize, offset } = args; const [rows] = await db.query(`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 { const { scopedWhere, params, pageSize, offset } = args; const [rows] = await db.query(`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 }; }