docs: add match live streams design spec
This commit is contained in:
@@ -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、回归测试、部署验证
|
||||||
Reference in New Issue
Block a user