Inventory System
1. Summary
Goal: Хранение предметов пользователя с поддержкой фильтрации, агрегации и Salvage (разбор на XP).
User Value: Централизованное место для управления выигранными предметами. Возможность конвертировать ненужные материалы в опыт для повышения уровня.
2. Business Logic
Item Types
- SKIN
- BLUEPRINT
- FRAGMENT
- RESOURCE
- BUFF
Описание: Финальный предмет — реальный скин CS2
Особенности:
- Каждый экземпляр уникален (разные sourceType = разные записи)
- Имеет собственный tier (редкость)
- Можно вывести в Steam (если
itemType === 'SKIN'и естьmarketHashName) - Нельзя Salvage — только вывод или хранение
Описание: Рецепт для крафта скина
Особенности:
- Агрегируется по itemId (суммируется quantity)
- Tier наследуется от targetSkin при отображении
- Можно Salvage → XP
- Необходим для крафта (1 blueprint + fragments + resources)
Описание: Осколок скина — основной материал для крафта
Особенности:
- Агрегируется по itemId
- Tier наследуется от targetSkin при отображении
- Можно Salvage → XP
- Требуется несколько для крафта (обычно 3-10)
Описание: Универсальный ресурс (Metal, Cloth)
Особенности:
- Агрегируется по itemId
- Не привязан к конкретному скину
- Можно Salvage → XP
- Используется в крафте всех скинов определённой категории
Описание: Временный бафф (XP Buff, Scrap Buff, Streak Shield)
Особенности:
- Агрегируется по itemId
- Активируется отдельным действием
- Нельзя Salvage — только активация
- См. Buffs
Aggregation Logic
- SKIN — каждая запись отдельная (разные sourceType = разные слоты)
- BLUEPRINT, FRAGMENT, RESOURCE, BUFF — агрегируются по itemId, суммируется quantity
Это позволяет показывать "5× Metal" вместо пяти отдельных слотов, но сохранять историю происхождения каждого SKIN.
Salvage (Разбор)
Конвертация предметов в XP:
Допустимые типы: BLUEPRINT, FRAGMENT, RESOURCE
Формула: xpGained = item.salvageXP × quantity
Процесс:
- Проверка наличия предмета в инвентаре
- Проверка типа (только BLUEPRINT/FRAGMENT/RESOURCE)
- Проверка достаточного quantity
- Atomic transaction: списание предмета + начисление XP
- Обновление счётчиков (
User.itemsSalvaged,Item.totalSalvaged) - Quest/Achievement progress (категория RECYCLE)
Salvage недоступен во время COUNTDOWN — нельзя получать XP между сезонами.
Protection
| Действие | Rate Limit | Auth | Validation | Atomic |
|---|---|---|---|---|
| Get Inventory | general (100/min) | Telegram | GetInventorySchema | - |
| Salvage Item | general (100/min) | Telegram + Season | SalvageItemSchema | ✓ |
См. Security Matrix для полного обзора защит.
Edge Cases
| Ситуация | UI поведение |
|---|---|
| Предмет не в инвентаре | Ошибка "Предмет не найден в инвентаре" |
| Недостаточное количество | Ошибка "Недостаточное количество предметов" |
| Попытка Salvage SKIN/BUFF | Ошибка (только BLUEPRINT/FRAGMENT/RESOURCE) |
| COUNTDOWN сезона | Кнопка Salvage disabled, tooltip "Недоступно между сезонами" |
Backend Error Codes
| Ошибка | HTTP | Сообщение |
|---|---|---|
| Item not found | 404 | "Предмет не найден в инвентаре" |
| Insufficient quantity | 400 | "Недостаточное количество предметов" |
| Invalid item type | 400 | "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
| Компонент | Путь | Описание |
|---|---|---|
| InventoryService | backend/src/domains/inventory/services/inventory.service.ts | Бизнес-логика: get, salvage |
| InventoryController | backend/src/domains/inventory/controllers/inventory.controller.ts | HTTP handlers |
| InventoryMapper | backend/src/domains/inventory/mappers/inventory.mapper.ts | Prisma → Plain object |
| Routes | backend/src/domains/inventory/routes/inventory.routes.ts | API endpoints |
| Schemas | backend/src/domains/inventory/schemas/inventory.schemas.ts | Swagger + 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 для аудита:
| Поле | Тип | Описание |
|---|---|---|
itemSnapshot | JSON | Снимок предмета на момент разборки: { name, tier, imageUrl, salvageXP, category } |
quantity | Int | Количество разобранных предметов |
xpGained | Int | Полученный XP |
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
- User API
- Admin API
| Метод | Эндпоинт | Описание | Docs |
|---|---|---|---|
| GET | /api/inventory | Список предметов с фильтрами | → |
| POST | /api/inventory/salvage | Разбор предмета на XP | → |
Query параметры GET:
| Параметр | Тип | Описание |
|---|---|---|
| itemType | enum | SKIN, BLUEPRINT, FRAGMENT, RESOURCE, BUFF |
| tier | enum | TIER_0 — TIER_5 |
| page | number | Страница (default: 1) |
| limit | number | Элементов на странице (default: 50, max: 100) |
7. Related
- Cases — источник предметов (CASE_OPENING)
- Daily Spins — источник предметов (DAILY_SPIN)
- Craft — использование предметов для крафта скинов
- Withdrawals — вывод SKIN в Steam
- Buffs — активация BUFF предметов
- Quests — RECYCLE квесты (salvage progress)
- Achievements — RECYCLE достижения