This commit is contained in:
@@ -1,26 +1,21 @@
|
||||
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 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";
|
||||
} from './electric.js';
|
||||
import {
|
||||
registerEtcOverviewRoute,
|
||||
type EtcOverviewDependencies,
|
||||
} from "./etc.js";
|
||||
registerHydrogenDailyRoute,
|
||||
type HydrogenDailyDependencies,
|
||||
} from './hydrogen-daily.js';
|
||||
import {
|
||||
registerHydrogenStationBoardRoute,
|
||||
type HydrogenStationBoardDependencies,
|
||||
} from "./hydrogen-station-board.js";
|
||||
import {
|
||||
registerHydrogenBiV2Routes,
|
||||
type HydrogenBiV2Dependencies,
|
||||
} from "./hydrogen-bi-v2.js";
|
||||
import { HYDROGEN_VERIFY_START_DATE } from "./constants.js";
|
||||
registerHydrogenOverviewRoute,
|
||||
type HydrogenOverviewDependencies,
|
||||
} from './hydrogen-overview.js';
|
||||
|
||||
interface QueryCall {
|
||||
sql: string;
|
||||
@@ -32,40 +27,6 @@ interface CacheCall {
|
||||
force: boolean | undefined;
|
||||
}
|
||||
|
||||
test("待核对的汇总和下钻统一从核对上线期初起统计,不影响其他核对范围", async () => {
|
||||
// 期初日期以字面量锁定:它同时被前端文案引用,改动必须是有意的。
|
||||
assert.equal(HYDROGEN_VERIFY_START_DATE, "2026-01-01");
|
||||
const unverifiedOnlyClauses = [
|
||||
// 期初之前的订单不算待核对。
|
||||
`b.refuel_time >= '${HYDROGEN_VERIFY_START_DATE}'`,
|
||||
// 无车牌、或未关联羚牛车辆(vehicle_id 为 NULL/0)的订单没有核对对象。
|
||||
"NULLIF(TRIM(b.license_plate), '') IS NOT NULL",
|
||||
"COALESCE(b.vehicle_id, 0) <> 0",
|
||||
];
|
||||
for (const scope of ["unverified", "all", "verified"]) {
|
||||
for (const endpoint of ["overview", "drill"]) {
|
||||
const calls: QueryCall[] = [];
|
||||
const app = new Hono();
|
||||
registerHydrogenBiV2Routes(app, {
|
||||
hydrogenPool: { query: createQueryMock([], calls) },
|
||||
} as unknown as HydrogenBiV2Dependencies);
|
||||
const response = await app.request(`/h2/v2/${endpoint}?year=2026&verifyScope=${scope}`);
|
||||
assert.equal(response.status, 200);
|
||||
const ledgerQueries = calls.filter(({ sql }) => sql.includes("FROM hydrogen_fuel_ledger b"));
|
||||
assert.ok(ledgerQueries.length > 0);
|
||||
for (const { sql } of ledgerQueries) {
|
||||
for (const clause of unverifiedOnlyClauses) {
|
||||
assert.equal(
|
||||
sql.includes(clause),
|
||||
scope === "unverified",
|
||||
`${endpoint}/${scope} 对待核对限定条件「${clause}」的处理不符合预期`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function createQueryMock(resultSets: unknown[][], calls: QueryCall[]) {
|
||||
return async (sql: string, params?: unknown) => {
|
||||
calls.push({ sql, params });
|
||||
@@ -85,647 +46,65 @@ function createCachedMock(calls: CacheCall[]) {
|
||||
}
|
||||
|
||||
function sqlHash(sql: string): string {
|
||||
return createHash("sha256").update(sql).digest("hex").slice(0, 12);
|
||||
return createHash('sha256').update(sql).digest('hex').slice(0, 12);
|
||||
}
|
||||
|
||||
test("能源路由按既有顺序注册并增加只读单站看板接口", () => {
|
||||
test('能源路由继续按原顺序注册四个 GET 接口', () => {
|
||||
assert.deepEqual(
|
||||
app.routes
|
||||
.filter((route) => route.method === "GET")
|
||||
.map((route) => route.path),
|
||||
app.routes.filter(route => route.method === 'GET').map(route => route.path),
|
||||
[
|
||||
"/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",
|
||||
'/hydrogen/overview',
|
||||
'/hydrogen/daily',
|
||||
'/electric/overview',
|
||||
'/electric/monthly',
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
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/);
|
||||
// 所有真实下钻共用此站点选项池:历史加氢量为正,而非仅当前区间活跃。
|
||||
assert.match(calls[1].sql, /b\.station_id = s\.id/);
|
||||
assert.match(calls[1].sql, /external_recharge_manual/);
|
||||
assert.doesNotMatch(calls[1].sql, /refuel_time\s*[<>]/);
|
||||
});
|
||||
|
||||
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&pageSize=1&page=2",
|
||||
);
|
||||
assert.equal(response.status, 200);
|
||||
const payload = await response.json();
|
||||
assert.equal(payload.amountScope, "customer");
|
||||
assert.equal(payload.page.hasMore, true);
|
||||
assert.deepEqual((calls[1].params as unknown[]).slice(-2), [1, 1]);
|
||||
assert.equal(payload.summary.revenue - payload.summary.cost, 60);
|
||||
assert.equal(calls.length, 2);
|
||||
assert.match(calls[1].sql, /ORDER BY kg DESC, id ASC, name ASC LIMIT \? OFFSET \?/);
|
||||
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 app = new Hono();
|
||||
registerHydrogenStationBoardRoute(app, {
|
||||
hydrogenPool: { query: createQueryMock([
|
||||
[
|
||||
{ id: 341, name: "佛山南海羚牛加氢站", kg: 0, fee: 0, recordCount: 0 },
|
||||
{ id: 342, name: "广州交投东鹏大道加氢站", kg: 0, fee: 0, recordCount: 0 },
|
||||
{ id: 343, name: "其他零业务站", kg: 0, fee: 0, recordCount: 0 },
|
||||
], [], [], [], [],
|
||||
], []) },
|
||||
cached: createCachedMock([]),
|
||||
} as unknown as HydrogenStationBoardDependencies);
|
||||
const response = await app.request("/hydrogen/station-board?startDate=2026-08-16&endDate=2026-08-17");
|
||||
assert.equal(response.status, 200);
|
||||
const payload = await response.json();
|
||||
assert.deepEqual(payload.stations.map((station: { id: number }) => station.id), [341, 342]);
|
||||
assert.equal(payload.summary.totalKg, 0);
|
||||
assert.equal(payload.summary.activeStationCount, 0);
|
||||
assert.ok(payload.stations.every((station: { dailyKg: { kg: number }[] }) => station.dailyKg.every(row => row.kg === 0)));
|
||||
});
|
||||
|
||||
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("单站外部客户按唯一标准化车牌聚合,进账保持租户客户级口径", async () => {
|
||||
const calls: QueryCall[] = [];
|
||||
const stationBoardApp = new Hono();
|
||||
registerHydrogenStationBoardRoute(stationBoardApp, {
|
||||
hydrogenPool: {
|
||||
query: createQueryMock(
|
||||
[
|
||||
[{ id: 341, name: "佛山南海羚牛加氢站", province: "广东省", city: "佛山市", kg: 10, fee: 300, recordCount: 1 }],
|
||||
[], [], [], [],
|
||||
[], [], [],
|
||||
[
|
||||
{
|
||||
month: "2026-09",
|
||||
customerName: "外部客户 A",
|
||||
kg: null,
|
||||
fee: null,
|
||||
recordCount: 1,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
id: "auto:9007199254740993",
|
||||
date: "2026-09-05",
|
||||
customerName: "外部客户 A",
|
||||
amount: 200,
|
||||
payMethod: "wechat",
|
||||
source: "spot_daily_auto",
|
||||
sourceRecordCount: 2,
|
||||
updatedAt: "2026-09-05 23:59:00",
|
||||
},
|
||||
{
|
||||
id: "manual:1",
|
||||
date: "2026-09-04",
|
||||
customerName: "外部客户 B",
|
||||
amount: 0,
|
||||
payMethod: "corporate",
|
||||
source: "external_recharge_manual",
|
||||
sourceRecordCount: 1,
|
||||
updatedAt: null,
|
||||
},
|
||||
],
|
||||
],
|
||||
calls,
|
||||
),
|
||||
},
|
||||
cached: createCachedMock([]),
|
||||
} as unknown as HydrogenStationBoardDependencies);
|
||||
|
||||
const response = await stationBoardApp.request(
|
||||
"/hydrogen/station-board?stationId=341&startDate=2026-09-01&endDate=2026-09-05",
|
||||
);
|
||||
assert.equal(response.status, 200);
|
||||
const payload = await response.json();
|
||||
assert.deepEqual(payload.selected.externalCustomerMonths, [{
|
||||
month: "2026-09", customerName: "外部客户 A", kg: 0, fee: 0, recordCount: 1,
|
||||
}]);
|
||||
assert.deepEqual(payload.selected.externalReceipts, {
|
||||
scope: "customer",
|
||||
reason: "当前租户全部外部客户进账,不按站点归属,不计入本单站经营收益或现结 KPI。",
|
||||
rows: [
|
||||
{
|
||||
id: "auto:9007199254740993", date: "2026-09-05", customerName: "外部客户 A", amount: 200,
|
||||
payMethod: "wechat", source: "spot_daily_auto", sourceRecordCount: 2, updatedAt: "2026-09-05 23:59:00",
|
||||
},
|
||||
{
|
||||
id: "manual:1", date: "2026-09-04", customerName: "外部客户 B", amount: 0,
|
||||
payMethod: "corporate", source: "external_recharge_manual", sourceRecordCount: 1, updatedAt: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.equal(calls.length, 10);
|
||||
for (const index of [0, 2, 3, 5, 7, 8, 9]) {
|
||||
assert.match(calls[index].sql, /tenant_id\s*=\s*\?/);
|
||||
assert.ok((calls[index].params as unknown[]).includes("000000"));
|
||||
}
|
||||
for (const index of [0, 2, 3, 5, 7, 8]) {
|
||||
assert.match(calls[index].sql, /record_source, ''\) <> \?/);
|
||||
assert.ok((calls[index].params as unknown[]).includes("external_recharge_manual"));
|
||||
}
|
||||
assert.match(calls[8].sql, /customerCount = 1/);
|
||||
// 自营站客户月度保留全部加氢事实:映射仅做补充,不过滤未匹配或内部车辆。
|
||||
assert.match(calls[7].sql, /LEFT JOIN external_vehicle_map/);
|
||||
assert.match(calls[7].sql, /COALESCE\(m.customerName/);
|
||||
assert.doesNotMatch(calls[7].sql.split('WHERE b.del_flag')[1], /vehicle_id IS NULL/);
|
||||
assert.match(calls[7].sql, /GROUP BY[\s\S]*customerName/);
|
||||
assert.match(calls[8].sql, /CONVERT\(UPPER\(REPLACE/);
|
||||
assert.match(calls[8].sql, /b\.vehicle_id IS NULL OR b\.vehicle_id = 0/);
|
||||
assert.match(calls[9].sql, /UNION ALL/);
|
||||
assert.match(calls[9].sql, /fee_total AS amount/);
|
||||
assert.match(calls[9].sql, /summary_date >= \?/);
|
||||
assert.match(calls[9].sql, /refuel_time < DATE_ADD\(\?, INTERVAL 1 DAY\)/);
|
||||
});
|
||||
|
||||
test("电能处理器继续使用原缓存语义、数据库、SQL 与参数", async () => {
|
||||
test('四个处理器继续使用原缓存语义、数据库、SQL 与参数', async () => {
|
||||
const hydrogenCalls: QueryCall[] = [];
|
||||
const electricCalls: QueryCall[] = [];
|
||||
const cacheCalls: CacheCall[] = [];
|
||||
const cached = createCachedMock(cacheCalls);
|
||||
const electricQuery = createQueryMock(
|
||||
const hydrogenQuery = createQueryMock([
|
||||
[{ y: 2025 }],
|
||||
[{}],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[
|
||||
[
|
||||
{
|
||||
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 }],
|
||||
{ d: '2025-01-02', stationId: 7, stationName: '测试站', kg: 10, pricePerKg: 35 },
|
||||
],
|
||||
electricCalls,
|
||||
], 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 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);
|
||||
|
||||
const electricApp = new Hono();
|
||||
const electricDependencies = {
|
||||
@@ -734,80 +113,55 @@ test("电能处理器继续使用原缓存语义、数据库、SQL 与参数", a
|
||||
} as unknown as ElectricDependencies;
|
||||
registerElectricOverviewRoute(electricApp, electricDependencies);
|
||||
registerElectricMonthlyRoute(electricApp, electricDependencies);
|
||||
const electricOverviewResponse = await electricApp.request(
|
||||
"/electric/overview?force=1",
|
||||
);
|
||||
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",
|
||||
'/electric/monthly?startDate=2025-01-01&endDate=2025-01-02&customer=external&force=1',
|
||||
);
|
||||
assert.equal(electricMonthlyResponse.status, 200);
|
||||
|
||||
assert.deepEqual(cacheCalls, [
|
||||
{ key: "electric/overview", force: true },
|
||||
{
|
||||
key: "electric/monthly?customer=external&start=2025-01-01&end=2025-01-02",
|
||||
force: true,
|
||||
},
|
||||
{ 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(
|
||||
electricCalls.map((call) => sqlHash(call.sql)),
|
||||
[...hydrogenCalls, ...electricCalls].map(call => sqlHash(call.sql)),
|
||||
[
|
||||
"03fd669f8077",
|
||||
"6e4031b10926",
|
||||
"4fc77463294c",
|
||||
"1886d54939d8",
|
||||
"09fc714ec4b2",
|
||||
'95f6d99b61c9',
|
||||
'63a63c5f78bb',
|
||||
'1367eddb3b7f',
|
||||
'83a52a7f9336',
|
||||
'f28fa4a3cb08',
|
||||
'dac9e86fc527',
|
||||
'a71da8f05765',
|
||||
'b562668859ce',
|
||||
'226fd8f950a1',
|
||||
'6e4031b10926',
|
||||
'4fc77463294c',
|
||||
'1886d54939d8',
|
||||
'09fc714ec4b2',
|
||||
],
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
electricCalls.map((call) => call.params),
|
||||
[undefined, undefined, undefined, undefined, ["2025-01-01", "2025-01-02"]],
|
||||
);
|
||||
});
|
||||
|
||||
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/);
|
||||
assert.deepEqual(hydrogenCalls.map(call => call.params), [
|
||||
['2024-01-01'],
|
||||
[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.deepEqual(electricCalls.map(call => call.params), [
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
['2025-01-01', '2025-01-02'],
|
||||
]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user