refactor: add electric order drilldown
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
parseHydrogenCustomerKind,
|
||||
parseHydrogenCustomerName,
|
||||
parseHydrogenStationId,
|
||||
parseEnergyDate,
|
||||
type HydrogenCustomerKind,
|
||||
} from './query.js';
|
||||
import type { AuthUser } from '../../auth/types.js';
|
||||
@@ -592,6 +593,81 @@ app.get('/electric/overview', async (c) => {
|
||||
return c.json(data);
|
||||
});
|
||||
|
||||
// =========================================================
|
||||
// 电能 订单下钻:指定自然日 + 车辆归属,最多返回 500 条明细
|
||||
// =========================================================
|
||||
app.get('/electric/orders', async (c) => {
|
||||
const dateParam = c.req.query('date');
|
||||
const date = parseEnergyDate(dateParam);
|
||||
if (date === null) return c.json({ error: 'date 必须是有效的 YYYY-MM-DD 日期' }, 400);
|
||||
|
||||
const customerParam = c.req.query('customer');
|
||||
if (customerParam !== undefined && customerParam !== 'lingniu' && customerParam !== 'external') {
|
||||
return c.json({ error: 'customer 必须是 lingniu 或 external' }, 400);
|
||||
}
|
||||
const customer = customerParam === 'external' ? 'external' : 'lingniu';
|
||||
const vehicleKind = customer === 'lingniu' ? 'internal' : 'external';
|
||||
|
||||
const data = await cached(`electric/orders?date=${date}&customer=${customer}`, async () => {
|
||||
const params = [date, date, vehicleKind];
|
||||
const [[summaryRows], [detailRows]] = await Promise.all([
|
||||
pool.query<RowDataPacket[]>(
|
||||
`SELECT COUNT(*) AS recordCount,
|
||||
SUM(kwh) AS totalKwh,
|
||||
SUM(fee) AS totalFee
|
||||
FROM bi_ele_charge_record
|
||||
WHERE start_time >= ? AND start_time < DATE_ADD(?, INTERVAL 1 DAY)
|
||||
AND vehicle_kind = ?`,
|
||||
params,
|
||||
),
|
||||
pool.query<RowDataPacket[]>(
|
||||
`SELECT id,
|
||||
order_no AS orderNo,
|
||||
DATE_FORMAT(start_time, '%Y-%m-%d %H:%i:%s') AS startTime,
|
||||
COALESCE(NULLIF(TRIM(station_name), ''), '未指定站点') AS stationName,
|
||||
COALESCE(NULLIF(TRIM(matched_plate), ''), NULLIF(TRIM(judged_plate), ''), NULLIF(TRIM(plate), ''), '未识别车辆') AS plate,
|
||||
vehicle_kind AS vehicleKind,
|
||||
COALESCE(NULLIF(TRIM(order_status), ''), '未指定状态') AS orderStatus,
|
||||
kwh,
|
||||
e_fee AS electricityFee,
|
||||
service_fee AS serviceFee,
|
||||
fee AS totalFee
|
||||
FROM bi_ele_charge_record
|
||||
WHERE start_time >= ? AND start_time < DATE_ADD(?, INTERVAL 1 DAY)
|
||||
AND vehicle_kind = ?
|
||||
ORDER BY start_time DESC, id DESC
|
||||
LIMIT 501`,
|
||||
params,
|
||||
),
|
||||
]);
|
||||
const summary = summaryRows[0] ?? {};
|
||||
const truncated = detailRows.length > 500;
|
||||
return {
|
||||
date,
|
||||
vehicleScope: customer,
|
||||
recordCount: Number(summary.recordCount) || 0,
|
||||
totalKwh: Math.round((Number(summary.totalKwh) || 0) * 100) / 100,
|
||||
totalFee: Math.round((Number(summary.totalFee) || 0) * 100) / 100,
|
||||
truncated,
|
||||
items: detailRows.slice(0, 500).map(row => ({
|
||||
id: Number(row.id),
|
||||
orderNo: String(row.orderNo),
|
||||
startTime: String(row.startTime),
|
||||
stationName: String(row.stationName),
|
||||
plate: String(row.plate),
|
||||
vehicleKind: row.vehicleKind as 'internal' | 'external' | 'unknown',
|
||||
orderStatus: String(row.orderStatus),
|
||||
kwh: Math.round((Number(row.kwh) || 0) * 100) / 100,
|
||||
electricityFee: Math.round((Number(row.electricityFee) || 0) * 100) / 100,
|
||||
serviceFee: Math.round((Number(row.serviceFee) || 0) * 100) / 100,
|
||||
totalFee: Math.round((Number(row.totalFee) || 0) * 100) / 100,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
return c.json(data);
|
||||
});
|
||||
|
||||
// =========================================================
|
||||
// 电能 每日:月份分组 + 日级行 —— 数据源:bi_ele_charge_record
|
||||
// 支持 range 参数(thisWeek / thisMonth / last15)
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
parseHydrogenCustomerKind,
|
||||
parseHydrogenCustomerName,
|
||||
parseHydrogenStationId,
|
||||
parseEnergyDate,
|
||||
} from './query.js';
|
||||
|
||||
test('parses only non-negative integer station ids', () => {
|
||||
@@ -39,3 +40,23 @@ test('rejects invalid entity filters before querying hydrogen data', async () =>
|
||||
assert.equal(invalidCustomer.status, 400);
|
||||
assert.deepEqual(await invalidCustomer.json(), { error: 'customerName 必须是 1-128 个字符' });
|
||||
});
|
||||
|
||||
test('parses only valid energy calendar dates', () => {
|
||||
assert.equal(parseEnergyDate('2026-07-24'), '2026-07-24');
|
||||
assert.equal(parseEnergyDate('2026-02-31'), null);
|
||||
assert.equal(parseEnergyDate('2026-7-24'), null);
|
||||
assert.equal(parseEnergyDate(undefined), null);
|
||||
});
|
||||
|
||||
test('rejects invalid electric order drill filters before querying data', async () => {
|
||||
const invalidDate = await app.request('/electric/orders?date=2026-02-31&customer=lingniu');
|
||||
assert.equal(invalidDate.status, 400);
|
||||
assert.deepEqual(await invalidDate.json(), { error: 'date 必须是有效的 YYYY-MM-DD 日期' });
|
||||
|
||||
const invalidScope = await app.request('/electric/orders?date=2026-07-28&customer=all');
|
||||
assert.equal(invalidScope.status, 400);
|
||||
assert.deepEqual(await invalidScope.json(), { error: 'customer 必须是 lingniu 或 external' });
|
||||
|
||||
const unknownScope = await app.request('/electric/orders?date=2026-07-28&customer=unknown');
|
||||
assert.equal(unknownScope.status, 400);
|
||||
});
|
||||
|
||||
@@ -16,3 +16,16 @@ export function parseHydrogenCustomerName(value: string | undefined): string | n
|
||||
const normalized = value.trim();
|
||||
return normalized.length > 0 && normalized.length <= 128 ? normalized : null;
|
||||
}
|
||||
|
||||
const YMD_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
||||
|
||||
export function parseEnergyDate(value: string | undefined): string | null {
|
||||
if (!value || !YMD_PATTERN.test(value)) return null;
|
||||
const [year, month, day] = value.split('-').map(Number);
|
||||
const date = new Date(Date.UTC(year, month - 1, day));
|
||||
return date.getUTCFullYear() === year
|
||||
&& date.getUTCMonth() === month - 1
|
||||
&& date.getUTCDate() === day
|
||||
? value
|
||||
: null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user