Statistics (golden)
Бек-сторона фічі statistics для Goldenbride.
Дві паралельні системи
| Legacy | V2 | |
|---|---|---|
| Колекція | golden_statistic | golden_statistic + golden_statistics_relations |
| Контролер | GoldenStatisticsController | StatisticsControllerV2 |
| Сервіс | StatisticsService · GoldenOperatorStatisticsService | StatisticsServiceV2 · StatisticsGenerateServiceV2 |
| Що додає | базовий рейтинг + журнал | історичну “правду” про команду на конкретну дату (хто був тімлідом TU на 5 травня) |
Обидві працюють паралельно. Legacy не виключаємо — деякі UI-екрани досі його смикають. Поступово витісняється V2.
Денний цикл вигрузки
- download-temp-statistics — щохв, миттєвий стан дня у
golden_temp_statistics - download-main-statistics — раз на добу о 13:06, повна перекладка
temp→golden_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, а не пропуск.
Гарди на вході
- Тільки головний VPS. Гейт живе у спільній основі воркера (див. index) — на secondary тік скіпається. Ручний HTTP-тригер пайплайну гейт не проходить, тому працює на будь-якому VPS (форс tech-команди).
- Не паралельно. Лок «вигрузка вже йде» — другий запуск (ручний + крон одночасно) не виконується.
- Ідемпотентність. Якщо з попереднього успішного запуску минуло менш ніж 12 годин — воркер пропускає тік; мітка успіху ставиться тільки в кінці, щоб не блокувати ретраї.
Етапи
| # | Крок | Що робить | Сервіс |
|---|---|---|---|
| 1 | downloadMainStatistics | Тягне нові бонуси через official-api по кожному адміну, конвертує і зберігає в golden_statistic з атрибуцією до оператора/тімліда | self |
| 2 | updateCollection | Перебудова golden_statistics_relations (V2 снапшот ієрархії) | StatisticsGenerateServiceV2 |
| 3 | convertOnlineUsers | Конвертація онлайну юзерів | OnlineAnalysisService |
| 4 | updateFavoritesV2 | Оновлення фаворитів за V2 логікою | GoldenFavoritesService |
| 5 | updateAgency | Оновлення golden_agensy_list | AgencyService |
| 6 | updateActualAgency | Оновлення актуального agency | AgencyService |
| 7 | generateStatistics | Генерація метрик (Metric module) | MetricGenerateService |
| 8 | cleanActualAgency | Чистка тимчасових даних actual agency | AgencyService |
Кожен крок (крім #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 }) — усі активні адміни. Для кожного:
lastDate= max(date) зgolden_statisticдляadminId_api. Якщо записів нема —2022-01-01(хардкоднутий старт для нових адмінів).getRangeDates(lastDate)— обчислює діапазон з обмеженням “не до 13:00”:- lastDate = вчора →
return null(нема що тягнути). - lastDate = позавчора AND зараз < 13:00 →
return null(партнерський сайт ще не закрив вчорашній день). - Інакше →
{ from: lastDate + 1 day, to: вчора }.
- lastDate = вчора →
officialApiService.finances({login, pass, from, to})— запит у official-api за діапазон, повертаєIOfficialApiFinancesResult[].convertBonuses— нормалізація:date(raw з API),dateString(YYYY-MM-DD),timeString(HH:mm:ss.0 з підбитим формат-padding)ladyId_api,manId_api,sum,operation,adminId_api
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-дата
- Унікальні дати з масиву bonuses, відсортовано зростаюче (якщо вигрузка за кілька днів, то почергово записуємо).
- Фільтруємо по кожній даті окремо.
- Унікальні
ladyId_apiз bonusesForDate. - Для кожної TU дістаємо останній запис з
golden_statistic. Це точка відліку (previousBonus): з якого timestamp читати логи. - Один запит у
golden_log_ladyз$orумовами для всіх ladies одночасно - Для кожної TU — фільтр логів і обчислюємо коли який оператор працював на ній.
- Для кожного бонусу TU — знаходимо період для оператора. Не знайдено → додатково логуємо в помилку.
- Збираємо запис з прив’язкою до оператора та тімліда і додаємо на запис в БД.
- Записуємо в БД за дату.
Алгоритм findOperatorsPeriods
Початковий стан від previousBonus: operatorFamilyId / supervisorFamilyId / from = previousBonus.timestamp.
Прохід по логах у порядку timestamp:
| Подія | Дія |
|---|---|
operator_login | Новий operatorFamilyId → закриваємо попередній період {from, to=log.timestamp, op, sup} і починаємо новий (from = log.timestamp). Той самий оператор → продовжуємо. Якщо це останній лог — закриваємо до dateTMs (кінець доби). |
off_operator | log.operatorFamilyId === поточний → закриваємо період, очищаємо operator = null, from = log.timestamp. Інакше — ігнор. |
off_supervisor | log.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 | дата снапшоту |
ladyId | TU (mongo id); null для запису-оператора без TU |
cmId / operatorId / supervisorId | власники TU на період |
bonuses[] | {sum, type, manId} за період власності |
sum | сума bonuses |
isActive / isDeleted | стан TU |
final | фінальний стан власників на кінець доби — стартова точка наступного дня |
Одна TU за день може мати кілька записів — по одному на кожну унікальну комбінацію (cmId, operatorId, supervisorId) протягом дня. final:true позначає останній (на кінець дня).
Інкрементальність
- Дати від
lastDate+1до сьогодні. - Один запит за весь період: бонуси (
golden_statistic, виключаючиGoldenIgnoredTypesоперації),golden_log_lady,LadyEventLog(Created / Activated / Deactivated / Deleted TU),GoldenLogOperator(create / block / delete / recovery family), усі TU. - Цикл по датах: за кожну — фільтр денних логів/бонусів, побудова
records,StatisticsRelationsModel.create(records), потімrelationsPreviousDay = records(ланцюгfinal→ наступний день).
Розподіл бонусів по періодах власності (per-TU)
Для кожної TU за день:
- Стартові власники — з
final-запису попереднього дня (previousRecord). Якщо нема і TU не створена сьогодні (LadyEvent.Created) — null. LadyEventLogоновлюютьisActive/isDeleted.- Прохід по
golden_log_ladyTU: між кожними двома сусідніми логамиaddBonuses(period)фіксує бонуси поточним власникам цього інтервалу. Потім подія оновлює власників:set_cm/off_cm→cmIdoperator_login→operatorId+supervisorId(обидва одночасно)off_operator→operatorId = null(якщо співпадає з поточним)off_supervisor→supervisorId = null(якщо співпадає)set_operator/set_supervisor— лише фіксують проміжний запис (власник уже стоїть черезoperator_login)- останній лог: період до
dateTMs(кінець доби) +final:true.
- Якщо логів 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 — інакше вони зникнуть зі списків статистики. Блоки (за коментарем у коді):
- Заблокували / видалили / змінили family — запис
isActive:false, final:true, щоб оператор не тягнувся далі. - Новий оператор без TU — створити запис (за логами
create/block/recovery family). - Зняли всі TU, але не блокували — протягнути далі (порожній активний).
- Порожні активні оператори з минулого дня — протягнути далі якщо не було блокування.
Споріднені методи
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); лок «вигрузка вже йде» залип з попереднього прогону |
| Окремий адмін зриває весь pipe | Download 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 |