From d1d79f1c7cd65826d08ca5a32f577f4a2112b44e Mon Sep 17 00:00:00 2001 From: kkfluous Date: Wed, 29 Apr 2026 19:11:52 +0800 Subject: [PATCH] =?UTF-8?q?feat(energy):=20=E7=94=B5=E8=83=BD=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E5=88=87=E5=88=B0=20bi=5Fele=5Fcharge=5Frecord?= =?UTF-8?q?=EF=BC=8C=E5=A4=96=E9=83=A8=E6=95=B0=E6=8D=AE=E6=8E=A5=E9=80=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /api/energy/electric/overview & /electric/monthly 不再读 tab_energy_electricity_bill - 改读 bi_ele_charge_record:kwh/fee/start_time - 外部/我司用 vehicle_kind 区分(external/internal) - 电能默认 customer 由 'external' 改 'lingniu',与导入页约定一致 - ElectricDaily 移除「数据对接中…」友好空状态(外部已有数据) ele 导入页同步收紧: - 命中系统车辆=internal,未命中(含车牌为空)一律 external - 移除 unknown 分类、KPI 卡、批次列、过滤按钮、UploadResult 字段 - 历史 unknown 行已 UPDATE 为 external Co-Authored-By: Claude Opus 4.7 (1M context) --- src/modules/ele/EleImportPage.tsx | 22 +++---- src/modules/energy/ElectricDaily.tsx | 23 +------- src/server/routes/ele/index.ts | 15 +++-- src/server/routes/energy/index.ts | 88 ++++++++++++---------------- 4 files changed, 55 insertions(+), 93 deletions(-) diff --git a/src/modules/ele/EleImportPage.tsx b/src/modules/ele/EleImportPage.tsx index 150ec52..b554dc8 100644 --- a/src/modules/ele/EleImportPage.tsx +++ b/src/modules/ele/EleImportPage.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { motion, AnimatePresence } from 'motion/react'; import { Upload, FileSpreadsheet, RotateCcw, CheckCircle2, AlertCircle, - Truck, ExternalLink, HelpCircle, Layers, Zap, + Truck, ExternalLink, Layers, Zap, } from 'lucide-react'; import { fetchJson } from '../../auth/api-client'; import { useAuth } from '../../auth/useAuth'; @@ -19,7 +19,7 @@ interface UploadResult { fileDuplicates: number; inserted: number; dbDuplicates: number; - breakdown: { internal: number; external: number; unknown: number }; + breakdown: { internal: number; external: number }; } interface ListItem { @@ -44,18 +44,16 @@ interface ListItem { imported_at: string; } -interface OverallRow { vehicle_kind: 'internal' | 'external' | 'unknown'; records: number; total_kwh: number; total_fee: number; } -interface BatchRow { batch_id: string; imported_at: string; records: number; internal_count: number; external_count: number; unknown_count: number; total_kwh: number; total_fee: number; } +interface OverallRow { vehicle_kind: 'internal' | 'external'; records: number; total_kwh: number; total_fee: number; } +interface BatchRow { batch_id: string; imported_at: string; records: number; internal_count: number; external_count: number; total_kwh: number; total_fee: number; } const KIND_LABEL: Record = { internal: '内部', external: '外部', - unknown: '未知', }; const KIND_STYLE: Record = { internal: 'bg-blue-50 text-blue-600 border-blue-200', external: 'bg-amber-50 text-amber-600 border-amber-200', - unknown: 'bg-slate-50 text-slate-500 border-slate-200', }; async function uploadFile(file: File): Promise { @@ -82,7 +80,7 @@ export default function EleImportPage() { const [total, setTotal] = useState(0); const [overall, setOverall] = useState([]); const [batches, setBatches] = useState([]); - const [filter, setFilter] = useState<'' | 'internal' | 'external' | 'unknown'>(''); + const [filter, setFilter] = useState<'' | 'internal' | 'external'>(''); const [batchFilter, setBatchFilter] = useState(''); const [search, setSearch] = useState(''); const [searchInput, setSearchInput] = useState(''); @@ -204,7 +202,7 @@ export default function EleImportPage() { - + @@ -227,13 +225,11 @@ export default function EleImportPage() { {/* 聚合卡 */} -
+
} label="总记录" value={totalRecords.toLocaleString()} /> } label="内部记录" value={(overallMap.get('internal')?.records ?? 0).toLocaleString()} accent="blue" /> } label="外部记录" value={(overallMap.get('external')?.records ?? 0).toLocaleString()} accent="amber" /> - } label="未知" value={(overallMap.get('unknown')?.records ?? 0).toLocaleString()} accent="slate" /> } label="累计电量" value={`${totalKwh.toLocaleString('zh-CN', { maximumFractionDigits: 1 })} 度`} /> - } label="累计费用" value={`¥${totalFee.toLocaleString('zh-CN', { maximumFractionDigits: 2 })}`} /> } label="内部电量" value={`${(overallMap.get('internal')?.total_kwh ?? 0).toLocaleString('zh-CN', { maximumFractionDigits: 1 })} 度`} accent="blue" /> } label="外部电量" value={`${(overallMap.get('external')?.total_kwh ?? 0).toLocaleString('zh-CN', { maximumFractionDigits: 1 })} 度`} accent="amber" />
@@ -255,7 +251,6 @@ export default function EleImportPage() { 总数 内部 外部 - 未知 电量(度) 费用(元) 批次 @@ -272,7 +267,6 @@ export default function EleImportPage() { {Number(b.records).toLocaleString()} {Number(b.internal_count).toLocaleString()} {Number(b.external_count).toLocaleString()} - {Number(b.unknown_count).toLocaleString()} {Number(b.total_kwh ?? 0).toLocaleString('zh-CN', { maximumFractionDigits: 1 })} ¥{Number(b.total_fee ?? 0).toLocaleString('zh-CN', { maximumFractionDigits: 2 })} {b.batch_id.slice(0, 12)} @@ -299,7 +293,7 @@ export default function EleImportPage() { className="bg-white border border-slate-200 rounded-lg px-2 py-1 text-[11px] outline-none focus:ring-1 focus:ring-blue-500/20 w-44" />
- {([['', '全部'], ['internal', '内部'], ['external', '外部'], ['unknown', '未知']] as const).map(([k, label]) => ( + {([['', '全部'], ['internal', '内部'], ['external', '外部']] as const).map(([k, label]) => (
- {/* 外部数据对接中 友好空状态 */} - {customer === 'external' && months !== null && months.length === 0 && ( - -
- - - -
-
数据对接中…
-
- 外部充电账单正在与合作方系统打通,上线后此处将展示完整数据 -
-
- )} - {/* 月份分组表 */} - {!(customer === 'external' && months !== null && months.length === 0) && (
月份 / 日期 @@ -142,7 +122,6 @@ export default function ElectricDaily() { ); })}
- )}
); } diff --git a/src/server/routes/ele/index.ts b/src/server/routes/ele/index.ts index 6b26105..a3d9d8e 100644 --- a/src/server/routes/ele/index.ts +++ b/src/server/routes/ele/index.ts @@ -213,7 +213,8 @@ app.post('/import', async (c) => { const values = records.map(r => { const plate = r.values.plate || r.values.judgedPlate; const matchedId = plate ? plateMap.get(plate) || null : null; - const kind = matchedId ? 'internal' : (plate ? 'external' : 'unknown'); + // 命中系统车辆=internal;其余(含车牌为空)一律 external + const kind = matchedId ? 'internal' : 'external'; return [ r.orderNo, r.values.stationNo, r.values.stationName, r.values.terminalName, @@ -234,13 +235,12 @@ app.post('/import', async (c) => { const inserted = result.affectedRows; const dbDuplicates = records.length - inserted; - // 统计内/外/未知 - let internal = 0, external = 0, unknown = 0; + // 统计内/外(无车牌也算外部) + let internal = 0, external = 0; for (const r of records) { const plate = r.values.plate || r.values.judgedPlate; if (plate && plateMap.has(plate)) internal++; - else if (plate) external++; - else unknown++; + else external++; } return c.json({ @@ -251,7 +251,7 @@ app.post('/import', async (c) => { fileDuplicates, inserted, dbDuplicates, - breakdown: { internal, external, unknown }, + breakdown: { internal, external }, }); }); @@ -268,7 +268,7 @@ app.get('/list', async (c) => { const where: string[] = ['1=1']; const params: (string | number)[] = []; - if (kind === 'internal' || kind === 'external' || kind === 'unknown') { + if (kind === 'internal' || kind === 'external') { where.push('vehicle_kind = ?'); params.push(kind); } @@ -313,7 +313,6 @@ app.get('/batches', async (c) => { COUNT(*) AS records, SUM(CASE WHEN vehicle_kind='internal' THEN 1 ELSE 0 END) AS internal_count, SUM(CASE WHEN vehicle_kind='external' THEN 1 ELSE 0 END) AS external_count, - SUM(CASE WHEN vehicle_kind='unknown' THEN 1 ELSE 0 END) AS unknown_count, ROUND(SUM(kwh), 2) AS total_kwh, ROUND(SUM(fee), 2) AS total_fee FROM bi_ele_charge_record diff --git a/src/server/routes/energy/index.ts b/src/server/routes/energy/index.ts index e36c807..24979ba 100644 --- a/src/server/routes/energy/index.ts +++ b/src/server/routes/energy/index.ts @@ -244,24 +244,21 @@ app.get('/hydrogen/daily', async (c) => { }); // ========================================================= -// 电能 总览:KPI + 本月每日柱图数据 +// 电能 总览:KPI + 本月每日柱图数据 —— 数据源:bi_ele_charge_record // ========================================================= app.get('/electric/overview', async (c) => { const data = await cached('electric/overview', async () => { const [kpiRows] = await pool.query( `SELECT - SUM(charging_degree) AS totalKwh, - SUM(cost_expense) AS totalFee, - SUM(CASE WHEN DATE_FORMAT(${ELECTRIC_LOCAL}, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m') - THEN charging_degree ELSE 0 END) AS monthKwh, - SUM(CASE WHEN DATE_FORMAT(${ELECTRIC_LOCAL}, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m') - THEN cost_expense ELSE 0 END) AS monthFee, - SUM(CASE WHEN DATE(${ELECTRIC_LOCAL}) = CURDATE() - THEN charging_degree ELSE 0 END) AS todayKwh, - SUM(CASE WHEN DATE(${ELECTRIC_LOCAL}) = CURDATE() - THEN cost_expense ELSE 0 END) AS todayFee - FROM tab_energy_electricity_bill - WHERE is_deleted = 0`, + 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 + FROM bi_ele_charge_record`, ); const k = kpiRows[0] ?? {}; const totalKwh = Number(k.totalKwh) || 0; @@ -273,29 +270,25 @@ app.get('/electric/overview', async (c) => { // 本月每日(用于柱图) const [trendRows] = await pool.query( - `SELECT DATE_FORMAT(${ELECTRIC_LOCAL}, '%Y-%m-%d') AS date, - SUM(charging_degree) AS kwh, - SUM(cost_expense) AS fee - FROM tab_energy_electricity_bill - WHERE is_deleted = 0 - AND DATE_FORMAT(${ELECTRIC_LOCAL}, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m') + `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`, ); - // 若本月无数据(电能数据滞后),降级展示最近一个有数据的自然月 + // 若本月无数据,降级展示最近一个有数据的自然月 let trend = trendRows; if (trend.length === 0) { const [fallback] = await pool.query( - `SELECT DATE_FORMAT(${ELECTRIC_LOCAL}, '%Y-%m-%d') AS date, - SUM(charging_degree) AS kwh, - SUM(cost_expense) AS fee - FROM tab_energy_electricity_bill - WHERE is_deleted = 0 - AND DATE_FORMAT(${ELECTRIC_LOCAL}, '%Y-%m') = ( - SELECT DATE_FORMAT(MAX(${ELECTRIC_LOCAL}), '%Y-%m') - FROM tab_energy_electricity_bill - WHERE is_deleted = 0 - ) + `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`, ); @@ -307,20 +300,17 @@ app.get('/electric/overview', async (c) => { fee: Math.round((Number(r.fee) || 0) * 100) / 100, chainPct: 0, })); - // 计算环比 for (let i = 1; i < trendArr.length; i++) { const prev = trendArr[i - 1].kwh; trendArr[i].chainPct = prev > 0 ? (trendArr[i].kwh - prev) / prev : 0; } - // 今日环比 = 今日 kwh / 上一个有数据的自然日 kwh - 1 let todayChainPct = 0; if (todayKwh > 0) { const [prevRow] = await pool.query( - `SELECT SUM(charging_degree) AS kwh - FROM tab_energy_electricity_bill - WHERE is_deleted = 0 - AND DATE(${ELECTRIC_LOCAL}) = DATE_SUB(CURDATE(), INTERVAL 1 DAY)`, + `SELECT SUM(kwh) AS kwh + FROM bi_ele_charge_record + WHERE DATE(start_time) = DATE_SUB(CURDATE(), INTERVAL 1 DAY)`, ); const prevKwh = Number(prevRow[0]?.kwh) || 0; todayChainPct = prevKwh > 0 ? (todayKwh - prevKwh) / prevKwh : 0; @@ -335,27 +325,27 @@ app.get('/electric/overview', async (c) => { }); // ========================================================= -// 电能 每日:月份分组 + 日级行 +// 电能 每日:月份分组 + 日级行 —— 数据源:bi_ele_charge_record // ========================================================= app.get('/electric/monthly', async (c) => { - const customer = (c.req.query('customer') || 'external') as CustomerKind; + const customer = (c.req.query('customer') || 'lingniu') as CustomerKind; const data = await cached(`electric/monthly?customer=${customer}`, async () => { - const where = [ - 'is_deleted = 0', - customerClause('truck_id', customer), - ].join(' AND '); + // bi_ele_charge_record 用 vehicle_kind 区分:internal=我司,external=外部 + let kindClause = '1=1'; + if (customer === 'lingniu') kindClause = `vehicle_kind = 'internal'`; + if (customer === 'external') kindClause = `vehicle_kind = 'external'`; // 取最近 6 个月 const [rows] = await pool.query( - `SELECT DATE_FORMAT(${ELECTRIC_LOCAL}, '%Y-%m') AS month, - DATE_FORMAT(${ELECTRIC_LOCAL}, '%Y-%m-%d') AS date, - SUM(charging_degree) AS kwh, - SUM(cost_expense) AS fee - FROM tab_energy_electricity_bill - WHERE ${where} - AND ${ELECTRIC_LOCAL} >= DATE_SUB(CURDATE(), INTERVAL 6 MONTH) + `SELECT DATE_FORMAT(start_time, '%Y-%m') AS month, + DATE_FORMAT(start_time, '%Y-%m-%d') AS date, + SUM(kwh) AS kwh, + SUM(fee) AS fee + FROM bi_ele_charge_record + WHERE ${kindClause} + AND start_time >= DATE_SUB(CURDATE(), INTERVAL 6 MONTH) GROUP BY month, date ORDER BY date DESC`, );