Skip to main content

Inventory System

1. Summary

Goal: Хранение предметов пользователя с поддержкой фильтрации, агрегации и Salvage (разбор на XP).

User Value: Централизованное место для управления выигранными предметами. Возможность конвертировать ненужные материалы в опыт для повышения уровня.


2. Business Logic

Item Types

Описание: Финальный предмет — реальный скин CS2

Особенности:

  • Каждый экземпляр уникален (разные sourceType = разные записи)
  • Имеет собственный tier (редкость)
  • Можно вывести в Steam (если itemType === 'SKIN' и есть marketHashName)
  • Нельзя Salvage — только вывод или хранение

Aggregation Logic

Правило агрегации
  • SKIN — каждая запись отдельная (разные sourceType = разные слоты)
  • BLUEPRINT, FRAGMENT, RESOURCE, BUFF — агрегируются по itemId, суммируется quantity

Это позволяет показывать "5× Metal" вместо пяти отдельных слотов, но сохранять историю происхождения каждого SKIN.

Salvage (Разбор)

Конвертация предметов в XP:

Допустимые типы: BLUEPRINT, FRAGMENT, RESOURCE

Формула: xpGained = item.salvageXP × quantity

Процесс:

  1. Проверка наличия предмета в инвентаре
  2. Проверка типа (только BLUEPRINT/FRAGMENT/RESOURCE)
  3. Проверка достаточного quantity
  4. Atomic transaction: списание предмета + начисление XP
  5. Обновление счётчиков (User.itemsSalvaged, Item.totalSalvaged)
  6. Quest/Achievement progress (категория RECYCLE)
Блокировка между сезонами

Salvage недоступен во время COUNTDOWN — нельзя получать XP между сезонами.

Protection

ДействиеRate LimitAuthValidationAtomic
Get Inventorygeneral (100/min)TelegramGetInventorySchema-
Salvage Itemgeneral (100/min)Telegram + SeasonSalvageItemSchema
Детали реализации

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

Edge Cases

СитуацияUI поведение
Предмет не в инвентареОшибка "Предмет не найден в инвентаре"
Недостаточное количествоОшибка "Недостаточное количество предметов"
Попытка Salvage SKIN/BUFFОшибка (только BLUEPRINT/FRAGMENT/RESOURCE)
COUNTDOWN сезонаКнопка Salvage disabled, tooltip "Недоступно между сезонами"
Backend Error Codes
ОшибкаHTTPСообщение
Item not found404"Предмет не найден в инвентаре"
Insufficient quantity400"Недостаточное количество предметов"
Invalid item type400"Only BLUEPRINT, FRAGMENT and RESOURCE items can be salvaged"

3. ADR (Architectural Decisions)

Почему агрегация в памяти, а не в SQL?

Проблема: Consumables (FRAGMENT, BLUEPRINT, RESOURCE, BUFF) нужно показывать как один слот с общим quantity, но SKIN — отдельно каждый.

Решение: Fetch всех записей, агрегация в aggregateInventory(), пагинация в памяти.

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

  • SQL GROUP BY — не позволяет легко разделить логику SKIN vs consumables
  • Отдельные запросы для SKIN и consumables — больше roundtrips, сложнее пагинация

Последствия: Простая логика, но загружает все предметы пользователя в память. При большом инвентаре (1000+ предметов) может быть неэффективно.

Почему Salvage деплетит по стекам?

Проблема: Один itemId может иметь несколько записей UserInventory с разным sourceType.

Решение: При Salvage сортируем по quantity ASC и списываем с меньших стеков сначала.

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

  • FIFO по acquiredAt — сложнее запрос, не даёт явного преимущества
  • Объединить записи в одну — потеряем историю источников

Последствия: Равномерное распределение, но порядок списания может быть неинтуитивным для пользователя.


4. Architecture

Sequence Diagram

Salvage Flow

Key Components

КомпонентПутьОписание
InventoryServicebackend/src/domains/inventory/services/inventory.service.tsБизнес-логика: get, salvage
InventoryControllerbackend/src/domains/inventory/controllers/inventory.controller.tsHTTP handlers
InventoryMapperbackend/src/domains/inventory/mappers/inventory.mapper.tsPrisma → Plain object
Routesbackend/src/domains/inventory/routes/inventory.routes.tsAPI endpoints
Schemasbackend/src/domains/inventory/schemas/inventory.schemas.tsSwagger + validation

5. Database Schema

Models

МодельОписаниеКлючевые поля
UserInventoryПредметы пользователяuserId, itemId, quantity, sourceType, acquiredAt
ItemОпределение предметаname, itemType, tier, salvageXP, targetSkinId, craftRecipe
SalvageHistoryАудит разборок предметовuserId, itemId, quantity, xpGained, itemSnapshot

Salvage Audit

Каждая операция Salvage логируется в SalvageHistory для аудита:

ПолеТипОписание
itemSnapshotJSONСнимок предмета на момент разборки: { name, tier, imageUrl, salvageXP, category }
quantityIntКоличество разобранных предметов
xpGainedIntПолученный XP
Для чего нужен itemSnapshot

Snapshot фиксирует состояние предмета на момент разборки. Даже если предмет позже изменится (имя, tier, salvageXP) — аудит сохранит оригинальные значения.

Inventory Source Types

ИсточникОписание
CASE_OPENINGИз кейса
DAILY_SPINС рулетки
TASK_REWARDНаграда за квест
ACHIEVEMENT_REWARDНаграда за достижение
CRAFTINGСкрафчено
ADMIN_GRANTВыдано админом
SEASON_REWARDТоп-10 сезона
RAFFLE_WINВыигрыш в розыгрыше
PROMO_CODEПо промокоду

Relationships

Unique Constraint

@@unique([userId, itemId, sourceType])

Один и тот же предмет может быть в инвентаре несколько раз, если получен из разных источников (важно для SKIN).


6. API Endpoints

МетодЭндпоинтОписаниеDocs
GET/api/inventoryСписок предметов с фильтрами
POST/api/inventory/salvageРазбор предмета на XP

Query параметры GET:

ПараметрТипОписание
itemTypeenumSKIN, BLUEPRINT, FRAGMENT, RESOURCE, BUFF
tierenumTIER_0 — TIER_5
pagenumberСтраница (default: 1)
limitnumberЭлементов на странице (default: 50, max: 100)

  • Cases — источник предметов (CASE_OPENING)
  • Daily Spins — источник предметов (DAILY_SPIN)
  • Craft — использование предметов для крафта скинов
  • Withdrawals — вывод SKIN в Steam
  • Buffs — активация BUFF предметов
  • Quests — RECYCLE квесты (salvage progress)
  • Achievements — RECYCLE достижения