Files
sbnews/docs/superpowers/specs/2026-06-21-match-live-streams-design.md

524 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 赛事直播子表与多线路播放设计
## 背景
当前赛事数据只在 `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、回归测试、部署验证