ci/woodpecker/push/woodpecker Pipeline was successful
Co-authored-by: HiFox Agent <agents-noreply@hifox.com>
942 lines
26 KiB
TypeScript
942 lines
26 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { createHash } from "node:crypto";
|
|
import test from "node:test";
|
|
import { Hono } from "hono";
|
|
import app from "./index.js";
|
|
import {
|
|
registerElectricMonthlyRoute,
|
|
registerElectricOverviewRoute,
|
|
type ElectricDependencies,
|
|
} from "./electric.js";
|
|
import {
|
|
registerEtcOverviewRoute,
|
|
type EtcOverviewDependencies,
|
|
} from "./etc.js";
|
|
import {
|
|
registerHydrogenDailyRoute,
|
|
type HydrogenDailyDependencies,
|
|
} from "./hydrogen-daily.js";
|
|
import {
|
|
registerHydrogenOverviewRoute,
|
|
type HydrogenOverviewDependencies,
|
|
} from "./hydrogen-overview.js";
|
|
import {
|
|
registerHydrogenSettlementRoute,
|
|
type HydrogenSettlementDependencies,
|
|
} from "./hydrogen-settlement.js";
|
|
import {
|
|
registerHydrogenStationBoardRoute,
|
|
type HydrogenStationBoardDependencies,
|
|
} from "./hydrogen-station-board.js";
|
|
import {
|
|
registerHydrogenBiV2Routes,
|
|
type HydrogenBiV2Dependencies,
|
|
} from "./hydrogen-bi-v2.js";
|
|
|
|
interface QueryCall {
|
|
sql: string;
|
|
params: unknown;
|
|
}
|
|
|
|
interface CacheCall {
|
|
key: string;
|
|
force: boolean | undefined;
|
|
}
|
|
|
|
function createQueryMock(resultSets: unknown[][], calls: QueryCall[]) {
|
|
return async (sql: string, params?: unknown) => {
|
|
calls.push({ sql, params });
|
|
return [resultSets.shift() ?? [], []];
|
|
};
|
|
}
|
|
|
|
function createCachedMock(calls: CacheCall[]) {
|
|
return async <T>(
|
|
key: string,
|
|
loader: () => Promise<T>,
|
|
opts: { force?: boolean } = {},
|
|
): Promise<T> => {
|
|
calls.push({ key, force: opts.force });
|
|
return loader();
|
|
};
|
|
}
|
|
|
|
function sqlHash(sql: string): string {
|
|
return createHash("sha256").update(sql).digest("hex").slice(0, 12);
|
|
}
|
|
|
|
test("能源路由按既有顺序注册并增加只读单站看板接口", () => {
|
|
assert.deepEqual(
|
|
app.routes
|
|
.filter((route) => route.method === "GET")
|
|
.map((route) => route.path),
|
|
[
|
|
"/hydrogen/overview",
|
|
"/hydrogen/overview-detail",
|
|
"/hydrogen/daily",
|
|
"/hydrogen/daily-detail",
|
|
"/hydrogen/settlement",
|
|
"/hydrogen/station-board",
|
|
"/h2/v2/meta",
|
|
"/h2/v2/overview",
|
|
"/h2/v2/daily",
|
|
"/h2/v2/daily-tree",
|
|
"/h2/v2/drill",
|
|
"/electric/overview",
|
|
"/electric/monthly",
|
|
"/etc/overview",
|
|
],
|
|
);
|
|
});
|
|
|
|
test("氢能 BI v2 使用独立真实账本合同,并且不让前端拼接旧接口", async () => {
|
|
const calls: QueryCall[] = [];
|
|
const v2App = new Hono();
|
|
registerHydrogenBiV2Routes(v2App, {
|
|
hydrogenPool: {
|
|
query: createQueryMock(
|
|
[
|
|
[{ value: 2026, startDate: "2026-01-01", endDate: "2026-08-18" }],
|
|
[{ id: 7, name: "测试站", province: "广东省", city: "佛山市" }],
|
|
[{ ledgerAt: "2026-08-18 10:00:00" }],
|
|
],
|
|
calls,
|
|
),
|
|
},
|
|
} as unknown as HydrogenBiV2Dependencies);
|
|
|
|
const response = await v2App.request("/h2/v2/meta");
|
|
assert.equal(response.status, 200);
|
|
assert.deepEqual(await response.json(), {
|
|
years: [{ value: 2026, startDate: "2026-01-01", endDate: "2026-08-18" }],
|
|
stations: [{ id: "7", name: "测试站", province: "广东省", city: "佛山市" }],
|
|
watermark: { ledgerAt: "2026-08-18 10:00:00", paymentAt: null },
|
|
});
|
|
assert.equal(calls.length, 3);
|
|
assert.match(calls[0].sql, /FROM hydrogen_fuel_ledger/);
|
|
assert.match(calls[1].sql, /FROM new_hydrogen_site/);
|
|
assert.match(calls[1].sql, /EXISTS\s*\([\s\S]*amount_kg[\s\S]*> 0/);
|
|
});
|
|
|
|
test("氢能 BI v2 下钻在日期、站点、客户、车辆及区域上下文中复用同一过滤口径", async () => {
|
|
const calls: QueryCall[] = [];
|
|
const v2App = new Hono();
|
|
registerHydrogenBiV2Routes(v2App, {
|
|
hydrogenPool: {
|
|
query: createQueryMock(
|
|
[
|
|
[{ id: "123" }],
|
|
[{ recordCount: 2, kg: 30, cost: 900, revenue: 1000 }],
|
|
[
|
|
{
|
|
id: "2026-08-18",
|
|
name: "2026-08-18",
|
|
province: null,
|
|
city: null,
|
|
recordCount: 2,
|
|
stationCount: 1,
|
|
customerCount: 1,
|
|
kg: 30,
|
|
cost: 900,
|
|
revenue: 1000,
|
|
lingniuKg: 0,
|
|
externalKg: 30,
|
|
},
|
|
],
|
|
],
|
|
calls,
|
|
),
|
|
},
|
|
} as unknown as HydrogenBiV2Dependencies);
|
|
|
|
const response = await v2App.request(
|
|
"/h2/v2/drill?date=2026-08-18&stationId=0&customerId=0&customerName=%E6%9C%AA%E5%85%B3%E8%81%94%E5%AE%A2%E6%88%B7&plateNo=%E6%97%A0%E8%BD%A6%E7%89%8C®ion=%E5%98%89%E5%85%B4%E5%B8%82®ionGranularity=city&vehicleScope=external&verifyScope=verified&groupBy=date&pageSize=20",
|
|
);
|
|
assert.equal(response.status, 200);
|
|
const payload = await response.json();
|
|
assert.equal(payload.groupBy, "date");
|
|
assert.equal(payload.filters.date, "2026-08-18");
|
|
assert.equal(payload.filters.stationId, "0");
|
|
assert.equal(payload.filters.customerId, 0);
|
|
assert.equal(payload.filters.customerName, "未关联客户");
|
|
assert.equal(payload.filters.plateNo, "无车牌");
|
|
assert.equal(payload.filters.region, "嘉兴市");
|
|
assert.equal(payload.groups[0].id, "2026-08-18");
|
|
assert.equal(payload.summary.kg, 30);
|
|
|
|
assert.equal(calls.length, 3);
|
|
assert.deepEqual(calls[0].params, ["嘉兴市"]);
|
|
assert.match(calls[0].sql, /FROM new_hydrogen_site rs/);
|
|
const commonParams = [
|
|
"2026-08-18",
|
|
"2026-08-18",
|
|
"0",
|
|
0,
|
|
"未关联客户",
|
|
"无车牌",
|
|
"123",
|
|
];
|
|
assert.deepEqual(calls[1].params, commonParams);
|
|
assert.deepEqual((calls[2].params as unknown[]).slice(0, -2), commonParams);
|
|
for (const call of calls.slice(1)) {
|
|
assert.match(call.sql, /COALESCE\(b\.station_id, 0\) = \?/);
|
|
assert.match(
|
|
call.sql,
|
|
/COALESCE\(b\.system_customer_id, b\.customer_id, 0\) = \?/,
|
|
);
|
|
assert.match(call.sql, /COALESCE\(NULLIF\(b\.license_plate/);
|
|
assert.match(call.sql, /b\.station_id IN \(\?\)/);
|
|
assert.match(call.sql, /b\.vehicle_id IS NULL/);
|
|
assert.match(call.sql, /verify_status/);
|
|
}
|
|
assert.match(
|
|
calls[2].sql,
|
|
/GROUP BY DATE_FORMAT\(b\.refuel_time, '%Y-%m-%d'\)/,
|
|
);
|
|
assert.match(calls[2].sql, /ORDER BY id DESC/);
|
|
});
|
|
|
|
test("氢能 BI v2 利润下钻只保留客户承担订单,并与对客总价使用同一订单集合", async () => {
|
|
const calls: QueryCall[] = [];
|
|
const v2App = new Hono();
|
|
registerHydrogenBiV2Routes(v2App, {
|
|
hydrogenPool: {
|
|
query: createQueryMock(
|
|
[
|
|
[{ recordCount: 1, kg: 10, cost: 300, revenue: 360 }],
|
|
[
|
|
{
|
|
id: "7",
|
|
name: "客户承担站",
|
|
province: "浙江省",
|
|
city: "嘉兴市",
|
|
recordCount: 1,
|
|
stationCount: 1,
|
|
customerCount: 1,
|
|
kg: 10,
|
|
cost: 300,
|
|
revenue: 360,
|
|
lingniuKg: 10,
|
|
externalKg: 0,
|
|
},
|
|
],
|
|
[
|
|
{
|
|
id: 1,
|
|
time: "2026-08-18 09:00:00",
|
|
orderNo: "H2-1",
|
|
stationId: 7,
|
|
stationName: "客户承担站",
|
|
customerId: 8,
|
|
customerName: "客户A",
|
|
plateNo: "浙A1",
|
|
source: "api",
|
|
verifyStatus: "VERIFIED",
|
|
vehicleId: 1,
|
|
kg: 10,
|
|
unitPrice: 30,
|
|
cost: 300,
|
|
revenue: 360,
|
|
},
|
|
],
|
|
],
|
|
calls,
|
|
),
|
|
},
|
|
} as unknown as HydrogenBiV2Dependencies);
|
|
|
|
const response = await v2App.request(
|
|
"/h2/v2/drill?year=2026&vehicleScope=all&verifyScope=all&groupBy=station&amountScope=customer",
|
|
);
|
|
assert.equal(response.status, 200);
|
|
const payload = await response.json();
|
|
assert.equal(payload.amountScope, "customer");
|
|
assert.equal(payload.summary.revenue - payload.summary.cost, 60);
|
|
assert.equal(calls.length, 2);
|
|
for (const call of calls) {
|
|
assert.match(
|
|
call.sql,
|
|
/b\.settlement_type = 1/,
|
|
);
|
|
}
|
|
});
|
|
|
|
test("氢能 BI v2 保留原型允许选择的 2023 历史年份,不回退到当前年", async () => {
|
|
const calls: QueryCall[] = [];
|
|
const v2App = new Hono();
|
|
registerHydrogenBiV2Routes(v2App, {
|
|
hydrogenPool: {
|
|
query: createQueryMock(
|
|
[
|
|
[
|
|
{
|
|
ledgerAt: null,
|
|
totalKg: 0,
|
|
totalCost: 0,
|
|
totalRevenue: 0,
|
|
customerCost: 0,
|
|
monthKg: 0,
|
|
monthCost: 0,
|
|
todayKg: 0,
|
|
todayCost: 0,
|
|
recordCount: 0,
|
|
stationCount: 0,
|
|
},
|
|
],
|
|
[],
|
|
[],
|
|
[],
|
|
],
|
|
calls,
|
|
),
|
|
},
|
|
} as unknown as HydrogenBiV2Dependencies);
|
|
|
|
const response = await v2App.request(
|
|
"/h2/v2/overview?year=2023&vehicleScope=all&verifyScope=all",
|
|
);
|
|
assert.equal(response.status, 200);
|
|
const payload = await response.json();
|
|
assert.deepEqual(payload.range, {
|
|
startDate: "2023-01-01",
|
|
endDate: "2023-12-31",
|
|
});
|
|
assert.equal(payload.filters.startDate, "2023-01-01");
|
|
assert.equal(payload.filters.endDate, "2023-12-31");
|
|
assert.equal(payload.kpis.totalKg, 0);
|
|
assert.equal(calls.length, 4);
|
|
});
|
|
|
|
test("氢能 BI v2 客户账单按 settlement_type 映射客户、我司和其他承担", async () => {
|
|
const calls: QueryCall[] = [];
|
|
const v2App = new Hono();
|
|
registerHydrogenBiV2Routes(v2App, {
|
|
hydrogenPool: {
|
|
query: createQueryMock(
|
|
[
|
|
[
|
|
{
|
|
ledgerAt: "2026-08-19 10:00:00",
|
|
totalKg: 60,
|
|
totalCost: 1800,
|
|
totalRevenue: 720,
|
|
customerBearingKg: 10,
|
|
companyBearingKg: 30,
|
|
otherBearingKg: 20,
|
|
customerRevenue: 360,
|
|
customerCost: 600,
|
|
companyCost: 900,
|
|
otherCost: 300,
|
|
monthKg: 60,
|
|
monthCost: 1800,
|
|
todayKg: 0,
|
|
todayCost: 0,
|
|
recordCount: 3,
|
|
stationCount: 1,
|
|
},
|
|
],
|
|
[
|
|
{
|
|
month: "2026-01",
|
|
totalKg: 60,
|
|
lingniuKg: 30,
|
|
externalKg: 30,
|
|
cost: 1800,
|
|
revenue: 720,
|
|
customerRevenue: 360,
|
|
customerCost: 600,
|
|
},
|
|
],
|
|
[],
|
|
[
|
|
{
|
|
id: 1,
|
|
name: "混合承担客户",
|
|
kg: 30,
|
|
customerBearingKg: 10,
|
|
companyBearingKg: 20,
|
|
otherBearingKg: 0,
|
|
cost: 900,
|
|
revenue: 300,
|
|
customerRevenue: 360,
|
|
customerCost: 600,
|
|
companyCost: 300,
|
|
otherCost: 0,
|
|
recordCount: 2,
|
|
},
|
|
{
|
|
id: 2,
|
|
name: "仅我司承担客户",
|
|
kg: 30,
|
|
customerBearingKg: 0,
|
|
companyBearingKg: 10,
|
|
otherBearingKg: 20,
|
|
cost: 900,
|
|
revenue: 0,
|
|
customerRevenue: 0,
|
|
customerCost: 0,
|
|
companyCost: 300,
|
|
otherCost: 600,
|
|
recordCount: 1,
|
|
},
|
|
],
|
|
],
|
|
calls,
|
|
),
|
|
},
|
|
} as unknown as HydrogenBiV2Dependencies);
|
|
|
|
const response = await v2App.request(
|
|
"/h2/v2/overview?year=2026&vehicleScope=all&verifyScope=all",
|
|
);
|
|
assert.equal(response.status, 200);
|
|
const payload = await response.json();
|
|
assert.equal(payload.kpis.customerBearingKg, 10);
|
|
assert.equal(payload.kpis.companyBearingKg, 30);
|
|
assert.equal(payload.kpis.otherBearingKg, 20);
|
|
assert.equal(
|
|
payload.kpis.totalKg,
|
|
payload.kpis.customerBearingKg +
|
|
payload.kpis.companyBearingKg +
|
|
payload.kpis.otherBearingKg,
|
|
);
|
|
assert.equal(payload.kpis.customerGrossProfit, -240);
|
|
assert.equal(payload.monthly[0].customerRevenue, 360);
|
|
assert.equal(payload.monthly[0].customerCost, 600);
|
|
assert.equal(payload.monthly[0].customerGrossProfit, -240);
|
|
assert.deepEqual(payload.customers.map((row: { name: string; bearer: string }) => ({
|
|
name: row.name,
|
|
bearer: row.bearer,
|
|
})), [
|
|
{ name: "混合承担客户", bearer: "both" },
|
|
{ name: "仅我司承担客户", bearer: "other" },
|
|
]);
|
|
assert.match(calls[3].sql, /AS customerBearingKg/);
|
|
assert.match(calls[3].sql, /AS companyBearingKg/);
|
|
assert.match(calls[3].sql, /AS otherBearingKg/);
|
|
assert.match(calls[2].sql, /AS customerRevenue/);
|
|
assert.match(calls[2].sql, /AS customerCost/);
|
|
assert.match(calls[2].sql, /AS companyCost/);
|
|
assert.match(calls[2].sql, /AS otherCost/);
|
|
assert.match(calls[3].sql, /AS customerRevenue/);
|
|
assert.match(calls[3].sql, /AS customerCost/);
|
|
assert.match(calls[3].sql, /AS companyCost/);
|
|
assert.match(calls[3].sql, /AS otherCost/);
|
|
assert.equal(payload.customers[0].customerRevenue, 360);
|
|
assert.equal(payload.customers[0].customerCost, 600);
|
|
assert.match(calls[3].sql, /b\.settlement_type = 1/);
|
|
assert.match(calls[3].sql, /b\.settlement_type = 2/);
|
|
assert.match(calls[3].sql, /b\.settlement_type NOT IN \(1, 2\)/);
|
|
});
|
|
|
|
test("氢能 BI v2 月度上下文换算为完整自然月并沿用至明细", async () => {
|
|
const calls: QueryCall[] = [];
|
|
const v2App = new Hono();
|
|
registerHydrogenBiV2Routes(v2App, {
|
|
hydrogenPool: {
|
|
query: createQueryMock(
|
|
[[{ recordCount: 0, kg: 0, cost: 0, revenue: 0 }], []],
|
|
calls,
|
|
),
|
|
},
|
|
} as unknown as HydrogenBiV2Dependencies);
|
|
|
|
const response = await v2App.request(
|
|
"/h2/v2/drill?month=2026-02&vehicleScope=all&verifyScope=all&groupBy=record",
|
|
);
|
|
assert.equal(response.status, 200);
|
|
const payload = await response.json();
|
|
assert.deepEqual(payload.filters.startDate, "2026-02-01");
|
|
assert.deepEqual(payload.filters.endDate, "2026-02-28");
|
|
assert.equal(payload.filters.month, "2026-02");
|
|
assert.equal(calls.length, 2);
|
|
assert.deepEqual(calls[0].params, ["2026-02-01", "2026-02-28"]);
|
|
assert.deepEqual((calls[1].params as unknown[]).slice(0, -2), [
|
|
"2026-02-01",
|
|
"2026-02-28",
|
|
]);
|
|
});
|
|
|
|
test("单站看板仅保留有加氢记录的站点并合并区间现结流水", async () => {
|
|
const calls: QueryCall[] = [];
|
|
const cacheCalls: CacheCall[] = [];
|
|
const stationBoardApp = new Hono();
|
|
registerHydrogenStationBoardRoute(stationBoardApp, {
|
|
hydrogenPool: {
|
|
query: createQueryMock(
|
|
[
|
|
[
|
|
{
|
|
id: 7,
|
|
name: "测试加氢站",
|
|
province: "广东省",
|
|
city: "佛山市",
|
|
kg: 100,
|
|
fee: 3500,
|
|
recordCount: 2,
|
|
latestLedgerTime: "2026-08-17 10:00:00",
|
|
},
|
|
{
|
|
id: 8,
|
|
name: "零业务站",
|
|
province: "广东省",
|
|
city: "广州市",
|
|
kg: 0,
|
|
fee: 0,
|
|
recordCount: 0,
|
|
latestLedgerTime: null,
|
|
},
|
|
],
|
|
[
|
|
{
|
|
stationId: 7,
|
|
amount: 5000,
|
|
paymentCount: 1,
|
|
latestPaymentDate: "2026-08-17",
|
|
},
|
|
],
|
|
[
|
|
{ date: "2026-08-16", kg: 40, fee: 1400, recordCount: 1 },
|
|
{ date: "2026-08-17", kg: 60, fee: 2100, recordCount: 1 },
|
|
],
|
|
[
|
|
{ stationId: 7, date: "2026-08-16", kg: 40 },
|
|
{ stationId: 7, date: "2026-08-17", kg: 60 },
|
|
],
|
|
[{ date: "2026-08-17", amount: 5000, paymentCount: 1 }],
|
|
],
|
|
calls,
|
|
),
|
|
},
|
|
cached: createCachedMock(cacheCalls),
|
|
} as unknown as HydrogenStationBoardDependencies);
|
|
|
|
const response = await stationBoardApp.request(
|
|
"/hydrogen/station-board?startDate=2026-08-16&endDate=2026-08-17&force=1",
|
|
);
|
|
assert.equal(response.status, 200);
|
|
const payload = await response.json();
|
|
assert.deepEqual(payload.summary, {
|
|
stationCount: 1,
|
|
activeStationCount: 1,
|
|
totalKg: 100,
|
|
totalFee: 3500,
|
|
recordCount: 2,
|
|
paymentAmount: 5000,
|
|
paymentCount: 1,
|
|
latestLedgerTime: "2026-08-17 10:00:00",
|
|
daily: [
|
|
{
|
|
date: "2026-08-16",
|
|
kg: 40,
|
|
fee: 1400,
|
|
recordCount: 1,
|
|
paymentAmount: 0,
|
|
paymentCount: 0,
|
|
},
|
|
{
|
|
date: "2026-08-17",
|
|
kg: 60,
|
|
fee: 2100,
|
|
recordCount: 1,
|
|
paymentAmount: 5000,
|
|
paymentCount: 1,
|
|
},
|
|
],
|
|
});
|
|
assert.equal(payload.stations.length, 1);
|
|
assert.equal(payload.stations[0].name, "测试加氢站");
|
|
assert.deepEqual(payload.stations[0].dailyKg, [
|
|
{ date: "2026-08-16", kg: 40 },
|
|
{ date: "2026-08-17", kg: 60 },
|
|
]);
|
|
assert.deepEqual(cacheCalls, [
|
|
{
|
|
key: "hydrogen/station-board?start=2026-08-16&end=2026-08-17",
|
|
force: true,
|
|
},
|
|
]);
|
|
assert.equal(calls.length, 5);
|
|
assert.match(calls[0].sql, /FROM hydrogen_station s/);
|
|
assert.match(calls[1].sql, /FROM hydrogen_station_payment/);
|
|
assert.match(calls[2].sql, /SUM\(b\.amount_kg\)/);
|
|
assert.match(calls[3].sql, /GROUP BY b\.station_id/);
|
|
assert.match(calls[4].sql, /GROUP BY DATE_FORMAT\(payment_date/);
|
|
});
|
|
|
|
test("站日现结台账仅读取付款流水,并按最近有数据的 15 天聚合", async () => {
|
|
const calls: QueryCall[] = [];
|
|
const cacheCalls: CacheCall[] = [];
|
|
const app = new Hono();
|
|
registerHydrogenSettlementRoute(app, {
|
|
hydrogenPool: {
|
|
query: createQueryMock(
|
|
[
|
|
[{ latestPaymentDate: "2026-05-26" }],
|
|
[
|
|
{
|
|
date: "2026-05-26",
|
|
stationId: 44,
|
|
stationName: "联新加氢站",
|
|
amount: 30000,
|
|
paymentCount: 1,
|
|
matchMode: "exact",
|
|
},
|
|
{
|
|
date: "2026-05-25",
|
|
stationId: null,
|
|
stationName: "佛山中石化",
|
|
amount: 20000,
|
|
paymentCount: 2,
|
|
matchMode: "group",
|
|
},
|
|
],
|
|
],
|
|
calls,
|
|
),
|
|
},
|
|
cached: createCachedMock(cacheCalls),
|
|
} as unknown as HydrogenSettlementDependencies);
|
|
|
|
const response = await app.request(
|
|
"/hydrogen/settlement?range=latest&force=1",
|
|
);
|
|
assert.equal(response.status, 200);
|
|
assert.deepEqual(await response.json(), {
|
|
range: { start: "2026-05-12", end: "2026-05-26", mode: "latest" },
|
|
summary: {
|
|
amount: 50000,
|
|
paymentCount: 3,
|
|
stationDayCount: 2,
|
|
stationCount: 2,
|
|
latestPaymentDate: "2026-05-26",
|
|
},
|
|
rows: [
|
|
{
|
|
date: "2026-05-26",
|
|
stationId: 44,
|
|
stationName: "联新加氢站",
|
|
amount: 30000,
|
|
paymentCount: 1,
|
|
matchMode: "exact",
|
|
},
|
|
{
|
|
date: "2026-05-25",
|
|
stationId: null,
|
|
stationName: "佛山中石化",
|
|
amount: 20000,
|
|
paymentCount: 2,
|
|
matchMode: "group",
|
|
},
|
|
],
|
|
});
|
|
assert.deepEqual(cacheCalls, [
|
|
{ key: "hydrogen/settlement?start=2026-05-12&end=2026-05-26", force: true },
|
|
]);
|
|
assert.equal(calls.length, 2);
|
|
assert.match(calls[0].sql, /MAX\(payment_date\)/);
|
|
assert.match(calls[1].sql, /FROM hydrogen_station_payment p/);
|
|
assert.match(calls[1].sql, /LIMIT 500/);
|
|
assert.deepEqual(calls[1].params, ["2026-05-12", "2026-05-26"]);
|
|
});
|
|
|
|
test("四个处理器继续使用原缓存语义、数据库、SQL 与参数", async () => {
|
|
const hydrogenCalls: QueryCall[] = [];
|
|
const electricCalls: QueryCall[] = [];
|
|
const cacheCalls: CacheCall[] = [];
|
|
const cached = createCachedMock(cacheCalls);
|
|
const hydrogenQuery = createQueryMock(
|
|
[
|
|
[{ y: 2025, latestLedgerTime: "2025-01-02 10:30:00" }],
|
|
[{}],
|
|
[],
|
|
[],
|
|
[],
|
|
[],
|
|
[
|
|
{
|
|
name: "混合承担客户",
|
|
payer: "mixed",
|
|
kg: 10,
|
|
cost: 350,
|
|
revenue: 360,
|
|
},
|
|
],
|
|
[
|
|
{
|
|
d: "2025-01-02",
|
|
stationId: 7,
|
|
stationName: "测试站",
|
|
kg: 10,
|
|
fee: 350,
|
|
pricePerKg: 35,
|
|
},
|
|
],
|
|
],
|
|
hydrogenCalls,
|
|
);
|
|
const electricQuery = createQueryMock(
|
|
[
|
|
[
|
|
{
|
|
totalKwh: 20,
|
|
totalFee: 30,
|
|
monthKwh: 15,
|
|
monthFee: 25,
|
|
todayKwh: 10,
|
|
todayFee: 12,
|
|
},
|
|
],
|
|
[],
|
|
[{ date: "2025-01-02", kwh: 10, fee: 12 }],
|
|
[{ kwh: 5 }],
|
|
[{ date: "2025-01-02", kwh: 10, fee: 12 }],
|
|
],
|
|
electricCalls,
|
|
);
|
|
|
|
const hydrogenOverviewApp = new Hono();
|
|
registerHydrogenOverviewRoute(hydrogenOverviewApp, {
|
|
hydrogenPool: { query: hydrogenQuery },
|
|
cached,
|
|
} as unknown as HydrogenOverviewDependencies);
|
|
const overviewResponse = await hydrogenOverviewApp.request(
|
|
"/hydrogen/overview?year=2025&force=1",
|
|
);
|
|
assert.equal(overviewResponse.status, 200);
|
|
const overviewPayload = (await overviewResponse.json()) as {
|
|
customers: { payer: string; name: string }[];
|
|
};
|
|
assert.deepEqual(overviewPayload.customers, [
|
|
{
|
|
name: "混合承担客户",
|
|
payer: "mixed",
|
|
kg: 10,
|
|
cost: 350,
|
|
revenue: 360,
|
|
},
|
|
]);
|
|
|
|
const hydrogenDailyApp = new Hono();
|
|
registerHydrogenDailyRoute(hydrogenDailyApp, {
|
|
hydrogenPool: { query: hydrogenQuery },
|
|
cached,
|
|
} as unknown as HydrogenDailyDependencies);
|
|
const dailyResponse = await hydrogenDailyApp.request(
|
|
"/hydrogen/daily?startDate=2025-01-01&endDate=2025-01-02&customer=external&force=1",
|
|
);
|
|
assert.equal(dailyResponse.status, 200);
|
|
assert.deepEqual(await dailyResponse.json(), [
|
|
{
|
|
date: "2025-01-02",
|
|
totalKg: 10,
|
|
totalFee: 350,
|
|
chainPct: 0,
|
|
customerType: "external",
|
|
stations: [
|
|
{
|
|
id: 7,
|
|
name: "测试站",
|
|
pricePerKg: 35,
|
|
kg: 10,
|
|
fee: 350,
|
|
chainPct: 0,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
date: "2025-01-01",
|
|
totalKg: 0,
|
|
totalFee: 0,
|
|
chainPct: 0,
|
|
customerType: "external",
|
|
stations: [],
|
|
},
|
|
]);
|
|
|
|
const electricApp = new Hono();
|
|
const electricDependencies = {
|
|
pool: { query: electricQuery },
|
|
cached,
|
|
} as unknown as ElectricDependencies;
|
|
registerElectricOverviewRoute(electricApp, electricDependencies);
|
|
registerElectricMonthlyRoute(electricApp, electricDependencies);
|
|
const electricOverviewResponse = await electricApp.request(
|
|
"/electric/overview?force=1",
|
|
);
|
|
assert.equal(electricOverviewResponse.status, 200);
|
|
const electricMonthlyResponse = await electricApp.request(
|
|
"/electric/monthly?startDate=2025-01-01&endDate=2025-01-02&customer=external&force=1",
|
|
);
|
|
assert.equal(electricMonthlyResponse.status, 200);
|
|
|
|
assert.deepEqual(cacheCalls, [
|
|
{ key: "hydrogen/overview?year=2025", force: true },
|
|
{
|
|
key: "hydrogen/daily?start=2025-01-01&end=2025-01-02&customer=external",
|
|
force: true,
|
|
},
|
|
{ key: "electric/overview", force: true },
|
|
{
|
|
key: "electric/monthly?customer=external&start=2025-01-01&end=2025-01-02",
|
|
force: true,
|
|
},
|
|
]);
|
|
|
|
// 指纹来自拆分前的 13 条 SQL,连空白符变化也会使测试失败。
|
|
assert.deepEqual(
|
|
[...hydrogenCalls, ...electricCalls].map((call) => sqlHash(call.sql)),
|
|
[
|
|
"515acf4dbb2a",
|
|
"de16af670e96",
|
|
"1367eddb3b7f",
|
|
"f125d4dde376",
|
|
"c547b8e1318e",
|
|
"0e3dfd8f4b81",
|
|
"f1f69f275993",
|
|
"688cfdb7a2bf",
|
|
"03fd669f8077",
|
|
"6e4031b10926",
|
|
"4fc77463294c",
|
|
"1886d54939d8",
|
|
"09fc714ec4b2",
|
|
],
|
|
);
|
|
|
|
assert.deepEqual(
|
|
hydrogenCalls.map((call) => call.params),
|
|
[
|
|
["2024-01-01"],
|
|
[
|
|
2025,
|
|
2025,
|
|
2025,
|
|
2025,
|
|
2025,
|
|
2025,
|
|
2025,
|
|
2025,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
"2024-01-01",
|
|
],
|
|
["2024-01-01", 2025],
|
|
["2024-01-01", 2025],
|
|
["2024-01-01", 2025],
|
|
["2024-01-01", 2025],
|
|
["2024-01-01", 2025],
|
|
["2025-01-01", "2025-01-02"],
|
|
],
|
|
);
|
|
assert.match(hydrogenCalls[7].sql, /b\.vehicle_id IS NULL/);
|
|
assert.deepEqual(
|
|
electricCalls.map((call) => call.params),
|
|
[undefined, undefined, undefined, undefined, ["2025-01-01", "2025-01-02"]],
|
|
);
|
|
});
|
|
|
|
test("氢能总览按车辆归属和单站筛选,并隔离缓存结果", async () => {
|
|
const calls: QueryCall[] = [];
|
|
const cacheCalls: CacheCall[] = [];
|
|
const app = new Hono();
|
|
registerHydrogenOverviewRoute(app, {
|
|
hydrogenPool: {
|
|
query: createQueryMock(
|
|
[
|
|
[{ y: 2026, latestLedgerTime: "2026-08-17 21:50:09" }],
|
|
[{ yearKg: 12, latestLedgerTime: "2026-08-17 21:50:09" }],
|
|
[],
|
|
[],
|
|
[],
|
|
[],
|
|
[],
|
|
],
|
|
calls,
|
|
),
|
|
},
|
|
cached: createCachedMock(cacheCalls),
|
|
} as unknown as HydrogenOverviewDependencies);
|
|
|
|
const response = await app.request(
|
|
"/hydrogen/overview?year=2026&stationId=7&vehicleScope=lingniu",
|
|
);
|
|
const payload = await response.json();
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.equal(payload.latestLedgerTime, "2026-08-17 21:50:09");
|
|
assert.deepEqual(cacheCalls, [
|
|
{
|
|
key: "hydrogen/overview?year=2026&station=7&vehicle=lingniu",
|
|
force: false,
|
|
},
|
|
]);
|
|
assert.deepEqual(payload.filter, {
|
|
stationId: 7,
|
|
vehicleScope: "lingniu",
|
|
verifyScope: "all",
|
|
});
|
|
// 年份候选项是全局元数据;其余查询都必须带上单站筛选参数。
|
|
assert.ok(
|
|
calls
|
|
.slice(1)
|
|
.every(
|
|
(call) =>
|
|
Array.isArray(call.params) && (call.params as unknown[]).includes(7),
|
|
),
|
|
);
|
|
assert.match(calls[1].sql, /station_id = \? AND vehicle_id IS NOT NULL/);
|
|
assert.match(
|
|
calls[2].sql,
|
|
/b\.station_id = \? AND b\.vehicle_id IS NOT NULL/,
|
|
);
|
|
});
|
|
|
|
test("ETC 总览对空台账返回明确空状态", async () => {
|
|
const calls: QueryCall[] = [];
|
|
const cacheCalls: CacheCall[] = [];
|
|
const app = new Hono();
|
|
registerEtcOverviewRoute(app, {
|
|
pool: {
|
|
query: createQueryMock(
|
|
[
|
|
[
|
|
{
|
|
tollRecordCount: 0,
|
|
vehicleCount: 0,
|
|
totalAmount: null,
|
|
latestTollTime: null,
|
|
billCount: 0,
|
|
receivableAmount: null,
|
|
paidAmount: null,
|
|
},
|
|
],
|
|
],
|
|
calls,
|
|
),
|
|
},
|
|
cached: createCachedMock(cacheCalls),
|
|
} as unknown as EtcOverviewDependencies);
|
|
|
|
const response = await app.request("/etc/overview?force=1");
|
|
assert.equal(response.status, 200);
|
|
assert.deepEqual(await response.json(), {
|
|
tollRecordCount: 0,
|
|
vehicleCount: 0,
|
|
totalAmount: 0,
|
|
latestTollTime: null,
|
|
billCount: 0,
|
|
receivableAmount: 0,
|
|
paidAmount: 0,
|
|
hasData: false,
|
|
});
|
|
assert.deepEqual(cacheCalls, [{ key: "etc/overview", force: true }]);
|
|
assert.match(calls[0].sql, /FROM etc_toll_record/);
|
|
assert.match(calls[0].sql, /FROM energy_etc_bill/);
|
|
});
|