feat(gps): фильтры heartbeat-эхо/valid=false/spike; доступ к GPS и Финансам — матрица на /users (пользователи+роли)

This commit is contained in:
2026-08-26 17:13:49 +03:00
parent 2fb7ad6606
commit ac4f89d42e
16 changed files with 431 additions and 227 deletions

View File

@@ -43,6 +43,8 @@ interface NormalizedPoint {
course: number | null;
accuracy: number | null;
recordedAt: Date | null;
/** false только у нативного формата Traccar (position.valid === false) */
valid: boolean;
}
/**
@@ -69,6 +71,7 @@ function normalizePoint(raw: any): NormalizedPoint | null {
course: parseNum(p.course),
accuracy: parseNum(p.accuracy),
recordedAt: parseDateParam(p.fixTime ?? p.deviceTime),
valid: p.valid !== false,
};
}
@@ -84,6 +87,7 @@ function normalizePoint(raw: any): NormalizedPoint | null {
course: parseNum(raw.course),
accuracy: parseNum(raw.accuracy),
recordedAt: parseDateParam(raw.timestamp),
valid: true,
};
}
@@ -118,6 +122,14 @@ async function handleIngest(req: AuthenticatedRequest, res: Response) {
continue;
}
// Traccar пометил позицию как невалидную (valid=false) — не сохраняем,
// но считаем признаком жизни устройства
if (!point.valid) {
await gpsStorage.touchAssetSeen(asset.id);
processed++;
continue;
}
try {
await processPosition(asset.id, asset.organizationId, {
lat: point.lat,
@@ -182,7 +194,7 @@ export function registerGpsRoutes(app: Express) {
}
const settings = await gpsStorage.getSettings(orgId);
const allowed = await hasGpsAccess(req.user!, settings);
const allowed = await hasGpsAccess(req.user!);
const defaultCenter =
settings.defaultLat !== null && settings.defaultLng !== null && settings.defaultZoom !== null
? { lat: settings.defaultLat, lng: settings.defaultLng, zoom: settings.defaultZoom }
@@ -298,15 +310,13 @@ export function registerGpsRoutes(app: Express) {
return badRequest(res, "tripStopMinutes: целое число от 0 до 120 (0 = не делить на поездки)");
}
// Поля центра карты и доступа — только для admin (у остальных молча игнорируются)
// Центр карты по умолчанию — только для admin (у остальных молча игнорируется)
let defaultLat = current.defaultLat;
let defaultLng = current.defaultLng;
let defaultZoom = current.defaultZoom;
let allowedUserIds = current.allowedUserIds;
let allowedRoleIds = current.allowedRoleIds;
if (isAdmin) {
// Центр по умолчанию: либо все три поля заданы, либо все null (очистка)
// Либо все три поля заданы, либо все null (очистка)
const hasCenterField =
req.body?.defaultLat !== undefined || req.body?.defaultLng !== undefined || req.body?.defaultZoom !== undefined;
if (hasCenterField) {
@@ -331,26 +341,6 @@ export function registerGpsRoutes(app: Express) {
defaultZoom = zoom;
}
}
// Списки доступа: массивы целых чисел или null (очистка)
if (req.body?.allowedUserIds !== undefined) {
if (req.body.allowedUserIds === null) {
allowedUserIds = null;
} else if (!isIntArray(req.body.allowedUserIds)) {
return badRequest(res, "allowedUserIds: массив целых чисел или null");
} else {
allowedUserIds = req.body.allowedUserIds;
}
}
if (req.body?.allowedRoleIds !== undefined) {
if (req.body.allowedRoleIds === null) {
allowedRoleIds = null;
} else if (!isIntArray(req.body.allowedRoleIds)) {
return badRequest(res, "allowedRoleIds: массив целых чисел или null");
} else {
allowedRoleIds = req.body.allowedRoleIds;
}
}
}
const row = await gpsStorage.upsertSettings(req.user!.organizationId, {
@@ -360,8 +350,6 @@ export function registerGpsRoutes(app: Express) {
defaultLat,
defaultLng,
defaultZoom,
allowedUserIds,
allowedRoleIds,
});
res.json({
success: true,
@@ -371,8 +359,6 @@ export function registerGpsRoutes(app: Express) {
defaultLat: row.defaultLat,
defaultLng: row.defaultLng,
defaultZoom: row.defaultZoom,
allowedUserIds: row.allowedUserIds,
allowedRoleIds: row.allowedRoleIds,
});
});
@@ -689,7 +675,3 @@ function isValidPolygon(polygon: unknown): polygon is Array<[number, number]> {
)
);
}
function isIntArray(value: unknown): value is number[] {
return Array.isArray(value) && value.every((v) => Number.isInteger(v));
}