Files
sbnews/server/app/api/controller/MatchController.php
T

696 lines
25 KiB
PHP

<?php
namespace app\api\controller;
use app\api\lists\match\MatchLists;
use app\common\model\match\MatchEvent;
use app\common\model\match\MatchHistory;
use app\common\model\match\MatchEventLog;
use app\common\model\match\MatchData;
use app\common\model\match\MatchRecent;
use app\common\model\match\MatchLiveText;
use app\common\model\match\MatchLineup;
use app\common\model\match\MatchOdds;
use app\common\model\match\WorldCupPersonRanking;
use app\common\model\match\WorldCupStanding;
use app\common\model\league\League;
use app\common\service\match\MatchLiveService;
class MatchController extends BaseApiController
{
public array $notNeedLogin = ['lists', 'detail', 'leagues', 'sportTypes', 'liveText', 'lineup', 'worldCupStandings', 'worldCupRankings', 'worldCupSchedule', 'worldCupLiveCards', 'worldCupOdds'];
protected int $worldCupSeasonId = 26123;
protected string $worldCupLeagueName = '世界杯';
protected string $worldCupDefaultLiveUrl = 'https://sports.cctv.com/?spm=C28340.P9mj1V5B06xk.E2XVQsMhlk44.7';
public function sportTypes()
{
$types = League::where('is_show', 1)
->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');
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] ?? '盘口';
}
}