feat(gps): скорость в км/ч (конверсия узлов Traccar + backfill), деление маршрута на поездки, проигрывание от любой точки, скорости x100-x1000

This commit is contained in:
2026-08-26 09:24:29 +03:00
parent a4b508f70e
commit 3e8849019a
6 changed files with 230 additions and 76 deletions

View File

@@ -58,11 +58,13 @@ function normalizePoint(raw: any): NormalizedPoint | null {
const lat = parseNum(p.latitude);
const lng = parseNum(p.longitude);
if (lat === null || lng === null) return null;
// Traccar отдаёт скорость в узлах (knots) — в БД храним км/ч
const speedKnots = parseNum(p.speed);
return {
externalId: String(raw.device.uniqueId),
lat,
lng,
speed: parseNum(p.speed),
speed: speedKnots !== null ? Math.round(speedKnots * 1.852 * 10) / 10 : null,
course: parseNum(p.course),
accuracy: parseNum(p.accuracy),
recordedAt: parseDateParam(p.fixTime ?? p.deviceTime),
@@ -178,13 +180,15 @@ export function registerGpsRoutes(app: Express) {
router.put("/settings", async (req: AuthenticatedRequest, res: Response) => {
// Обновляем только переданные поля (обратная совместимость с клиентом,
// который шлёт только jitterMeters)
// который шлёт только часть настроек)
const current = await gpsStorage.getSettings(req.user!.organizationId);
const jitterMeters =
req.body?.jitterMeters !== undefined ? Number(req.body.jitterMeters) : current.jitterMeters;
const maxJumpSpeedKmh =
req.body?.maxJumpSpeedKmh !== undefined ? Number(req.body.maxJumpSpeedKmh) : current.maxJumpSpeedKmh;
const tripStopMinutes =
req.body?.tripStopMinutes !== undefined ? Number(req.body.tripStopMinutes) : current.tripStopMinutes;
if (!Number.isInteger(jitterMeters) || jitterMeters < 0 || jitterMeters > 1000) {
return badRequest(res, "jitterMeters: целое число от 0 до 1000");
@@ -192,9 +196,17 @@ export function registerGpsRoutes(app: Express) {
if (!Number.isInteger(maxJumpSpeedKmh) || maxJumpSpeedKmh < 0 || maxJumpSpeedKmh > 5000) {
return badRequest(res, "maxJumpSpeedKmh: целое число от 0 до 5000 (0 = фильтр выключен)");
}
if (!Number.isInteger(tripStopMinutes) || tripStopMinutes < 0 || tripStopMinutes > 120) {
return badRequest(res, "tripStopMinutes: целое число от 0 до 120 (0 = не делить на поездки)");
}
const row = await gpsStorage.upsertSettings(req.user!.organizationId, { jitterMeters, maxJumpSpeedKmh });
res.json({ success: true, jitterMeters: row.jitterMeters, maxJumpSpeedKmh: row.maxJumpSpeedKmh });
const row = await gpsStorage.upsertSettings(req.user!.organizationId, { jitterMeters, maxJumpSpeedKmh, tripStopMinutes });
res.json({
success: true,
jitterMeters: row.jitterMeters,
maxJumpSpeedKmh: row.maxJumpSpeedKmh,
tripStopMinutes: row.tripStopMinutes,
});
});
// =====================

View File

@@ -462,7 +462,7 @@ export class GpsStorage {
// =====================
/** Настройки GPS организации; если строки нет — дефолты */
async getSettings(organizationId: number): Promise<{ jitterMeters: number; maxJumpSpeedKmh: number }> {
async getSettings(organizationId: number): Promise<{ jitterMeters: number; maxJumpSpeedKmh: number; tripStopMinutes: number }> {
const [row] = await db
.select()
.from(gpsSettings)
@@ -470,12 +470,13 @@ export class GpsStorage {
return {
jitterMeters: row?.jitterMeters ?? 25,
maxJumpSpeedKmh: row?.maxJumpSpeedKmh ?? 250,
tripStopMinutes: row?.tripStopMinutes ?? 10,
};
}
async upsertSettings(
organizationId: number,
settings: { jitterMeters: number; maxJumpSpeedKmh: number }
settings: { jitterMeters: number; maxJumpSpeedKmh: number; tripStopMinutes: number }
): Promise<GpsSettings> {
const [row] = await db
.insert(gpsSettings)