Для тех кто любит смотреть спорт

Документация

Наглядное описание источников данных, маршрутов, контроллеров, моделей и синхронизации.

Другие страницы документации

Ключевая цепочка

  1. Маршрут GET / указывает на BetController@index — рендер главной страницы.
  2. Мы делаем команду Консольную команду ребилда событий, чтобы события по api из сайта sstats.net загрузились в базу данных
        // Консольная команда ребилда(добавления/обновления) событий
            Просто PHP - 
            php artisan events:rebuild
    
            Если через докер - 
            docker compose exec app php artisan events:rebuild
            
            Создание с тестовыми если через докер - 
            docker compose exec app php artisan events:rebuild --json=upcoming
    
            # Опционально: жёсткий ребилд с очисткой таблицы (опасно)
            php artisan events:rebuild --hard
        
    

    Лиги подтягиваются по ID в массиве в файле src/config/leagues.php

        'leagues' => [
            'UCL' => ['id' => 2,   'title' => 'Лига чемпионов УЕФА',       'slug' => 'ucl'],
            'EPL' => ['id' => 39,  'title' => 'Английская Премьер-лига',   'slug' => 'epl'],
            'FRA' => ['id' => 61,  'title' => 'Французская Лига 1',        'slug' => 'ligue-1'],
            'GER' => ['id' => 78,  'title' => 'Бундеслига',                'slug' => 'bundesliga'],
            'ARG' => ['id' => 128, 'title' => 'Аргентинская Премьер-лига', 'slug' => 'primera-division'],
            'ITA' => ['id' => 135, 'title' => 'Итальянская Серия А',       'slug' => 'serie-a'],
            'ESP' => ['id' => 140, 'title' => 'Испанская Ла Лига',         'slug' => 'la-liga'],
            'RUS' => ['id' => 235, 'title' => 'Российская Премьер-лига',   'slug' => 'rpl'],
            'RUS2'=> ['id' => 236, 'title' => 'Российская Первая лига',    'slug' => 'fnl'],
        ],
        
    

    И дефолтный набор лиг для агрегированной статистики главной страницы

        $defLeagueIds = [ 'EPL' => 39, 'UCL' =>  2, 'ITA' => 135 , 'RUS2' => 236];
    

    Далее идет $prepareForView, для обработки событий перед отображением на главной странице.

    Делается это с помощью map

    Пример работы map:

        
        $collection = collect([1, 2, 3]);
    
        $multipliedCollection = $collection->map(function ($item) {
            return $item * 2;
        });
    
        // $multipliedCollection will be collect([2, 4, 6])
        
    
        
            $prepareForView = function ($collection) {
                return $collection->map(function ($ev) {
                    try {
                        $home = trim((string)($ev->home_team ?? ''));
                        $away = trim((string)($ev->away_team ?? ''));
                        $ev->title = ($home !== '' || $away !== '') ? trim($home.' vs '.$away) : ($ev->title ?? '');
                    } catch (\Throwable $e) { /* no-op */ }
                    return $ev;
                });
            };
        
    

    И в конце циклом формируем человекочитаемый заголовок для каждой лиги.

        
            $leagueTitlesByCode = [];
            foreach (config('leagues.leagues') as $code => $info) {
                // $code содержит:
                // "UCL" например
    
                // Массив $info содержит:
                // "id" => 2
                // "title" => "Лига чемпионов УЕФА"
                // "slug" => "ucl"
    
                // Формируем человекочитаемый заголовок: используем "title" или сам код, если "title" отсутствует
                $leagueTitlesByCode[$code] = $info['title'] ?? $code;
            }
        
    
  3. Доп. маршруты: GET /odds, GET /events/{event}/markets, GET /odds/game/{gameId}.
  4. index() собирает ленты EPL/UCL/ITA, карту event_id→external_id и историю купонов.
  5. Представление: resources/views/home.blade.php показывает матчи, коэффициенты, купоны и купон-форму (Vue).
    // routes/web.php
    Route::get('/', [BetController::class, 'index'])->name('home');
    Route::get('/odds', [OddsController::class, 'odds'])->name('odds.index');
    Route::get('/events/{event}/markets', [OddsController::class, 'markets'])->name('events.markets');
    Route::get('/odds/game/{gameId}', [OddsController::class, 'marketsByGame'])->name('odds.byGame');
    
    // app/Http/Controllers/BetController.php (фрагмент)
    public function index()
    {
        $marketsMap = [];
        $gameIdsMap = [];
    
        foreach ([$eventsEpl, $eventsUcl, $eventsIta] as $collection) {
            foreach ($collection as $ev) {
                if (!empty($ev->external_id)) {
                    $gameIdsMap[$ev->id] = (string)$ev->external_id;
                }
            }
        }
    
        $coupons = Coupon::with(['bets.event'])->latest()->limit(50)->get();
    
        $leagues = [
            ['title' => 'Чемпионат Англии (EPL)', 'events' => $eventsEpl],
            ['title' => 'Лига чемпионов (UCL)', 'events' => $eventsUcl],
            ['title' => 'Серия А (ITA)', 'events' => $eventsIta],
        ];
    
        return view('home', [
            'leagues' => $leagues,
            'eventsEpl' => $eventsEpl,
            'eventsUcl' => $eventsUcl,
            'eventsIta' => $eventsIta,
            'coupons' => $coupons,
            'marketsMap' => $marketsMap,
            'gameIdsMap' => $gameIdsMap,
        ]);
    }
    

Что принимает и возвращает view()

  • view(name, data) принимает имя шаблона и массив данных.
  • Возвращает Illuminate\View\View, который преобразуется в HTML.
  • Ключи массива становятся переменными в Blade: например, $leagues, $coupons.
  • Этот блок документирует именно главную страницу.

Миграции: команды

  • Локально: php artisan migrate
  • Прод: php artisan migrate --force
  • Конкретный файл: php artisan migrate --path=database/migrations/2025_11_15_120000_add_role_to_users_table.php
  • Полная пересоздание: php artisan migrate:fresh --seed
  • Сидеры: php artisan db:seed --class=ModeratorUserSeeder
# Локально
php artisan migrate

# В продакшне
php artisan migrate --force

# Конкретная миграция по пути
php artisan migrate --path=database/migrations/2025_11_15_120000_add_role_to_users_table.php --force

# Полная пересоздание схемы + сиды (ОПАСНО)
php artisan migrate:fresh --seed --force

# Сиды
php artisan db:seed --class=ModeratorUserSeeder --force

Модель и миграции

  • Таблица events: title, starts_at, ends_at, status, result.
  • Поля EPL: home_team, away_team, home_odds, draw_odds, away_odds.
// database/migrations/create_events_table.php
$table->string('title');
$table->dateTime('starts_at')->nullable();
$table->enum('status', ['scheduled','live','finished'])->default('scheduled');
$table->enum('result', ['home','draw','away'])->nullable();

// add_epl_fields_to_events_table.php
$table->string('home_team')->nullable();
$table->string('away_team')->nullable();
$table->decimal('home_odds', 8, 2)->nullable();
$table->decimal('draw_odds', 8, 2)->nullable();
$table->decimal('away_odds', 8, 2)->nullable();

Синхронизация коэффициентов (epl:sync-odds)

  • Команды: sstats.net (список EPL).
  • Коэффициенты: sstats.net (рынок 1x2/Match Odds, формат decimal).
  • Сохранение: Event::updateOrCreate(...) со статусом scheduled.
// app/Console/Commands/SyncEplOdds.php
Event::updateOrCreate(
  ['title' => $m['home_team'].' vs '.$m['away_team'], 'starts_at' => $m['commence_time']],
  [
    'home_team' => $m['home_team'],
    'away_team' => $m['away_team'],
    'status' => 'scheduled',
    'home_odds' => $m['home_odds'],
    'draw_odds' => $m['draw_odds'],
    'away_odds' => $m['away_odds'],
  ]
);

Запуск: php artisan epl:sync-odds --limit=10 • ключи SSTATS_API_KEY и SSTATS_BASE в .env.

Разбор SyncEplOdds.php для новичков

  • Сигнатура команды: объявляет имя epl:sync-odds и опцию --limit.
  • Конфигурация: читает SSTATS_API_KEY и SSTATS_BASE из config/services.php.
  • HTTP‑клиент: получает список игр и коэффициенты, подстраиваясь под разные форматы ответа.
  • Сохранение: использует Event::updateOrCreate по паре title+starts_at.
  • Хелперы: методы fetchUpcomingWithOddsFromSstats, parseOddsFromGame, fetchOddsForGame, extractStartTime, avg.
// Ключевые строки из app/Console/Commands/SyncEplOdds.php
protected $signature = 'epl:sync-odds {--limit=10}'; // имя команды и опция
protected $description = 'Sync upcoming EPL matches and odds from sstats.net API';

$base = rtrim(config('services.sstats.base_url', 'https://api.sstats.net'), '/'); // базовый URL
$apiKey = config('services.sstats.key'); // ключ API из конфигурации
$headers = ['X-API-KEY' => $apiKey, 'Accept' => 'application/json']; // заголовки запроса

$matches = $this->fetchUpcomingWithOddsFromSstats($base, $headers, $limit); // загрузка матчей и коэффициентов
if (!empty($matches)) {
  // upsert событий по title+starts_at
  Event::updateOrCreate([
    'title' => $m['home_team'].' vs '.$m['away_team'],
    'starts_at' => $m['commence_time'],
  ], [
    'home_team' => $m['home_team'],
    'away_team' => $m['away_team'],
    'status' => 'scheduled',
    'home_odds' => $m['home_odds'],
    'draw_odds' => $m['draw_odds'],
    'away_odds' => $m['away_odds'],
  ]);
} else {
  // фоллбэк: создаём события с базовыми кэфами
  Event::firstOrCreate([
    'title' => $title,
  ], [
    'home_team' => $home,
    'away_team' => $away,
    'status' => 'scheduled',
    'starts_at' => now()->addDays(rand(1,7)),
    'home_odds' => 2.00,
    'draw_odds' => 3.40,
    'away_odds' => 3.60,
  ]);
}

Эти строки отражают общий ход команды: чтение конфигурации, запросы к API, сохранение событий и безопасный фоллбэк при проблемах с внешними сервисами.

Синхронизация результатов (epl:sync-results)

  • Источник: sstats.net (прошедшие матчи EPL).
  • Обновляет status=finished и result (home/draw/away).
  • Рассчитывает связанные ставки: победа/проигрыш и выплату.
// app/Console/Commands/SyncEplResults.php
$ev->status = 'finished';
$ev->result = $result; // home/draw/away
$ev->ends_at = $apiTime ?: now();
$ev->save();

$ev->bets()->each(function(Bet $bet) use ($ev) {
  $win = $bet->selection === $ev->result;
  $odds = match ($bet->selection) {
    'home' => $ev->home_odds,
    'draw' => $ev->draw_odds,
    'away' => $ev->away_odds,
  };
  $bet->is_win = $win;
  $bet->payout_demo = $win ? ($bet->amount_demo * ($odds ?? 2)) : 0;
  $bet->settled_at = now();
  $bet->save();
});

Запуск: php artisan epl:sync-results • планировщик ежечасно.

Отображение и купон на главной

  • В колонке «Коэфф. (П1 / Ничья / П2)» показаны home_odds/draw_odds/away_odds.
  • Доп. рынки подгружаются по требованию и кнопки имеют data-market, data-selection, data-odds.
  • Клик по коэффициенту добавляет исход в купон (Vue компонент). Купон поддерживает несколько событий.
  • Отправка на POST /bets массивом items — сервер создаёт один купон с несколькими ставками.
<!-- Домашние кэфы 1x2 -->
<span class="odd-btn" data-event-id="{{ $ev->id }}" data-selection="home" data-odds="{{ number_format($h, 2) }}">П1 {{ number_format($h, 2) }}</span>

<!-- Кнопки доп. рынков: есть market, selection, odds -->
<button class="odd-btn" data-event-id="{{ $ev->id }}" data-market="Тотал 2.5" data-selection="Больше" data-odds="1.90">Больше 2.5 (1.90)</button>

// resources/js/components/BetSlip.vue
function handleOddClick(e) {
  const btn = e.target.closest('.odd-btn');
  if (!btn) return;
  const eventId = btn.getAttribute('data-event-id');
  const market = btn.getAttribute('data-market');
  const selection = btn.getAttribute('data-selection');
  const home = btn.getAttribute('data-home');
  const away = btn.getAttribute('data-away');
  const odds = btn.getAttribute('data-odds');
  addOrReplaceSlipItem({ eventId, home, away, selection, odds, market });
}

// Купон поддерживает несколько разных событий.
// Для одного события хранится один выбранный исход; повторный клик обновляет его.

Ключи и планировщик

  • SSTATS_API_KEY, SSTATS_BASE — sstats.net (коэффициенты).
  • Планировщик: ежедневно epl:sync-odds в 06:00; ежечасно epl:sync-results.
  • Маршрут быстрого обновления результатов: GET /events/sync-results.
  • Ребилд событий: php artisan events:rebuild или php artisan events:rebuild --hard (опасно, очищает таблицу).

Статистика (страница /stats)

  • Маршрут: GET /stats, имя stats.index, контроллер StatsController.
  • Представление: resources/views/stats.blade.php — сводка по командам и агрегаты.
  • Источник данных: sstats.net; ключ и базовый URL берутся из config/services.php (SSTATS_API_KEY, SSTATS_BASE).
  • Турнир по умолчанию: EPL (tournamentId=17); период выборки — последние 120 дней.
  • Кеширование результатов матчей и расчёт метрик: матчи, забитые/пропущенные, победы/ничьи/поражения, дома/в гостях.
  • Агрегаты: самые забивающие/пропускающие (дом/гости), топ-10 по голам и победам.
  • Обработка ошибок: при недоступности API выводится сообщение и используются безопасные значения .
// routes/web.php
Route::get('/stats', StatsController::class)->name('stats.index');

// config/services.php (.env)
// sstats
SSTATS_API_KEY=your_sstats_key
SSTATS_BASE=https://<base>

// Примечание: смена турнира — корректируйте ID в StatsController

Быстрая проверка: установите ключи в .env, запустите сервер и откройте /stats.

Деплой на хостинг

Краткое резюме процесса и команд по материалам DEPLOY_INSTRUCTIONS.md.

Envoy: задачи и команды

  • setup — первичная настройка сервера: обновление пакетов, создание директорий, установка прав.
  • deploy — основной деплой: вытягивание кода из origin, установка зависимостей, прогрев кешей.
  • migrate-fresh — полная пересоздание схемы БД: migrate:fresh для чистого развёртывания.
  • seed — запуск сидов: заполнение начальными данными (db:seed).
  • assets — сборка фронта на сервере: npm ci и npm run build.
  • assets-build — быстрая пересборка ассетов без установки зависимостей (если уже установлен node_modules).
  • sync-odds — ручной запуск синхронизации коэффициентов (epl:sync-odds).
  • sync-results — ручной запуск синхронизации результатов (epl:sync-results).
  • admin-update — сброс пароля и обновление данных администратора через tinker.
  • release — сборная задача для релиза: кеши, миграции, очистка и подготовка окружения.
# Примеры запуска задач Envoy
# Укажите сервер, если их несколько (например, beget или local)
envoy run setup --server=beget
envoy run deploy --server=beget --branch=main
envoy run migrate-fresh --server=beget
envoy run seed --server=beget
envoy run assets --server=beget
envoy run assets-build --server=beget
envoy run sync-odds --server=beget
envoy run sync-results --server=beget
envoy run admin-update --server=beget --admin_email=admin@example.com
envoy run release --server=beget

Перенос данных БД (локальная → удалённая)

  • Рекомендуется использовать дамп/импорт СУБД.
  • Seeder'ы не переносят текущие данные, они генерируют предопределённые.
# MySQL/MariaDB: экспорт локальной БД
mysqldump -u <user> -p <database> > dump.sql

# Копируем dump.sql на сервер
scp dump.sql user@server:/path/to/

# Импорт на удалённой БД
mysql -u <remote_user> -p <remote_database> < /path/to/dump.sql

# PostgreSQL
pg_dump -U <user> -d <database> -f dump.sql
psql -U <remote_user> -d <remote_database> -f dump.sql

# SQLite: копирование файла БД
cp database/database.sqlite /remote/path/database.sqlite

# Через Laravel (две коннекции):
# добавьте "remote" в config/database.php и перенесите данные командой с chunkById+upsert

Запуск релиза

  • Что делает: последовательно запускает deploy, assets-build, assets, admin-update.
  • Предусловия: корректно настроены @servers, переменные $path, $branch, доступ по SSH; установлены PHP/Composer/Node на сервере.
  • Параметры (опционально): --branch=main, --server=beget, для admin-update можно передать --admin_username, --admin_email, --admin_password.
# Базовый запуск релиза на beget
envoy run release --server=beget --branch=main

# С обновлением администратора в рамках релиза
envoy run release --server=beget --branch=main \
  --admin_username=admin \
  --admin_email=admin@example.com \
  --admin_password="S3curePass!"

# Локальная проверка ассетов (если требуется)
envoy run assets-build --server=local && envoy run assets --server=beget

Как запустить Envoy

  • Через Docker Compose (рекомендуется): команды исполняются внутри контейнера app.
  • Локально (Windows): выполните из каталога src команду php vendor/bin/envoy ....

                        
# Проверить, что Envoy доступен (в контейнере)
docker compose exec app vendor/bin/envoy list

# Запустить релиз (т.е загрузить на удаленный сервер) (в контейнере)
docker compose exec app vendor/bin/envoy run release --server=beget --branch=main

# Запустить релиз локально (если PHP установлен и вы в каталоге src)
php vendor/bin/envoy run release --server=beget --branch=main

# Подсказка: чтобы увидеть список задач
docker compose exec app vendor/bin/envoy tasks

# Запустить Laravel dev-сервер (в контейнере)
docker compose exec app php artisan serve --host=0.0.0.0 --port=8000 --no-reload

# Альтернатива без Compose (по имени контейнера)
docker exec games_app php artisan serve --host=0.0.0.0 --port=8000 --no-reload

Самые частоиспользуемые команды

# Запустить релиз локально (если PHP установлен и вы в каталоге src)
php vendor/bin/envoy run release --server=beget --branch=main

# Альтернатива без Compose (по имени контейнера)
docker exec games_app php artisan serve --host=0.0.0.0 --port=8000 --no-reload

# Создание тестовой лиги
docker compose exec app php artisan events:create-test

# DebugBar
// \Barryvdh\Debugbar\Facades\Debugbar::addMessage($event->external_id, 'external_id');