refactor: modularize application domains
This commit is contained in:
@@ -3,46 +3,28 @@ import type { RowDataPacket } from 'mysql2';
|
||||
import pool from '../db.js';
|
||||
import type { AuthUser } from '../auth/types.js';
|
||||
import { canAccessEnergy } from '../auth/types.js';
|
||||
import {
|
||||
buildHydrogenPoints,
|
||||
filterHydrogenStationsByRadius,
|
||||
HYDROGEN_HEATMAP_DEFAULT_END,
|
||||
HYDROGEN_HEATMAP_DEFAULT_START,
|
||||
parseHydrogenHeatmapQuery,
|
||||
parseNearbyQuery,
|
||||
rankHydrogenStations,
|
||||
serializeHydrogenStation,
|
||||
type HydrogenPayer,
|
||||
type HydrogenStationRecord,
|
||||
} from './hydrogen-heatmap-model.js';
|
||||
|
||||
type HydrogenHeatmapMetric = 'kg' | 'refuels' | 'vehicles';
|
||||
type Payer = 'all' | 'lingniu' | 'customer';
|
||||
type StationRow = RowDataPacket & HydrogenStationRecord;
|
||||
|
||||
type StationRow = RowDataPacket & {
|
||||
station_id: string;
|
||||
station_name: string;
|
||||
address: string;
|
||||
longitude: string | number;
|
||||
latitude: string | number;
|
||||
kg: string | number;
|
||||
refuel_count: string | number;
|
||||
vehicle_count: string | number;
|
||||
first_refuel: string;
|
||||
last_refuel: string;
|
||||
};
|
||||
|
||||
const DEFAULT_START = '2026-01-01';
|
||||
const DEFAULT_END = '2026-07-13';
|
||||
const VALID_COORDINATE = `
|
||||
o.longitude BETWEEN 73.5 AND 135.1
|
||||
AND o.latitude BETWEEN 18 AND 53.6
|
||||
AND COALESCE(o.province, '') NOT LIKE '%西藏%'
|
||||
`;
|
||||
|
||||
function isDate(value: string | undefined): value is string {
|
||||
return Boolean(value && /^\d{4}-\d{2}-\d{2}$/.test(value));
|
||||
}
|
||||
|
||||
function parseMetric(value: string | undefined): HydrogenHeatmapMetric {
|
||||
if (value === 'refuels' || value === 'vehicles') return value;
|
||||
return 'kg';
|
||||
}
|
||||
|
||||
function parsePayer(value: string | undefined): Payer {
|
||||
if (value === 'lingniu' || value === 'customer') return value;
|
||||
return 'all';
|
||||
}
|
||||
|
||||
function buildWhere(startDate: string, endDate: string, query: string, payer: Payer) {
|
||||
function buildWhere(startDate: string, endDate: string, query: string, payer: HydrogenPayer) {
|
||||
const clauses = [
|
||||
`b.del_flag = '0'`,
|
||||
`DATE(b.refuel_time) BETWEEN ? AND ?`,
|
||||
@@ -62,7 +44,12 @@ function buildWhere(startDate: string, endDate: string, query: string, payer: Pa
|
||||
return { sql: clauses.join(' AND '), params };
|
||||
}
|
||||
|
||||
async function loadStations(startDate: string, endDate: string, query: string, payer: Payer): Promise<StationRow[]> {
|
||||
async function loadStations(
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
query: string,
|
||||
payer: HydrogenPayer,
|
||||
): Promise<StationRow[]> {
|
||||
const where = buildWhere(startDate, endDate, query, payer);
|
||||
const [rows] = await pool.execute<StationRow[]>(`
|
||||
SELECT
|
||||
@@ -91,38 +78,6 @@ async function loadStations(startDate: string, endDate: string, query: string, p
|
||||
return rows;
|
||||
}
|
||||
|
||||
function metricValue(row: StationRow, metric: HydrogenHeatmapMetric): number {
|
||||
if (metric === 'refuels') return Number(row.refuel_count) || 0;
|
||||
if (metric === 'vehicles') return Number(row.vehicle_count) || 0;
|
||||
return Number(row.kg) || 0;
|
||||
}
|
||||
|
||||
function serializeStation(row: StationRow, metric: HydrogenHeatmapMetric) {
|
||||
return {
|
||||
stationId: row.station_id,
|
||||
stationName: row.station_name,
|
||||
address: row.address,
|
||||
lng: Number(row.longitude),
|
||||
lat: Number(row.latitude),
|
||||
kg: Number(row.kg) || 0,
|
||||
refuelCount: Number(row.refuel_count) || 0,
|
||||
vehicleCount: Number(row.vehicle_count) || 0,
|
||||
firstRefuel: row.first_refuel,
|
||||
lastRefuel: row.last_refuel,
|
||||
metricValue: metricValue(row, metric),
|
||||
};
|
||||
}
|
||||
|
||||
function haversineKm(leftLng: number, leftLat: number, rightLng: number, rightLat: number): number {
|
||||
const radians = (degrees: number) => degrees * Math.PI / 180;
|
||||
const earthRadius = 6371;
|
||||
const dLat = radians(rightLat - leftLat);
|
||||
const dLng = radians(rightLng - leftLng);
|
||||
const a = Math.sin(dLat / 2) ** 2
|
||||
+ Math.cos(radians(leftLat)) * Math.cos(radians(rightLat)) * Math.sin(dLng / 2) ** 2;
|
||||
return earthRadius * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
}
|
||||
|
||||
const router = new Hono();
|
||||
|
||||
router.use('*', async (c, next) => {
|
||||
@@ -173,8 +128,8 @@ router.get('/meta', async (c) => {
|
||||
const totalRefuelCount = Number(profile.total_refuel_count) || 0;
|
||||
const eligibleRefuelCount = Number(profile.eligible_refuel_count) || 0;
|
||||
return c.json({
|
||||
startDate: profile.start_date || DEFAULT_START,
|
||||
endDate: profile.end_date || DEFAULT_END,
|
||||
startDate: profile.start_date || HYDROGEN_HEATMAP_DEFAULT_START,
|
||||
endDate: profile.end_date || HYDROGEN_HEATMAP_DEFAULT_END,
|
||||
totalRefuelCount,
|
||||
eligibleRefuelCount,
|
||||
excludedRefuelCount: Math.max(0, totalRefuelCount - eligibleRefuelCount),
|
||||
@@ -192,19 +147,17 @@ router.get('/meta', async (c) => {
|
||||
});
|
||||
|
||||
router.get('/points', async (c) => {
|
||||
const startDate = isDate(c.req.query('startDate')) ? c.req.query('startDate')! : DEFAULT_START;
|
||||
const endDate = isDate(c.req.query('endDate')) ? c.req.query('endDate')! : DEFAULT_END;
|
||||
const { startDate, endDate, query, payer, metric } = parseHydrogenHeatmapQuery({
|
||||
startDate: c.req.query('startDate'),
|
||||
endDate: c.req.query('endDate'),
|
||||
query: c.req.query('query'),
|
||||
payer: c.req.query('payer'),
|
||||
metric: c.req.query('metric'),
|
||||
});
|
||||
if (startDate > endDate) return c.json({ error: '开始日期不能晚于结束日期' }, 400);
|
||||
const query = (c.req.query('query') || '').trim();
|
||||
const payer = parsePayer(c.req.query('payer'));
|
||||
const metric = parseMetric(c.req.query('metric'));
|
||||
const stations = await loadStations(startDate, endDate, query, payer);
|
||||
const ranked = [...stations].sort((left, right) => metricValue(right, metric) - metricValue(left, metric));
|
||||
const points = ranked.map((row) => ({
|
||||
lng: Number(row.longitude),
|
||||
lat: Number(row.latitude),
|
||||
count: metricValue(row, metric),
|
||||
}));
|
||||
const ranked = rankHydrogenStations(stations, metric);
|
||||
const { points, max } = buildHydrogenPoints(ranked, metric);
|
||||
const where = buildWhere(startDate, endDate, query, payer);
|
||||
const [summaryRows] = await pool.execute<RowDataPacket[]>(`
|
||||
SELECT
|
||||
@@ -230,28 +183,27 @@ router.get('/points', async (c) => {
|
||||
stationCount: Number(summary.station_count) || 0,
|
||||
dayCount: Number(summary.day_count) || 0,
|
||||
points,
|
||||
max: Math.max(1, ...points.map((point) => point.count)),
|
||||
topStations: ranked.slice(0, 10).map((row) => serializeStation(row, metric)),
|
||||
max,
|
||||
topStations: ranked.slice(0, 10).map((row) => serializeHydrogenStation(row, metric)),
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/nearby', async (c) => {
|
||||
const longitude = Number(c.req.query('lng'));
|
||||
const latitude = Number(c.req.query('lat'));
|
||||
if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) return c.json({ error: '经纬度参数无效' }, 400);
|
||||
const startDate = isDate(c.req.query('startDate')) ? c.req.query('startDate')! : DEFAULT_START;
|
||||
const endDate = isDate(c.req.query('endDate')) ? c.req.query('endDate')! : DEFAULT_END;
|
||||
const query = (c.req.query('query') || '').trim();
|
||||
const payer = parsePayer(c.req.query('payer'));
|
||||
const metric = parseMetric(c.req.query('metric'));
|
||||
const radiusKm = Math.min(200, Math.max(5, Number(c.req.query('radiusKm')) || 50));
|
||||
const { center, radiusKm } = parseNearbyQuery({
|
||||
lng: c.req.query('lng'),
|
||||
lat: c.req.query('lat'),
|
||||
radiusKm: c.req.query('radiusKm'),
|
||||
});
|
||||
if (!center) return c.json({ error: '经纬度参数无效' }, 400);
|
||||
const { startDate, endDate, query, payer, metric } = parseHydrogenHeatmapQuery({
|
||||
startDate: c.req.query('startDate'),
|
||||
endDate: c.req.query('endDate'),
|
||||
query: c.req.query('query'),
|
||||
payer: c.req.query('payer'),
|
||||
metric: c.req.query('metric'),
|
||||
});
|
||||
const stations = await loadStations(startDate, endDate, query, payer);
|
||||
const nearby = stations.filter((row) => haversineKm(
|
||||
longitude,
|
||||
latitude,
|
||||
Number(row.longitude),
|
||||
Number(row.latitude),
|
||||
) <= radiusKm);
|
||||
const nearby = filterHydrogenStationsByRadius(stations, center, radiusKm);
|
||||
const nearbyIds = nearby.map((row) => row.station_id);
|
||||
let nearbySummary = { kg: 0, refuelCount: 0, vehicleCount: 0, stationCount: 0 };
|
||||
if (nearbyIds.length) {
|
||||
@@ -275,15 +227,15 @@ router.get('/nearby', async (c) => {
|
||||
stationCount: Number(rows[0]?.station_count) || 0,
|
||||
};
|
||||
}
|
||||
const ranked = [...nearby].sort((left, right) => metricValue(right, metric) - metricValue(left, metric));
|
||||
const ranked = rankHydrogenStations(nearby, metric);
|
||||
return c.json({
|
||||
center: { lng: longitude, lat: latitude },
|
||||
center,
|
||||
radiusKm,
|
||||
kg: nearbySummary.kg,
|
||||
refuelCount: nearbySummary.refuelCount,
|
||||
vehicleCount: nearbySummary.vehicleCount,
|
||||
stationCount: nearbySummary.stationCount,
|
||||
topStations: ranked.slice(0, 10).map((row) => serializeStation(row, metric)),
|
||||
topStations: ranked.slice(0, 10).map((row) => serializeHydrogenStation(row, metric)),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user