service = $service; } public function index(Request $request) { $scheduleDate = (string)$request->input('schedule_date', ''); $matches = $scheduleDate !== '' ? $this->service->matchesByDate($scheduleDate) : $this->service->allMatches(); return view('admin.world_cup.schedule', [ 'scheduleDate' => $scheduleDate, 'matches' => $matches, 'result' => $request->session()->get('world_cup_schedule_update_result'), 'scheduleUrls' => $this->scheduleUrls(), ]); } public function update(Request $request) { $scheduleDate = (string)$request->input('schedule_date', ''); $matches = $this->parseMatches($request); if ($matches === null) { $result = [ 'success' => false, 'message' => 'Invalid matches payload', 'data' => [ 'updated' => 0, 'skipped' => 0, 'errors' => [], ], ]; return $this->respond($request, $result, $scheduleDate); } $result = $this->service->updateMatches($matches, $this->actor($request)); return $this->respond($request, $result, $scheduleDate); } public function import(Request $request) { $rows = $this->parseCsvRows($request); $result = $rows === null ? [ 'success' => false, 'message' => 'Invalid CSV file', 'data' => [ 'updated' => 0, 'skipped' => 0, 'errors' => [], ], ] : $this->service->importExistingMatches($rows, $this->actor($request)); return $this->respond($request, $result, (string)$request->input('schedule_date', '')); } private function parseMatches(Request $request): ?array { $matches = $request->input('matches'); if (is_array($matches)) { return $matches; } return []; } private function respond(Request $request, array $result, string $scheduleDate) { if ($request->ajax() || $request->expectsJson() || $request->input('format') === 'json') { return $this->json($result['success'] ? 200 : 400, $result['message'], $result['data']); } $url = '/admin/world-cup/schedule'; if ($scheduleDate !== '') { $url .= '?schedule_date=' . urlencode($scheduleDate); } return redirect($url) ->with('world_cup_schedule_update_result', $result); } private function actor(Request $request): string { return (string)($request->session()->get('admin.username') ?: 'admin'); } private function scheduleUrls(): array { return [ 'FIFA 官方赛程' => 'https://www.fifa.com/en/tournaments/mens/worldcup/canadamexicousa2026/articles/match-schedule-fixtures-results-teams-stadiums', 'FIFA 赛程更新时间公告' => 'https://vod.fifa.com/media-releases/updated-world-cup-2026-match-schedule-venues-kick-off-times-104-matches', '可读赛程 PDF' => 'https://www.kickoffclock.com/downloads/world-cup-2026-schedule.pdf', ]; } private function parseCsvRows(Request $request): ?array { if (!$request->hasFile('csv_file') || !$request->file('csv_file')->isValid()) { return null; } $handle = fopen($request->file('csv_file')->getRealPath(), 'r'); if ($handle === false) { return null; } $headers = null; $rows = []; while (($data = fgetcsv($handle)) !== false) { if ($headers === null) { $headers = $this->normalizeCsvHeaders($data); continue; } if ($this->isEmptyCsvRow($data)) { continue; } $values = array_slice(array_pad($data, count($headers), ''), 0, count($headers)); $rows[] = array_combine($headers, $values); } fclose($handle); return $headers === null ? null : $rows; } private function normalizeCsvHeaders(array $headers): array { return array_map(function ($header) { return trim(str_replace("\xEF\xBB\xBF", '', (string)$header)); }, $headers); } private function isEmptyCsvRow(array $row): bool { foreach ($row as $value) { if (trim((string)$value) !== '') { return false; } } return true; } }