1 line
13 KiB
JSON
1 line
13 KiB
JSON
{"updatedAt":"2025-12-11T08:19:23.847Z","createdAt":"2025-12-11T07:26:21.150Z","id":"VCPJdSaOUiAvXjBI","name":"Traffic Forecast via Telegram","active":false,"isArchived":false,"nodes":[{"parameters":{"updates":["message"],"additionalFields":{}},"name":"Telegram Trigger","type":"n8n-nodes-base.telegramTrigger","typeVersion":1,"position":[-592,-96],"id":"8971e1d6-fca4-4f4e-8e1d-c7e38823f716","webhookId":"traffic-forecast-bot","credentials":{"telegramApi":{"id":"L9KHcMiteyKROF5r","name":"Telegram main n8n"}}},{"parameters":{"resource":"file","fileId":"={{ $json.message.document.file_id }}","additionalFields":{}},"name":"Get File","type":"n8n-nodes-base.telegram","typeVersion":1,"position":[-368,-96],"id":"54455673-f9ca-4f5a-982e-b4f94d97aed7","webhookId":"b572f623-3010-47da-b480-eae5ae111b38","credentials":{"telegramApi":{"id":"L9KHcMiteyKROF5r","name":"Telegram main n8n"}}},{"parameters":{"options":{}},"name":"Parse File","type":"n8n-nodes-base.spreadsheetFile","typeVersion":2,"position":[-160,-96],"id":"a7cf8415-5a2e-44d7-8211-ae63de825efd"},{"parameters":{"jsCode":"// --- ПОЛУЧЕНИЕ ДАННЫХ ---\nconst items = $input.all();\nconst inputData = items.map(item => item.json);\n\n// --- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ---\nfunction parseCleanNumber(value) {\n if (typeof value === 'number') return value;\n if (!value) return 0;\n const cleanStr = String(value).replace(/\\s/g, '').replace(/,/g, '.');\n const parsed = parseFloat(cleanStr);\n return isNaN(parsed) ? 0 : parsed;\n}\n\nfunction getJsDateFromExcel(serial) {\n const utc_days = Math.floor(serial - 25569);\n const utc_value = utc_days * 86400; \n const date_info = new Date(utc_value * 1000);\n return new Date(date_info.getFullYear(), date_info.getMonth(), date_info.getDate());\n}\n\nfunction formatDate(date) {\n const year = date.getFullYear();\n const month = String(date.getMonth() + 1).padStart(2, '0');\n const day = String(date.getDate()).padStart(2, '0');\n return `${year}-${month}-${day}`;\n}\n\n// --- ШАГ 1: АНАЛИЗ СТАТИСТИКИ ---\n\nlet totalTrafficSum = 0;\nlet totalCount = 0;\n\nconst trafficByDay = {}; // Дни недели (0-6)\nconst trafficByMonth = {}; // Месяцы (0-11)\nconst trafficByYear = {}; // Годы (2022, 2023...) -> ЭТО НОВОЕ\n\nif (inputData.length === 0) return [];\nconst keys = Object.keys(inputData[0]);\nconst dateKey = keys.find(k => /date|дата|день/i.test(k)) || keys[0];\nconst trafficKey = keys.find(k => /traffic|трафик|кол-во|value/i.test(k)) || keys[1];\n\n// 1. Сбор данных\nfor (const row of inputData) {\n const rawTraffic = row[trafficKey];\n const rawDate = row[dateKey];\n const traffic = parseCleanNumber(rawTraffic);\n \n // Игнорируем нули, чтобы не портить среднее\n if (traffic === 0) continue;\n\n let date;\n if (typeof rawDate === 'number') date = getJsDateFromExcel(rawDate);\n else date = new Date(rawDate);\n \n if (isNaN(date.getTime())) continue;\n\n totalTrafficSum += traffic;\n totalCount++;\n\n const d = date.getDay();\n const m = date.getMonth();\n const y = date.getFullYear(); // Получаем год\n\n // Суммы по дням недели\n trafficByDay[d] = (trafficByDay[d] || {s:0, c:0});\n trafficByDay[d].s += traffic;\n trafficByDay[d].c++;\n\n // Суммы по месяцам\n trafficByMonth[m] = (trafficByMonth[m] || {s:0, c:0});\n trafficByMonth[m].s += traffic;\n trafficByMonth[m].c++;\n\n // Суммы по годам (Тренд)\n trafficByYear[y] = (trafficByYear[y] || {s:0, c:0});\n trafficByYear[y].s += traffic;\n trafficByYear[y].c++;\n}\n\nconst avgTotalTraffic = totalCount > 0 ? totalTrafficSum / totalCount : 0;\n\n// 2. Расчет коэффициентов\nconst coeffWeekday = {};\nconst coeffMonth = {};\nconst coeffYear = {}; // Коэффициент силы года\n\n// Дни недели\nfor (let i = 0; i < 7; i++) {\n const data = trafficByDay[i];\n coeffWeekday[i] = data ? (data.s / data.c) / avgTotalTraffic : 1.0;\n}\n// Месяцы\nfor (let i = 0; i < 12; i++) {\n const data = trafficByMonth[i];\n coeffMonth[i] = data ? (data.s / data.c) / avgTotalTraffic : 1.0;\n}\n// Годы (Новое!)\nfor (const y in trafficByYear) {\n const data = trafficByYear[y];\n // Если среднее за 2024 год = 300, а общее среднее = 200, то K_Year = 1.5\n coeffYear[y] = data ? (data.s / data.c) / avgTotalTraffic : 1.0;\n}\n\n// --- ШАГ 2: РАСЧЕТ ПРОГНОЗА ---\n\nconst finalOutput = inputData.map(row => {\n const rawDate = row[dateKey];\n const rawTraffic = row[trafficKey];\n \n let date;\n let formattedDate = rawDate;\n\n if (typeof rawDate === 'number') {\n date = getJsDateFromExcel(rawDate);\n formattedDate = formatDate(date);\n } else {\n date = new Date(rawDate);\n if (!isNaN(date.getTime())) formattedDate = formatDate(date);\n }\n\n let forecastValue = 0;\n\n if (date && !isNaN(date.getTime()) && avgTotalTraffic > 0) {\n const d = date.getDay();\n const m = date.getMonth();\n const y = date.getFullYear();\n \n const K_DN = coeffWeekday[d] || 1;\n const K_SEZ = coeffMonth[m] || 1;\n \n // Добавляем коэффициент года. \n // Если для этого года нет статистики (будущее), берем последний известный год или 1.0\n let K_YEAR = coeffYear[y];\n \n // Если год неизвестен (например, 2026, а статистики нет), берем коэф последнего года из данных\n if (!K_YEAR) {\n const knownYears = Object.keys(coeffYear).sort();\n const lastYear = knownYears[knownYears.length - 1];\n K_YEAR = coeffYear[lastYear] || 1.0;\n }\n \n // Новая формула\n forecastValue = Math.round(avgTotalTraffic * K_DN * K_SEZ * K_YEAR);\n }\n\n return {\n json: {\n \"Дата\": formattedDate,\n \"Исторический трафик (Факт)\": rawTraffic,\n \"Прогнозный трафик\": forecastValue\n }\n };\n});\n\nreturn finalOutput;"},"name":"Calculate Forecast","type":"n8n-nodes-base.code","typeVersion":2,"position":[64,-96],"id":"100a6bbb-fe43-4f65-a185-df835b3f90da"},{"parameters":{"operation":"toFile","fileFormat":"xlsx","options":{}},"name":"Create CSV","type":"n8n-nodes-base.spreadsheetFile","typeVersion":2,"position":[288,-96],"id":"f0bcaa19-0c67-49c4-bd16-ff5b44276049"},{"parameters":{"operation":"sendDocument","chatId":"={{ $('Telegram Trigger').first().json.message.chat.id }}","binaryData":true,"additionalFields":{}},"name":"Send Result","type":"n8n-nodes-base.telegram","typeVersion":1,"position":[512,-96],"id":"1b4771a2-dd94-4053-88c2-04bd9e12cbad","webhookId":"b7e31308-df8c-44ee-9e89-87c4b1b585f7","credentials":{"telegramApi":{"id":"L9KHcMiteyKROF5r","name":"Telegram main n8n"}}},{"parameters":{"jsCode":"// --- ПОЛУЧЕНИЕ ДАННЫХ ---\nconst items = $input.all();\nconst inputData = items.map(item => item.json);\n\n// --- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ---\n\nfunction parseCleanNumber(value) {\n if (typeof value === 'number') return value;\n if (!value) return 0;\n // Удаляем пробелы и меняем запятую на точку\n const cleanStr = String(value).replace(/\\s/g, '').replace(/,/g, '.');\n const parsed = parseFloat(cleanStr);\n return isNaN(parsed) ? 0 : parsed;\n}\n\n// Конвертация Excel-даты (44562 -> Date)\nfunction getJsDateFromExcel(serial) {\n const utc_days = Math.floor(serial - 25569);\n const utc_value = utc_days * 86400; \n const date_info = new Date(utc_value * 1000);\n return new Date(date_info.getFullYear(), date_info.getMonth(), date_info.getDate());\n}\n\n// Красивый вывод даты YYYY-MM-DD\nfunction formatDate(date) {\n const year = date.getFullYear();\n const month = String(date.getMonth() + 1).padStart(2, '0');\n const day = String(date.getDate()).padStart(2, '0');\n return `${year}-${month}-${day}`;\n}\n\n// --- ШАГ 1: АНАЛИЗ ДАННЫХ И РАСЧЕТ КОЭФФИЦИЕНТОВ ---\n// Нам нужно сначала пройтись по всем данным, чтобы понять \"норму\" (среднее, сезонность)\n\nlet totalTrafficSum = 0;\nlet totalCount = 0;\nconst trafficByDay = {}; // Статистика по дням недели\nconst trafficByMonth = {}; // Статистика по месяцам\n\n// Находим названия колонок один раз\nif (inputData.length === 0) return [];\nconst keys = Object.keys(inputData[0]);\n// Ищем колонку даты (содержит date, дата, день) и трафика\nconst dateKey = keys.find(k => /date|дата|день/i.test(k)) || keys[0];\nconst trafficKey = keys.find(k => /traffic|трафик|кол-во|value/i.test(k)) || keys[1];\n\n// Первый проход: Считаем суммы\nfor (const row of inputData) {\n const rawTraffic = row[trafficKey];\n const rawDate = row[dateKey];\n\n const traffic = parseCleanNumber(rawTraffic);\n \n // Если трафика нет (0 или пусто), мы НЕ учитываем это в расчете среднего,\n // иначе прогноз занизится.\n if (traffic === 0) continue;\n\n let date;\n if (typeof rawDate === 'number') date = getJsDateFromExcel(rawDate);\n else date = new Date(rawDate);\n \n if (isNaN(date.getTime())) continue;\n\n totalTrafficSum += traffic;\n totalCount++;\n\n const d = date.getDay();\n const m = date.getMonth();\n\n trafficByDay[d] = (trafficByDay[d] || {s:0, c:0});\n trafficByDay[d].s += traffic;\n trafficByDay[d].c++;\n\n trafficByMonth[m] = (trafficByMonth[m] || {s:0, c:0});\n trafficByMonth[m].s += traffic;\n trafficByMonth[m].c++;\n}\n\n// Если данных нет, возвращаем как есть\nconst avgTotalTraffic = totalCount > 0 ? totalTrafficSum / totalCount : 0;\n\n// Расчет коэффициентов\nconst coeffWeekday = {};\nconst coeffMonth = {};\n\nfor (let i = 0; i < 7; i++) {\n const data = trafficByDay[i];\n // Если данных по этому дню нет, коэффициент = 1\n coeffWeekday[i] = data ? (data.s / data.c) / avgTotalTraffic : 1.0;\n}\nfor (let i = 0; i < 12; i++) {\n const data = trafficByMonth[i];\n coeffMonth[i] = data ? (data.s / data.c) / avgTotalTraffic : 1.0;\n}\n\n// --- ШАГ 2: ЗАПОЛНЕНИЕ КОЛОНКИ ПРОГНОЗ ---\n// Теперь идем по тем же строкам и дописываем прогноз\n\nconst finalOutput = inputData.map(row => {\n const rawDate = row[dateKey];\n const rawTraffic = row[trafficKey];\n const factTraffic = parseCleanNumber(rawTraffic); // Факт может быть 0\n\n let date;\n let formattedDate = rawDate; // По умолчанию оставляем как было\n\n // Нормализуем дату для расчета\n if (typeof rawDate === 'number') {\n date = getJsDateFromExcel(rawDate);\n formattedDate = formatDate(date);\n } else {\n date = new Date(rawDate);\n if (!isNaN(date.getTime())) {\n formattedDate = formatDate(date);\n }\n }\n\n let forecastValue = 0;\n\n // Если дата валидная, считаем прогноз\n if (date && !isNaN(date.getTime()) && avgTotalTraffic > 0) {\n const d = date.getDay();\n const m = date.getMonth();\n \n const K_DN = coeffWeekday[d];\n const K_SEZ = coeffMonth[m];\n \n forecastValue = Math.round(avgTotalTraffic * K_DN * K_SEZ);\n }\n\n // Возвращаем объект в нужном порядке\n return {\n json: {\n \"Дата\": formattedDate,\n \"Исторический трафик (Факт)\": rawTraffic, // Оставляем оригинальное значение\n \"Прогнозный трафик\": forecastValue\n }\n };\n});\n\nreturn finalOutput;"},"name":"Calculate Forecast1","type":"n8n-nodes-base.code","typeVersion":2,"position":[64,80],"id":"26c95bbe-ef72-4331-ae6e-70dfe4fcc86e","disabled":true}],"connections":{"Telegram Trigger":{"main":[[{"node":"Get File","type":"main","index":0}]]},"Get File":{"main":[[{"node":"Parse File","type":"main","index":0}]]},"Parse File":{"main":[[{"node":"Calculate Forecast","type":"main","index":0}]]},"Calculate Forecast":{"main":[[{"node":"Create CSV","type":"main","index":0}]]},"Create CSV":{"main":[[{"node":"Send Result","type":"main","index":0}]]}},"settings":{"executionOrder":"v1"},"staticData":null,"meta":{"templateCredsSetupCompleted":true},"pinData":{},"versionId":"4b8d3db7-ff4d-44c7-91f9-dab154b2dae2","activeVersionId":null,"triggerCount":0,"shared":[{"updatedAt":"2025-12-11T07:26:21.150Z","createdAt":"2025-12-11T07:26:21.150Z","role":"workflow:owner","workflowId":"VCPJdSaOUiAvXjBI","projectId":"sYuiFAb87pAir6jV"}],"activeVersion":null,"tags":[]} |