fix(gps): отсев устаревших копий и LBS-телепортов (stale/jump фильтры), настройка maxJumpSpeedKmh, фильтрация трека при выдаче

This commit is contained in:
2026-08-25 17:49:38 +03:00
parent b635d1e16e
commit 3d24a15fb3
8 changed files with 189 additions and 38 deletions

View File

@@ -2,7 +2,7 @@ import { Router } from "express";
import type { Express, Response } from "express";
import { authenticateToken, type AuthenticatedRequest } from "../middleware/auth.middleware";
import { gpsStorage } from "./storage";
import { processPosition } from "./geozone.service";
import { processPosition, classifyPoint } from "./geozone.service";
import { ensureDevice, deleteDevice } from "./traccar.client";
import { storage } from "../storage";
@@ -173,16 +173,28 @@ export function registerGpsRoutes(app: Express) {
router.get("/settings", async (req: AuthenticatedRequest, res: Response) => {
const settings = await gpsStorage.getSettings(req.user!.organizationId);
res.json({ success: true, jitterMeters: settings.jitterMeters });
res.json({ success: true, ...settings });
});
router.put("/settings", async (req: AuthenticatedRequest, res: Response) => {
const jitterMeters = Number(req.body?.jitterMeters);
// Обновляем только переданные поля (обратная совместимость с клиентом,
// который шлёт только 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;
if (!Number.isInteger(jitterMeters) || jitterMeters < 0 || jitterMeters > 1000) {
return badRequest(res, "jitterMeters: целое число от 0 до 1000");
}
const row = await gpsStorage.upsertSettings(req.user!.organizationId, jitterMeters);
res.json({ success: true, jitterMeters: row.jitterMeters });
if (!Number.isInteger(maxJumpSpeedKmh) || maxJumpSpeedKmh < 0 || maxJumpSpeedKmh > 5000) {
return badRequest(res, "maxJumpSpeedKmh: целое число от 0 до 5000 (0 = фильтр выключен)");
}
const row = await gpsStorage.upsertSettings(req.user!.organizationId, { jitterMeters, maxJumpSpeedKmh });
res.json({ success: true, jitterMeters: row.jitterMeters, maxJumpSpeedKmh: row.maxJumpSpeedKmh });
});
// =====================
@@ -306,19 +318,36 @@ export function registerGpsRoutes(app: Express) {
const all = await gpsStorage.getTrack(id, orgId, from ?? undefined, to ?? undefined);
const total = all.length;
let points = all;
// Фильтрация сохранённых данных при выдаче (без удаления из БД):
// пропускаем stale (устаревшие копии) и jump (LBS-скачки) — те же правила,
// что при приёме, сравнение с предыдущей принятой точкой
const settings = await gpsStorage.getSettings(orgId);
const filtered: typeof all = [];
let filteredCount = 0;
let prevAccepted: { lat: number; lng: number; recordedAt: Date } | null = null;
for (const point of all) {
const current = { lat: point.lat, lng: point.lng, recordedAt: point.recordedAt };
if (classifyPoint(prevAccepted, current, settings) === "accept") {
filtered.push(point);
prevAccepted = current;
} else {
filteredCount++;
}
}
let points = filtered;
let sampled = false;
if (total > MAX_TRACK_POINTS) {
const step = Math.ceil(total / MAX_TRACK_POINTS);
points = all.filter((_, idx) => idx % step === 0);
if (points.length > MAX_TRACK_POINTS) {
const step = Math.ceil(points.length / MAX_TRACK_POINTS);
points = points.filter((_, idx) => idx % step === 0);
// Всегда включаем последнюю точку трека
if (points[points.length - 1] !== all[total - 1]) {
points.push(all[total - 1]);
if (points[points.length - 1] !== filtered[filtered.length - 1]) {
points.push(filtered[filtered.length - 1]);
}
sampled = true;
}
res.json({ success: true, points, total, sampled });
res.json({ success: true, points, total, filteredCount, sampled });
});
// =====================