statistics

Statistics (golden)

Бек-сторона фічі statistics для Goldenbride.

Дві паралельні системи

LegacyV2
Колекціяgolden_statisticgolden_statistic + golden_statistics_relations
КонтролерGoldenStatisticsControllerStatisticsControllerV2
СервісStatisticsService · GoldenOperatorStatisticsServiceStatisticsServiceV2 · StatisticsGenerateServiceV2
Що додаєбазовий рейтинг + журналісторичну “правду” про команду на конкретну дату (хто був тімлідом TU на 5 травня)

Обидві працюють паралельно. Legacy не виключаємо — деякі UI-екрани досі його смикають. Поступово витісняється V2.

Денний цикл вигрузки

  • download-temp-statistics — щохв, миттєвий стан дня у golden_temp_statistics
  • download-main-statistics — раз на добу о 13:06, повна перекладка tempgolden_statistic + перебудова golden_statistics_relations + cascade-апдейти (online / favorites / agency / metrics)

Між цими двома прогонами temp і main — це “поточна” і “вчорашня + раніше” відповідно.

Pipeline downloadMainStatisticsProcess

src/golden/StatisticsV2/statistics-downloading.service.ts

Тригериться з download-main-statistics. Виконується послідовно (await за await); помилка в будь-якому кроці тригерить retry, а не пропуск.

Гарди на вході

  1. Тільки головний VPS. Гейт живе у спільній основі воркера (див. index) — на secondary тік скіпається. Ручний HTTP-тригер пайплайну гейт не проходить, тому працює на будь-якому VPS (форс tech-команди).
  2. Не паралельно. Лок «вигрузка вже йде» — другий запуск (ручний + крон одночасно) не виконується.
  3. Ідемпотентність. Якщо з попереднього успішного запуску минуло менш ніж 12 годин — воркер пропускає тік; мітка успіху ставиться тільки в кінці, щоб не блокувати ретраї.

Етапи

#КрокЩо робитьСервіс
1downloadMainStatisticsТягне нові бонуси через official-api по кожному адміну, конвертує і зберігає в golden_statistic з атрибуцією до оператора/тімлідаself
2updateCollectionПеребудова golden_statistics_relations (V2 снапшот ієрархії)StatisticsGenerateServiceV2
3convertOnlineUsersКонвертація онлайну юзерівOnlineAnalysisService
4updateFavoritesV2Оновлення фаворитів за V2 логікоюGoldenFavoritesService
5updateAgencyОновлення golden_agensy_listAgencyService
6updateActualAgencyОновлення актуального agencyAgencyService
7generateStatisticsГенерація метрик (Metric module)MetricGenerateService
8cleanActualAgencyЧистка тимчасових даних actual agencyAgencyService

Кожен крок (крім #7) пише success-маркер у golden_error_report через saveCriticalLogs({isError:false, type:worker}). Це трасування фази, не помилка — побачив “Конвертація онлайн юзерів завершена” → знаєш що етап #3 пройшов і pipe вже на #4.

Retry

Пайплайн нічого не ковтає — перша помилка валить спробу цілком і йде нагору у воркер. Ретраїть Bull у durable-режимі: 3 спроби з паузою 30 хв, таймаут 1 год (деталі — download-main-statistics). Власного retry-механізму з таймерами в сервісі більше нема.

Рестарт сервера під час вигрузки теж покриває durable-черга: незавершений джоб визнається завислим і перезапускається, окремої перевірки «чи довантажились після рестарту» в коді більше нема.

downloadMainStatistics: пер-адмін цикл

Внутрішній етап #1. AdminModel.find({ isBlocked: false }) — усі активні адміни. Для кожного:

  1. lastDate = max(date) з golden_statistic для adminId_api. Якщо записів нема — 2022-01-01 (хардкоднутий старт для нових адмінів).
  2. getRangeDates(lastDate) — обчислює діапазон з обмеженням “не до 13:00”:
    • lastDate = вчора → return null (нема що тягнути).
    • lastDate = позавчора AND зараз < 13:00 → return null (партнерський сайт ще не закрив вчорашній день).
    • Інакше → { from: lastDate + 1 day, to: вчора }.
  3. officialApiService.finances({login, pass, from, to}) — запит у official-api за діапазон, повертає IOfficialApiFinancesResult[].
  4. convertBonuses — нормалізація:
    • date (raw з API), dateString (YYYY-MM-DD), timeString (HH:mm:ss.0 з підбитим формат-padding)
    • ladyId_api, manId_api, sum, operation, adminId_api
  5. saveBonuses(convertedBonuses, lastDate) — атрибуція операторам і запис.

Помилка одного адміна → зриває весь цикл. try/catch пише Download by admin <id_api> failed у error-report і throw new Error(...) → переривається весь pipeline (наступні адміни не обробляються; виходимо в зовнішній catch і йдемо у retry).

Атрибуція бонусів операторам (saveBonuses)

Дані з API містять ladyId_api + timestamp + sum, але не несуть ID оператора. Зв’язок відновлюємо з логів golden_log_lady.

Логіка per-дата

  1. Унікальні дати з масиву bonuses, відсортовано зростаюче (якщо вигрузка за кілька днів, то почергово записуємо).
  2. Фільтруємо по кожній даті окремо.
  3. Унікальні ladyId_api з bonusesForDate.
  4. Для кожної TU дістаємо останній запис з golden_statistic. Це точка відліку (previousBonus): з якого timestamp читати логи.
  5. Один запит у golden_log_lady з $or умовами для всіх ladies одночасно
  6. Для кожної TU — фільтр логів і обчислюємо коли який оператор працював на ній.
  7. Для кожного бонусу TU — знаходимо період для оператора. Не знайдено → додатково логуємо в помилку.
  8. Збираємо запис з прив’язкою до оператора та тімліда і додаємо на запис в БД.
  9. Записуємо в БД за дату.

Алгоритм findOperatorsPeriods

Початковий стан від previousBonus: operatorFamilyId / supervisorFamilyId / from = previousBonus.timestamp.

Прохід по логах у порядку timestamp:

ПодіяДія
operator_loginНовий operatorFamilyId → закриваємо попередній період {from, to=log.timestamp, op, sup} і починаємо новий (from = log.timestamp). Той самий оператор → продовжуємо. Якщо це останній лог — закриваємо до dateTMs (кінець доби).
off_operatorlog.operatorFamilyId === поточний → закриваємо період, очищаємо operator = null, from = log.timestamp. Інакше — ігнор.
off_supervisorlog.supervisorFamilyId === поточний → закриваємо період, очищаємо обох (operator = null, supervisor = null). Інакше — ігнор.
set_operator і set_supervisor свідомо пропускаються — бо operator_login несе обидві ролі (оператор + його тімлід) одночасно. Дублювання не потрібне.

updateCollection: перебудова golden_statistics_relations

StatisticsGenerateServiceV2.updateCollection() — етап #2 pipeline. Якщо downloadMainStatistics (етап #1) пише “сирі” бонуси в golden_statistic з атрибуцією operator/supervisor по timestamp, то updateCollection будує денний снапшот ієрархії: хто володів TU у кожен період дня, з бонусами розподіленими по цих періодах. Це джерело для V2-зрізів UI (supervisor-*, top-supervisors-*).

Запис golden_statistics_relations

ПолеЗначення
dateдата снапшоту
ladyIdTU (mongo id); null для запису-оператора без TU
cmId / operatorId / supervisorIdвласники TU на період
bonuses[]{sum, type, manId} за період власності
sumсума bonuses
isActive / isDeletedстан TU
finalфінальний стан власників на кінець доби — стартова точка наступного дня

Одна TU за день може мати кілька записів — по одному на кожну унікальну комбінацію (cmId, operatorId, supervisorId) протягом дня. final:true позначає останній (на кінець дня).

Інкрементальність

  1. Дати від lastDate+1 до сьогодні.
  2. Один запит за весь період: бонуси (golden_statistic, виключаючи GoldenIgnoredTypes операції), golden_log_lady, LadyEventLog (Created / Activated / Deactivated / Deleted TU), GoldenLogOperator (create / block / delete / recovery family), усі TU.
  3. Цикл по датах: за кожну — фільтр денних логів/бонусів, побудова records, StatisticsRelationsModel.create(records), потім relationsPreviousDay = records (ланцюг final → наступний день).

Розподіл бонусів по періодах власності (per-TU)

Для кожної TU за день:

  1. Стартові власники — з final-запису попереднього дня (previousRecord). Якщо нема і TU не створена сьогодні (LadyEvent.Created) — null.
  2. LadyEventLog оновлюють isActive / isDeleted.
  3. Прохід по golden_log_lady TU: між кожними двома сусідніми логами addBonuses(period) фіксує бонуси поточним власникам цього інтервалу. Потім подія оновлює власників:
    • set_cm / off_cmcmId
    • operator_loginoperatorId + supervisorId (обидва одночасно)
    • off_operatoroperatorId = null (якщо співпадає з поточним)
    • off_supervisorsupervisorId = null (якщо співпадає)
    • set_operator / set_supervisor — лише фіксують проміжний запис (власник уже стоїть через operator_login)
    • останній лог: період до dateTMs (кінець доби) + final:true.
  4. Якщо логів TU за день нема — усі бонуси одному власнику (з previousRecord) за весь день + final:true.

addData() дедуплікує: шукає запис з тим самим (ladyId, cmId, operatorId, supervisorId), акумулює sum / bonuses, оновлює final. Це і дає кілька записів на TU за день при зміні власників.

if (usedBonuses.length !== ladyBonuses.length)console.log('missed', ...) — debug-сигнал що частина бонусів не лягла в жоден період. Зараз лише в консоль, не в golden_error_report.

Оператори без TU (4 блоки після per-TU)

TU-цикл покриває тільки операторів які мали TU. Окремо треба “протягнути” решту операторів у relations — інакше вони зникнуть зі списків статистики. Блоки (за коментарем у коді):

  1. Заблокували / видалили / змінили family — запис isActive:false, final:true, щоб оператор не тягнувся далі.
  2. Новий оператор без TU — створити запис (за логами create / block / recovery family).
  3. Зняли всі TU, але не блокували — протягнути далі (порожній активний).
  4. Порожні активні оператори з минулого дня — протягнути далі якщо не було блокування.

Споріднені методи

  • createAdditionalRelations() — майже дублікат updateCollection, але джерело бонусів — golden_temp_statistics (поточний день), а не golden_statistic. Дає relations “на сьогодні” між основними добовими прогонами.

Бізнес-нюанси

  • golden_statistics_relations — снапшот ієрархії на день X. Фіксує: яка TU з яким балансом була у якого оператора, який у нього був тімлід, КМ.
  • Часовий поріг 13:00. Партнерський сайт публікує бонуси за день із затримкою — до 13:00 за вчора API повертає неповні дані. Тому main-воркер о 13:06 (буфер 6 хв на старт) і getRangeDates явно блокує дозавантаження за вчора до 13:00.

Діагностика

СимптомКуди дивитися
Воркер не запустився сьогоднілог skip — не головний VPS (secondary); лок «вигрузка вже йде» залип з попереднього прогону
Окремий адмін зриває весь pipeDownload by admin <id_api> failed у golden_error_report
Бонус без оператора (supervisor/operator = null у golden_statistic)Bonus not assigned to any operator period: <JSON> у golden_error_report
Реальна помилка вигрузкиранній сигнал у ТГ після першої невдалої спроби, FAILED після вичерпання ретраїв → див. download-main-statistics