Документация
Наглядное описание источников данных, маршрутов, контроллеров, моделей и синхронизации.
Ключевая цепочка
- Маршрут
GET /указывает наBetController@index— рендер главной страницы. - Мы делаем команду Консольную команду ребилда событий, чтобы события по 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; } - Доп. маршруты:
GET /odds,GET /events/{event}/markets,GET /odds/game/{gameId}. index()собирает ленты EPL/UCL/ITA, картуevent_id→external_idи историю купонов.- Представление:
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');