Switch hydrogen BI to ledger data source
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
kkfluous
2026-05-28 16:47:19 +08:00
parent e7ba5315e1
commit 1d2c3a0cd5
2 changed files with 129 additions and 110 deletions

17
src/server/hydrogen-db.ts Normal file
View File

@@ -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;

View File

@@ -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<RowDataPacket[]>(
const [yearListRows] = await hydrogenPool.query<RowDataPacket[]>(
`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<RowDataPacket[]>(
const [kpiRows] = await hydrogenPool.query<RowDataPacket[]>(
`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<RowDataPacket[]>(
`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<RowDataPacket[]>(
`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<RowDataPacket[]>(
`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<RowDataPacket[]>(
`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<RowDataPacket[]>(
const [regionRows] = await hydrogenPool.query<RowDataPacket[]>(
`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<RowDataPacket[]>(
// 利润 = 客户单收入 - 客户单成本( customer_price/fee_total 判断客户承担
const [monthRows] = await hydrogenPool.query<RowDataPacket[]>(
`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
// payercost_type=2 → 客户承担cost_type=3 → 羚牛承担;其他 → 客户(默认)
const [customerRows] = await pool.query<RowDataPacket[]>(
// payer有客户单价/收入 → 客户承担;否则 → 羚牛承担
const [customerRows] = await hydrogenPool.query<RowDataPacket[]>(
`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<RowDataPacket[]>(
`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<RowDataPacket[]>(
`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`,
);