Skip to main content

Profile

1. Summary

Goal: Центральная точка данных пользователя — балансы, статистика, прогресс. Единый источник правды для состояния аккаунта во всех частях приложения.

User Value: Личный кабинет с полной информацией об активности, балансах, достижениях и готовности к выводу скинов.


2. Business Logic

Types of Data

Scrap — основная внутриигровая валюта

  • Источники: кейсы, спины, квесты, ачивменты, рефералы
  • Трата: платные кейсы, платные спины
  • Денормализация: scrapFromQuizzes, scrapFromCases, scrapFromSpins и др.

XP (Experience) — метрика прогресса в сезоне

  • Только растёт (никогда не тратится)
  • Сбрасывается при завершении сезона
  • Влияет на позицию в рейтинге

Streak Points (SP) — валюта лояльности

  • Источники: ежедневные входы, milestone бонусы
  • Трата: билеты в розыгрышах, Streak кейсы

Profile Data Assembly

При запросе профиля (GET /api/users/profile) происходит сборка данных:

1. Fetch User Data

  • Загрузка 50+ полей из User модели
  • Включая все денормализованные счётчики

2. Check and Update Streak

  • StreakService.checkAndUpdateStreak() вызывается при каждом запросе
  • Автоматическое обновление dailyLoginStreak если прошёл день

3. Ensure Season Participation

  • Upsert записи UserSeasonStats для активного сезона
  • Создаёт участие автоматически при первом входе в сезон

4. Calculate Derived Fields

  • avatar — URL из static сервера
  • nextDailyCaseAvailable — время следующего бесплатного кейса
  • activeBuffs — список активных баффов через BuffService
  • streakInfo — информация о использовании щитов (если применимо)
Side Effect on Read

Запрос профиля — не чистый read! Происходит обновление стрика и создание записи сезона. Это сделано для UX: пользователь не должен явно "начинать день".

Avatar Caching System

Аватарки пользователей кэшируются на static сервере вместо прямого использования Telegram URL.

Проблемы которые решает:

ПроблемаОписание
Временные URLTelegram Bot API возвращает URL действительные ~1 час
Утечка токенаПрямой URL содержит токен бота — нельзя отдавать на frontend
ПроизводительностьЗапрос к Telegram API на каждый показ аватара — медленно

Как работает:

Хранение:

  • Путь на диске: /static/images/avatars/{telegramId}.jpg
  • URL для клиента: https://static.goloot.online/images/avatars/{telegramId}.jpg
  • TTL кэша: 24 часа (после этого перезагружается с Telegram)
Почему 24 часа?
TTLПлюсыМинусы
1 часБыстрое обновлениеМного запросов к Telegram API
24 часа ✓Баланс свежести и нагрузкиАватар обновится максимум через сутки
7 днейМинимум запросовСлишком устаревшие аватары

Люди редко меняют аватарки. Сутки — разумный компромисс.

Где используется:

  • profile.photoUrl — аватар текущего пользователя
  • leaderboard[].avatarUrl — аватары топ-10 в рейтинге
  • Feed events — аватары в ленте активности

Файлы:

  • Service: backend/src/domains/users/services/avatar-cache.service.ts
  • URL Builder: backend/src/common/constants/url.constants.tsbuildAvatarUrl()

Level System

Прогрессивная формула: каждый уровень требует больше XP, чем предыдущий.

Детали расчёта уровня

Уровень рассчитывается на основе накопленного XP с прогрессивно растущими требованиями.

УровеньТребуется XP
10
2100
3250
......

Уровень влияет на:

  • Визуальное отображение в профиле
  • Возможные будущие разблокировки

Steam Verification Flow

Установка Trade URL:

  1. Пользователь вводит Steam Trade URL
  2. Backend валидирует формат URL
  3. Извлекается SteamID из URL
  4. Вызывается SteamVerificationService для проверки:
    • Существование аккаунта
    • Возраст аккаунта (steamAccountCreatedAt)
    • Стоимость библиотеки (steamLibraryValueUsd)
    • Количество игр (steamGamesCount)
  5. Результат: автоматическая верификация или необходимость ручной

Ручная верификация:

  • Админ просматривает заявку через тикет
  • Одобряет или отклоняет с комментарием
  • steamManuallyVerified = true при одобрении

Withdraw Readiness Checks

7-точечная проверка готовности к выводу:

#ПроверкаОписание
1hasTradeUrlTrade URL установлен
2isSteamVerifiedSteam аккаунт верифицирован (авто или ручной)
3isInventoryPublicИнвентарь Steam публичный
4noActiveWithdrawalНет активного вывода в процессе
5itemExistsПредмет существует в инвентаре
6itemWithdrawableТип предмета поддерживает вывод (SKIN)
7botAvailableБот с предметом доступен

Protection

ДействиеRate LimitAuthValidation
Get profilegeneral (100/min)TelegramGetProfileSchema
Get statsgeneral (100/min)TelegramGetStatsSchema
Submit feedbackfeedback (1/min)TelegramFeedbackSchema
Set trade URLgeneral (100/min)TelegramUpdateTradeUrlSchema
Delete trade URLgeneral (100/min)TelegramDeleteTradeUrlSchema
Get verificationgeneral (100/min)TelegramGetVerificationSchema
Get withdraw readinessgeneral (100/min)TelegramGetWithdrawReadinessSchema
Get Rust statusgeneral (100/min)TelegramGetRustOnlineStatusSchema
Детали реализации

См. Security Matrix для полного обзора защит.

Edge Cases

СитуацияПоведениеUI
Активный выводБлок изменения Trade URLСообщение "Дождитесь завершения вывода"
Steam не верифицированWithdraw Readiness = falseКнопка вывода неактивна
Streak shield использованstreakInfo в ответеПоказ информации о защите
Первый вход в сезонAuto-create UserSeasonStatsПрозрачно для пользователя
Feedback rate limitHTTP 429Toast "Подождите N секунд"

3. ADR (Architectural Decisions)

Почему денормализованные поля на User?

Проблема: Подсчёт статистики из истории (QuizResult, CaseOpening, etc.) слишком медленный для каждого запроса профиля.

Решение: 30+ денормализованных полей на User модели:

  • scrapFromQuizzes, scrapFromCases, scrapFromSpins
  • xpFromQuests, xpFromAchievements, xpFromReferrals
  • quizzesCompleted, correctAnswers, casesOpened

Альтернативы (отклонены):

  • Real-time агрегация — O(n) на каждый запрос, неприемлемо
  • Redis cache — сложность инвалидации, eventual consistency
  • Materialized views — дополнительная нагрузка на PostgreSQL

Последствия:

  • Быстрые чтения (O(1) вместо O(n))
  • Каждая операция должна атомарно обновлять счётчики
  • Риск drift при ошибках — нужны reconciliation механизмы

Почему UserSeasonStats отдельно от User?

Проблема: Нужна статистика, которая сбрасывается между сезонами, но all-time данные должны сохраняться.

Решение: Отдельная таблица UserSeasonStats с compound unique (userId, seasonId).

Последствия:

  • All-time статистика = SUM/MAX через все записи UserSeasonStats
  • Сезонный сброс = создание новой записи без удаления старых

Dual-Write Pattern: User + UserSeasonStats

Критичное архитектурное решение

При инкременте счётчика на User ОБЯЗАТЕЛЬНО инкрементировать соответствующее поле в UserSeasonStats.

Проблема: Нужны быстрые O(1) чтения балансов (scrap, streakPoints) и счётчиков, но также нужна сезонная агрегация для All-Time статистики.

Решение: Dual-write в обе таблицы:

User = Materialized View (текущее состояние, O(1) чтение)
UserSeasonStats = Source of Truth по сезонам (для агрегации)

Почему не только UserSeasonStats?

ОперацияТолько UserSeasonStatsDual-write (текущий)
Проверка балансаaggregate() ~10-50msuser.scrap ~1-5ms
Отображение профиляaggregate на каждый запроспрямое чтение
Покупка кейсаaggregate → check → updateread → check → update

Альтернативы (отклонены):

  • Только UserSeasonStats — медленные агрегации на каждый запрос баланса
  • Redis cache — сложность инвалидации, eventual consistency
  • Computed columns — PostgreSQL не поддерживает cross-table computed

Синхронизируемые поля:

User полеUserSeasonStats полеУтилита
scrap, scrapFrom*scrap, scrapFrom*updateSeasonScrap()
xp, xpFrom*xp, xpFrom*updateSeasonXP()
friendsInvitedfriendsInvitedupdateSeasonStats()
achievementsUnlockedachievementsUnlockedupdateSeasonStats()
casesOpenedcasesOpenedupdateSeasonStats()
dailyCasesOpeneddailyCasesOpenedupdateSeasonStats()
dailySpinsUseddailySpinsUsedupdateSeasonStats()
itemsCrafteditemsCraftedupdateSeasonStats()
itemsSalvageditemsSalvagedupdateSeasonStats()
streakPointsTotalstreakPointsEarnedupdateSeasonStats()
streakPointsSpentstreakPointsSpentupdateSeasonStats()
bestDailyLoginStreakbestDailyLoginStreakdirect upsert (MAX)

Правило для разработчиков:

// НЕПРАВИЛЬНО — обновляем только User
await tx.user.update({
where: { id: userId },
data: { friendsInvited: { increment: 1 } }
});

// ПРАВИЛЬНО — обновляем User + UserSeasonStats
await tx.user.update({
where: { id: userId },
data: { friendsInvited: { increment: 1 } }
});
await updateSeasonStats(tx, userId, {
incrementFields: { friendsInvited: 1 }
});

Последствия:

  • Быстрые O(1) чтения балансов и счётчиков
  • All-Time = SUM(UserSeasonStats) работает корректно
  • Требуется дисциплина: не забывать синхронизировать
  • Риск drift при ошибках — нужен периодический reconciliation
Утилита updateSeasonStats

Путь: backend/src/domains/seasons/utils/season-stats-updater.ts

await updateSeasonStats(tx, userId, {
incrementFields: {
friendsInvited: 1,
casesOpened: 1
}
});

Автоматически:

  • Находит активный сезон
  • Upsert в UserSeasonStats
  • Инкрементирует указанные поля

Почему Streak Update на Read?

Проблема: Пользователь не должен явно "чекиниться" каждый день.

Решение: checkAndUpdateStreak() вызывается при GET /profile.

Альтернативы (отклонены):

  • Отдельный endpoint /checkin — лишний шаг для пользователя
  • Cron job — сложность с timezone, нагрузка пиком в полночь

Последствия:

  • Read endpoint имеет side effects (не REST pure)
  • Гарантированное обновление при первом входе за день
  • Streak Shield автоматически применяется при пропуске

4. Architecture

Profile Data Flow

Key Components

КомпонентПутьОписание
ProfileControllerbackend/src/domains/users/controllers/profile.controller.tsHTTP handlers для профиля
StatisticsServicebackend/src/domains/users/services/statistics.service.tsАгрегация статистики
StreakServicebackend/src/domains/streaks/services/streak.service.tsУправление стриками
BuffServicebackend/src/domains/buffs/services/buff.service.tsАктивные баффы
SteamVerificationServicebackend/src/domains/steam-verification/services/steam-verification.service.tsВерификация Steam
WithdrawReadinessServicebackend/src/domains/inventory/services/withdraw-readiness.service.tsПроверка готовности к выводу
Routesbackend/src/domains/users/routes/profile.routes.tsAPI endpoints
Schemasbackend/src/domains/users/schemas/profile.schemas.tsRequest/Response validation

5. Database Schema

Models

МодельОписаниеКлючевые поля
UserОсновная модель (116 полей)telegramId, scrap, xp, level, балансы, стрики
UserSeasonStatsСтатистика по сезонамuserId, seasonId, XP/Scrap breakdown
UserCategoryStreakСтрики по категориям квизовuserId, category, currentStreak, bestStreak

User Key Fields (subset)

КатегорияПоля
Identityid, telegramId, username, firstName
Balancesscrap, xp, level, streakPoints
Scrap SourcesscrapFromQuizzes, scrapFromCases, scrapFromSpins, scrapFromReferrals
XP SourcesxpFromQuests, xpFromAchievements, xpFromStreaks, xpFromReferrals
StreaksdailyLoginStreak, bestDailyLoginStreak, correctAnswersStreak
SteamsteamTradeUrl, steamId, steamVerified, steamManuallyVerified
StatusisActive, isBanned, isPremium
Полная схема

См. SCHEMA_GUIDE для полного списка полей User (строки 14-193).

Relationships


6. API Endpoints

User API

МетодЭндпоинтОписаниеDocs
GET/api/users/profileПолный профиль с балансами
GET/api/users/statsРанг и лимиты
GET/api/users/statistics/categoriesСтатистика квизов по категориям
GET/api/users/statistics/all-timeAll-time статистика
GET/api/users/statistics/streak-pointsStreak Points breakdown
POST/api/users/feedbackОтправка фидбека
PUT/api/users/steam/trade-urlУстановка Steam Trade URL
DELETE/api/users/steam/trade-urlУдаление Steam Trade URL
GET/api/users/verification-statusСтатус Steam верификации
GET/api/users/withdraw-readinessГотовность к выводу
GET/api/users/rust-online-statusОнлайн статус в Rust
Response Examples

GET /api/users/profile

{
"success": true,
"data": {
"id": "user-id",
"telegramId": 123456789,
"firstName": "Alex",
"scrap": 1500,
"xp": 2300,
"level": 5,
"streakPoints": 450,
"dailyLoginStreak": 7,
"avatar": "https://static.goloot.online/avatars/user-id.jpg",
"nextDailyCaseAvailable": "2024-01-16T10:00:00Z",
"activeBuffs": [
{ "type": "XP_BUFF", "multiplier": 1.5, "expiresAt": "..." }
],
"streakInfo": null
}
}

GET /api/users/statistics/categories

{
"success": true,
"data": [
{ "category": "weapons", "displayName": "Оружие", "total": 50, "correct": 42, "accuracy": 84.0 },
{ "category": "maps", "displayName": "Карты", "total": 30, "correct": 21, "accuracy": 70.0 }
]
}

7. Account Reset (Internal)

Только для владельца/админов

Эта секция описывает внутренние механизмы сброса аккаунта.

Partial Reset (UserDataResetService)

Частичный сброс данных пользователя с фокусом на referral систему:

  1. Деактивация исходящих referral sessions → state=USER_RESET
  2. Деактивация входящих referral sessions → state=USER_RESET
  3. Пересчёт лимитов для затронутых рефереров
  4. Установка isBanned=true

Сохраняется: Балансы, инвентарь, история (для аналитики).

Full Reset (UserFullResetService)

Полная очистка по команде /stop в Telegram боте:

ШагОперация
1Отмена активных выводов → status=FAILED
2Удаление инвентаря
3Удаление истории кейсов
4Удаление результатов квизов и category streaks
5Удаление результатов спинов
6Удаление истории крафтов
7Удаление достижений
8Удаление сезонной статистики
9Удаление feed events
10Удаление bot interactions
11Маркировка referral sessions как USER_RESET
12Обнуление всех балансов на User

Сохраняется:

  • referral_codes — пользователь может снова пригласить друзей после /start
  • user_feedback — ценно для бизнеса
  • Analytics данные (banner/push interactions)

Технические детали:

  • Выполняется в транзакции с 30s timeout
  • botStatus=BLOCKED после сброса
  • Extensive logging для audit trail

  • Profile State Management — фронтенд архитектура: React Query как sole source of truth
  • Settings — настройки пользователя
  • Onboarding — регистрация и активация
  • Streaks — система лояльности
  • Buffs — временные бонусы
  • Seasons — сезонный рейтинг
  • Feedback — система обратной связи
  • Steam Trade — вывод скинов
  • Security Matrix — обзор защит