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

13 KiB
Raw Permalink Blame History

赛事直播子表与多线路播放设计

背景

当前赛事数据只在 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
  • 高清 m3u8play_url_hd_m3u8
  • 标清 flvplay_url_flv
  • 高清 flvplay_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 + 60now + 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、回归测试、部署验证