From 1d2c3a0cd5826dda361491a962101f057878807d Mon Sep 17 00:00:00 2001 From: kkfluous Date: Thu, 28 May 2026 16:47:19 +0800 Subject: [PATCH] Switch hydrogen BI to ledger data source --- src/server/hydrogen-db.ts | 17 +++ src/server/routes/energy/index.ts | 222 +++++++++++++++--------------- 2 files changed, 129 insertions(+), 110 deletions(-) create mode 100644 src/server/hydrogen-db.ts diff --git a/src/server/hydrogen-db.ts b/src/server/hydrogen-db.ts new file mode 100644 index 0000000..ff300c0 --- /dev/null +++ b/src/server/hydrogen-db.ts @@ -0,0 +1,17 @@ +import mysql from 'mysql2/promise'; +import dotenv from 'dotenv'; + +dotenv.config(); + +const hydrogenPool = mysql.createPool({ + host: process.env.HYDROGEN_DB_HOST || '47.99.185.173', + port: Number(process.env.HYDROGEN_DB_PORT) || 3306, + user: process.env.HYDROGEN_DB_USER || 'root', + password: process.env.HYDROGEN_DB_PASSWORD || 'lnMysql.', + database: process.env.HYDROGEN_DB_NAME || 'ln_asset_management', + waitForConnections: true, + connectionLimit: 5, + queueLimit: 0, +}); + +export default hydrogenPool; diff --git a/src/server/routes/energy/index.ts b/src/server/routes/energy/index.ts index 10bc903..e7b5a7e 100644 --- a/src/server/routes/energy/index.ts +++ b/src/server/routes/energy/index.ts @@ -1,6 +1,7 @@ import { Hono } from 'hono'; import type { RowDataPacket } from 'mysql2'; import pool from '../../db.js'; +import hydrogenPool from '../../hydrogen-db.js'; import { cached } from './cache.js'; import type { AuthUser } from '../../auth/types.js'; import { canAccessEnergy } from '../../auth/types.js'; @@ -19,16 +20,19 @@ app.use('*', async (c, next) => { const HYDROGEN_MIN_DATE = '2024-01-01'; -// hydrogen_time 已是 CST 字面值,直接使用即可(不再 +8 小时) -const HYDROGEN_LOCAL = `hydrogen_time`; +// hydrogen_fuel_ledger.refuel_time 已是业务本地时间字面值,直接使用即可(不再 +8 小时) +const HYDROGEN_TABLE = 'hydrogen_fuel_ledger'; +const HYDROGEN_LOCAL = `refuel_time`; +const HYDROGEN_BASE_WHERE = `del_flag = '0' AND is_duplicate = 0`; +const HYDROGEN_BASE_WHERE_B = `b.del_flag = '0' AND b.is_duplicate = 0`; const ELECTRIC_LOCAL = `charging_start_time`; type CustomerKind = 'external' | 'lingniu' | 'all'; -// 外部/我司判定:truck_id 为空 = 外部;truck_id 非空 = 我司(羚牛车辆) -function customerClause(field: string, customer: CustomerKind): string { - if (customer === 'external') return `${field} IS NULL`; - if (customer === 'lingniu') return `${field} IS NOT NULL`; +// 新账本没有旧表 truck_id 空/非空口径;按客户是否计费区分:计费=外部,未计费=羚牛承担。 +function customerClause(customer: CustomerKind): string { + if (customer === 'external') return `(COALESCE(customer_price, 0) > 0 OR COALESCE(fee_total, 0) > 0)`; + if (customer === 'lingniu') return `(COALESCE(customer_price, 0) <= 0 AND COALESCE(fee_total, 0) <= 0)`; return '1=1'; } @@ -80,10 +84,10 @@ app.get('/hydrogen/overview', async (c) => { const data = await cached(`hydrogen/overview?year=${requestedYear}`, async () => { // 可选年份(数据自 HYDROGEN_MIN_DATE 起) - const [yearListRows] = await pool.query( + const [yearListRows] = await hydrogenPool.query( `SELECT DISTINCT YEAR(${HYDROGEN_LOCAL}) AS y - FROM tab_energy_hydrogen_bill - WHERE is_deleted = 0 AND ${HYDROGEN_LOCAL} >= ? + FROM ${HYDROGEN_TABLE} + WHERE ${HYDROGEN_BASE_WHERE} AND ${HYDROGEN_LOCAL} >= ? ORDER BY y DESC`, [HYDROGEN_MIN_DATE], ); @@ -92,44 +96,46 @@ app.get('/hydrogen/overview', async (c) => { const isCurrentYear = year === todayYear; // KPI(按 year 分桶;月/日仅在 isCurrentYear 时取本月/今日) - const [kpiRows] = await pool.query( + const [kpiRows] = await hydrogenPool.query( `SELECT SUM(CASE WHEN YEAR(${HYDROGEN_LOCAL}) = ? - THEN hydrogen_quantity ELSE 0 END) AS yearKg, + THEN amount_kg ELSE 0 END) AS yearKg, SUM(CASE WHEN YEAR(${HYDROGEN_LOCAL}) = ? - THEN cost_expense ELSE 0 END) AS yearFee, - SUM(CASE WHEN YEAR(${HYDROGEN_LOCAL}) = ? AND cost_type = 2 - THEN cost_expense ELSE 0 END) AS yearCustomerCost, + THEN cost_total ELSE 0 END) AS yearFee, + SUM(CASE WHEN YEAR(${HYDROGEN_LOCAL}) = ? AND (COALESCE(customer_price, 0) > 0 OR COALESCE(fee_total, 0) > 0) + THEN cost_total ELSE 0 END) AS yearCustomerCost, SUM(CASE WHEN YEAR(${HYDROGEN_LOCAL}) = ? - THEN customer_expense ELSE 0 END) AS yearRevenue, - SUM(CASE WHEN YEAR(${HYDROGEN_LOCAL}) = ? AND cost_type = 3 - THEN hydrogen_quantity ELSE 0 END) AS ourYearKg, - SUM(CASE WHEN YEAR(${HYDROGEN_LOCAL}) = ? AND cost_type = 3 - THEN cost_expense ELSE 0 END) AS ourYearFee, - SUM(CASE WHEN YEAR(${HYDROGEN_LOCAL}) = ? AND cost_type = 2 - THEN hydrogen_quantity ELSE 0 END) AS customerYearKg, + THEN fee_total ELSE 0 END) AS yearRevenue, + SUM(CASE WHEN YEAR(${HYDROGEN_LOCAL}) = ? AND COALESCE(customer_price, 0) <= 0 AND COALESCE(fee_total, 0) <= 0 + THEN amount_kg ELSE 0 END) AS ourYearKg, + SUM(CASE WHEN YEAR(${HYDROGEN_LOCAL}) = ? AND COALESCE(customer_price, 0) <= 0 AND COALESCE(fee_total, 0) <= 0 + THEN cost_total ELSE 0 END) AS ourYearFee, + SUM(CASE WHEN YEAR(${HYDROGEN_LOCAL}) = ? AND (COALESCE(customer_price, 0) > 0 OR COALESCE(fee_total, 0) > 0) + THEN amount_kg ELSE 0 END) AS customerYearKg, SUM(CASE WHEN ? = 1 AND DATE_FORMAT(${HYDROGEN_LOCAL}, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m') - THEN hydrogen_quantity ELSE 0 END) AS monthKg, + THEN amount_kg ELSE 0 END) AS monthKg, SUM(CASE WHEN ? = 1 AND DATE_FORMAT(${HYDROGEN_LOCAL}, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m') - THEN cost_expense ELSE 0 END) AS monthFee, - SUM(CASE WHEN ? = 1 AND DATE_FORMAT(${HYDROGEN_LOCAL}, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m') AND cost_type = 2 - THEN cost_expense ELSE 0 END) AS monthCustomerCost, + THEN cost_total ELSE 0 END) AS monthFee, SUM(CASE WHEN ? = 1 AND DATE_FORMAT(${HYDROGEN_LOCAL}, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m') - THEN customer_expense ELSE 0 END) AS monthRevenue, + AND (COALESCE(customer_price, 0) > 0 OR COALESCE(fee_total, 0) > 0) + THEN cost_total ELSE 0 END) AS monthCustomerCost, + SUM(CASE WHEN ? = 1 AND DATE_FORMAT(${HYDROGEN_LOCAL}, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m') + THEN fee_total ELSE 0 END) AS monthRevenue, SUM(CASE WHEN ? = 1 AND DATE(${HYDROGEN_LOCAL}) = CURDATE() - THEN hydrogen_quantity ELSE 0 END) AS todayKg, + THEN amount_kg ELSE 0 END) AS todayKg, SUM(CASE WHEN ? = 1 AND DATE(${HYDROGEN_LOCAL}) = CURDATE() - THEN cost_expense ELSE 0 END) AS todayFee, - SUM(CASE WHEN ? = 1 AND DATE(${HYDROGEN_LOCAL}) = CURDATE() AND cost_type = 2 - THEN cost_expense ELSE 0 END) AS todayCustomerCost, + THEN cost_total ELSE 0 END) AS todayFee, SUM(CASE WHEN ? = 1 AND DATE(${HYDROGEN_LOCAL}) = CURDATE() - THEN customer_expense ELSE 0 END) AS todayRevenue, - SUM(CASE WHEN truck_id IS NOT NULL - THEN hydrogen_quantity ELSE 0 END) AS lingniuBornKg, - SUM(CASE WHEN truck_id IS NOT NULL - THEN cost_expense ELSE 0 END) AS lingniuBornFee - FROM tab_energy_hydrogen_bill - WHERE is_deleted = 0 AND ${HYDROGEN_LOCAL} >= ?`, + AND (COALESCE(customer_price, 0) > 0 OR COALESCE(fee_total, 0) > 0) + THEN cost_total ELSE 0 END) AS todayCustomerCost, + SUM(CASE WHEN ? = 1 AND DATE(${HYDROGEN_LOCAL}) = CURDATE() + THEN fee_total ELSE 0 END) AS todayRevenue, + SUM(CASE WHEN vehicle_id IS NOT NULL + THEN amount_kg ELSE 0 END) AS lingniuBornKg, + SUM(CASE WHEN vehicle_id IS NOT NULL + THEN cost_total ELSE 0 END) AS lingniuBornFee + FROM ${HYDROGEN_TABLE} + WHERE ${HYDROGEN_BASE_WHERE} AND ${HYDROGEN_LOCAL} >= ?`, [year, year, year, year, year, year, year, isCurrentYear ? 1 : 0, isCurrentYear ? 1 : 0, isCurrentYear ? 1 : 0, isCurrentYear ? 1 : 0, isCurrentYear ? 1 : 0, isCurrentYear ? 1 : 0, isCurrentYear ? 1 : 0, isCurrentYear ? 1 : 0, @@ -166,23 +172,19 @@ app.get('/hydrogen/overview', async (c) => { }; // Top5 加氢站(指定年份) - const [top5Rows] = await pool.query( - `SELECT b.hydrogen_station_id AS id, - COALESCE(MAX(s.short_name), MAX(s.name), - MAX(os.fixed_station_name), MAX(os.station_name), - MAX(i.hydrogen_station_name), - CASE WHEN b.hydrogen_station_id IS NULL THEN '未关联站点' - ELSE CONCAT('未知站点 #', b.hydrogen_station_id) END) AS name, - SUM(b.hydrogen_quantity) AS kg, - SUM(b.cost_expense) AS fee - FROM tab_energy_hydrogen_bill b - LEFT JOIN tab_hydrogen_site s ON s.id = b.hydrogen_station_id - LEFT JOIN tab_outside_hydrogen_site os ON os.inner_site_id = b.hydrogen_station_id - LEFT JOIN tab_import_hydrogen_order i ON i.bill_code = b.bill_code - WHERE b.is_deleted = 0 + const [top5Rows] = await hydrogenPool.query( + `SELECT b.station_id AS id, + COALESCE(MAX(s.station_short_name), MAX(s.station_name), MAX(b.station_name), + CASE WHEN b.station_id IS NULL THEN '未关联站点' + ELSE CONCAT('未知站点 #', b.station_id) END) AS name, + SUM(b.amount_kg) AS kg, + SUM(b.cost_total) AS fee + FROM ${HYDROGEN_TABLE} b + LEFT JOIN hydrogen_station s ON s.id = b.station_id AND s.del_flag = '0' + WHERE ${HYDROGEN_BASE_WHERE_B} AND b.${HYDROGEN_LOCAL} >= ? AND YEAR(b.${HYDROGEN_LOCAL}) = ? - GROUP BY b.hydrogen_station_id + GROUP BY b.station_id ORDER BY kg DESC LIMIT 5`, [HYDROGEN_MIN_DATE, year], @@ -197,23 +199,19 @@ app.get('/hydrogen/overview', async (c) => { })); // 加氢站全量汇总(同年所有站,按加氢量降序) - const [stationFullRows] = await pool.query( - `SELECT b.hydrogen_station_id AS id, - COALESCE(MAX(s.short_name), MAX(s.name), - MAX(os.fixed_station_name), MAX(os.station_name), - MAX(i.hydrogen_station_name), - CASE WHEN b.hydrogen_station_id IS NULL THEN '未关联站点' - ELSE CONCAT('未知站点 #', b.hydrogen_station_id) END) AS name, - SUM(b.hydrogen_quantity) AS kg, - SUM(b.customer_expense) AS revenue - FROM tab_energy_hydrogen_bill b - LEFT JOIN tab_hydrogen_site s ON s.id = b.hydrogen_station_id - LEFT JOIN tab_outside_hydrogen_site os ON os.inner_site_id = b.hydrogen_station_id - LEFT JOIN tab_import_hydrogen_order i ON i.bill_code = b.bill_code - WHERE b.is_deleted = 0 + const [stationFullRows] = await hydrogenPool.query( + `SELECT b.station_id AS id, + COALESCE(MAX(s.station_short_name), MAX(s.station_name), MAX(b.station_name), + CASE WHEN b.station_id IS NULL THEN '未关联站点' + ELSE CONCAT('未知站点 #', b.station_id) END) AS name, + SUM(b.amount_kg) AS kg, + SUM(b.fee_total) AS revenue + FROM ${HYDROGEN_TABLE} b + LEFT JOIN hydrogen_station s ON s.id = b.station_id AND s.del_flag = '0' + WHERE ${HYDROGEN_BASE_WHERE_B} AND b.${HYDROGEN_LOCAL} >= ? AND YEAR(b.${HYDROGEN_LOCAL}) = ? - GROUP BY b.hydrogen_station_id + GROUP BY b.station_id ORDER BY kg DESC`, [HYDROGEN_MIN_DATE, year], ); @@ -228,14 +226,22 @@ app.get('/hydrogen/overview', async (c) => { })); // 区域占比(按城市,指定年份)— 取前 8,其余合并为"其他" - const [regionRows] = await pool.query( + const [regionRows] = await hydrogenPool.query( `SELECT region, SUM(kg) AS kg FROM ( - SELECT REPLACE(REPLACE(SUBSTRING_INDEX(COALESCE(s.city, os.city, '未知'), '-', -1), '市', ''), '省', '') AS region, - b.hydrogen_quantity AS kg - FROM tab_energy_hydrogen_bill b - LEFT JOIN tab_hydrogen_site s ON s.id = b.hydrogen_station_id - LEFT JOIN tab_outside_hydrogen_site os ON os.inner_site_id = b.hydrogen_station_id - WHERE b.is_deleted = 0 + SELECT CASE + WHEN COALESCE(s.station_name, b.station_name, '') LIKE '%嘉兴%' OR COALESCE(s.station_name, b.station_name, '') LIKE '%平湖%' THEN '嘉兴' + WHEN COALESCE(s.station_name, b.station_name, '') LIKE '%广州%' THEN '广州' + WHEN COALESCE(s.station_name, b.station_name, '') LIKE '%佛山%' THEN '佛山' + WHEN COALESCE(s.station_name, b.station_name, '') LIKE '%成都%' THEN '成都' + WHEN COALESCE(s.station_name, b.station_name, '') LIKE '%重庆%' THEN '重庆' + WHEN COALESCE(s.station_name, b.station_name, '') LIKE '%乌鲁木齐%' THEN '乌鲁木齐' + WHEN COALESCE(s.station_name, b.station_name, '') LIKE '%昆山%' THEN '昆山' + ELSE COALESCE(NULLIF(s.station_name, ''), NULLIF(b.station_name, ''), '未知') + END AS region, + b.amount_kg AS kg + FROM ${HYDROGEN_TABLE} b + LEFT JOIN hydrogen_station s ON s.id = b.station_id AND s.del_flag = '0' + WHERE ${HYDROGEN_BASE_WHERE_B} AND b.${HYDROGEN_LOCAL} >= ? AND YEAR(b.${HYDROGEN_LOCAL}) = ? ) r @@ -257,15 +263,15 @@ app.get('/hydrogen/overview', async (c) => { ]; // 月度趋势(指定年份内 12 个月,缺失月补 0)含成本/收入/利润 - // 利润 = 客户单收入 - 客户单成本(仅 cost_type = 2) - const [monthRows] = await pool.query( + // 利润 = 客户单收入 - 客户单成本(按 customer_price/fee_total 判断客户承担) + const [monthRows] = await hydrogenPool.query( `SELECT DATE_FORMAT(${HYDROGEN_LOCAL}, '%Y-%m') AS m, - ROUND(SUM(hydrogen_quantity), 2) AS kg, - ROUND(SUM(cost_expense), 2) AS fee, - ROUND(SUM(CASE WHEN cost_type = 2 THEN cost_expense ELSE 0 END), 2) AS customerCost, - ROUND(SUM(customer_expense), 2) AS revenue - FROM tab_energy_hydrogen_bill - WHERE is_deleted = 0 + ROUND(SUM(amount_kg), 2) AS kg, + ROUND(SUM(cost_total), 2) AS fee, + ROUND(SUM(CASE WHEN COALESCE(customer_price, 0) > 0 OR COALESCE(fee_total, 0) > 0 THEN cost_total ELSE 0 END), 2) AS customerCost, + ROUND(SUM(fee_total), 2) AS revenue + FROM ${HYDROGEN_TABLE} + WHERE ${HYDROGEN_BASE_WHERE} AND ${HYDROGEN_LOCAL} >= ? AND YEAR(${HYDROGEN_LOCAL}) = ? GROUP BY m @@ -290,16 +296,16 @@ app.get('/hydrogen/overview', async (c) => { } // 客户账单 Top(指定年份;按加氢量降序,前 30) - // payer:cost_type=2 → 客户承担;cost_type=3 → 羚牛承担;其他 → 客户(默认) - const [customerRows] = await pool.query( + // payer:有客户单价/收入 → 客户承担;否则 → 羚牛承担 + const [customerRows] = await hydrogenPool.query( `SELECT COALESCE(NULLIF(TRIM(customer_name), ''), '未指定客户') AS name, - CASE WHEN MAX(cost_type) = 3 AND MIN(cost_type) = 3 THEN 'lingniu' + CASE WHEN MAX(COALESCE(customer_price, 0)) <= 0 AND MAX(COALESCE(fee_total, 0)) <= 0 THEN 'lingniu' ELSE 'customer' END AS payer, - SUM(hydrogen_quantity) AS kg, - SUM(cost_expense) AS cost, - SUM(customer_expense) AS revenue - FROM tab_energy_hydrogen_bill - WHERE is_deleted = 0 + SUM(amount_kg) AS kg, + SUM(cost_total) AS cost, + SUM(fee_total) AS revenue + FROM ${HYDROGEN_TABLE} + WHERE ${HYDROGEN_BASE_WHERE} AND ${HYDROGEN_LOCAL} >= ? AND YEAR(${HYDROGEN_LOCAL}) = ? GROUP BY name @@ -331,32 +337,28 @@ app.get('/hydrogen/daily', async (c) => { const data = await cached(`hydrogen/daily?range=${range}&customer=${customer}`, async () => { const where = [ - 'b.is_deleted = 0', - `b.hydrogen_time >= '${HYDROGEN_MIN_DATE}'`, - rangeClause(`b.hydrogen_time`, range), - customerClause('b.truck_id', customer), + HYDROGEN_BASE_WHERE_B, + `b.${HYDROGEN_LOCAL} >= '${HYDROGEN_MIN_DATE}'`, + rangeClause(`b.${HYDROGEN_LOCAL}`, range), + customerClause(customer).replaceAll('customer_price', 'b.customer_price').replaceAll('fee_total', 'b.fee_total'), ].join(' AND '); // 站点级聚合(每日 × 每站)。前端组装成 day → stations - // 站点名 fallback:内部站表 → 外部站表 → 导入订单表(tab_import_hydrogen_order,按 bill_code 关联) - // 单价不重算:同价组显示原价,混合价组返回 NULL,前端显示「—」 - const [stationRows] = await pool.query( - `SELECT DATE_FORMAT(b.hydrogen_time, '%Y-%m-%d') AS d, - b.hydrogen_station_id AS stationId, - COALESCE(MAX(s.short_name), MAX(s.name), - MAX(os.fixed_station_name), MAX(os.station_name), - MAX(i.hydrogen_station_name), - CASE WHEN b.hydrogen_station_id IS NULL THEN '未关联站点' - ELSE CONCAT('未知站点 #', b.hydrogen_station_id) END) AS stationName, - ROUND(SUM(b.hydrogen_quantity), 2) AS kg, + // 站点名 fallback:站点主数据 → 账本冗余站点名 → 未关联站点 + // 单价不重算:直接取账本成本价。 + const [stationRows] = await hydrogenPool.query( + `SELECT DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m-%d') AS d, + COALESCE(b.station_id, 0) AS stationId, + COALESCE(MAX(s.station_short_name), MAX(s.station_name), MAX(b.station_name), + CASE WHEN MAX(b.station_id) IS NULL THEN '未关联站点' + ELSE CONCAT('未知站点 #', MAX(b.station_id)) END) AS stationName, + ROUND(SUM(b.amount_kg), 2) AS kg, -- 单价:直接取订单中的成本价(不重算)。MAX 自然忽略 0 元的免费/赠送单 MAX(b.cost_price) AS pricePerKg - FROM tab_energy_hydrogen_bill b - LEFT JOIN tab_hydrogen_site s ON s.id = b.hydrogen_station_id - LEFT JOIN tab_outside_hydrogen_site os ON os.inner_site_id = b.hydrogen_station_id - LEFT JOIN tab_import_hydrogen_order i ON i.bill_code = b.bill_code + FROM ${HYDROGEN_TABLE} b + LEFT JOIN hydrogen_station s ON s.id = b.station_id AND s.del_flag = '0' WHERE ${where} - GROUP BY d, b.hydrogen_station_id + GROUP BY d, COALESCE(b.station_id, 0) ORDER BY d DESC, kg DESC`, );