Sport.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. <?php
  2. namespace App\Console\Commands;
  3. use Illuminate\Console\Command;
  4. use App\Models\Sport as SportModel;
  5. use App\Models\SportTeam;
  6. use App\Models\SportLeague;
  7. use App\Models\SportStatistics;
  8. use App\Services\SportClientService;
  9. use Carbon\Carbon;
  10. use Illuminate\Support\Facades\DB;
  11. use App\Models\SportEvent;
  12. use App\Models\Config;
  13. class Sport extends Command
  14. {
  15. /**
  16. * 命令名称和签名
  17. *
  18. * @var string
  19. */
  20. protected $signature = 'sport {is_live=0}';
  21. protected $is_live = 0;
  22. protected $short_status = [
  23. 'TBD' => 0,
  24. 'NS' => 0,
  25. '1H' => 1,
  26. 'HT' => 1,
  27. '2H' => 1,
  28. 'ET' => 1,
  29. 'BT' => 1,
  30. 'P' => 1,
  31. 'SUSP' => 1,
  32. 'INT' => 1,
  33. 'FT' => 2,
  34. 'AET' => 2,
  35. 'PEN' => 2,
  36. 'PST' => 3,
  37. 'CANC' => 4,
  38. 'ABD' => 4,
  39. 'AWD' => 4,
  40. 'WO' => 4,
  41. 'LIVE' => 1,
  42. ];
  43. protected $long_status = [
  44. 'Time To Be Defined' => 0,
  45. 'Not Started' => 0,
  46. 'First Half' => 1,
  47. 'First Half, Kick Off' => 1,
  48. 'Halftime' => 1,
  49. 'Second Half' => 1,
  50. 'Second Half, 2nd Half Started' => 1,
  51. 'Extra Time' => 1,
  52. 'Break Time' => 1,
  53. 'Penalty In Progress' => 1,
  54. 'Match Suspended' => 1,
  55. 'Match Interrupted' => 1,
  56. 'Match Finished' => 2,
  57. 'Match Finished' => 2,
  58. 'Match Finished' => 2,
  59. 'Match Postponed' => 3,
  60. 'Match Cancelled' => 4,
  61. 'Match Abandoned' => 4,
  62. 'Technical Loss' => 4,
  63. 'WalkOver' => 4,
  64. 'In Progress' => 1,
  65. ];
  66. protected $fixture_status = [
  67. 'First Half' => 1,
  68. 'First Half, Kick Off' => 1,
  69. 'Halftime' => 1,
  70. 'Second Half' => 1,
  71. 'Second Half, 2nd Half Started' => 1,
  72. 'Extra Time' => 1,
  73. 'Break Time' => 1,
  74. 'Penalty In Progress' => 1,
  75. ];
  76. /**
  77. * 命令描述
  78. *
  79. * @var string
  80. */
  81. protected $description = '当天会去更新明天的赛事(23:59:00执行一次)';
  82. /**
  83. * 执行命令
  84. *
  85. * @return int
  86. */
  87. public function handle()
  88. {
  89. // $this->info('开始执行统计比赛数据任务...');
  90. $this->is_live = $this->argument('is_live');
  91. if ($this->is_live == 0) {
  92. //未开始的赛事拉取
  93. $this->fixtures();
  94. } elseif ($this->is_live == 1) {
  95. //进行中的赛事,定时更新
  96. $this->liveFixtures();
  97. } elseif ($this->is_live == 2){
  98. $this->checkLiveFixtures();
  99. //$this->updateOvertimeFixtures();
  100. } elseif ($this->is_live == 3) {
  101. $this->checkOvertimeFixtures();
  102. } elseif ($this->is_live == 4) {
  103. $this->leagueFixtures();
  104. } else {
  105. $this->initOdds();
  106. }
  107. }
  108. public function leagueFixtures()
  109. {
  110. //体育赛事结束前几(分钟)锁盘,90分钟结束
  111. $sport_locked = Config::where('field', 'sport_locked')->first()->val ?? 1;
  112. //查询世界杯的赛事
  113. $data = SportClientService::fixtures(['league' => 1, 'season' => 2026]);
  114. //file_put_contents('leagueFixtures.json', json_encode($data));
  115. $this->updateOrCreateSport($data, $sport_locked);
  116. return true;
  117. }
  118. //到了比赛开始时间,但是状态还是未开始,检查比赛
  119. public function checkOvertimeFixtures()
  120. {
  121. //体育赛事结束前几(分钟)锁盘,90分钟结束
  122. $sport_locked = Config::where('field', 'sport_locked')->first()->val ?? 1;
  123. // 比赛开始后,状态还是未开始的数据检测
  124. $ids = SportModel::where('state', 0)->where('game_time', '<=', time())->pluck('data_id')->toArray();
  125. if ($ids) {
  126. $chunks = array_chunk($ids, 10);
  127. foreach ($chunks as $chunk) {
  128. $ids = implode('-', $chunk);
  129. $data = SportClientService::fixtures(['ids' => $ids]);
  130. $this->updateOrCreateSport($data, $sport_locked, 1);
  131. }
  132. }
  133. return true;
  134. }
  135. //进行中超过3分钟没有更新数据的赛事,检查比赛是否结束
  136. public function checkLiveFixtures()
  137. {
  138. //体育赛事结束前几(分钟)锁盘,90分钟结束
  139. $sport_locked = Config::where('field', 'sport_locked')->first()->val ?? 1;
  140. //1. 统一锁盘时间(比赛开始前1分钟)
  141. SportModel::where(['is_locked' => 0, 'is_roll' => 0])->where('game_time', '<=', time() + 90)->update(['is_locked' => 1]);
  142. //2. 比赛进行中,超过3分钟未更新的数据检测
  143. $end_time = date("Y-m-d H:i:s", time() - 180);
  144. $ids = SportModel::where('state', 1)->where('updated_at', '<=', $end_time)->pluck('data_id')->toArray();
  145. if ($ids) {
  146. $chunks = array_chunk($ids, 10);
  147. foreach ($chunks as $chunk) {
  148. $ids = implode('-', $chunk);
  149. $data = SportClientService::fixtures(['ids' => $ids]);
  150. $this->updateOrCreateSport($data, $sport_locked, 1);
  151. }
  152. }
  153. return true;
  154. }
  155. //更新进行中的赛事
  156. public function liveFixtures()
  157. {
  158. //体育赛事结束前几(分钟)锁盘,90分钟结束
  159. $sport_locked = Config::where('field', 'sport_locked')->first()->val ?? 1;
  160. $data = SportClientService::fixtures(['live' => 'all']);
  161. file_put_contents("fixturesLive.json", json_encode($data));
  162. $this->updateOrCreateSport($data, $sport_locked);
  163. return true;
  164. }
  165. private function updateOrCreateSport($data, $sport_locked, $is_check = 0) {
  166. $data = $data['response'];
  167. $tableData = [];
  168. $status = $this->short_status;
  169. foreach ($data as $item) {
  170. $home_statistics = !empty($item['statistics']) ? $item['statistics'][0]['statistics'] : '';
  171. $away_statistics = !empty($item['statistics']) ? $item['statistics'][1]['statistics'] : '';
  172. $sport_data = [
  173. 'data_id' => $item['fixture']['id'],
  174. 'home_team_id' => $item['teams']['home']['id'],
  175. 'home_team_en' => $item['teams']['home']['name'],
  176. 'home_team_logo' => $item['teams']['home']['logo'],
  177. 'guest_team_id' => $item['teams']['away']['id'],
  178. 'guest_team_en' => $item['teams']['away']['name'],
  179. 'guest_team_logo' => $item['teams']['away']['logo'],
  180. 'half_score' => "{$item['score']['halftime']['home']}-{$item['score']['halftime']['away']}",
  181. 'rbt' => $item['fixture']['timestamp'],
  182. 'score' => isset($item['goals']) ? "{$item['goals']['home']}-{$item['goals']['away']}":'-',
  183. 'extra_score' => isset($item['score']['extratime']) ? "{$item['score']['extratime']['home']}-{$item['score']['extratime']['away']}" : "",//加时赛比分
  184. 'league_id' => $item['league']['id'],
  185. 'league_en' => $item['league']['name'],
  186. 'league_data' => json_encode($item['league']),
  187. 'state' => $status[$item['fixture']['status']['short']],//比赛状态:0未开始1进行中2已完场3延期4取消
  188. 'game_time' => $item['fixture']['timestamp'],
  189. 'updated_at' => date('Y-m-d H:i:s'),
  190. 'home_statistics' => json_encode($home_statistics),
  191. 'away_statistics' => json_encode($away_statistics),
  192. 'is_send' => 0,
  193. ];
  194. $info = SportModel::where('data_id', $item['fixture']['id'])->first();
  195. $fixture_status = null;
  196. if (isset($item['fixture']['status']['long']) ) {
  197. $long = $item['fixture']['status']['long'];
  198. if (isset($this->fixture_status[$long])) {
  199. $fixture_status = json_encode($item['fixture']['status']);
  200. $sport_data['fixture_status'] = $fixture_status;
  201. }
  202. if (isset($this->long_status[$long])) {
  203. $sport_data['state'] = $this->long_status[$long];
  204. }
  205. }
  206. //提前锁盘(比赛进行时长,分钟)
  207. if (isset($item['fixture']['status']['elapsed'])) {
  208. $elapsed = $item['fixture']['status']['elapsed'];
  209. if ((int)$elapsed >= 90 - $sport_locked ) {
  210. $sport_data['is_locked'] = 1;
  211. }
  212. }
  213. $sport_data['score'] = $sport_data['score'] == '-' ? '' : $sport_data['score'];
  214. $sport_data['half_score'] = $sport_data['half_score'] == '-' ? '' : $sport_data['half_score'];
  215. $sport_data['extra_score'] = $sport_data['extra_score'] == '-' ? '' : $sport_data['extra_score'];
  216. //锁盘
  217. if (isset($item['fixture']['status']['blocked']) && $item['fixture']['status']['blocked']) {
  218. $sport_data['is_locked'] = 1;
  219. }
  220. //已结束
  221. if (isset($item['fixture']['status']['finished']) && $item['fixture']['status']['finished']) {
  222. $sport_data['state'] = 2;
  223. }
  224. //如果赛事取消、延期等,标记需要退款
  225. if ($sport_data['state'] > 2) {
  226. $sport_data['refund_status'] = 1;
  227. }
  228. if (!$info) {
  229. $sport_data['created_at'] = date('Y-m-d H:i:s');
  230. $sport_data['status'] = 1;
  231. $sport_data['home_team'] = SportTeam::getTeamName($sport_data['home_team_id']);
  232. $sport_data['guest_team'] = SportTeam::getTeamName($sport_data['guest_team_id']);
  233. $sport_data['league'] = SportLeague::getLeagueName($sport_data['league_id']);
  234. $tableData[] = $sport_data;
  235. } else {
  236. if (empty($info['league'])) {
  237. $sport_data['league'] = SportLeague::getLeagueName($sport_data['league_id']);
  238. }
  239. if (empty($info['home_team'])) {
  240. $sport_data['home_team'] = SportTeam::getTeamName($sport_data['home_team_id']);
  241. }
  242. if (empty($info['guest_team'])) {
  243. $sport_data['guest_team'] = SportTeam::getTeamName($sport_data['guest_team_id']);
  244. }
  245. SportModel::where('data_id', $item['fixture']['id'])->update($sport_data);
  246. }
  247. if ($sport_data['state'] == 2) {
  248. $data_id = $item['fixture']['id'];
  249. //比赛结束,插入比赛事件
  250. if (!empty($item['events'])) {
  251. $this->insertSportEvents($data_id, $item['events']);
  252. }
  253. //比赛结束,插入比赛统计数据
  254. $data = SportClientService::statistics(['fixture' => $data_id,'half' => 'true']);
  255. // file_put_contents("fixturesStatistics.json", json_encode($data));
  256. $statistics_data = empty($data['response']) ? [] : $data['response'];
  257. foreach($statistics_data as $item) {
  258. $team_id = $item['team']['id'];
  259. //上半场的数据
  260. if(!empty($item['statistics_1h'])) {
  261. $statistics_1h = $item['statistics_1h'];
  262. $this->insertSportStatistics($data_id, $team_id, $statistics_1h, 1);
  263. }
  264. //下半场的数据
  265. if(!empty($item['statistics_2h'])) {
  266. $statistics_2h = $item['statistics_2h'];
  267. $this->insertSportStatistics($data_id, $team_id, $statistics_2h, 2);
  268. }
  269. }
  270. }
  271. }
  272. if ($tableData) {
  273. SportModel::insert($tableData);
  274. }
  275. return true;
  276. }
  277. /**
  278. * 获取指定日期的所有赛事
  279. *
  280. * @return array
  281. */
  282. public function fixtures()
  283. {
  284. //根据配置拉取多少天的赛事信息
  285. $days = Config::where('field', 'sport_days')->first()->val ?? 1;
  286. for($i=0;$i<$days;$i++) {
  287. $date = Carbon::today()->addDay($i)->toDateString();
  288. $data = SportClientService::fixtures(['date' => $date]);
  289. $data = $data['response'];
  290. $tableData = [];
  291. $status = $this->short_status;
  292. foreach ($data as $item) {
  293. $home_statistics = !empty($item['statistics']) ? $item['statistics'][0]['statistics'] : '';
  294. $away_statistics = !empty($item['statistics']) ? $item['statistics'][1]['statistics'] : '';
  295. $sport_data = [
  296. 'data_id' => $item['fixture']['id'],
  297. 'home_team_id' => $item['teams']['home']['id'],
  298. 'home_team_en' => $item['teams']['home']['name'],
  299. 'home_team_logo' => $item['teams']['home']['logo'],
  300. 'guest_team_id' => $item['teams']['away']['id'],
  301. 'guest_team_en' => $item['teams']['away']['name'],
  302. 'guest_team_logo' => $item['teams']['away']['logo'],
  303. 'half_score' => "{$item['score']['halftime']['home']}-{$item['score']['halftime']['away']}",
  304. 'rbt' => $item['fixture']['timestamp'],
  305. 'score' => isset($item['score']['fulltime']) ? "{$item['score']['fulltime']['home']}-{$item['score']['fulltime']['away']}":'-',
  306. 'league_id' => $item['league']['id'],
  307. 'league_en' => $item['league']['name'],
  308. 'league_data' => json_encode($item['league']),
  309. // 'state' => $status[$item['fixture']['status']['short']],//比赛状态:0未开始1进行中2已完场3延期4取消
  310. 'game_time' => $item['fixture']['timestamp'],
  311. 'updated_at' => now(),
  312. 'home_statistics' => $home_statistics,
  313. 'away_statistics' => $away_statistics,
  314. 'is_send' => 0,
  315. ];
  316. $status_short = strtoupper($item['fixture']['status']['short']);
  317. if (isset($status[$status_short])) {
  318. $sport_data['state'] = $status[$status_short];
  319. }
  320. $sport_data['score'] = $sport_data['score'] == '-' ? '' : $sport_data['score'];
  321. $sport_data['half_score'] = $sport_data['half_score'] == '-' ? '' : $sport_data['half_score'];
  322. if (!SportModel::where('data_id', $item['fixture']['id'])->exists()) {
  323. $sport_data['created_at'] = now();
  324. $sport_data['home_team'] = SportTeam::getTeamName($sport_data['home_team_id']);
  325. $sport_data['guest_team'] = SportTeam::getTeamName($sport_data['guest_team_id']);
  326. $sport_data['league'] = SportLeague::getLeagueName($sport_data['league_id']);
  327. $tableData[] = $sport_data;
  328. } else {
  329. $sport_data['home_team'] = SportTeam::getTeamName($sport_data['home_team_id']);
  330. $sport_data['guest_team'] = SportTeam::getTeamName($sport_data['guest_team_id']);
  331. $sport_data['league'] = SportLeague::getLeagueName($sport_data['league_id']);
  332. SportModel::where('data_id', $item['fixture']['id'])->update($sport_data);
  333. }
  334. //更新或创建球队和联赛信息
  335. SportModel::addSportTeam($sport_data);
  336. SportModel::addSportLeague($item['league']);
  337. //比赛结束,插入比赛事件
  338. if (isset($sport_data['state']) && $sport_data['state'] == 2 ) {
  339. $data_id = $item['fixture']['id'];
  340. if (!empty($item['events'])) {
  341. $this->insertSportEvents($data_id, $item['events']);
  342. }
  343. //比赛结束,插入比赛事件
  344. $data = SportClientService::statistics(['fixture' => $data_id,'half' => 'true']);
  345. // file_put_contents("fixturesStatistics.json", json_encode($data));
  346. $statistics_data = empty($data['response']) ? [] : $data['response'];
  347. foreach($statistics_data as $item) {
  348. $team_id = $item['team']['id'];
  349. //上半场的数据
  350. if(!empty($item['statistics_1h'])) {
  351. $statistics_1h = $item['statistics_1h'];
  352. $this->insertSportStatistics($data_id, $team_id, $statistics_1h, 1);
  353. }
  354. //下半场的数据
  355. if(!empty($item['statistics_2h'])) {
  356. $statistics_2h = $item['statistics_2h'];
  357. $this->insertSportStatistics($data_id, $team_id, $statistics_2h, 2);
  358. }
  359. }
  360. }
  361. }
  362. if ($tableData) {
  363. SportModel::insert($tableData);
  364. }
  365. }
  366. return true;
  367. }
  368. public function initOdds(){
  369. $page = 1;
  370. $limit = 10;
  371. while (true) {
  372. $list = SportModel::where('odds','<>', null)->forPage($page, $limit)->get()->toArray();
  373. if (empty($list)) {
  374. break;
  375. }
  376. echo $page.PHP_EOL;
  377. foreach($list as $item) {
  378. $odds = json_decode($item['odds'], true);
  379. foreach($odds as $odd) {
  380. $odd_id = $odd['id'];
  381. $odd_name = $odd['name'];
  382. $info = DB::table('sport_odds')->where('odd_id',$odd_id)->where('odd_name_en',$odd_name)->first();
  383. if ($info && (!$info->odd_name || $odd_name != $info->odd_name_en)) {
  384. DB::table('sport_odds')->where('id', $info->id)->update([
  385. 'odd_name_en' => $odd_name,
  386. ]);
  387. } elseif (!$info) {
  388. DB::table('sport_odds')->insert([
  389. 'odd_id' => $odd_id,
  390. 'odd_name_en' => $odd_name,
  391. 'created_at' => date('Y-m-d H:i:s'),
  392. 'updated_at' => date('Y-m-d H:i:s'),
  393. ]);
  394. }
  395. }
  396. }
  397. $page++;
  398. }
  399. }
  400. //比赛开始后,超过3个小时,并且更新时间超过10分钟,还是进行中的数据,更新成已完成
  401. public function updateOvertimeFixtures()
  402. {
  403. // 比赛开始后,状态还是未开始的数据检测
  404. $end_time = date("Y-m-d H:i:s", time() - 600);
  405. SportModel::where('status', 1)->where('state', 1)->where('game_time', '<=', time() - 60*60*3)->where('updated_at', '<=', $end_time)->update(['state' =>2]);
  406. return true;
  407. }
  408. //插入比赛事件数据
  409. public function insertSportEvents($data_id, $events)
  410. {
  411. foreach($events as $event) {
  412. SportEvent::create([
  413. 'data_id' => $data_id,
  414. 'type' => $event['type'],
  415. 'time_elapsed' => $event['time']['elapsed'],
  416. 'time' => json_encode($event['time']),
  417. 'detail' => $event['detail'],
  418. 'player' => $event['player'] ? json_encode($event['player']) : $event['player'],
  419. 'team_id' => $event['team']['id'],
  420. 'comments' => $event['comments'],
  421. 'assist' => $event['assist'] ? json_encode($event['assist']) : $event['assist'],
  422. ]);
  423. }
  424. }
  425. //插入比赛统计数据
  426. public function insertSportStatistics($data_id, $team_id, $statistics, $half)
  427. {
  428. $insertData = [];
  429. foreach($statistics as $item) {
  430. if (!empty($item['type'])) {
  431. switch ($item['type']) {
  432. case 'Shots on Goal':
  433. $insertData['shots_on_goal'] = $item['value'];
  434. break;
  435. case 'Shots off Goal':
  436. $insertData['shots_off_goal'] = $item['value'];
  437. break;
  438. case 'Shots insidebox':
  439. $insertData['shots_insidebox'] = $item['value'];
  440. break;
  441. case 'Shots outsidebox':
  442. $insertData['shots_outsidebox'] = $item['value'];
  443. break;
  444. case 'Total Shots':
  445. $insertData['total_shots'] = $item['value'];
  446. break;
  447. case 'Blocked Shots':
  448. $insertData['blocked_shots'] = $item['value'];
  449. break;
  450. case 'Fouls':
  451. $insertData['fouls'] = $item['value'];
  452. break;
  453. case 'Corner Kicks':
  454. $insertData['corner_kicks'] = $item['value'];
  455. break;
  456. case 'Offsides':
  457. $insertData['offsides'] = $item['value'];
  458. break;
  459. case 'Ball Possession':
  460. $insertData['ball_possession'] = $item['value'];
  461. break;
  462. case 'Yellow Cards':
  463. $insertData['yellow_cards'] = $item['value'];
  464. break;
  465. case 'Red Cards':
  466. $insertData['red_cards'] = $item['value'];
  467. break;
  468. case 'Goalkeeper Saves':
  469. $insertData['goalkeeper_saves'] = $item['value'];
  470. break;
  471. case 'Total passes':
  472. $insertData['total_passes'] = $item['value'];
  473. break;
  474. case 'Passes accurate':
  475. $insertData['passes_accurate'] = $item['value'];
  476. break;
  477. case 'Passes %':
  478. $insertData['passes_percent'] = $item['value'];
  479. break;
  480. default:
  481. break;
  482. }
  483. }
  484. }
  485. if ($insertData) {
  486. $insertData['data_id'] = $data_id;
  487. $insertData['team_id'] = $team_id;
  488. $insertData['half'] = $half;
  489. SportStatistics::create($insertData);
  490. }
  491. }
  492. }