13 KiB
赛事直播子表与多线路播放设计
背景
当前赛事数据只在 la_match.live_url 中保存单个第三方直播链接,存在几个限制:
- 无法为同一场赛事维护多条直播线路
- 无法保存同一线路解析出的多种播放流地址
- 无法区分人工维护的入口链接和程序抓取的真实播放地址
- 前台比赛详情页只能展示单个直播入口,无法切换播放流
本次改造需要把“赛事直播”从主表单字段升级为“赛事 -> 直播线路子表”,同时保留 la_match.live_url 作为兼容字段。
目标
- 新增赛事直播子表,一场赛事可维护多条直播线路。
- 每条线路保存 4 个播放地址:
play_url_flvplay_url_hd_flvplay_url_m3u8play_url_hd_m3u8
- 直播抓取逻辑放到
docker/crawler,按现有 Python crawler 惯例接入。 - 后台赛事列表改成“主表 + 展开子表”的直播线路管理方式。
- 前台比赛详情页展示多条直播线路,并允许用户自由切换播放流。
- 保留
la_match.live_url,自动回填为该赛事“最早新增的一条直播线路”的source_url。
非目标
- 本轮不改 PC 前台直播 UI。
- 本轮不增加直播抓取历史日志表。
- 本轮不抽象多站点通用抓取框架,只先支持当前已知直播站解析。
当前现状
数据层
- 赛事主表:
la_match - 兼容直播字段:
la_match.live_url - 当前没有直播子表
后台
admin/src/views/match/lists/index.vue编辑弹窗中直接维护live_urlserver/app/adminapi/logic/match/MatchLogic.php允许编辑live_url
前台
server/app/api/controller/MatchController.php::detail()返回单个live_urluniapp/src/pages/match_detail/match_detail.vue只渲染单个直播入口卡
调度
- Docker crawler 任务由
docker/crawler/main.py注册 action docker/crawler/config/crawler_tasks.yaml维护 Docker 内部 cronla_dev_crontab仍保留任务记录,用于后台展示和沿用“已迁移到 Docker”惯例
总体设计
方案选型
采用“单子表保存当前线路状态”的方案,不新增抓取历史表。
每条直播线路在子表中保留当前最新抓取结果;抓取失败时保留旧值,不清空历史可用地址。
设计总览
- 新增
la_match_live子表。 - 后端新增
MatchLive模型、adminapi 线路管理接口、前台详情聚合返回。 - 后台赛事列表新增展开子表,替换原单字段
live_url手工编辑。 docker/crawler新增match_live_streamPython 任务,每分钟执行一次。- 比赛详情页新增多线路展示和播放流切换,默认使用
play_url_m3u8。
数据模型设计
新增数据表
表名:la_match_live
建议字段:
idbigint unsigned,主键match_idint unsigned,关联la_match.idtitlevarchar(255),直播标题source_urlvarchar(500),指向网址play_url_flvvarchar(1000),标清 flvplay_url_hd_flvvarchar(1000),高清 flvplay_url_m3u8varchar(1000),标清 m3u8play_url_hd_m3u8varchar(1000),高清 m3u8fetch_statustinyint unsigned,抓取状态0= pending1= success2= failed
fetch_errorvarchar(500),最近一次抓取错误信息last_fetch_atint unsigned,最近抓取时间next_fetch_atint unsigned,下次抓取时间create_timeint unsignedupdate_timeint unsigneddelete_timeint 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) - 抓取状态
- 最近抓取时间
- 创建时间
- 更新时间
后台操作
子表提供:
- 新增线路
- 编辑线路
- 删除线路
人工可维护字段仅有:
titlesource_url
以下字段只读:
- 4 个播放地址
fetch_statusfetch_errorlast_fetch_atnext_fetch_atcreate_timeupdate_time
旧字段替换
从赛事编辑弹窗中移除可编辑的 live_url 输入框。
可选保留只读展示:
- 字段名:
兼容主链接(自动) - 值:
la_match.live_url
adminapi 接口
新增控制器建议:
server/app/adminapi/controller/match/MatchLiveController.php
新增接口:
GET /adminapi/match.matchLive/listsGET /adminapi/match.matchLive/detailPOST /adminapi/match.matchLive/addPOST /adminapi/match.matchLive/editPOST /adminapi/match.matchLive/delete
新增对应层:
lists/match/MatchLiveLists.phplogic/match/MatchLiveLogic.phpvalidate/match/MatchLiveValidate.php
match.match/edit 只维护赛事本身字段,不再接收人工写入的 live_url。
前台接口设计
/api/match/detail
在现有赛事详情返回结构上新增 live_list。
live_list 单项字段:
idtitlesource_urlplay_url_flvplay_url_hd_flvplay_url_m3u8play_url_hd_m3u8fetch_statuslast_fetch_atupdate_timedefault_play_urlstream_options
default_play_url
固定优先取值顺序:
play_url_m3u8play_url_hd_m3u8play_url_flvplay_url_hd_flv
当前业务上默认以 play_url_m3u8 作为主播放流。
stream_options
后端组装为可直接渲染的数组,顺序固定:
M3U8->play_url_m3u8HD M3U8->play_url_hd_m3u8FLV->play_url_flvHD 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_streamname: 赛事直播流抓取action: match_live_streamcron: "*/1 * * * *"
la_dev_crontab 记录
新增一条后台可见任务记录:
name: 赛事直播流抓取command:crawlerparams:match_live_streamexpression:* * * * *
这条记录沿用当前项目的“迁移到 Docker crawler”惯例:
- 后台可展示
- 真实执行由 Docker 调度
- PHP
crontab不直接执行抓取逻辑
抓取调度规则
选取规则
每分钟执行一次,从 la_match_live 中选择:
next_fetch_at <= now- 未删除
排序:
next_fetch_at ascid 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_flvplay_url_hd_flvplay_url_m3u8play_url_hd_m3u8fetch_status = 1fetch_error = ''last_fetch_at = nownext_fetch_at = now + 600update_time = now
抓取失败后更新:
fetch_status = 2fetch_error = 最近错误last_fetch_at = nownext_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.phpserver/app/adminapi/controller/match/MatchLiveController.phpserver/app/adminapi/lists/match/MatchLiveLists.phpserver/app/adminapi/logic/match/MatchLiveLogic.phpserver/app/adminapi/validate/match/MatchLiveValidate.phpserver/app/adminapi/logic/match/MatchLogic.phpserver/app/api/controller/MatchController.php
admin
admin/src/api/match.tsadmin/src/views/match/lists/index.vue
uniapp
uniapp/src/pages/match_detail/match_detail.vue- 如需独立播放页,再新增对应页面及路由注册
docker/crawler
docker/crawler/match_live_stream.pydocker/crawler/main.pydocker/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
上线边界
本轮改造包含:
serveradminuniappdocker/crawlerdocs/sql
本轮不包含:
- PC 前台直播线路 UI
- 抓取历史日志表
- 多站点通用解析抽象
风险点
- 第三方直播站页面结构可能变更,导致解析失败。
m3u8/flv地址多为时效链接,抓取周期与前台展示需要允许“旧值短期继续可用”。uniapp不同平台对直链播放兼容性不同,因此必须保留source_url跳转兜底。- 后台一旦放开人工编辑播放地址,容易与爬虫更新冲突,因此必须禁止手改播放地址字段。
实施顺序建议
- 建表 + 后端模型与逻辑
- 后台子表接口与 UI
- Python crawler 与 Docker 调度
- 前台
/api/match/detail聚合 - uniapp 多线路播放 UI
- SQL、回归测试、部署验证