From 5a8585d0275d3956d990e0e80500a471a94c6c6f Mon Sep 17 00:00:00 2001 From: hajimi Date: Sun, 21 Jun 2026 10:50:10 +0800 Subject: [PATCH] docs: add match live streams design spec --- .../2026-06-21-match-live-streams-design.md | 523 ++++++++++++++++++ 1 file changed, 523 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-21-match-live-streams-design.md diff --git a/docs/superpowers/specs/2026-06-21-match-live-streams-design.md b/docs/superpowers/specs/2026-06-21-match-live-streams-design.md new file mode 100644 index 0000000..268eb65 --- /dev/null +++ b/docs/superpowers/specs/2026-06-21-match-live-streams-design.md @@ -0,0 +1,523 @@ +# 赛事直播子表与多线路播放设计 + +## 背景 + +当前赛事数据只在 `la_match.live_url` 中保存单个第三方直播链接,存在几个限制: + +- 无法为同一场赛事维护多条直播线路 +- 无法保存同一线路解析出的多种播放流地址 +- 无法区分人工维护的入口链接和程序抓取的真实播放地址 +- 前台比赛详情页只能展示单个直播入口,无法切换播放流 + +本次改造需要把“赛事直播”从主表单字段升级为“赛事 -> 直播线路子表”,同时保留 `la_match.live_url` 作为兼容字段。 + +## 目标 + +1. 新增赛事直播子表,一场赛事可维护多条直播线路。 +2. 每条线路保存 4 个播放地址: + - `play_url_flv` + - `play_url_hd_flv` + - `play_url_m3u8` + - `play_url_hd_m3u8` +3. 直播抓取逻辑放到 `docker/crawler`,按现有 Python crawler 惯例接入。 +4. 后台赛事列表改成“主表 + 展开子表”的直播线路管理方式。 +5. 前台比赛详情页展示多条直播线路,并允许用户自由切换播放流。 +6. 保留 `la_match.live_url`,自动回填为该赛事“最早新增的一条直播线路”的 `source_url`。 + +## 非目标 + +- 本轮不改 PC 前台直播 UI。 +- 本轮不增加直播抓取历史日志表。 +- 本轮不抽象多站点通用抓取框架,只先支持当前已知直播站解析。 + +## 当前现状 + +### 数据层 + +- 赛事主表:`la_match` +- 兼容直播字段:`la_match.live_url` +- 当前没有直播子表 + +### 后台 + +- `admin/src/views/match/lists/index.vue` 编辑弹窗中直接维护 `live_url` +- `server/app/adminapi/logic/match/MatchLogic.php` 允许编辑 `live_url` + +### 前台 + +- `server/app/api/controller/MatchController.php::detail()` 返回单个 `live_url` +- `uniapp/src/pages/match_detail/match_detail.vue` 只渲染单个直播入口卡 + +### 调度 + +- Docker crawler 任务由 `docker/crawler/main.py` 注册 action +- `docker/crawler/config/crawler_tasks.yaml` 维护 Docker 内部 cron +- `la_dev_crontab` 仍保留任务记录,用于后台展示和沿用“已迁移到 Docker”惯例 + +## 总体设计 + +### 方案选型 + +采用“单子表保存当前线路状态”的方案,不新增抓取历史表。 + +每条直播线路在子表中保留当前最新抓取结果;抓取失败时保留旧值,不清空历史可用地址。 + +### 设计总览 + +1. 新增 `la_match_live` 子表。 +2. 后端新增 `MatchLive` 模型、adminapi 线路管理接口、前台详情聚合返回。 +3. 后台赛事列表新增展开子表,替换原单字段 `live_url` 手工编辑。 +4. `docker/crawler` 新增 `match_live_stream` Python 任务,每分钟执行一次。 +5. 比赛详情页新增多线路展示和播放流切换,默认使用 `play_url_m3u8`。 + +## 数据模型设计 + +### 新增数据表 + +表名:`la_match_live` + +建议字段: + +- `id` bigint unsigned,主键 +- `match_id` int unsigned,关联 `la_match.id` +- `title` varchar(255),直播标题 +- `source_url` varchar(500),指向网址 +- `play_url_flv` varchar(1000),标清 flv +- `play_url_hd_flv` varchar(1000),高清 flv +- `play_url_m3u8` varchar(1000),标清 m3u8 +- `play_url_hd_m3u8` varchar(1000),高清 m3u8 +- `fetch_status` tinyint unsigned,抓取状态 + - `0` = pending + - `1` = success + - `2` = failed +- `fetch_error` varchar(500),最近一次抓取错误信息 +- `last_fetch_at` int unsigned,最近抓取时间 +- `next_fetch_at` int unsigned,下次抓取时间 +- `create_time` int unsigned +- `update_time` int unsigned +- `delete_time` int unsigned nullable + +### 索引与约束 + +- 主键:`id` +- 唯一约束:`uk_match_source_url (match_id, source_url)` +- 普通索引: + - `idx_match_id (match_id)` + - `idx_next_fetch_at (next_fetch_at)` + - `idx_fetch_status (fetch_status)` + - `idx_match_create_time (match_id, create_time)` + +### 模型约定 + +- 新增模型:`server/app/common/model/match/MatchLive.php` +- 继续沿用 `BaseModel` +- 使用 `create_time / update_time / delete_time` 与现有 `la_match` 保持一致 + +## 兼容规则 + +### `la_match.live_url` + +保留 `la_match.live_url`,但不再允许后台人工直接维护。 + +自动回填规则: + +- 若赛事存在直播线路,则回填“该赛事最早新增的一条直播线路”的 `source_url` +- 若赛事不存在直播线路,则将 `la_match.live_url` 置空 + +触发时机: + +- 新增线路后 +- 编辑线路 `source_url` 后 +- 删除线路后 +- 恢复已删除线路后(若后续有恢复能力) + +### 前台兼容 + +`/api/match/detail` 继续返回: + +- `live_url`:旧兼容字段 +- `live_list`:新多线路列表 + +旧页面仍可只使用 `live_url`,新页面优先使用 `live_list`。 + +## 后台设计 + +### 页面交互 + +页面:`admin/src/views/match/lists/index.vue` + +改造为: + +- 赛事主表保留现有字段 +- 新增展开列 +- 展开后展示当前赛事的直播线路子表 + +子表字段: + +- 直播标题 +- 指向网址 +- 主播放流(`play_url_m3u8`) +- 高清 m3u8(`play_url_hd_m3u8`) +- 标清 flv(`play_url_flv`) +- 高清 flv(`play_url_hd_flv`) +- 抓取状态 +- 最近抓取时间 +- 创建时间 +- 更新时间 + +### 后台操作 + +子表提供: + +- 新增线路 +- 编辑线路 +- 删除线路 + +人工可维护字段仅有: + +- `title` +- `source_url` + +以下字段只读: + +- 4 个播放地址 +- `fetch_status` +- `fetch_error` +- `last_fetch_at` +- `next_fetch_at` +- `create_time` +- `update_time` + +### 旧字段替换 + +从赛事编辑弹窗中移除可编辑的 `live_url` 输入框。 + +可选保留只读展示: + +- 字段名:`兼容主链接(自动)` +- 值:`la_match.live_url` + +### adminapi 接口 + +新增控制器建议: + +- `server/app/adminapi/controller/match/MatchLiveController.php` + +新增接口: + +- `GET /adminapi/match.matchLive/lists` +- `GET /adminapi/match.matchLive/detail` +- `POST /adminapi/match.matchLive/add` +- `POST /adminapi/match.matchLive/edit` +- `POST /adminapi/match.matchLive/delete` + +新增对应层: + +- `lists/match/MatchLiveLists.php` +- `logic/match/MatchLiveLogic.php` +- `validate/match/MatchLiveValidate.php` + +`match.match/edit` 只维护赛事本身字段,不再接收人工写入的 `live_url`。 + +## 前台接口设计 + +### `/api/match/detail` + +在现有赛事详情返回结构上新增 `live_list`。 + +`live_list` 单项字段: + +- `id` +- `title` +- `source_url` +- `play_url_flv` +- `play_url_hd_flv` +- `play_url_m3u8` +- `play_url_hd_m3u8` +- `fetch_status` +- `last_fetch_at` +- `update_time` +- `default_play_url` +- `stream_options` + +#### `default_play_url` + +固定优先取值顺序: + +1. `play_url_m3u8` +2. `play_url_hd_m3u8` +3. `play_url_flv` +4. `play_url_hd_flv` + +当前业务上默认以 `play_url_m3u8` 作为主播放流。 + +#### `stream_options` + +后端组装为可直接渲染的数组,顺序固定: + +1. `M3U8` -> `play_url_m3u8` +2. `HD M3U8` -> `play_url_hd_m3u8` +3. `FLV` -> `play_url_flv` +4. `HD FLV` -> `play_url_hd_flv` + +空值不返回。 + +## uniapp 页面设计 + +页面:`uniapp/src/pages/match_detail/match_detail.vue` + +### 直播区域 + +将当前单个“直播详情”入口卡改造成“直播线路列表”。 + +每条线路展示: + +- 直播标题 +- 最近更新时间 +- 抓取状态 +- 默认播放流标识 + +### 播放行为 + +用户点击某条线路后: + +- 进入独立播放页或播放器弹层 +- 默认使用 `default_play_url` +- 播放流切换区展示 `stream_options` +- 用户可自由切换 4 条播放流 + +若某个流为空,则不展示该流的切换按钮。 + +### 兜底行为 + +- 若 `live_list` 不为空,优先渲染 `live_list` +- 若 `live_list` 为空但 `live_url` 有值,则保留旧入口卡 +- 若两者都为空,则不显示直播区域 + +### 打开源网页 + +播放器区域保留“打开源网页”按钮,跳转 `source_url`,作为播放器兼容性兜底。 + +## Docker / Python 抓取设计 + +### 任务入口 + +新增文件: + +- `docker/crawler/match_live_stream.py` + +在 `docker/crawler/main.py` 注册 action: + +- `match_live_stream` + +### Docker 调度 + +在 `docker/crawler/config/crawler_tasks.yaml` 新增任务: + +- `key: match_live_stream` +- `name: 赛事直播流抓取` +- `action: match_live_stream` +- `cron: "*/1 * * * *"` + +### `la_dev_crontab` 记录 + +新增一条后台可见任务记录: + +- `name`: 赛事直播流抓取 +- `command`: `crawler` +- `params`: `match_live_stream` +- `expression`: `* * * * *` + +这条记录沿用当前项目的“迁移到 Docker crawler”惯例: + +- 后台可展示 +- 真实执行由 Docker 调度 +- PHP `crontab` 不直接执行抓取逻辑 + +## 抓取调度规则 + +### 选取规则 + +每分钟执行一次,从 `la_match_live` 中选择: + +- `next_fetch_at <= now` +- 未删除 + +排序: + +- `next_fetch_at asc` +- `id asc` + +每轮限制 `20-50` 条,首版建议 `30` 条。 + +### 新线路 / 未完整抓取线路 + +判定为“未完整抓取”: + +- 4 个播放地址中任意一个为空,或至少主流地址 `play_url_m3u8` 为空 + +行为: + +- 新增或编辑 `source_url` 后立即置为 `fetch_status = 0` +- `next_fetch_at = now` +- 若抓取失败,`next_fetch_at = now + 60` + +### 已抓到地址的线路 + +若线路已有可用播放地址: + +- 抓取成功:`next_fetch_at = now + 600` +- 抓取失败:保留旧值,`next_fetch_at = now + 600` + +### 解析来源 + +抓取入口为 `source_url`。 + +首版仅支持当前确认的直播站页面解析逻辑,不抽象成复杂通用框架。 + +### 播放地址回填 + +抓取成功后更新: + +- `play_url_flv` +- `play_url_hd_flv` +- `play_url_m3u8` +- `play_url_hd_m3u8` +- `fetch_status = 1` +- `fetch_error = ''` +- `last_fetch_at = now` +- `next_fetch_at = now + 600` +- `update_time = now` + +抓取失败后更新: + +- `fetch_status = 2` +- `fetch_error = 最近错误` +- `last_fetch_at = now` +- `next_fetch_at = now + 60` 或 `now + 600`(视线路是否已有可用地址) +- `update_time = now` + +## 稳定性设计 + +### 锁 + +`match_live_stream` action 增加跨进程锁,避免 Docker cron 重入。 + +### 超时 + +- 单条线路抓取需要超时 +- 整个 action 在 `docker/crawler/main.py` 中配置 action 级别超时 + +### 失败保护 + +抓取失败时不得清空已有播放地址,避免前台线路因为一次失败整体不可用。 + +## 错误处理 + +- 页面结构变更导致解析不到 4 个地址时: + - 新线路维持 `pending/failed` + - 老线路保留旧地址 +- 删除直播线路后,立即重新计算 `la_match.live_url` +- 后台人工编辑 `source_url` 后,播放地址字段只由 Python 任务刷新 + +## 代码改造范围 + +### server + +- `server/app/common/model/match/MatchLive.php` +- `server/app/adminapi/controller/match/MatchLiveController.php` +- `server/app/adminapi/lists/match/MatchLiveLists.php` +- `server/app/adminapi/logic/match/MatchLiveLogic.php` +- `server/app/adminapi/validate/match/MatchLiveValidate.php` +- `server/app/adminapi/logic/match/MatchLogic.php` +- `server/app/api/controller/MatchController.php` + +### admin + +- `admin/src/api/match.ts` +- `admin/src/views/match/lists/index.vue` + +### uniapp + +- `uniapp/src/pages/match_detail/match_detail.vue` +- 如需独立播放页,再新增对应页面及路由注册 + +### docker/crawler + +- `docker/crawler/match_live_stream.py` +- `docker/crawler/main.py` +- `docker/crawler/config/crawler_tasks.yaml` +- 对应测试文件 + +### docs/sql + +- 新建 `create_match_live_table.sql` +- 新建 `insert_crontab_match_live_stream.sql` +- 如需停用旧逻辑,再补停用 SQL + +## 验证方案 + +### 后端 + +- `php -l` 覆盖新增/修改的 PHP 文件 +- 静态/最小回归脚本覆盖: + - 直播线路新增、编辑、删除 + - `la_match.live_url` 自动回填 + - `/api/match/detail` 返回 `live_list` + +### Python crawler + +单测覆盖: + +- 目标站解析出 4 个播放地址 +- 失败时保留旧地址 +- 新线路 1 分钟重试 +- 已抓地址线路 10 分钟更新 +- 互斥锁防重入 + +### 管理后台 + +- 展开子表展示 +- 子表新增、编辑、删除 +- 播放地址只读展示 +- `npm run build` + +### uniapp + +- 比赛详情页展示多条直播线路 +- 默认使用 `play_url_m3u8` +- 允许手动切换流 +- 空流不展示切换按钮 +- `npm run build:h5` + +## 上线边界 + +本轮改造包含: + +- `server` +- `admin` +- `uniapp` +- `docker/crawler` +- `docs/sql` + +本轮不包含: + +- PC 前台直播线路 UI +- 抓取历史日志表 +- 多站点通用解析抽象 + +## 风险点 + +1. 第三方直播站页面结构可能变更,导致解析失败。 +2. `m3u8` / `flv` 地址多为时效链接,抓取周期与前台展示需要允许“旧值短期继续可用”。 +3. `uniapp` 不同平台对直链播放兼容性不同,因此必须保留 `source_url` 跳转兜底。 +4. 后台一旦放开人工编辑播放地址,容易与爬虫更新冲突,因此必须禁止手改播放地址字段。 + +## 实施顺序建议 + +1. 建表 + 后端模型与逻辑 +2. 后台子表接口与 UI +3. Python crawler 与 Docker 调度 +4. 前台 `/api/match/detail` 聚合 +5. uniapp 多线路播放 UI +6. SQL、回归测试、部署验证