refactor(stage15): 氢能 v2 移入 repository,能源域完成并加入守护清单

改动
- hydrogen-bi-v2.ts 的 14 条查询移入 energy/repository.ts(SQL 与参数表达式逐条对应)。
- 片段组装也随之搬走,因为它们本质是 SQL 构造,留在路由里会让"路由不含 SQL"形同虚设:
  resolvedWhere / stationMasterRegionSql / buildFilterClauses / buildGroupSelect /
  buildGroupExpression / buildScopedWhere。
- 承担方映射片段(CUSTOMER/COMPANY/OTHER_BEARING_ORDER)移入 constants.ts 供多处共享。
- 结果形状从 mysql2 的 [rows, fields] 改为直接返回 rows,路由侧 13 处 [0] 索引同步调整。
- 架构守护的"已完整分层"清单加入 energy(现为 7 个域)。

提取方式与一次失误
- 查询文本用"反引号定界 + 括号配对"扫描提取。第一次用纯括号配对扫描时,
  因为把 SQL 文本里的括号也算了进去,导致调用点跨度越界、文件被改坏;
  已从 git 恢复并改用反引号定界(并对跨度做了"严格递增且不重叠"的断言)后一次通过。
- 中途还有一次变量写错文件(把 repository 内容写进了路由文件),
  被随后的正常写入覆盖,未造成实际损坏;已核对两个文件的首行与关键符号确认无误。

验证
- energy/routes.test.ts 的 13 个用例(含氢能 v2 的口径、利润、账单、路由顺序、
  SQL 与参数指纹)在提取前后全部通过。
- 全量 lint / test(191) / build 全绿,可达性 0 未引用文件。
This commit is contained in:
dsh-agent
2026-09-11 10:53:56 +08:00
parent 7802e5b0eb
commit 9750d35ba4
5 changed files with 490 additions and 350 deletions
+5 -4
View File
@@ -95,12 +95,12 @@ server/
| `ele/` | `routes.ts` `repository.ts` `model.ts`+ `model.test.ts` `routes.test.ts`) | ✅ 完整分层;改造前后 SQL/参数/响应体已做等价性验证 |
| `feedback/` | `routes.ts` `repository.ts` `oss.ts`+ `routes.test.ts` | ✅ 完整分层;同上 |
| `mileage/` | `index.ts`(聚合)+ `monitoring.ts` `targets.ts` `trend.ts` `daily-report.ts` `vehicle-recent.ts` + `*-model.ts` + `cache.ts` `oneos-api.ts` `daily-report-{service,store,scheduler}.ts` | 路由 / 模型 / 服务已分开,**SQL 仍在各路由文件内** |
| `energy/` | `index.ts`(聚合)+ `electric.ts` `etc.ts` `hydrogen-station-board.ts` + `repository.ts` + `hydrogen-bi-v2.ts`(仍有 SQL+ `query-model.ts` `cache.ts` `constants.ts` | 🔶 **部分分层**:前三个路由文件的 SQL 已迁入 repositorySQL 片段进了 `constants.ts``hydrogen-bi-v2.ts` 仍有 7 条带动态插值的查询,因此该域尚未进守护清单 |
| `energy/` | `index.ts`(聚合)+ `electric.ts` `etc.ts` `hydrogen-station-board.ts` `hydrogen-bi-v2.ts` + `repository.ts` + `query-model.ts` `cache.ts` `constants.ts` | ✅ 完整分层;WHERE 片段组装(`buildFilterClauses` / `buildGroupSelect` / `buildScopedWhere` / `resolvedWhere`)与共享 SQL 片段也都在 repository / constants |
| `scheduling/` | `index.ts`(聚合)+ `suggestions.ts` `notify.ts` `repository.ts` + `algorithm.ts` `notification-model.ts` | ✅ 完整分层;跨域数据源(里程车辆信息 / OneOS)显式注入 |
| `hydrogen-heatmap/` | `routes.ts` `repository.ts` `model.ts`+ `routes.test.ts` | ✅ 完整分层;`buildWhere` 片段与参数顺序已锁定 |
| `vehicle-heatmap/` | `routes.ts` `repository.ts` `model.ts`+ `routes.test.ts`) | ✅ 完整分层;同时覆盖 MySQL(考核批次)与 PG(定位点)两个库 |
已完整分层的个域(`vehicles` / `ele` / `feedback` / `vehicle-heatmap` / `hydrogen-heatmap` / `scheduling`
已完整分层的个域(`vehicles` / `ele` / `feedback` / `vehicle-heatmap` / `hydrogen-heatmap` / `scheduling` / `energy`
由架构测试守护:必须存在 `repository.ts`,且该域**其他任何非测试文件都不得含 SQL**。
其余域(`energy` / `mileage`)尚未拆出 repository——拆分时**不要改变 SQL 与参数顺序**,
请按 `ele/routes.test.ts` 的配方先补契约测试,并用"改造前后同一批请求对比落库 SQL 与响应体"做等价性验证。
@@ -139,8 +139,9 @@ cors → read-only → /api/auth(公开) → authMiddleware → 各业务域
诚实记录,避免后来者以为已经做完:
- **后端仍有 2 个域没拆出 repository**`energy` / `mileage` 的 SQL 仍在各自的路由文件里
(形状见上表)。已完成的有 6 个域,可作为模板:路由只做校验与组装,SQL 进 `repository.ts`
- **后端仍有 1 个域没拆出 repository**`mileage` 的 SQL 仍分散在 `cache.ts` /
`daily-report-store.ts` / `targets.ts` / `monitoring.ts` / `trend.ts` / `vehicle-recent.ts` 等文件里。
已完成的有 7 个域,可作为模板:路由只做校验与组装,SQL 进 `repository.ts`
纯逻辑进 `model.ts`,并用 mock pool 的契约测试锁定 SQL 与参数。
守卫用的是"语句形状"正则(如 `update <table> set`)而不是裸关键字,避免把
`UpdateNotification` 或日志里的 "update error" 误判为 SQL;小写 SQL 同样能被抓到。
+1
View File
@@ -179,6 +179,7 @@ test("已完整分层的域:repository.ts 存在,且 SQL 只允许出现在
"server/routes/vehicle-heatmap",
"server/routes/hydrogen-heatmap",
"server/routes/scheduling",
"server/routes/energy",
];
// 用"语句形状"而不是裸关键字:UpdateNotification 或 "update error" 这类标识符/日志
// 不应误判,而小写 SQL 也依然能被抓到。
+10
View File
@@ -37,3 +37,13 @@ export const EXTERNAL_CUSTOMER_MAP_CTE = `
WHERE customerCount = 1
AND customerName IS NOT NULL
)`;
// ---------------------------------------------------------------------------
// 氢能 v2 承担方映射片段(b.settlement_type1 客户 / 2 我司 / 其余“其他”)。
// 与站点区域回退表达式一样,这些片段被多条查询共享,集中放置避免各处复制。
// ---------------------------------------------------------------------------
export const CUSTOMER_BEARING_ORDER = "b.settlement_type = 1";
export const COMPANY_BEARING_ORDER = "b.settlement_type = 2";
export const OTHER_BEARING_ORDER =
"(b.settlement_type NOT IN (1, 2) OR b.settlement_type IS NULL)";
+65 -346
View File
@@ -2,12 +2,26 @@ import type { Hono } from "hono";
import type { RowDataPacket } from "mysql2";
import type hydrogenPool from "../../db/hydrogen.js";
import {
HYDROGEN_FUEL_ONLY_WHERE,
HYDROGEN_FUEL_ONLY_WHERE_B,
HYDROGEN_LOCAL,
HYDROGEN_MIN_DATE,
HYDROGEN_TABLE,
} from "./constants.js";
buildFilterClauses,
buildGroupExpression,
buildGroupSelect,
buildScopedWhere,
loadDailyRows,
loadDailyTreeCustomers,
loadDailyTreeStations,
loadDailyWatermark,
loadDrillGroups,
loadDrillRecords,
loadDrillSummary,
loadMetaStations,
loadMetaWatermark,
loadMetaYears,
loadOverviewCustomers,
loadOverviewMonthly,
loadOverviewStations,
loadOverviewSummary,
resolvedWhere,
} from "./repository.js";
/**
* The BI v2 contract is intentionally independent of the legacy hydrogen
@@ -60,24 +74,7 @@ function endOfMonth(month: string) {
return new Date(Date.UTC(year, monthNumber, 0)).toISOString().slice(0, 10);
}
function stationMasterRegionSql(granularity: RegionGranularity) {
const districtColumn = granularity === "province" ? "rs.province" : "rs.city";
const stationName = "COALESCE(NULLIF(rs.site_short_name, ''), NULLIF(rs.site_name, ''), '')";
const fallback =
granularity === "province"
? `CASE WHEN ${stationName} LIKE '%嘉兴%' OR ${stationName} LIKE '%平湖%' THEN '浙江省'
WHEN ${stationName} LIKE '%广州%' OR ${stationName} LIKE '%佛山%' THEN '广东省'
WHEN ${stationName} LIKE '%成都%' THEN '四川省'
WHEN ${stationName} LIKE '%昆山%' THEN '江苏省'
ELSE '未归属省份' END`
: `CASE WHEN ${stationName} LIKE '%嘉兴%' OR ${stationName} LIKE '%平湖%' THEN '嘉兴市'
WHEN ${stationName} LIKE '%广州%' THEN '广州市'
WHEN ${stationName} LIKE '%佛山%' THEN '佛山市'
WHEN ${stationName} LIKE '%成都%' THEN '成都市'
WHEN ${stationName} LIKE '%昆山%' THEN '昆山市'
ELSE '未归属区域' END`;
return `COALESCE(NULLIF(rd.NAME, ''), ${fallback})`;
}
function todayYmd() {
const now = new Date();
@@ -150,44 +147,16 @@ function resolveFilter(query: (key: string) => string | undefined): Filter {
query("verifyScope") === "verified" || query("verifyScope") === "unverified"
? (query("verifyScope") as VerifyScope)
: "all";
const clauses = [
HYDROGEN_FUEL_ONLY_WHERE_B,
`b.${HYDROGEN_LOCAL} >= ?`,
`b.${HYDROGEN_LOCAL} < DATE_ADD(?, INTERVAL 1 DAY)`,
];
const params: unknown[] = [
safeStart < HYDROGEN_MIN_DATE ? HYDROGEN_MIN_DATE : safeStart,
safeEnd,
];
if (stationId !== null) {
clauses.push("COALESCE(b.station_id, 0) = ?");
params.push(stationId);
}
if (customerId !== null) {
clauses.push("COALESCE(b.system_customer_id, b.customer_id, 0) = ?");
params.push(customerId);
}
if (customerName) {
clauses.push(
"COALESCE(NULLIF(b.system_customer_name, ''), NULLIF(b.customer_name, ''), '未关联客户') = ?",
);
params.push(customerName);
}
if (plateNo) {
clauses.push("COALESCE(NULLIF(b.license_plate, ''), '无车牌') = ?");
params.push(plateNo);
}
if (vehicleScope === "lingniu") clauses.push("b.vehicle_id IS NOT NULL");
if (vehicleScope === "external") clauses.push("b.vehicle_id IS NULL");
if (verifyScope === "verified")
clauses.push(
"LOWER(COALESCE(NULLIF(TRIM(b.verify_status), ''), 'unverified')) = 'verified'",
);
if (verifyScope === "unverified")
clauses.push(
"LOWER(COALESCE(NULLIF(TRIM(b.verify_status), ''), 'unverified')) <> 'verified'",
"b.refuel_time >= '2026-05-01'",
);
const { clauses, params } = buildFilterClauses({
startDate: safeStart,
endDate: safeEnd,
stationId,
customerId,
customerName,
plateNo,
vehicleScope,
verifyScope,
});
return {
startDate: safeStart,
endDate: safeEnd,
@@ -206,38 +175,6 @@ function resolveFilter(query: (key: string) => string | undefined): Filter {
};
}
async function resolvedWhere(
hydrogenPool: HydrogenBiV2Dependencies["hydrogenPool"],
filter: Filter,
) {
const clauses = [...filter.clauses];
const params = [...filter.params];
if (filter.region) {
// Resolve the small station master once. The old implementation ran two
// correlated station/district subqueries for every ledger row and repeated
// that work in summary, grouping and record queries.
const districtColumn =
filter.regionGranularity === "province" ? "rs.province" : "rs.city";
const [rows] = await hydrogenPool.query<RowDataPacket[]>(
`SELECT CAST(rs.id AS CHAR) AS id
FROM new_hydrogen_site rs
LEFT JOIN common_district rd
ON CONVERT(rd.CODE USING utf8mb4) COLLATE utf8mb4_unicode_ci = CONVERT(${districtColumn} USING utf8mb4) COLLATE utf8mb4_unicode_ci
AND rd.STATUS = 'VALID'
WHERE rs.del_flag = '0'
AND ${stationMasterRegionSql(filter.regionGranularity)} = ?`,
[filter.region],
);
const stationIds = [...new Set(rows.map((row) => String(row.id)))];
if (stationIds.length === 0) {
clauses.push("1 = 0");
} else {
clauses.push(`b.station_id IN (${stationIds.map(() => "?").join(", ")})`);
params.push(...stationIds);
}
}
return { sql: clauses.join(" AND "), params };
}
function filterContext(filter: Filter) {
return {
startDate: filter.startDate,
@@ -261,12 +198,6 @@ function number(value: unknown, digits = 2) {
}
// 承担方必须以账本专用字段 settlement_type 为准,不能由对客单价/金额反推。
// hydrogen_fuel_ledger_log 中的业务映射:1 = 客户承担,2 = 我司承担,3 = 客户自行结算。
// 其余值(含 3、4、NULL)统一归入“其他”,避免把未知/自行结算误计为我司承担。
const CUSTOMER_BEARING_ORDER = "b.settlement_type = 1";
const COMPANY_BEARING_ORDER = "b.settlement_type = 2";
const OTHER_BEARING_ORDER =
"(b.settlement_type NOT IN (1, 2) OR b.settlement_type IS NULL)";
function resolveAmountScope(value: string | undefined): AmountScope {
return value === "customer" || value === "company" || value === "other"
@@ -306,55 +237,24 @@ function dateKeys(start: string, end: string) {
async function meta(hydrogenPool: HydrogenBiV2Dependencies["hydrogenPool"]) {
const [yearRows, stationRows, watermarkRows] = await Promise.all([
hydrogenPool.query<RowDataPacket[]>(
`SELECT YEAR(${HYDROGEN_LOCAL}) AS value,
DATE_FORMAT(MIN(${HYDROGEN_LOCAL}), '%Y-%m-%d') AS startDate,
DATE_FORMAT(MAX(${HYDROGEN_LOCAL}), '%Y-%m-%d') AS endDate
FROM ${HYDROGEN_TABLE}
WHERE ${HYDROGEN_FUEL_ONLY_WHERE} AND ${HYDROGEN_LOCAL} >= ?
GROUP BY YEAR(${HYDROGEN_LOCAL})
ORDER BY value DESC`,
[HYDROGEN_MIN_DATE],
),
hydrogenPool.query<RowDataPacket[]>(
`SELECT CAST(s.id AS CHAR) AS id,
COALESCE(NULLIF(s.site_short_name, ''), NULLIF(s.site_name, ''), CONCAT('站点 #', s.id)) AS name,
COALESCE(NULLIF(p.NAME, ''), '未归属') AS province,
COALESCE(NULLIF(ct.NAME, ''), '未归属') AS city
FROM new_hydrogen_site s
LEFT JOIN common_district p ON CONVERT(p.CODE USING utf8mb4) COLLATE utf8mb4_unicode_ci = CONVERT(s.province USING utf8mb4) COLLATE utf8mb4_unicode_ci AND p.STATUS = 'VALID'
LEFT JOIN common_district ct ON CONVERT(ct.CODE USING utf8mb4) COLLATE utf8mb4_unicode_ci = CONVERT(s.city USING utf8mb4) COLLATE utf8mb4_unicode_ci AND ct.STATUS = 'VALID'
WHERE s.del_flag = '0'
AND EXISTS (
SELECT 1
FROM ${HYDROGEN_TABLE} b
WHERE ${HYDROGEN_FUEL_ONLY_WHERE_B}
AND b.station_id = s.id
AND COALESCE(b.amount_kg, 0) > 0
)
ORDER BY name`,
),
hydrogenPool.query<RowDataPacket[]>(
`SELECT DATE_FORMAT(MAX(${HYDROGEN_LOCAL}), '%Y-%m-%d %H:%i:%s') AS ledgerAt
FROM ${HYDROGEN_TABLE}
WHERE ${HYDROGEN_FUEL_ONLY_WHERE} AND ${HYDROGEN_LOCAL} >= ?`,
[HYDROGEN_MIN_DATE],
),
loadMetaYears(hydrogenPool),
loadMetaStations(hydrogenPool),
loadMetaWatermark(hydrogenPool),
]);
return {
years: yearRows[0].map((row) => ({
years: yearRows.map((row) => ({
value: Number(row.value),
startDate: String(row.startDate),
endDate: String(row.endDate),
})),
stations: stationRows[0].map((row) => ({
stations: stationRows.map((row) => ({
id: String(row.id),
name: String(row.name),
province: String(row.province),
city: String(row.city),
})),
watermark: {
ledgerAt: formatWatermark(watermarkRows[0][0]?.ledgerAt),
ledgerAt: formatWatermark(watermarkRows[0]?.ledgerAt),
paymentAt: null,
},
};
@@ -368,100 +268,18 @@ async function overview(
const month = filter.endDate.slice(0, 7);
const [summaryRows, monthlyRows, stationRows, customerRows] =
await Promise.all([
hydrogenPool.query<RowDataPacket[]>(
`SELECT DATE_FORMAT(MAX(b.${HYDROGEN_LOCAL}), '%Y-%m-%d %H:%i:%s') AS ledgerAt,
ROUND(COALESCE(SUM(b.amount_kg), 0), 3) AS totalKg,
ROUND(COALESCE(SUM(b.cost_total), 0), 2) AS totalCost,
ROUND(COALESCE(SUM(b.fee_total), 0), 2) AS totalRevenue,
ROUND(COALESCE(SUM(CASE WHEN ${CUSTOMER_BEARING_ORDER} THEN b.amount_kg ELSE 0 END), 0), 3) AS customerBearingKg,
ROUND(COALESCE(SUM(CASE WHEN ${COMPANY_BEARING_ORDER} THEN b.amount_kg ELSE 0 END), 0), 3) AS companyBearingKg,
ROUND(COALESCE(SUM(CASE WHEN ${OTHER_BEARING_ORDER} THEN b.amount_kg ELSE 0 END), 0), 3) AS otherBearingKg,
ROUND(COALESCE(SUM(CASE WHEN ${CUSTOMER_BEARING_ORDER} THEN b.fee_total ELSE 0 END), 0), 2) AS customerRevenue,
ROUND(COALESCE(SUM(CASE WHEN ${CUSTOMER_BEARING_ORDER} THEN b.cost_total ELSE 0 END), 0), 2) AS customerCost,
ROUND(COALESCE(SUM(CASE WHEN ${COMPANY_BEARING_ORDER} THEN b.cost_total ELSE 0 END), 0), 2) AS companyCost,
ROUND(COALESCE(SUM(CASE WHEN ${OTHER_BEARING_ORDER} THEN b.cost_total ELSE 0 END), 0), 2) AS otherCost,
ROUND(COALESCE(SUM(CASE WHEN DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m') = ? THEN b.amount_kg ELSE 0 END), 0), 3) AS monthKg,
ROUND(COALESCE(SUM(CASE WHEN DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m') = ? THEN b.cost_total ELSE 0 END), 0), 2) AS monthCost,
ROUND(COALESCE(SUM(CASE WHEN DATE(b.${HYDROGEN_LOCAL}) = ? THEN b.amount_kg ELSE 0 END), 0), 3) AS todayKg,
ROUND(COALESCE(SUM(CASE WHEN DATE(b.${HYDROGEN_LOCAL}) = ? THEN b.cost_total ELSE 0 END), 0), 2) AS todayCost,
COUNT(*) AS recordCount,
COUNT(DISTINCT COALESCE(b.station_id, 0)) AS stationCount
FROM ${HYDROGEN_TABLE} b
WHERE ${sqlWhere}`,
[month, month, filter.endDate, filter.endDate, ...params],
),
hydrogenPool.query<RowDataPacket[]>(
`SELECT DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m') AS month,
ROUND(COALESCE(SUM(b.amount_kg), 0), 3) AS totalKg,
ROUND(COALESCE(SUM(CASE WHEN b.vehicle_id IS NOT NULL THEN b.amount_kg ELSE 0 END), 0), 3) AS lingniuKg,
ROUND(COALESCE(SUM(CASE WHEN b.vehicle_id IS NULL THEN b.amount_kg ELSE 0 END), 0), 3) AS externalKg,
ROUND(COALESCE(SUM(b.cost_total), 0), 2) AS cost,
ROUND(COALESCE(SUM(b.fee_total), 0), 2) AS revenue,
ROUND(COALESCE(SUM(CASE WHEN ${CUSTOMER_BEARING_ORDER} THEN b.fee_total ELSE 0 END), 0), 2) AS customerRevenue,
ROUND(COALESCE(SUM(CASE WHEN ${CUSTOMER_BEARING_ORDER} THEN b.cost_total ELSE 0 END), 0), 2) AS customerCost,
ROUND(COALESCE(SUM(CASE WHEN ${COMPANY_BEARING_ORDER} THEN b.cost_total ELSE 0 END), 0), 2) AS companyCost,
ROUND(COALESCE(SUM(CASE WHEN ${OTHER_BEARING_ORDER} THEN b.cost_total ELSE 0 END), 0), 2) AS otherCost
FROM ${HYDROGEN_TABLE} b
WHERE ${sqlWhere}
GROUP BY DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m')
ORDER BY month`,
params,
),
hydrogenPool.query<RowDataPacket[]>(
`SELECT CAST(MAX(COALESCE(b.station_id, 0)) AS CHAR) AS id,
COALESCE(MAX(NULLIF(s.site_short_name, '')), MAX(NULLIF(s.site_name, '')), MAX(NULLIF(b.station_name, '')), '未关联站点') AS name,
COALESCE(MAX(NULLIF(p.NAME, '')), '未归属') AS province,
COALESCE(MAX(NULLIF(ct.NAME, '')), '未归属') AS city,
ROUND(COALESCE(SUM(b.amount_kg), 0), 3) AS kg,
ROUND(COALESCE(SUM(CASE WHEN b.vehicle_id IS NOT NULL THEN b.amount_kg ELSE 0 END), 0), 3) AS lingniuKg,
ROUND(COALESCE(SUM(CASE WHEN b.vehicle_id IS NULL THEN b.amount_kg ELSE 0 END), 0), 3) AS externalKg,
ROUND(COALESCE(SUM(b.cost_total), 0), 2) AS cost,
ROUND(COALESCE(SUM(b.fee_total), 0), 2) AS revenue,
ROUND(COALESCE(SUM(CASE WHEN ${CUSTOMER_BEARING_ORDER} THEN b.fee_total ELSE 0 END), 0), 2) AS customerRevenue,
ROUND(COALESCE(SUM(CASE WHEN ${CUSTOMER_BEARING_ORDER} THEN b.cost_total ELSE 0 END), 0), 2) AS customerCost,
ROUND(COALESCE(SUM(CASE WHEN ${COMPANY_BEARING_ORDER} THEN b.cost_total ELSE 0 END), 0), 2) AS companyCost,
ROUND(COALESCE(SUM(CASE WHEN ${OTHER_BEARING_ORDER} THEN b.cost_total ELSE 0 END), 0), 2) AS otherCost,
COUNT(*) AS recordCount,
COUNT(DISTINCT COALESCE(b.system_customer_id, b.customer_id, 0)) AS customerCount
FROM ${HYDROGEN_TABLE} b
LEFT JOIN new_hydrogen_site s ON s.id = b.station_id AND s.del_flag = '0'
LEFT JOIN common_district p ON CONVERT(p.CODE USING utf8mb4) COLLATE utf8mb4_unicode_ci = CONVERT(s.province USING utf8mb4) COLLATE utf8mb4_unicode_ci AND p.STATUS = 'VALID'
LEFT JOIN common_district ct ON CONVERT(ct.CODE USING utf8mb4) COLLATE utf8mb4_unicode_ci = CONVERT(s.city USING utf8mb4) COLLATE utf8mb4_unicode_ci AND ct.STATUS = 'VALID'
WHERE ${sqlWhere}
GROUP BY COALESCE(b.station_id, 0)
HAVING SUM(COALESCE(b.amount_kg, 0)) > 0
ORDER BY kg DESC`,
params,
),
hydrogenPool.query<RowDataPacket[]>(
`SELECT COALESCE(b.system_customer_id, b.customer_id, 0) AS id,
COALESCE(NULLIF(b.system_customer_name, ''), NULLIF(b.customer_name, ''), '未关联客户') AS name,
ROUND(COALESCE(SUM(b.amount_kg), 0), 3) AS kg,
ROUND(COALESCE(SUM(CASE WHEN ${CUSTOMER_BEARING_ORDER} THEN b.amount_kg ELSE 0 END), 0), 3) AS customerBearingKg,
ROUND(COALESCE(SUM(CASE WHEN ${COMPANY_BEARING_ORDER} THEN b.amount_kg ELSE 0 END), 0), 3) AS companyBearingKg,
ROUND(COALESCE(SUM(CASE WHEN ${OTHER_BEARING_ORDER} THEN b.amount_kg ELSE 0 END), 0), 3) AS otherBearingKg,
ROUND(COALESCE(SUM(b.cost_total), 0), 2) AS cost,
ROUND(COALESCE(SUM(b.fee_total), 0), 2) AS revenue,
ROUND(COALESCE(SUM(CASE WHEN ${CUSTOMER_BEARING_ORDER} THEN b.fee_total ELSE 0 END), 0), 2) AS customerRevenue,
ROUND(COALESCE(SUM(CASE WHEN ${CUSTOMER_BEARING_ORDER} THEN b.cost_total ELSE 0 END), 0), 2) AS customerCost,
ROUND(COALESCE(SUM(CASE WHEN ${COMPANY_BEARING_ORDER} THEN b.cost_total ELSE 0 END), 0), 2) AS companyCost,
ROUND(COALESCE(SUM(CASE WHEN ${OTHER_BEARING_ORDER} THEN b.cost_total ELSE 0 END), 0), 2) AS otherCost,
COUNT(*) AS recordCount
FROM ${HYDROGEN_TABLE} b
WHERE ${sqlWhere}
GROUP BY COALESCE(b.system_customer_id, b.customer_id, 0), COALESCE(NULLIF(b.system_customer_name, ''), NULLIF(b.customer_name, ''), '未关联客户')
ORDER BY kg DESC
LIMIT 200`,
params,
),
loadOverviewSummary(hydrogenPool, { sqlWhere, params, month, endDate: filter.endDate }),
loadOverviewMonthly(hydrogenPool, { sqlWhere, params }),
loadOverviewStations(hydrogenPool, { sqlWhere, params }),
loadOverviewCustomers(hydrogenPool, { sqlWhere, params }),
]);
const summary: RowDataPacket = summaryRows[0][0] ?? ({} as RowDataPacket);
const summary: RowDataPacket = summaryRows[0] ?? ({} as RowDataPacket);
const totalKg = number(summary.totalKg, 3);
const totalRevenue = number(summary.totalRevenue);
const customerRevenue = number(summary.customerRevenue);
const customerCost = number(summary.customerCost);
const monthlyMap = new Map(
monthlyRows[0].map((row) => [String(row.month), row]),
monthlyRows.map((row) => [String(row.month), row]),
);
const monthly = monthKeys(filter.startDate, filter.endDate).map((key) => {
const row = monthlyMap.get(key);
@@ -485,7 +303,7 @@ async function overview(
customerGrossProfit: number(customerMonthRevenue - customerMonthCost),
};
});
const stations = stationRows[0].map((row) => ({
const stations = stationRows.map((row) => ({
id: String(row.id),
name: String(row.name),
province: String(row.province),
@@ -562,7 +380,7 @@ async function overview(
topStations: stations.slice(0, 5),
stations,
regions,
customers: customerRows[0].map((row) => {
customers: customerRows.map((row) => {
const customerBearingKg = number(row.customerBearingKg, 3);
const companyBearingKg = number(row.companyBearingKg, 3);
const otherBearingKg = number(row.otherBearingKg, 3);
@@ -599,27 +417,10 @@ async function daily(
) {
const { sql: sqlWhere, params } = await resolvedWhere(hydrogenPool, filter);
const [rows, watermarkRows] = await Promise.all([
hydrogenPool.query<RowDataPacket[]>(
`SELECT DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m-%d') AS date,
ROUND(COALESCE(SUM(b.amount_kg), 0), 3) AS kg,
ROUND(COALESCE(SUM(CASE WHEN b.vehicle_id IS NOT NULL THEN b.amount_kg ELSE 0 END), 0), 3) AS lingniuKg,
ROUND(COALESCE(SUM(CASE WHEN b.vehicle_id IS NULL THEN b.amount_kg ELSE 0 END), 0), 3) AS externalKg,
ROUND(COALESCE(SUM(b.cost_total), 0), 2) AS cost,
COUNT(*) AS recordCount,
COUNT(DISTINCT COALESCE(b.station_id, 0)) AS stationCount
FROM ${HYDROGEN_TABLE} b
WHERE ${sqlWhere}
GROUP BY DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m-%d')
ORDER BY date`,
params,
),
hydrogenPool.query<RowDataPacket[]>(
`SELECT DATE_FORMAT(MAX(b.${HYDROGEN_LOCAL}), '%Y-%m-%d %H:%i:%s') AS ledgerAt
FROM ${HYDROGEN_TABLE} b WHERE ${sqlWhere}`,
params,
),
loadDailyRows(hydrogenPool, { sqlWhere, params }),
loadDailyWatermark(hydrogenPool, { sqlWhere, params }),
]);
const map = new Map(rows[0].map((row) => [String(row.date), row]));
const map = new Map(rows.map((row) => [String(row.date), row]));
let previousKg = 0;
const days = dateKeys(filter.startDate, filter.endDate).map((date) => {
const row = map.get(date);
@@ -644,7 +445,7 @@ async function daily(
return {
range: { startDate: filter.startDate, endDate: filter.endDate },
watermark: {
ledgerAt: formatWatermark(watermarkRows[0][0]?.ledgerAt),
ledgerAt: formatWatermark(watermarkRows[0]?.ledgerAt),
paymentAt: null,
},
filters: filterContext(filter),
@@ -671,32 +472,8 @@ async function dailyTree(
) {
const { sql: sqlWhere, params } = await resolvedWhere(hydrogenPool, filter);
const [stationRows, customerRows] = await Promise.all([
hydrogenPool.query<RowDataPacket[]>(
`SELECT CAST(MAX(COALESCE(b.station_id, 0)) AS CHAR) AS id,
COALESCE(MAX(NULLIF(s.site_short_name, '')), MAX(NULLIF(s.site_name, '')), MAX(NULLIF(b.station_name, '')), '未关联站点') AS name,
ROUND(COALESCE(SUM(b.amount_kg), 0), 3) AS kg,
ROUND(COALESCE(SUM(b.cost_total), 0), 2) AS cost,
COUNT(*) AS recordCount
FROM ${HYDROGEN_TABLE} b
LEFT JOIN new_hydrogen_site s ON s.id = b.station_id AND s.del_flag = '0'
WHERE ${sqlWhere}
GROUP BY COALESCE(b.station_id, 0)
ORDER BY kg DESC`,
params,
),
hydrogenPool.query<RowDataPacket[]>(
`SELECT CAST(MAX(COALESCE(b.station_id, 0)) AS CHAR) AS stationId,
COALESCE(b.system_customer_id, b.customer_id, 0) AS id,
COALESCE(NULLIF(b.system_customer_name, ''), NULLIF(b.customer_name, ''), '未关联客户') AS name,
ROUND(COALESCE(SUM(b.amount_kg), 0), 3) AS kg,
ROUND(COALESCE(SUM(b.cost_total), 0), 2) AS cost,
COUNT(*) AS recordCount
FROM ${HYDROGEN_TABLE} b
WHERE ${sqlWhere}
GROUP BY COALESCE(b.station_id, 0), COALESCE(b.system_customer_id, b.customer_id, 0), COALESCE(NULLIF(b.system_customer_name, ''), NULLIF(b.customer_name, ''), '未关联客户')
ORDER BY stationId, kg DESC`,
params,
),
loadDailyTreeStations(hydrogenPool, { sqlWhere, params }),
loadDailyTreeCustomers(hydrogenPool, { sqlWhere, params }),
]);
const customersByStation = new Map<
string,
@@ -708,7 +485,7 @@ async function dailyTree(
recordCount: number;
}>
>();
customerRows[0].forEach((row) => {
customerRows.forEach((row) => {
const stationId = String(row.stationId);
const list = customersByStation.get(stationId) || [];
list.push({
@@ -722,7 +499,7 @@ async function dailyTree(
});
return {
date: filter.startDate,
stations: stationRows[0].map((row) => ({
stations: stationRows.map((row) => ({
id: String(row.id),
name: String(row.name),
kg: number(row.kg, 3),
@@ -742,82 +519,24 @@ async function drill(
amountScope: AmountScope,
) {
const { sql: sqlWhere, params } = await resolvedWhere(hydrogenPool, filter);
const scopedWhere =
amountScope === "customer"
? `${sqlWhere} AND ${CUSTOMER_BEARING_ORDER}`
: amountScope === "company"
? `${sqlWhere} AND ${COMPANY_BEARING_ORDER}`
: amountScope === "other"
? `${sqlWhere} AND ${OTHER_BEARING_ORDER}`
: sqlWhere;
const scopedWhere = buildScopedWhere(sqlWhere, amountScope);
const groupSelect = buildGroupSelect(groupBy);
const groupExpression = buildGroupExpression(groupBy);
const offset = (page - 1) * pageSize;
const groupSelect =
groupBy === "station"
? "CAST(MAX(COALESCE(b.station_id, 0)) AS CHAR) AS id, COALESCE(MAX(NULLIF(s.site_short_name, '')), MAX(NULLIF(s.site_name, '')), MAX(NULLIF(b.station_name, '')), '未关联站点') AS name, COALESCE(MAX(NULLIF(p.NAME, '')), '未归属') AS province, COALESCE(MAX(NULLIF(ct.NAME, '')), '未归属') AS city"
: groupBy === "customer"
? "COALESCE(b.system_customer_id, b.customer_id, 0) AS id, COALESCE(NULLIF(b.system_customer_name, ''), NULLIF(b.customer_name, ''), '未关联客户') AS name, NULL AS province, NULL AS city"
: groupBy === "date"
? `DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m-%d') AS id, DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m-%d') AS name, NULL AS province, NULL AS city`
: "COALESCE(NULLIF(b.license_plate, ''), '无车牌') AS id, COALESCE(NULLIF(b.license_plate, ''), '无车牌') AS name, NULL AS province, NULL AS city";
const groupExpression =
groupBy === "station"
? "COALESCE(b.station_id, 0)"
: groupBy === "customer"
? "COALESCE(b.system_customer_id, b.customer_id, 0), COALESCE(NULLIF(b.system_customer_name, ''), NULLIF(b.customer_name, ''), '未关联客户')"
: groupBy === "date"
? `DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m-%d')`
: "COALESCE(NULLIF(b.license_plate, ''), '无车牌')";
// Stable tie-breakers prevent equal-volume groups drifting between pages.
const groupOrder = groupBy === "date" ? "id DESC" : "kg DESC, id ASC, name ASC";
const groupHaving =
groupBy === "station" ? "HAVING SUM(COALESCE(b.amount_kg, 0)) > 0" : "";
const [summaryRows, groupRows, recordRows] = await Promise.all([
hydrogenPool.query<RowDataPacket[]>(
`SELECT COUNT(*) AS recordCount,
COUNT(DISTINCT COALESCE(b.station_id, 0)) AS stationCount,
COUNT(DISTINCT COALESCE(b.system_customer_id, b.customer_id, 0)) AS customerCount,
SUM(CASE WHEN NULLIF(b.record_source, '') IS NOT NULL OR b.source IS NOT NULL THEN 1 ELSE 0 END) AS traceableRecordCount,
ROUND(COALESCE(SUM(b.amount_kg), 0), 3) AS kg,
ROUND(COALESCE(SUM(b.cost_total), 0), 2) AS cost, ROUND(COALESCE(SUM(b.fee_total), 0), 2) AS revenue
FROM ${HYDROGEN_TABLE} b WHERE ${scopedWhere}`,
params,
),
loadDrillSummary(hydrogenPool, { scopedWhere, params }),
groupBy === "record"
? Promise.resolve([[] as RowDataPacket[]])
: hydrogenPool.query<RowDataPacket[]>(
`SELECT ${groupSelect},
GROUP_CONCAT(DISTINCT COALESCE(CAST(b.settlement_type AS CHAR), 'unknown') ORDER BY COALESCE(CAST(b.settlement_type AS CHAR), 'unknown')) AS settlementTypes,
COUNT(*) AS recordCount, COUNT(DISTINCT COALESCE(b.station_id, 0)) AS stationCount,
COUNT(DISTINCT COALESCE(b.system_customer_id, b.customer_id, 0)) AS customerCount,
ROUND(COALESCE(SUM(b.amount_kg), 0), 3) AS kg, ROUND(COALESCE(SUM(b.cost_total), 0), 2) AS cost, ROUND(COALESCE(SUM(b.fee_total), 0), 2) AS revenue,
ROUND(COALESCE(SUM(CASE WHEN b.vehicle_id IS NOT NULL THEN b.amount_kg ELSE 0 END), 0), 3) AS lingniuKg,
ROUND(COALESCE(SUM(CASE WHEN b.vehicle_id IS NULL THEN b.amount_kg ELSE 0 END), 0), 3) AS externalKg
FROM ${HYDROGEN_TABLE} b
LEFT JOIN new_hydrogen_site s ON s.id = b.station_id AND s.del_flag = '0'
LEFT JOIN common_district p ON CONVERT(p.CODE USING utf8mb4) COLLATE utf8mb4_unicode_ci = CONVERT(s.province USING utf8mb4) COLLATE utf8mb4_unicode_ci AND p.STATUS = 'VALID'
LEFT JOIN common_district ct ON CONVERT(ct.CODE USING utf8mb4) COLLATE utf8mb4_unicode_ci = CONVERT(s.city USING utf8mb4) COLLATE utf8mb4_unicode_ci AND ct.STATUS = 'VALID'
WHERE ${scopedWhere}
GROUP BY ${groupExpression}
${groupHaving}
ORDER BY ${groupOrder} LIMIT ? OFFSET ?`,
[...params, pageSize, offset],
),
? Promise.resolve([] as RowDataPacket[])
: loadDrillGroups(hydrogenPool, { scopedWhere, params, groupSelect, groupExpression, groupHaving, groupOrder, pageSize, offset }),
groupBy === "record"
? hydrogenPool.query<RowDataPacket[]>(
`SELECT b.id, b.settlement_type AS settlementType, DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m-%d %H:%i:%s') AS time, b.order_no AS orderNo,
CAST(COALESCE(b.station_id, 0) AS CHAR) AS stationId, COALESCE(NULLIF(b.station_name, ''), '未关联站点') AS stationName,
COALESCE(b.system_customer_id, b.customer_id, 0) AS customerId, COALESCE(NULLIF(b.system_customer_name, ''), NULLIF(b.customer_name, ''), '未关联客户') AS customerName,
COALESCE(NULLIF(b.license_plate, ''), '无车牌') AS plateNo, COALESCE(NULLIF(b.record_source, ''), CAST(b.source AS CHAR), '未知来源') AS source,
COALESCE(NULLIF(b.verify_status, ''), 'UNVERIFIED') AS verifyStatus, b.vehicle_id AS vehicleId,
ROUND(COALESCE(b.amount_kg, 0), 3) AS kg, ROUND(COALESCE(b.cost_price, 0), 2) AS unitPrice,
ROUND(COALESCE(b.cost_total, 0), 2) AS cost, ROUND(COALESCE(b.fee_total, 0), 2) AS revenue
FROM ${HYDROGEN_TABLE} b WHERE ${scopedWhere}
ORDER BY b.${HYDROGEN_LOCAL} DESC, b.id DESC LIMIT ? OFFSET ?`,
[...params, pageSize, offset],
)
: Promise.resolve([[] as RowDataPacket[]]),
? loadDrillRecords(hydrogenPool, { scopedWhere, params, pageSize, offset })
: Promise.resolve([] as RowDataPacket[]),
]);
const summary = summaryRows[0][0] ?? {};
const summary = summaryRows[0] ?? {};
return {
groupBy,
amountScope,
@@ -831,7 +550,7 @@ async function drill(
cost: number(summary.cost),
revenue: number(summary.revenue),
},
groups: groupRows[0].map((row) => ({
groups: groupRows.map((row) => ({
id: String(row.id),
name: String(row.name),
province: row.province ? String(row.province) : null,
@@ -846,7 +565,7 @@ async function drill(
externalKg: number(row.externalKg, 3),
settlementTypes: row.settlementTypes == null ? null : String(row.settlementTypes),
})),
records: recordRows[0].map((row) => ({
records: recordRows.map((row) => ({
settlementType: row.settlementType == null ? null : String(row.settlementType),
id: String(row.id),
time: String(row.time),
@@ -867,7 +586,7 @@ async function drill(
cost: number(row.cost),
revenue: number(row.revenue),
})),
page: { page, pageSize, hasMore: (groupBy === "record" ? recordRows[0] : groupRows[0]).length === pageSize },
page: { page, pageSize, hasMore: (groupBy === "record" ? recordRows : groupRows).length === pageSize },
};
}
+409
View File
@@ -1,10 +1,16 @@
import type { RowDataPacket } from 'mysql2';
import { dateRangeClause, type CustomerKind } from './query-model.js';
import {
COMPANY_BEARING_ORDER,
CUSTOMER_BEARING_ORDER,
EXTERNAL_CUSTOMER_MAP_CTE,
OTHER_BEARING_ORDER,
HYDROGEN_BASE_WHERE,
HYDROGEN_BASE_WHERE_B,
HYDROGEN_FUEL_ONLY_WHERE,
HYDROGEN_FUEL_ONLY_WHERE_B,
HYDROGEN_LOCAL,
HYDROGEN_MIN_DATE,
HYDROGEN_TABLE,
HYDROGEN_TENANT_ID,
MANUAL_RECHARGE_SOURCE,
@@ -360,3 +366,406 @@ export async function loadExternalReceipts(db: Database, args: { start: string;
ORDER BY date DESC, customerName ASC, id ASC`, [HYDROGEN_TENANT_ID, start, end, HYDROGEN_TENANT_ID, MANUAL_RECHARGE_SOURCE, start, end]);
return rows;
}
// ---------------------------------------------------------------------------
// 氢能 v2/h2/v2/*
// ---------------------------------------------------------------------------
export type RegionGranularity = 'province' | 'city';
/** 区域筛选所需的最小输入(与路由的 Filter 结构兼容)。 */
export interface RegionFilter {
clauses: string[];
params: unknown[];
region: string | null;
regionGranularity: RegionGranularity;
}
export function stationMasterRegionSql(granularity: RegionGranularity) {
const districtColumn = granularity === "province" ? "rs.province" : "rs.city";
const stationName = "COALESCE(NULLIF(rs.site_short_name, ''), NULLIF(rs.site_name, ''), '')";
const fallback =
granularity === "province"
? `CASE WHEN ${stationName} LIKE '%嘉兴%' OR ${stationName} LIKE '%平湖%' THEN '浙江省'
WHEN ${stationName} LIKE '%广州%' OR ${stationName} LIKE '%佛山%' THEN '广东省'
WHEN ${stationName} LIKE '%成都%' THEN '四川省'
WHEN ${stationName} LIKE '%昆山%' THEN '江苏省'
ELSE '未归属省份' END`
: `CASE WHEN ${stationName} LIKE '%嘉兴%' OR ${stationName} LIKE '%平湖%' THEN '嘉兴市'
WHEN ${stationName} LIKE '%广州%' THEN '广州市'
WHEN ${stationName} LIKE '%佛山%' THEN '佛山市'
WHEN ${stationName} LIKE '%成都%' THEN '成都市'
WHEN ${stationName} LIKE '%昆山%' THEN '昆山市'
ELSE '未归属区域' END`;
return `COALESCE(NULLIF(rd.NAME, ''), ${fallback})`;
}
export async function resolvedWhere(
db: Database,
filter: RegionFilter,
) {
const clauses = [...filter.clauses];
const params = [...filter.params];
if (filter.region) {
// Resolve the small station master once. The old implementation ran two
// correlated station/district subqueries for every ledger row and repeated
// that work in summary, grouping and record queries.
const districtColumn =
filter.regionGranularity === "province" ? "rs.province" : "rs.city";
const [rows] = await db.query<RowDataPacket[]>(
`SELECT CAST(rs.id AS CHAR) AS id
FROM new_hydrogen_site rs
LEFT JOIN common_district rd
ON CONVERT(rd.CODE USING utf8mb4) COLLATE utf8mb4_unicode_ci = CONVERT(${districtColumn} USING utf8mb4) COLLATE utf8mb4_unicode_ci
AND rd.STATUS = 'VALID'
WHERE rs.del_flag = '0'
AND ${stationMasterRegionSql(filter.regionGranularity)} = ?`,
[filter.region],
);
const stationIds = [...new Set(rows.map((row) => String(row.id)))];
if (stationIds.length === 0) {
clauses.push("1 = 0");
} else {
clauses.push(`b.station_id IN (${stationIds.map(() => "?").join(", ")})`);
params.push(...stationIds);
}
}
return { sql: clauses.join(" AND "), params };
}
/** 氢能 v2 查询(SQL 与参数顺序由 routes.test.ts 的指纹断言锁定)。 */
export async function loadMetaYears(db: Database): Promise<RowDataPacket[]> {
const [rows] = await db.query<RowDataPacket[]>(`SELECT YEAR(${HYDROGEN_LOCAL}) AS value,
DATE_FORMAT(MIN(${HYDROGEN_LOCAL}), '%Y-%m-%d') AS startDate,
DATE_FORMAT(MAX(${HYDROGEN_LOCAL}), '%Y-%m-%d') AS endDate
FROM ${HYDROGEN_TABLE}
WHERE ${HYDROGEN_FUEL_ONLY_WHERE} AND ${HYDROGEN_LOCAL} >= ?
GROUP BY YEAR(${HYDROGEN_LOCAL})
ORDER BY value DESC`, [HYDROGEN_MIN_DATE]);
return rows;
}
/** 氢能 v2 查询(SQL 与参数顺序由 routes.test.ts 的指纹断言锁定)。 */
export async function loadMetaStations(db: Database): Promise<RowDataPacket[]> {
const [rows] = await db.query<RowDataPacket[]>(`SELECT CAST(s.id AS CHAR) AS id,
COALESCE(NULLIF(s.site_short_name, ''), NULLIF(s.site_name, ''), CONCAT('站点 #', s.id)) AS name,
COALESCE(NULLIF(p.NAME, ''), '未归属') AS province,
COALESCE(NULLIF(ct.NAME, ''), '未归属') AS city
FROM new_hydrogen_site s
LEFT JOIN common_district p ON CONVERT(p.CODE USING utf8mb4) COLLATE utf8mb4_unicode_ci = CONVERT(s.province USING utf8mb4) COLLATE utf8mb4_unicode_ci AND p.STATUS = 'VALID'
LEFT JOIN common_district ct ON CONVERT(ct.CODE USING utf8mb4) COLLATE utf8mb4_unicode_ci = CONVERT(s.city USING utf8mb4) COLLATE utf8mb4_unicode_ci AND ct.STATUS = 'VALID'
WHERE s.del_flag = '0'
AND EXISTS (
SELECT 1
FROM ${HYDROGEN_TABLE} b
WHERE ${HYDROGEN_FUEL_ONLY_WHERE_B}
AND b.station_id = s.id
AND COALESCE(b.amount_kg, 0) > 0
)
ORDER BY name`, []);
return rows;
}
/** 氢能 v2 查询(SQL 与参数顺序由 routes.test.ts 的指纹断言锁定)。 */
export async function loadMetaWatermark(db: Database): Promise<RowDataPacket[]> {
const [rows] = await db.query<RowDataPacket[]>(`SELECT DATE_FORMAT(MAX(${HYDROGEN_LOCAL}), '%Y-%m-%d %H:%i:%s') AS ledgerAt
FROM ${HYDROGEN_TABLE}
WHERE ${HYDROGEN_FUEL_ONLY_WHERE} AND ${HYDROGEN_LOCAL} >= ?`, [HYDROGEN_MIN_DATE]);
return rows;
}
/** 氢能 v2 查询(SQL 与参数顺序由 routes.test.ts 的指纹断言锁定)。 */
export async function loadOverviewSummary(db: Database, args: { sqlWhere: string; params: unknown[]; month: string; endDate: string }): Promise<RowDataPacket[]> {
const { sqlWhere, params, month, endDate } = args;
const [rows] = await db.query<RowDataPacket[]>(`SELECT DATE_FORMAT(MAX(b.${HYDROGEN_LOCAL}), '%Y-%m-%d %H:%i:%s') AS ledgerAt,
ROUND(COALESCE(SUM(b.amount_kg), 0), 3) AS totalKg,
ROUND(COALESCE(SUM(b.cost_total), 0), 2) AS totalCost,
ROUND(COALESCE(SUM(b.fee_total), 0), 2) AS totalRevenue,
ROUND(COALESCE(SUM(CASE WHEN ${CUSTOMER_BEARING_ORDER} THEN b.amount_kg ELSE 0 END), 0), 3) AS customerBearingKg,
ROUND(COALESCE(SUM(CASE WHEN ${COMPANY_BEARING_ORDER} THEN b.amount_kg ELSE 0 END), 0), 3) AS companyBearingKg,
ROUND(COALESCE(SUM(CASE WHEN ${OTHER_BEARING_ORDER} THEN b.amount_kg ELSE 0 END), 0), 3) AS otherBearingKg,
ROUND(COALESCE(SUM(CASE WHEN ${CUSTOMER_BEARING_ORDER} THEN b.fee_total ELSE 0 END), 0), 2) AS customerRevenue,
ROUND(COALESCE(SUM(CASE WHEN ${CUSTOMER_BEARING_ORDER} THEN b.cost_total ELSE 0 END), 0), 2) AS customerCost,
ROUND(COALESCE(SUM(CASE WHEN ${COMPANY_BEARING_ORDER} THEN b.cost_total ELSE 0 END), 0), 2) AS companyCost,
ROUND(COALESCE(SUM(CASE WHEN ${OTHER_BEARING_ORDER} THEN b.cost_total ELSE 0 END), 0), 2) AS otherCost,
ROUND(COALESCE(SUM(CASE WHEN DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m') = ? THEN b.amount_kg ELSE 0 END), 0), 3) AS monthKg,
ROUND(COALESCE(SUM(CASE WHEN DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m') = ? THEN b.cost_total ELSE 0 END), 0), 2) AS monthCost,
ROUND(COALESCE(SUM(CASE WHEN DATE(b.${HYDROGEN_LOCAL}) = ? THEN b.amount_kg ELSE 0 END), 0), 3) AS todayKg,
ROUND(COALESCE(SUM(CASE WHEN DATE(b.${HYDROGEN_LOCAL}) = ? THEN b.cost_total ELSE 0 END), 0), 2) AS todayCost,
COUNT(*) AS recordCount,
COUNT(DISTINCT COALESCE(b.station_id, 0)) AS stationCount
FROM ${HYDROGEN_TABLE} b
WHERE ${sqlWhere}`, [month, month, endDate, endDate, ...params]);
return rows;
}
/** 氢能 v2 查询(SQL 与参数顺序由 routes.test.ts 的指纹断言锁定)。 */
export async function loadOverviewMonthly(db: Database, args: { sqlWhere: string; params: unknown[] }): Promise<RowDataPacket[]> {
const { sqlWhere, params } = args;
const [rows] = await db.query<RowDataPacket[]>(`SELECT DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m') AS month,
ROUND(COALESCE(SUM(b.amount_kg), 0), 3) AS totalKg,
ROUND(COALESCE(SUM(CASE WHEN b.vehicle_id IS NOT NULL THEN b.amount_kg ELSE 0 END), 0), 3) AS lingniuKg,
ROUND(COALESCE(SUM(CASE WHEN b.vehicle_id IS NULL THEN b.amount_kg ELSE 0 END), 0), 3) AS externalKg,
ROUND(COALESCE(SUM(b.cost_total), 0), 2) AS cost,
ROUND(COALESCE(SUM(b.fee_total), 0), 2) AS revenue,
ROUND(COALESCE(SUM(CASE WHEN ${CUSTOMER_BEARING_ORDER} THEN b.fee_total ELSE 0 END), 0), 2) AS customerRevenue,
ROUND(COALESCE(SUM(CASE WHEN ${CUSTOMER_BEARING_ORDER} THEN b.cost_total ELSE 0 END), 0), 2) AS customerCost,
ROUND(COALESCE(SUM(CASE WHEN ${COMPANY_BEARING_ORDER} THEN b.cost_total ELSE 0 END), 0), 2) AS companyCost,
ROUND(COALESCE(SUM(CASE WHEN ${OTHER_BEARING_ORDER} THEN b.cost_total ELSE 0 END), 0), 2) AS otherCost
FROM ${HYDROGEN_TABLE} b
WHERE ${sqlWhere}
GROUP BY DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m')
ORDER BY month`, params);
return rows;
}
/** 氢能 v2 查询(SQL 与参数顺序由 routes.test.ts 的指纹断言锁定)。 */
export async function loadOverviewStations(db: Database, args: { sqlWhere: string; params: unknown[] }): Promise<RowDataPacket[]> {
const { sqlWhere, params } = args;
const [rows] = await db.query<RowDataPacket[]>(`SELECT CAST(MAX(COALESCE(b.station_id, 0)) AS CHAR) AS id,
COALESCE(MAX(NULLIF(s.site_short_name, '')), MAX(NULLIF(s.site_name, '')), MAX(NULLIF(b.station_name, '')), '未关联站点') AS name,
COALESCE(MAX(NULLIF(p.NAME, '')), '未归属') AS province,
COALESCE(MAX(NULLIF(ct.NAME, '')), '未归属') AS city,
ROUND(COALESCE(SUM(b.amount_kg), 0), 3) AS kg,
ROUND(COALESCE(SUM(CASE WHEN b.vehicle_id IS NOT NULL THEN b.amount_kg ELSE 0 END), 0), 3) AS lingniuKg,
ROUND(COALESCE(SUM(CASE WHEN b.vehicle_id IS NULL THEN b.amount_kg ELSE 0 END), 0), 3) AS externalKg,
ROUND(COALESCE(SUM(b.cost_total), 0), 2) AS cost,
ROUND(COALESCE(SUM(b.fee_total), 0), 2) AS revenue,
ROUND(COALESCE(SUM(CASE WHEN ${CUSTOMER_BEARING_ORDER} THEN b.fee_total ELSE 0 END), 0), 2) AS customerRevenue,
ROUND(COALESCE(SUM(CASE WHEN ${CUSTOMER_BEARING_ORDER} THEN b.cost_total ELSE 0 END), 0), 2) AS customerCost,
ROUND(COALESCE(SUM(CASE WHEN ${COMPANY_BEARING_ORDER} THEN b.cost_total ELSE 0 END), 0), 2) AS companyCost,
ROUND(COALESCE(SUM(CASE WHEN ${OTHER_BEARING_ORDER} THEN b.cost_total ELSE 0 END), 0), 2) AS otherCost,
COUNT(*) AS recordCount,
COUNT(DISTINCT COALESCE(b.system_customer_id, b.customer_id, 0)) AS customerCount
FROM ${HYDROGEN_TABLE} b
LEFT JOIN new_hydrogen_site s ON s.id = b.station_id AND s.del_flag = '0'
LEFT JOIN common_district p ON CONVERT(p.CODE USING utf8mb4) COLLATE utf8mb4_unicode_ci = CONVERT(s.province USING utf8mb4) COLLATE utf8mb4_unicode_ci AND p.STATUS = 'VALID'
LEFT JOIN common_district ct ON CONVERT(ct.CODE USING utf8mb4) COLLATE utf8mb4_unicode_ci = CONVERT(s.city USING utf8mb4) COLLATE utf8mb4_unicode_ci AND ct.STATUS = 'VALID'
WHERE ${sqlWhere}
GROUP BY COALESCE(b.station_id, 0)
HAVING SUM(COALESCE(b.amount_kg, 0)) > 0
ORDER BY kg DESC`, params);
return rows;
}
/** 氢能 v2 查询(SQL 与参数顺序由 routes.test.ts 的指纹断言锁定)。 */
export async function loadOverviewCustomers(db: Database, args: { sqlWhere: string; params: unknown[] }): Promise<RowDataPacket[]> {
const { sqlWhere, params } = args;
const [rows] = await db.query<RowDataPacket[]>(`SELECT COALESCE(b.system_customer_id, b.customer_id, 0) AS id,
COALESCE(NULLIF(b.system_customer_name, ''), NULLIF(b.customer_name, ''), '未关联客户') AS name,
ROUND(COALESCE(SUM(b.amount_kg), 0), 3) AS kg,
ROUND(COALESCE(SUM(CASE WHEN ${CUSTOMER_BEARING_ORDER} THEN b.amount_kg ELSE 0 END), 0), 3) AS customerBearingKg,
ROUND(COALESCE(SUM(CASE WHEN ${COMPANY_BEARING_ORDER} THEN b.amount_kg ELSE 0 END), 0), 3) AS companyBearingKg,
ROUND(COALESCE(SUM(CASE WHEN ${OTHER_BEARING_ORDER} THEN b.amount_kg ELSE 0 END), 0), 3) AS otherBearingKg,
ROUND(COALESCE(SUM(b.cost_total), 0), 2) AS cost,
ROUND(COALESCE(SUM(b.fee_total), 0), 2) AS revenue,
ROUND(COALESCE(SUM(CASE WHEN ${CUSTOMER_BEARING_ORDER} THEN b.fee_total ELSE 0 END), 0), 2) AS customerRevenue,
ROUND(COALESCE(SUM(CASE WHEN ${CUSTOMER_BEARING_ORDER} THEN b.cost_total ELSE 0 END), 0), 2) AS customerCost,
ROUND(COALESCE(SUM(CASE WHEN ${COMPANY_BEARING_ORDER} THEN b.cost_total ELSE 0 END), 0), 2) AS companyCost,
ROUND(COALESCE(SUM(CASE WHEN ${OTHER_BEARING_ORDER} THEN b.cost_total ELSE 0 END), 0), 2) AS otherCost,
COUNT(*) AS recordCount
FROM ${HYDROGEN_TABLE} b
WHERE ${sqlWhere}
GROUP BY COALESCE(b.system_customer_id, b.customer_id, 0), COALESCE(NULLIF(b.system_customer_name, ''), NULLIF(b.customer_name, ''), '未关联客户')
ORDER BY kg DESC
LIMIT 200`, params);
return rows;
}
/** 氢能 v2 查询(SQL 与参数顺序由 routes.test.ts 的指纹断言锁定)。 */
export async function loadDailyRows(db: Database, args: { sqlWhere: string; params: unknown[] }): Promise<RowDataPacket[]> {
const { sqlWhere, params } = args;
const [rows] = await db.query<RowDataPacket[]>(`SELECT DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m-%d') AS date,
ROUND(COALESCE(SUM(b.amount_kg), 0), 3) AS kg,
ROUND(COALESCE(SUM(CASE WHEN b.vehicle_id IS NOT NULL THEN b.amount_kg ELSE 0 END), 0), 3) AS lingniuKg,
ROUND(COALESCE(SUM(CASE WHEN b.vehicle_id IS NULL THEN b.amount_kg ELSE 0 END), 0), 3) AS externalKg,
ROUND(COALESCE(SUM(b.cost_total), 0), 2) AS cost,
COUNT(*) AS recordCount,
COUNT(DISTINCT COALESCE(b.station_id, 0)) AS stationCount
FROM ${HYDROGEN_TABLE} b
WHERE ${sqlWhere}
GROUP BY DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m-%d')
ORDER BY date`, params);
return rows;
}
/** 氢能 v2 查询(SQL 与参数顺序由 routes.test.ts 的指纹断言锁定)。 */
export async function loadDailyWatermark(db: Database, args: { sqlWhere: string; params: unknown[] }): Promise<RowDataPacket[]> {
const { sqlWhere, params } = args;
const [rows] = await db.query<RowDataPacket[]>(`SELECT DATE_FORMAT(MAX(b.${HYDROGEN_LOCAL}), '%Y-%m-%d %H:%i:%s') AS ledgerAt
FROM ${HYDROGEN_TABLE} b WHERE ${sqlWhere}`, params);
return rows;
}
/** 氢能 v2 查询(SQL 与参数顺序由 routes.test.ts 的指纹断言锁定)。 */
export async function loadDailyTreeStations(db: Database, args: { sqlWhere: string; params: unknown[] }): Promise<RowDataPacket[]> {
const { sqlWhere, params } = args;
const [rows] = await db.query<RowDataPacket[]>(`SELECT CAST(MAX(COALESCE(b.station_id, 0)) AS CHAR) AS id,
COALESCE(MAX(NULLIF(s.site_short_name, '')), MAX(NULLIF(s.site_name, '')), MAX(NULLIF(b.station_name, '')), '未关联站点') AS name,
ROUND(COALESCE(SUM(b.amount_kg), 0), 3) AS kg,
ROUND(COALESCE(SUM(b.cost_total), 0), 2) AS cost,
COUNT(*) AS recordCount
FROM ${HYDROGEN_TABLE} b
LEFT JOIN new_hydrogen_site s ON s.id = b.station_id AND s.del_flag = '0'
WHERE ${sqlWhere}
GROUP BY COALESCE(b.station_id, 0)
ORDER BY kg DESC`, params);
return rows;
}
/** 氢能 v2 查询(SQL 与参数顺序由 routes.test.ts 的指纹断言锁定)。 */
export async function loadDailyTreeCustomers(db: Database, args: { sqlWhere: string; params: unknown[] }): Promise<RowDataPacket[]> {
const { sqlWhere, params } = args;
const [rows] = await db.query<RowDataPacket[]>(`SELECT CAST(MAX(COALESCE(b.station_id, 0)) AS CHAR) AS stationId,
COALESCE(b.system_customer_id, b.customer_id, 0) AS id,
COALESCE(NULLIF(b.system_customer_name, ''), NULLIF(b.customer_name, ''), '未关联客户') AS name,
ROUND(COALESCE(SUM(b.amount_kg), 0), 3) AS kg,
ROUND(COALESCE(SUM(b.cost_total), 0), 2) AS cost,
COUNT(*) AS recordCount
FROM ${HYDROGEN_TABLE} b
WHERE ${sqlWhere}
GROUP BY COALESCE(b.station_id, 0), COALESCE(b.system_customer_id, b.customer_id, 0), COALESCE(NULLIF(b.system_customer_name, ''), NULLIF(b.customer_name, ''), '未关联客户')
ORDER BY stationId, kg DESC`, params);
return rows;
}
/** 氢能 v2 查询(SQL 与参数顺序由 routes.test.ts 的指纹断言锁定)。 */
export async function loadDrillSummary(db: Database, args: { scopedWhere: string; params: unknown[] }): Promise<RowDataPacket[]> {
const { scopedWhere, params } = args;
const [rows] = await db.query<RowDataPacket[]>(`SELECT COUNT(*) AS recordCount,
COUNT(DISTINCT COALESCE(b.station_id, 0)) AS stationCount,
COUNT(DISTINCT COALESCE(b.system_customer_id, b.customer_id, 0)) AS customerCount,
SUM(CASE WHEN NULLIF(b.record_source, '') IS NOT NULL OR b.source IS NOT NULL THEN 1 ELSE 0 END) AS traceableRecordCount,
ROUND(COALESCE(SUM(b.amount_kg), 0), 3) AS kg,
ROUND(COALESCE(SUM(b.cost_total), 0), 2) AS cost, ROUND(COALESCE(SUM(b.fee_total), 0), 2) AS revenue
FROM ${HYDROGEN_TABLE} b WHERE ${scopedWhere}`, params);
return rows;
}
/** 氢能 v2 查询(SQL 与参数顺序由 routes.test.ts 的指纹断言锁定)。 */
export async function loadDrillGroups(db: Database, args: { scopedWhere: string; params: unknown[]; groupSelect: string; groupExpression: string; groupHaving: string; groupOrder: string; pageSize: number; offset: number }): Promise<RowDataPacket[]> {
const { scopedWhere, params, groupSelect, groupExpression, groupHaving, groupOrder, pageSize, offset } = args;
const [rows] = await db.query<RowDataPacket[]>(`SELECT ${groupSelect},
GROUP_CONCAT(DISTINCT COALESCE(CAST(b.settlement_type AS CHAR), 'unknown') ORDER BY COALESCE(CAST(b.settlement_type AS CHAR), 'unknown')) AS settlementTypes,
COUNT(*) AS recordCount, COUNT(DISTINCT COALESCE(b.station_id, 0)) AS stationCount,
COUNT(DISTINCT COALESCE(b.system_customer_id, b.customer_id, 0)) AS customerCount,
ROUND(COALESCE(SUM(b.amount_kg), 0), 3) AS kg, ROUND(COALESCE(SUM(b.cost_total), 0), 2) AS cost, ROUND(COALESCE(SUM(b.fee_total), 0), 2) AS revenue,
ROUND(COALESCE(SUM(CASE WHEN b.vehicle_id IS NOT NULL THEN b.amount_kg ELSE 0 END), 0), 3) AS lingniuKg,
ROUND(COALESCE(SUM(CASE WHEN b.vehicle_id IS NULL THEN b.amount_kg ELSE 0 END), 0), 3) AS externalKg
FROM ${HYDROGEN_TABLE} b
LEFT JOIN new_hydrogen_site s ON s.id = b.station_id AND s.del_flag = '0'
LEFT JOIN common_district p ON CONVERT(p.CODE USING utf8mb4) COLLATE utf8mb4_unicode_ci = CONVERT(s.province USING utf8mb4) COLLATE utf8mb4_unicode_ci AND p.STATUS = 'VALID'
LEFT JOIN common_district ct ON CONVERT(ct.CODE USING utf8mb4) COLLATE utf8mb4_unicode_ci = CONVERT(s.city USING utf8mb4) COLLATE utf8mb4_unicode_ci AND ct.STATUS = 'VALID'
WHERE ${scopedWhere}
GROUP BY ${groupExpression}
${groupHaving}
ORDER BY ${groupOrder} LIMIT ? OFFSET ?`, [...params, pageSize, offset]);
return rows;
}
/** 氢能 v2 查询(SQL 与参数顺序由 routes.test.ts 的指纹断言锁定)。 */
export async function loadDrillRecords(db: Database, args: { scopedWhere: string; params: unknown[]; pageSize: number; offset: number }): Promise<RowDataPacket[]> {
const { scopedWhere, params, pageSize, offset } = args;
const [rows] = await db.query<RowDataPacket[]>(`SELECT b.id, b.settlement_type AS settlementType, DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m-%d %H:%i:%s') AS time, b.order_no AS orderNo,
CAST(COALESCE(b.station_id, 0) AS CHAR) AS stationId, COALESCE(NULLIF(b.station_name, ''), '未关联站点') AS stationName,
COALESCE(b.system_customer_id, b.customer_id, 0) AS customerId, COALESCE(NULLIF(b.system_customer_name, ''), NULLIF(b.customer_name, ''), '未关联客户') AS customerName,
COALESCE(NULLIF(b.license_plate, ''), '无车牌') AS plateNo, COALESCE(NULLIF(b.record_source, ''), CAST(b.source AS CHAR), '未知来源') AS source,
COALESCE(NULLIF(b.verify_status, ''), 'UNVERIFIED') AS verifyStatus, b.vehicle_id AS vehicleId,
ROUND(COALESCE(b.amount_kg, 0), 3) AS kg, ROUND(COALESCE(b.cost_price, 0), 2) AS unitPrice,
ROUND(COALESCE(b.cost_total, 0), 2) AS cost, ROUND(COALESCE(b.fee_total, 0), 2) AS revenue
FROM ${HYDROGEN_TABLE} b WHERE ${scopedWhere}
ORDER BY b.${HYDROGEN_LOCAL} DESC, b.id DESC LIMIT ? OFFSET ?`, [...params, pageSize, offset]);
return rows;
}
/** 下钻的金额口径片段:在基础 WHERE 之上追加承担方条件。 */
export function buildScopedWhere(sqlWhere: string, amountScope: string): string {
if (amountScope === 'customer') return `${sqlWhere} AND ${CUSTOMER_BEARING_ORDER}`;
if (amountScope === 'company') return `${sqlWhere} AND ${COMPANY_BEARING_ORDER}`;
if (amountScope === 'other') return `${sqlWhere} AND ${OTHER_BEARING_ORDER}`;
return sqlWhere;
}
/** 下钻分组列(站点 / 客户 / 日期 / 车牌)。 */
export function buildGroupSelect(groupBy: string): string {
if (groupBy === 'station') return "CAST(MAX(COALESCE(b.station_id, 0)) AS CHAR) AS id, COALESCE(MAX(NULLIF(s.site_short_name, '')), MAX(NULLIF(s.site_name, '')), MAX(NULLIF(b.station_name, '')), '未关联站点') AS name, COALESCE(MAX(NULLIF(p.NAME, '')), '未归属') AS province, COALESCE(MAX(NULLIF(ct.NAME, '')), '未归属') AS city";
if (groupBy === 'customer') return "COALESCE(b.system_customer_id, b.customer_id, 0) AS id, COALESCE(NULLIF(b.system_customer_name, ''), NULLIF(b.customer_name, ''), '未关联客户') AS name, NULL AS province, NULL AS city";
if (groupBy === 'date') return `DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m-%d') AS id, DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m-%d') AS name, NULL AS province, NULL AS city`;
return "COALESCE(NULLIF(b.license_plate, ''), '无车牌') AS id, COALESCE(NULLIF(b.license_plate, ''), '无车牌') AS name, NULL AS province, NULL AS city";
}
/** 下钻分组表达式(与 buildGroupSelect 必须成对使用)。 */
export function buildGroupExpression(groupBy: string): string {
if (groupBy === 'station') return 'COALESCE(b.station_id, 0)';
if (groupBy === 'customer') return "COALESCE(b.system_customer_id, b.customer_id, 0), COALESCE(NULLIF(b.system_customer_name, ''), NULLIF(b.customer_name, ''), '未关联客户')";
if (groupBy === 'date') return `DATE_FORMAT(b.${HYDROGEN_LOCAL}, '%Y-%m-%d')`;
return "COALESCE(NULLIF(b.license_plate, ''), '无车牌')";
}
/** 由筛选条件构建 WHERE 片段与参数(顺序即 SQL 参数顺序,由契约测试锁定)。 */
export function buildFilterClauses(input: {
startDate: string;
endDate: string;
stationId: string | null;
customerId: number | null;
customerName: string | null;
plateNo: string | null;
vehicleScope: string;
verifyScope: string;
}): { clauses: string[]; params: unknown[] } {
const { startDate: safeStart, endDate: safeEnd, stationId, customerId, customerName, plateNo, vehicleScope, verifyScope } = input;
const clauses = [
HYDROGEN_FUEL_ONLY_WHERE_B,
`b.${HYDROGEN_LOCAL} >= ?`,
`b.${HYDROGEN_LOCAL} < DATE_ADD(?, INTERVAL 1 DAY)`,
];
const params: unknown[] = [
safeStart < HYDROGEN_MIN_DATE ? HYDROGEN_MIN_DATE : safeStart,
safeEnd,
];
if (stationId !== null) {
clauses.push("COALESCE(b.station_id, 0) = ?");
params.push(stationId);
}
if (customerId !== null) {
clauses.push("COALESCE(b.system_customer_id, b.customer_id, 0) = ?");
params.push(customerId);
}
if (customerName) {
clauses.push(
"COALESCE(NULLIF(b.system_customer_name, ''), NULLIF(b.customer_name, ''), '未关联客户') = ?",
);
params.push(customerName);
}
if (plateNo) {
clauses.push("COALESCE(NULLIF(b.license_plate, ''), '无车牌') = ?");
params.push(plateNo);
}
if (vehicleScope === "lingniu") clauses.push("b.vehicle_id IS NOT NULL");
if (vehicleScope === "external") clauses.push("b.vehicle_id IS NULL");
if (verifyScope === "verified")
clauses.push(
"LOWER(COALESCE(NULLIF(TRIM(b.verify_status), ''), 'unverified')) = 'verified'",
);
if (verifyScope === "unverified")
clauses.push(
"LOWER(COALESCE(NULLIF(TRIM(b.verify_status), ''), 'unverified')) <> 'verified'",
"b.refuel_time >= '2026-05-01'",
);
return { clauses, params };
}