From 80e0e6a8a63d05cd78dfb6d64eac3b05182de8ad Mon Sep 17 00:00:00 2001 From: hajimi Date: Sun, 21 Jun 2026 11:52:02 +0800 Subject: [PATCH] feat: wire match live backend api --- qa/backend/test_match_live_backend_static.php | 42 ++++++++ .../controller/match/MatchLiveController.php | 53 ++++++++++ .../adminapi/lists/match/MatchLiveLists.php | 63 ++++++++++++ .../adminapi/logic/match/MatchLiveLogic.php | 97 +++++++++++++++++++ .../app/adminapi/logic/match/MatchLogic.php | 1 - .../validate/match/MatchLiveValidate.php | 69 +++++++++++++ server/app/api/controller/MatchController.php | 5 + 7 files changed, 329 insertions(+), 1 deletion(-) create mode 100644 server/app/adminapi/controller/match/MatchLiveController.php create mode 100644 server/app/adminapi/lists/match/MatchLiveLists.php create mode 100644 server/app/adminapi/logic/match/MatchLiveLogic.php create mode 100644 server/app/adminapi/validate/match/MatchLiveValidate.php diff --git a/qa/backend/test_match_live_backend_static.php b/qa/backend/test_match_live_backend_static.php index 6dd9c41..d88bff8 100644 --- a/qa/backend/test_match_live_backend_static.php +++ b/qa/backend/test_match_live_backend_static.php @@ -2,12 +2,27 @@ $root = dirname(__DIR__, 2); $servicePath = $root . '/server/app/common/service/match/MatchLiveService.php'; +$apiControllerPath = $root . '/server/app/api/controller/MatchController.php'; +$adminLogicPath = $root . '/server/app/adminapi/logic/match/MatchLogic.php'; +$backendChecks = [ + $root . '/server/app/adminapi/controller/match/MatchLiveController.php', + $root . '/server/app/adminapi/lists/match/MatchLiveLists.php', + $root . '/server/app/adminapi/logic/match/MatchLiveLogic.php', + $root . '/server/app/adminapi/validate/match/MatchLiveValidate.php', +]; if (!is_file($servicePath)) { fwrite(STDERR, "missing service file\n"); exit(1); } +foreach ($backendChecks as $path) { + if (!is_file($path)) { + fwrite(STDERR, "missing backend file: {$path}\n"); + exit(1); + } +} + $service = file_get_contents($servicePath); $requiredSnippets = [ 'class MatchLiveService', @@ -36,6 +51,33 @@ if ($whereNullCount < 2) { exit(1); } +if (!is_file($apiControllerPath)) { + fwrite(STDERR, "missing api controller file\n"); + exit(1); +} + +$apiController = file_get_contents($apiControllerPath); +foreach ([ + "'live_list'", + 'MatchLiveService::getLiveListForApi', +] as $needle) { + if (strpos($apiController, $needle) === false) { + fwrite(STDERR, "missing api controller token: {$needle}\n"); + exit(1); + } +} + +if (!is_file($adminLogicPath)) { + fwrite(STDERR, "missing admin logic file\n"); + exit(1); +} + +$adminLogic = file_get_contents($adminLogicPath); +if (strpos($adminLogic, "'live_url'") !== false) { + fwrite(STDERR, "match admin logic still manually allows live_url\n"); + exit(1); +} + $orderedOptionSnippets = [ "['label' => 'M3U8', 'field' => 'play_url_m3u8']", "['label' => 'HD M3U8', 'field' => 'play_url_hd_m3u8']", diff --git a/server/app/adminapi/controller/match/MatchLiveController.php b/server/app/adminapi/controller/match/MatchLiveController.php new file mode 100644 index 0000000..ff60eed --- /dev/null +++ b/server/app/adminapi/controller/match/MatchLiveController.php @@ -0,0 +1,53 @@ +dataLists(new MatchLiveLists()); + } + + public function detail() + { + $params = (new MatchLiveValidate())->goCheck('detail'); + $result = MatchLiveLogic::detail($params); + return $this->data($result); + } + + public function add() + { + $params = (new MatchLiveValidate())->post()->goCheck('add'); + $result = MatchLiveLogic::add($params); + if (true === $result) { + return $this->success('添加成功', [], 1, 1); + } + return $this->fail(MatchLiveLogic::getError()); + } + + public function edit() + { + $params = (new MatchLiveValidate())->post()->goCheck('edit'); + $result = MatchLiveLogic::edit($params); + if (true === $result) { + return $this->success('编辑成功', [], 1, 1); + } + return $this->fail(MatchLiveLogic::getError()); + } + + public function delete() + { + $params = (new MatchLiveValidate())->post()->goCheck('delete'); + $result = MatchLiveLogic::delete($params); + if (true === $result) { + return $this->success('删除成功', [], 1, 1); + } + return $this->fail(MatchLiveLogic::getError()); + } +} diff --git a/server/app/adminapi/lists/match/MatchLiveLists.php b/server/app/adminapi/lists/match/MatchLiveLists.php new file mode 100644 index 0000000..42bf862 --- /dev/null +++ b/server/app/adminapi/lists/match/MatchLiveLists.php @@ -0,0 +1,63 @@ + ['match_id', 'fetch_status'], + ]; + } + + public function lists(): array + { + $lists = $this->buildQuery() + ->field('id,match_id,title,source_url,play_url_m3u8,play_url_hd_m3u8,play_url_flv,play_url_hd_flv,fetch_status,fetch_error,last_fetch_at,next_fetch_at,create_time,update_time') + ->limit($this->limitOffset, $this->limitLength) + ->order('create_time', 'asc') + ->order('id', 'asc') + ->select() + ->toArray(); + + foreach ($lists as &$item) { + $item['fetch_status_text'] = self::formatFetchStatus((int) ($item['fetch_status'] ?? 0)); + } + unset($item); + + return $lists; + } + + public function count(): int + { + return $this->buildQuery()->count(); + } + + public static function formatFetchStatus(int $status): string + { + return match ($status) { + 0 => '待抓取', + 1 => '抓取成功', + 2 => '抓取失败', + default => '未知', + }; + } + + protected function buildQuery() + { + $query = MatchLive::whereNull('delete_time') + ->where($this->searchWhere); + + $matchId = (int) ($this->params['match_id'] ?? 0); + if ($matchId <= 0) { + $query->where('match_id', 0); + } + + return $query; + } +} diff --git a/server/app/adminapi/logic/match/MatchLiveLogic.php b/server/app/adminapi/logic/match/MatchLiveLogic.php new file mode 100644 index 0000000..123ee51 --- /dev/null +++ b/server/app/adminapi/logic/match/MatchLiveLogic.php @@ -0,0 +1,97 @@ +toArray(); + } + + public static function add(array $params): bool + { + try { + $now = time(); + MatchLive::create([ + 'match_id' => (int) $params['match_id'], + 'title' => trim((string) $params['title']), + 'source_url' => trim((string) $params['source_url']), + 'fetch_status' => 0, + 'fetch_error' => '', + 'last_fetch_at' => 0, + 'next_fetch_at' => $now, + 'create_time' => $now, + 'update_time' => $now, + ]); + + MatchLiveService::syncLegacyLiveUrl((int) $params['match_id']); + return true; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } + + public static function edit(array $params): bool + { + try { + $live = MatchLive::findOrEmpty($params['id']); + if ($live->isEmpty()) { + self::setError('直播线路不存在'); + return false; + } + + $oldMatchId = (int) $live->match_id; + $newMatchId = (int) $params['match_id']; + $now = time(); + + $live->save([ + 'match_id' => $newMatchId, + 'title' => trim((string) $params['title']), + 'source_url' => trim((string) $params['source_url']), + 'play_url_m3u8' => '', + 'play_url_hd_m3u8' => '', + 'play_url_flv' => '', + 'play_url_hd_flv' => '', + 'fetch_status' => 0, + 'fetch_error' => '', + 'last_fetch_at' => 0, + 'next_fetch_at' => $now, + 'update_time' => $now, + ]); + + if ($oldMatchId > 0 && $oldMatchId !== $newMatchId) { + MatchLiveService::syncLegacyLiveUrl($oldMatchId); + } + MatchLiveService::syncLegacyLiveUrl($newMatchId); + return true; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } + + public static function delete(array $params): bool + { + try { + $live = MatchLive::findOrEmpty($params['id']); + if ($live->isEmpty()) { + self::setError('直播线路不存在'); + return false; + } + + $matchId = (int) $live->match_id; + $live->delete(); + MatchLiveService::syncLegacyLiveUrl($matchId); + return true; + } catch (\Exception $e) { + self::setError($e->getMessage()); + return false; + } + } +} diff --git a/server/app/adminapi/logic/match/MatchLogic.php b/server/app/adminapi/logic/match/MatchLogic.php index c898fa6..716e31a 100644 --- a/server/app/adminapi/logic/match/MatchLogic.php +++ b/server/app/adminapi/logic/match/MatchLogic.php @@ -24,7 +24,6 @@ class MatchLogic extends BaseLogic 'league_name', 'round_name', 'stage', - 'live_url', 'home_team', 'home_icon', 'home_score', diff --git a/server/app/adminapi/validate/match/MatchLiveValidate.php b/server/app/adminapi/validate/match/MatchLiveValidate.php new file mode 100644 index 0000000..7965c70 --- /dev/null +++ b/server/app/adminapi/validate/match/MatchLiveValidate.php @@ -0,0 +1,69 @@ + 'require|integer|gt:0|checkLive', + 'match_id' => 'require|integer|gt:0|checkMatch', + 'title' => 'require|length:1,255', + 'source_url' => 'require|max:500', + ]; + + protected $message = [ + 'id.require' => '直播线路id不能为空', + 'id.integer' => '直播线路id须为整数', + 'id.gt' => '直播线路id须大于0', + 'match_id.require' => '赛事id不能为空', + 'match_id.integer' => '赛事id须为整数', + 'match_id.gt' => '赛事id须大于0', + 'title.require' => '直播标题不能为空', + 'title.length' => '直播标题长度须在1-255位字符', + 'source_url.require' => '源地址不能为空', + 'source_url.max' => '源地址长度不能超过500位字符', + ]; + + public function sceneDetail() + { + return $this->only(['id']); + } + + public function sceneAdd() + { + return $this->remove('id', 'require|integer|gt:0|checkLive') + ->only(['match_id', 'title', 'source_url']); + } + + public function sceneEdit() + { + return $this->only(['id', 'match_id', 'title', 'source_url']); + } + + public function sceneDelete() + { + return $this->only(['id']); + } + + public function checkLive($value) + { + $live = MatchLive::findOrEmpty($value); + if ($live->isEmpty()) { + return '直播线路不存在'; + } + return true; + } + + public function checkMatch($value) + { + $match = MatchEvent::findOrEmpty($value); + if ($match->isEmpty()) { + return '赛事不存在'; + } + return true; + } +} diff --git a/server/app/api/controller/MatchController.php b/server/app/api/controller/MatchController.php index d32b792..e0a44fc 100644 --- a/server/app/api/controller/MatchController.php +++ b/server/app/api/controller/MatchController.php @@ -13,6 +13,7 @@ use app\common\model\match\MatchLineup; 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 { @@ -157,6 +158,10 @@ class MatchController extends BaseApiController 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; }