where('type', 'sport') ->field('sport_type, label, icon') ->order('sort asc') ->select() ->toArray(); $result = []; foreach ($types as $item) { $result[] = [ 'value' => $item['sport_type'], 'label' => $item['label'], 'icon' => $item['icon'], ]; } return $this->data($result); } public function leagues() { $sportType = $this->request->get('sport_type/d', 0); $query = League::where('is_show', 1) ->where('type', 'league'); if ($sportType > 0) { $query->where('sport_type', $sportType); } $leagues = $query->field('id, label, sport_type, icon, sort') ->order('sort asc') ->select() ->toArray(); $leagueNames = array_column($leagues, 'label'); $counts = []; if (!empty($leagueNames)) { $rows = MatchEvent::where('is_show', 1) ->where('status', 1) ->whereIn('league_name', $leagueNames) ->field('league_name, COUNT(*) as cnt') ->group('league_name') ->select() ->toArray(); foreach ($rows as $row) { $counts[$row['league_name']] = (int) $row['cnt']; } } $grouped = []; foreach ($leagues as $item) { $st = $item['sport_type']; if (!isset($grouped[$st])) { $grouped[$st] = []; } $grouped[$st][] = [ 'id' => $item['id'], 'league_name' => $item['label'], 'icon' => $item['icon'], 'count' => $counts[$item['label']] ?? 0, ]; } return $this->data($grouped); } public function lists() { $sportType = $this->request->get('sport_type/d', 0); $leagueName = $this->request->get('league_name/s', ''); $endedPage = $this->request->get('ended_page/d', 1); $endedPageSize = $this->request->get('ended_page_size/d', 20); $endedOnly = $this->request->get('ended_only/d', 0); $field = 'id,league_name,league_icon,home_team,home_icon,home_score,away_team,away_icon,away_score,sport_type,status,match_time,current_minute,half_score,home_odds,draw_odds,away_odds,home_corner,away_corner,home_yellow,away_yellow,home_red,away_red,is_hot,round_name'; // 基础筛选 $baseWhere = [['is_show', '=', 1]]; if ($sportType > 0) { $baseWhere[] = ['sport_type', '=', $sportType]; } if (!empty($leagueName)) { $baseWhere[] = ['league_name', '=', $leagueName]; } elseif ($sportType <= 0) { $validLeagues = League::where('is_show', 1)->where('type', 'league')->column('label'); if (!empty($validLeagues)) { $baseWhere[] = ['league_name', 'in', $validLeagues]; } } // 已结束:分页,按时间倒序 $endedOffset = ($endedPage - 1) * $endedPageSize; $ended = MatchEvent::where($baseWhere)->where('status', 2) ->field($field)->order('match_time desc') ->limit($endedOffset, $endedPageSize)->select()->toArray(); $endedCount = MatchEvent::where($baseWhere)->where('status', 2)->count(); // 加载更多时只返回已结束数据 if ($endedOnly) { return $this->data([ 'ended' => [ 'lists' => $ended, 'count' => $endedCount, 'page_no' => $endedPage, 'page_size' => $endedPageSize, ], ]); } // 进行中:全部返回 $live = MatchEvent::where($baseWhere)->where('status', 1) ->field($field)->order('match_time asc')->select()->toArray(); // 未开始:只取前20条,按时间正序 $upcoming = MatchEvent::where($baseWhere)->where('status', 0) ->field($field)->order('match_time asc')->limit(20)->select()->toArray(); return $this->data([ 'live' => $live, 'upcoming' => $upcoming, 'ended' => [ 'lists' => $ended, 'count' => $endedCount, 'page_no' => $endedPage, 'page_size' => $endedPageSize, ], ]); } public function detail() { $id = $this->request->get('id/d'); $match = MatchEvent::where('id', $id)->where('is_show', 1)->findOrEmpty(); if ($match->isEmpty()) { return $this->fail('赛事不存在'); } $data = $match->toArray(); $data['live_list'] = MatchLiveService::getLiveListForApi((int) $data['id']); if (empty($data['live_url']) && !empty($data['live_list'][0]['source_url'])) { $data['live_url'] = (string) $data['live_list'][0]['source_url']; } if (empty($data['live_url']) && $this->isWorldCupMatch($data)) { $data['live_url'] = $this->worldCupDefaultLiveUrl; } $data['living_tv'] = ''; $matchDataRaw = MatchData::where('match_id', $data['match_id']) ->order('id desc') ->value('raw_data'); if (!empty($matchDataRaw)) { $rawData = json_decode($matchDataRaw, true); if (json_last_error() === JSON_ERROR_NONE && is_array($rawData)) { $data['living_tv'] = $this->resolveLiveSourceText($rawData); } } $homeTeam = $data['home_team']; $awayTeam = $data['away_team']; // 对赛往绩 $data['history'] = MatchHistory::where(function ($query) use ($homeTeam, $awayTeam) { $query->where([['home_team', '=', $homeTeam], ['away_team', '=', $awayTeam]]) ->whereOr([['home_team', '=', $awayTeam], ['away_team', '=', $homeTeam]]); })->order('match_time desc')->limit(5)->select()->toArray(); // 精彩瞬间(按业务字段去重) $data['events'] = MatchEventLog::where('match_id', $data['match_id']) ->group('minute,event_type,player_name,team_side') ->order('minute asc') ->select()->toArray(); // 主队近期战绩 $data['home_recent'] = MatchRecent::where('team_name', $homeTeam) ->order('match_time desc')->limit(5)->select()->toArray(); // 客队近期战绩 $data['away_recent'] = MatchRecent::where('team_name', $awayTeam) ->order('match_time desc')->limit(5)->select()->toArray(); // 文字直播(最新50条) $data['live_text'] = MatchLiveText::where('match_id', $data['match_id']) ->field('id, msg_id, event_type, username, avatar, message, image, timestamp, create_time') ->order('msg_id desc') ->limit(50) ->select()->toArray(); $data['live_text'] = array_reverse($data['live_text']); return $this->data($data); } private function resolveLiveSourceText(array $rawData): string { foreach (['livingTv', 'live_source', 'live_no_source', 'tv_live_info'] as $key) { if (!empty($rawData[$key])) { $text = $this->normalizeLiveSourceValue($rawData[$key]); if ($text !== '') { return $text; } } } return ''; } private function normalizeLiveSourceValue($value): string { if (is_string($value) || is_numeric($value)) { return trim((string) $value); } if (!is_array($value)) { return ''; } $items = []; foreach ($value as $item) { if (is_string($item) || is_numeric($item)) { $text = trim((string) $item); if ($text !== '') { $items[] = $text; } continue; } if (!is_array($item)) { continue; } foreach (['live_tag', 'name', 'title', 'source_name', 'label'] as $field) { if (!empty($item[$field])) { $text = trim((string) $item[$field]); if ($text !== '') { $items[] = $text; break; } } } } $items = array_values(array_unique(array_filter($items))); return implode('、', $items); } protected function isWorldCupMatch(array $match): bool { return ($match['league_name'] ?? '') === $this->worldCupLeagueName || (int)($match['competition_id'] ?? 0) === 61; } public function liveText() { $matchId = $this->request->get('match_id/d'); if (!$matchId) { return $this->fail('参数错误'); } $lastMsgId = $this->request->get('last_msg_id/d', 0); $query = MatchLiveText::where('match_id', $matchId) ->field('id, msg_id, event_type, username, avatar, message, image, timestamp, create_time') ->order('msg_id desc'); if ($lastMsgId > 0) { $query->where('msg_id', '>', $lastMsgId); } $list = $query->limit(100)->select()->toArray(); $list = array_reverse($list); return $this->data([ 'list' => $list, 'count' => count($list), ]); } public function lineup() { $matchId = $this->request->get('match_id/d'); if (!$matchId) { return $this->fail('参数错误'); } $list = MatchLineup::where('match_id', $matchId) ->field('id, match_id, team_side, is_starter, person_id, person_name, person_logo, shirt_number, captain, position') ->order('team_side asc, is_starter desc, formation_place asc') ->select()->toArray(); return $this->data([ 'list' => $list, ]); } public function worldCupStandings() { $rows = WorldCupStanding::where('season_id', $this->worldCupSeasonId) ->order('stage_name asc') ->order('group_name asc') ->order('row_order asc') ->order('rank asc') ->select() ->toArray(); $stageName = ''; $groups = []; $header = ['球队', '赛', '胜', '平', '负', '进/失', '积分']; foreach ($rows as $row) { if ($stageName === '' && !empty($row['stage_name'])) { $stageName = $row['stage_name']; } $groupName = $row['group_name'] ?: '未分组'; if (!isset($groups[$groupName])) { $groups[$groupName] = [ 'group_name' => $groupName, 'header' => $header, 'teams' => [], ]; } $groups[$groupName]['teams'][] = [ 'team_id' => (int) $row['team_id'], 'team_name' => $row['team_name'], 'team_logo' => $row['team_logo'], 'rank' => (int) $row['rank'], 'points' => (int) $row['points'], 'played' => (int) $row['played'], 'won' => (int) $row['won'], 'drawn' => (int) $row['drawn'], 'lost' => (int) $row['lost'], 'goals_for' => (int) $row['goals_for'], 'goals_against' => (int) $row['goals_against'], 'goal_diff' => (int) $row['goal_diff'], 'row_order' => (int) $row['row_order'], ]; } return $this->data([ 'season_id' => $this->worldCupSeasonId, 'stage_name' => $stageName, 'groups' => array_values($groups), ]); } public function worldCupRankings() { $type = $this->request->get('type/s', 'goals'); if (!in_array($type, ['goals', 'assists'], true)) { return $this->fail('排行榜类型错误'); } $rows = WorldCupPersonRanking::where('season_id', $this->worldCupSeasonId) ->where('rank_type', $type) ->order('rank asc') ->order('count desc') ->order('id asc') ->select() ->toArray(); $header = $type === 'goals' ? ['球员', '球队', '进球'] : ['球员', '球队', '助攻']; $list = []; foreach ($rows as $row) { $list[] = [ 'person_id' => (int) $row['person_id'], 'person_name' => $row['person_name'], 'person_logo' => $row['person_logo'], 'team_id' => (int) $row['team_id'], 'team_name' => $row['team_name'], 'team_logo' => $row['team_logo'], 'rank' => (int) $row['rank'], 'count' => (int) $row['count'], ]; } return $this->data([ 'season_id' => $this->worldCupSeasonId, 'type' => $type, 'header' => $header, 'list' => $list, ]); } public function worldCupSchedule() { $roundOrder = [ '小组赛 第1轮', '小组赛 第2轮', '小组赛 第3轮', '1/16决赛', '1/8决赛', '1/4决赛', '半决赛', '三四名决赛', '决赛', ]; $bracketRounds = ['1/16决赛', '1/8决赛', '1/4决赛', '半决赛', '三四名决赛', '决赛']; $roundWeightMap = []; foreach ($roundOrder as $index => $roundName) { $roundWeightMap[$roundName] = $index; } $rows = MatchEvent::where('league_name', $this->worldCupLeagueName) ->where('sport_type', 1) ->where('is_show', 1) ->field('id,match_id,competition_id,league_name,round_name,stage,home_team,home_icon,home_score,away_team,away_icon,away_score,status,match_time,current_minute,half_score') ->order('sort asc') ->order('match_time asc') ->order('id asc') ->select() ->toArray(); $scheduleMap = []; foreach ($rows as $row) { $roundName = $row['round_name'] ?: '未分轮次'; if (!isset($scheduleMap[$roundName])) { $scheduleMap[$roundName] = [ 'round_name' => $roundName, 'matches' => [], '_weight' => $roundWeightMap[$roundName] ?? 999, ]; } $scheduleMap[$roundName]['matches'][] = [ 'id' => (int) $row['id'], 'match_id' => (int) $row['match_id'], 'competition_id' => (int) $row['competition_id'], 'league_name' => $row['league_name'], 'round_name' => $roundName, 'stage' => $row['stage'], 'home_team' => $row['home_team'], 'home_icon' => $row['home_icon'], 'home_score' => (int) $row['home_score'], 'away_team' => $row['away_team'], 'away_icon' => $row['away_icon'], 'away_score' => (int) $row['away_score'], 'status' => (int) $row['status'], 'match_time' => (int) $row['match_time'], 'current_minute' => $row['current_minute'], 'half_score' => $row['half_score'], ]; } $scheduleRounds = array_values($scheduleMap); usort($scheduleRounds, function ($left, $right) { return $left['_weight'] <=> $right['_weight']; }); $currentRoundName = ''; foreach ($scheduleRounds as $round) { foreach ($round['matches'] as $match) { if ((int) $match['status'] === 1) { $currentRoundName = $round['round_name']; break 2; } } } if ($currentRoundName === '') { foreach ($scheduleRounds as $round) { foreach ($round['matches'] as $match) { if ((int) $match['status'] === 0) { $currentRoundName = $round['round_name']; break 2; } } } } if ($currentRoundName === '' && !empty($scheduleRounds)) { $currentRoundName = $scheduleRounds[count($scheduleRounds) - 1]['round_name']; } $bracketRoundList = []; foreach ($scheduleRounds as &$round) { unset($round['_weight']); if (in_array($round['round_name'], $bracketRounds, true)) { $bracketRoundList[] = $round; } } unset($round); return $this->data([ 'season_id' => $this->worldCupSeasonId, 'current_round_name' => $currentRoundName, 'bracket_rounds' => $bracketRoundList, 'schedule_rounds' => $scheduleRounds, ]); } public function worldCupLiveCards() { return $this->data([ 'season_id' => $this->worldCupSeasonId, 'list' => MatchLiveService::getWorldCupLiveCards(), ]); } public function worldCupOdds() { $showType = $this->request->get('show_type/s', ''); $includeSpecial = $this->request->get('include_special/d', 0); $limit = min(max($this->request->get('limit/d', 120), 1), 300); $validShowTypes = ['live', 'today', 'early']; $query = MatchOdds::where('sport_type', 'FT') ->where('league_name', 'like', '%世界杯%'); if (!$includeSpecial) { $query->where('league_name', 'not like', '%特定球员%'); } if (in_array($showType, $validShowTypes, true)) { $query->where('show_type', $showType); } $rows = $query ->field('id,source_site,source_url,source_match_id,source_league_id,sport_type,show_type,odds_type,league_name,match_time_text,home_team,away_team,handicap_line,handicap_home_odds,handicap_away_odds,total_line,total_over_odds,total_under_odds,home_win_odds,draw_odds,away_win_odds,update_time') ->order('update_time desc') ->order('id desc') ->limit($limit) ->select() ->toArray(); $eventMap = $this->buildWorldCupEventMap(); $matches = []; foreach ($rows as $row) { $matchKey = $this->buildOddsMatchKey($row); if (!isset($matches[$matchKey])) { $event = $eventMap[$this->buildTeamPairKey($row['home_team'] ?? '', $row['away_team'] ?? '')] ?? []; $matches[$matchKey] = [ 'id' => (int)($event['id'] ?? 0), 'match_id' => (int)($event['match_id'] ?? 0), 'source_match_ids' => [], 'league_name' => $row['league_name'], 'match_time_text' => $row['match_time_text'], 'match_time' => (int)($event['match_time'] ?? 0), 'home_team' => $row['home_team'], 'home_icon' => $event['home_icon'] ?? '', 'home_score' => (int)($event['home_score'] ?? 0), 'away_team' => $row['away_team'], 'away_icon' => $event['away_icon'] ?? '', 'away_score' => (int)($event['away_score'] ?? 0), 'status' => (int)($event['status'] ?? 0), 'current_minute' => $event['current_minute'] ?? '', 'platforms' => [], 'update_time' => 0, ]; } $matches[$matchKey]['source_match_ids'][] = (string)$row['source_match_id']; $matches[$matchKey]['platforms'][] = $this->formatOddsPlatform($row); $matches[$matchKey]['update_time'] = max((int)$matches[$matchKey]['update_time'], (int)$row['update_time']); } $list = array_values($matches); foreach ($list as &$item) { $item['source_match_ids'] = array_values(array_unique($item['source_match_ids'])); usort($item['platforms'], function ($left, $right) { return (int)$right['update_time'] <=> (int)$left['update_time']; }); } unset($item); usort($list, function ($left, $right) { return (int)$right['update_time'] <=> (int)$left['update_time']; }); return $this->data([ 'season_id' => $this->worldCupSeasonId, 'list' => $list, ]); } private function buildWorldCupEventMap(): array { $rows = MatchEvent::where('league_name', $this->worldCupLeagueName) ->where('sport_type', 1) ->where('is_show', 1) ->field('id,match_id,home_team,home_icon,home_score,away_team,away_icon,away_score,status,match_time,current_minute') ->select() ->toArray(); $map = []; foreach ($rows as $row) { $key = $this->buildTeamPairKey($row['home_team'] ?? '', $row['away_team'] ?? ''); if ($key !== '') { $map[$key] = $row; } } return $map; } private function buildOddsMatchKey(array $row): string { return implode('|', [ trim((string)($row['league_name'] ?? '')), trim((string)($row['match_time_text'] ?? '')), $this->buildTeamPairKey($row['home_team'] ?? '', $row['away_team'] ?? ''), ]); } private function buildTeamPairKey(string $homeTeam, string $awayTeam): string { $homeTeam = preg_replace('/\s+/u', '', trim($homeTeam)); $awayTeam = preg_replace('/\s+/u', '', trim($awayTeam)); if ($homeTeam === '' || $awayTeam === '') { return ''; } return $homeTeam . 'vs' . $awayTeam; } private function formatOddsPlatform(array $row): array { return [ 'platform_key' => (string)$row['source_site'], 'platform_name' => $this->resolveOddsPlatformName((string)$row['source_site']), 'show_type' => (string)$row['show_type'], 'show_type_text' => $this->resolveOddsShowTypeText((string)$row['show_type']), 'odds_type' => (string)$row['odds_type'], 'source_match_id' => (string)$row['source_match_id'], 'update_time' => (int)$row['update_time'], 'markets' => array_values(array_filter([ $this->buildOddsMarket('独赢', '', [ ['label' => '主胜', 'value' => $row['home_win_odds']], ['label' => '平', 'value' => $row['draw_odds']], ['label' => '客胜', 'value' => $row['away_win_odds']], ]), $this->buildOddsMarket('让球', (string)$row['handicap_line'], [ ['label' => '主队', 'value' => $row['handicap_home_odds']], ['label' => '客队', 'value' => $row['handicap_away_odds']], ]), $this->buildOddsMarket('大小球', (string)$row['total_line'], [ ['label' => '大', 'value' => $row['total_over_odds']], ['label' => '小', 'value' => $row['total_under_odds']], ]), ])), ]; } private function buildOddsMarket(string $label, string $line, array $values): ?array { $filtered = []; foreach ($values as $item) { $value = trim((string)($item['value'] ?? '')); if ($value === '') { continue; } $filtered[] = [ 'label' => $item['label'], 'value' => $value, ]; } if (empty($filtered)) { return null; } return [ 'label' => $label, 'line' => trim($line), 'values' => $filtered, ]; } private function resolveOddsPlatformName(string $sourceSite): string { $map = [ 'hg' => '滚球', ]; return $map[$sourceSite] ?? ($sourceSite !== '' ? strtoupper($sourceSite) : '平台'); } private function resolveOddsShowTypeText(string $showType): string { $map = [ 'live' => '滚球盘', 'today' => '今日盘', 'early' => '早盘', ]; return $map[$showType] ?? '盘口'; } }