diff --git a/docs/sql/insert_menu_match_live.sql b/docs/sql/insert_menu_match_live.sql index 7dbe9a5..e3a7764 100644 --- a/docs/sql/insert_menu_match_live.sql +++ b/docs/sql/insert_menu_match_live.sql @@ -1,7 +1,21 @@ +UPDATE `la_system_menu` child +JOIN `la_system_menu` parent + ON parent.`type` = 'M' + AND parent.`paths` = 'match' +SET child.`pid` = parent.`id`, + child.`paths` = 'match_live', + child.`component` = 'match/live/index', + child.`selected` = '/match/lists', + child.`is_show` = 0, + child.`update_time` = UNIX_TIMESTAMP() +WHERE child.`perms` = 'match.matchLive/index' + AND child.`pid` <> parent.`id`; + INSERT INTO `la_system_menu` (`pid`, `type`, `name`, `icon`, `sort`, `perms`, `paths`, `component`, `selected`, `params`, `is_cache`, `is_show`, `is_disable`, `create_time`, `update_time`) SELECT parent.`id`, 'C', '直播线路管理', '', 0, 'match.matchLive/index', 'match_live', 'match/live/index', '/match/lists', '', 0, 0, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP() FROM `la_system_menu` parent -WHERE parent.`perms` = 'match.match/lists' +WHERE parent.`type` = 'M' + AND parent.`paths` = 'match' AND NOT EXISTS ( SELECT 1 FROM `la_system_menu` WHERE `perms` = 'match.matchLive/index' diff --git a/docs/sql/split_consumer_account_menus.sql b/docs/sql/split_consumer_account_menus.sql new file mode 100644 index 0000000..c4a1903 --- /dev/null +++ b/docs/sql/split_consumer_account_menus.sql @@ -0,0 +1,31 @@ +UPDATE `la_system_menu` +SET + `name` = '自注册账号', + `paths` = 'lists', + `component` = 'consumer/lists/index', + `params` = '{\"account_scope\":\"registered\"}', + `is_show` = 1, + `update_time` = UNIX_TIMESTAMP() +WHERE `perms` = 'user.user/lists' + AND `component` = 'consumer/lists/index'; + +INSERT INTO `la_system_menu` (`pid`, `type`, `name`, `icon`, `sort`, `perms`, `paths`, `component`, `selected`, `params`, `is_cache`, `is_show`, `is_disable`, `create_time`, `update_time`) +SELECT COALESCE((SELECT `id` FROM `la_system_menu` WHERE `paths` = 'consumer' AND `type` = 'M' LIMIT 1), 117), + 'C', '系统账号', '', 95, 'user.user/lists', 'system_lists', 'consumer/lists/index', '', '{\"account_scope\":\"system\"}', 0, 1, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP() +WHERE NOT EXISTS ( + SELECT 1 FROM `la_system_menu` + WHERE `paths` = 'system_lists' + AND `component` = 'consumer/lists/index' +); + +INSERT INTO `la_system_role_menu` (`role_id`, `menu_id`) +SELECT 1, m.`id` +FROM `la_system_menu` m +WHERE m.`paths` IN ('lists', 'system_lists') + AND m.`component` = 'consumer/lists/index' + AND NOT EXISTS ( + SELECT 1 + FROM `la_system_role_menu` rm + WHERE rm.`role_id` = 1 + AND rm.`menu_id` = m.`id` + ); diff --git a/qa/frontend/test_match_detail_live_static.py b/qa/frontend/test_match_detail_live_static.py index 5020f82..ccaa170 100644 --- a/qa/frontend/test_match_detail_live_static.py +++ b/qa/frontend/test_match_detail_live_static.py @@ -21,10 +21,11 @@ def main() -> None: assert 'v-if="liveList.length"' in detail_source, "match detail should render live_list block with v-if" assert 'v-else-if="thirdPartyLiveUrl"' in detail_source, "match detail should keep thirdPartyLiveUrl fallback with v-else-if" - assert "MatchLivePopup" in detail_source, "match detail should use MatchLivePopup" + assert 'v-if="showLiveLineSelector"' in detail_source, "match detail should render inline live line selector" assert "fetch_status" in detail_source, "match detail should reference fetch_status" assert "last_fetch_at" in detail_source, "match detail should reference last_fetch_at" assert "update_time" in detail_source, "match detail should reference update_time" + assert ':show-progress="false"' in detail_source, "match detail live video should hide time progress" assert "default_play_url" in popup_source, "popup should prefer default_play_url" assert "stream_options" in popup_source, "popup should support stream_options" assert "/pages/webview/webview?url=" in popup_source, "selected stream should route through existing webview page" @@ -39,10 +40,10 @@ def main() -> None: assert field_name in popup_source, f"popup should cover {field_name}" assert field_name in api_source, f"match live types should cover {field_name}" - assert re.search(r"const\s+defaultPlayUrl\s*=\s*normalizeUrl\(props\.line\?\.default_play_url\)", popup_source), \ - "popup should explicitly prefer props.line?.default_play_url" - assert re.search(r"defaultPlayUrl.*?resetSelected", popup_source, re.S), \ - "popup selection reset should prefer default_play_url" + assert "push(line.default_play_url)" in popup_source, \ + "popup should explicitly prefer line.default_play_url in candidate collection" + assert "selectedLineKey.value = lineOptions.value[0]?.key || ''" in popup_source, \ + "popup should reset to the first available line option" assert re.search(r"fetch_status.*last_fetch_at", popup_source, re.S) or "update_time" in popup_source, \ "popup metadata should use fetch_status and last_fetch_at/update_time" assert re.search(r"normalized === '0'.*待抓取", popup_source, re.S), \ diff --git a/qa/frontend/test_match_live_admin_static.py b/qa/frontend/test_match_live_admin_static.py index fcfaaeb..c04e6c7 100644 --- a/qa/frontend/test_match_live_admin_static.py +++ b/qa/frontend/test_match_live_admin_static.py @@ -23,7 +23,7 @@ def main() -> None: assert MENU_SQL.is_file(), f"missing menu seed file: {MENU_SQL}" menu_sql = MENU_SQL.read_text(encoding="utf-8") normalized_menu_sql = " ".join(menu_sql.split()) - first_insert_block = menu_sql.split(";", 1)[0] + ";" + first_insert_block = "INSERT INTO `la_system_menu`" + menu_sql.split("INSERT INTO `la_system_menu`", 1)[1].split(";", 1)[0] + ";" normalized_first_insert = " ".join(first_insert_block.split()) assert MATCH_LIVE_PAGE.is_file(), f"missing page file: {MATCH_LIVE_PAGE}" @@ -111,8 +111,8 @@ def main() -> None: "match.matchLive/detail", "match.matchLive/add", "match.matchLive/edit", - "match.match/lists", "match.matchLive/delete", + "/match/lists", ): assert needle in menu_sql, f"{MENU_SQL} missing permission token: {needle}" @@ -127,20 +127,28 @@ def main() -> None: assert ( "FROM `la_system_menu` parent" in menu_sql - and "parent.`perms` = 'match.match/lists'" in menu_sql - ), f"{MENU_SQL} must bind the match live menu to parent match.match/lists explicitly" + and "parent.`type` = 'M'" in menu_sql + and "parent.`paths` = 'match'" in menu_sql + ), f"{MENU_SQL} must bind the match live menu to the top-level match catalogue explicitly" assert ( "SELECT parent.`id`, 'C', '直播线路管理'" in menu_sql - ), f"{MENU_SQL} must insert the page menu only when the parent match.match/lists record exists" + ), f"{MENU_SQL} must insert the page menu only when the parent match catalogue record exists" assert ( - "WHERE parent.`perms` = 'match.match/lists' AND NOT EXISTS (" in normalized_first_insert - ), f"{MENU_SQL} first insert must combine parent binding with AND NOT EXISTS in a single WHERE predicate" + "WHERE parent.`type` = 'M' AND parent.`paths` = 'match' AND NOT EXISTS (" in normalized_first_insert + ), f"{MENU_SQL} first insert must combine top-level match catalogue binding with AND NOT EXISTS in a single WHERE predicate" assert ( - "WHERE parent.`perms` = 'match.match/lists' WHERE NOT EXISTS (" not in normalized_first_insert + "WHERE parent.`type` = 'M' AND parent.`paths` = 'match' WHERE NOT EXISTS (" not in normalized_first_insert ), f"{MENU_SQL} first insert must not contain a second standalone WHERE NOT EXISTS after the parent WHERE" assert normalized_first_insert.count("WHERE") == 2, ( f"{MENU_SQL} first insert should only have the outer WHERE plus the nested WHERE inside NOT EXISTS" ) + assert "UPDATE `la_system_menu` child" in menu_sql, f"{MENU_SQL} must repair existing live menu parent records" + assert "child.`perms` = 'match.matchLive/index'" in normalized_menu_sql, ( + f"{MENU_SQL} must target the existing match.matchLive/index menu when repairing the parent node" + ) + assert "child.`pid` <> parent.`id`" in normalized_menu_sql, ( + f"{MENU_SQL} repair statement must only move the live menu when it is attached to the wrong parent" + ) assert ( "SELECT (SELECT `id` FROM `la_system_menu` WHERE `perms` = 'match.matchLive/index' LIMIT 1)" in normalized_menu_sql ), f"{MENU_SQL} child permissions must bind to the inserted match.matchLive/index menu" diff --git a/tmp-worldcup-dev.err.log b/tmp-worldcup-dev.err.log new file mode 100644 index 0000000..e69de29 diff --git a/tmp-worldcup-dev.log b/tmp-worldcup-dev.log new file mode 100644 index 0000000..d08c4b9 --- /dev/null +++ b/tmp-worldcup-dev.log @@ -0,0 +1,15 @@ + +> uni-preset-vue@0.0.0 dev:h5 +> uni --host 127.0.0.1 --port 5173 + +请注意运行模式下,因日志输出、sourcemap 以及未压缩源码等原因,性能和包体积,均不及发行模式。 +vite是按需编译,运行时点击某个未编译页面会先编译后加载,导致显示较慢,发行后无此问题。 +编译器版本:5.07(vue3) +正在编译中... + + vite v5.2.8 dev server running at: + + ➜ Local: http://127.0.0.1:5173/ + + ready in 3111ms. + diff --git a/tmp-worldcup-route.png b/tmp-worldcup-route.png new file mode 100644 index 0000000..edc7655 Binary files /dev/null and b/tmp-worldcup-route.png differ diff --git a/tmp-worldcup-scroll-check.png b/tmp-worldcup-scroll-check.png new file mode 100644 index 0000000..edc7655 Binary files /dev/null and b/tmp-worldcup-scroll-check.png differ diff --git a/业务进度管理.md b/业务进度管理.md index 1a5e6be..d533186 100644 --- a/业务进度管理.md +++ b/业务进度管理.md @@ -13,6 +13,30 @@ ## 一、已完成事项 +### 74. 赛事直播流定时抓取恢复与 Docker 调度优化 + +- 状态:已完成 - 时间:2026-06-21 + +完成内容: +- 排查赛事直播线路新增后长期停留在待抓取,确认 `la_dev_crontab` 中的 `crawler match_live_stream` 仅用于管理态记录;服务端 `php think crontab` 对已迁移的 crawler 任务会直接跳过,真实执行入口是测试服 `sport-era-crawler` 容器内由 `docker_task_runner.py` 渲染出的 `/etc/cron.d/sport-era-crawler`。 +- 确认测试服 `sport-era-crawler` 容器已连续运行 8 天且镜像未重建,旧容器内缺少 `/app/match_live_stream.py`,也没有渲染 `match_live_stream` 的 cron 任务,导致服务器目录虽然已同步源码,但新增线路不会被定时抓取。 +- 已在测试服重建并重启 `sport-era-crawler` 容器,确认容器内 `config/crawler_tasks.yaml` 与 `/etc/cron.d/sport-era-crawler` 都已包含 `match_live_stream` 每分钟调度。 +- 继续排查发现 `docker/crawler/scripts/docker_task_runner.py` 外层已对动作加锁,而 `docker/crawler/match_live_stream.py` 内部再次抢同名 `match_live_stream.lock`,任务会把自己判定成“已在执行中”并直接跳过,表现为 `la_crawler_task_log` 连续成功但 `la_match_live` 数据不更新。 +- 优化 `docker_task_runner.py` 在子进程执行时注入 `DQD_RUNNING_UNDER_TASK_RUNNER=1`,并让 `match_live_stream.py` 在该上下文下跳过内部同名锁,保留脚本独立执行时的文件锁能力,避免双重加锁导致的假成功。 +- 已补充 `match_live_stream` 回归测试,并在测试服手动触发一轮任务,确认 `la_match_live.play_url_m3u8` 成功写入,`fetch_status=1`,已抓到播放地址的线路会按 10 分钟规则刷新。 + +涉及模块: +- `docker/crawler/match_live_stream.py` +- `docker/crawler/scripts/docker_task_runner.py` +- `docker/crawler/tests/test_match_live_stream.py` +- `docs/sql/insert_crontab_match_live_stream.sql` + +验证记录: +- 已执行 `C:\Python311\python.exe -m unittest docker.crawler.tests.test_match_live_stream.MatchLiveStreamTest.test_run_does_not_skip_due_rows_when_invoked_by_docker_task_runner`,确认修复前红灯场景已被回归测试覆盖。 +- 已执行 `C:\Python311\python.exe -m unittest docker.crawler.tests.test_match_live_stream`,10 个直播流抓取单测通过。 +- 已在测试服执行 `sudo docker compose -f docker-compose.crawler.yml build crawler && sudo docker compose -f docker-compose.crawler.yml up -d --no-deps crawler`,确认新容器已加载 `match_live_stream.py` 与对应 cron。 +- 已在测试库补齐 `la_dev_crontab` 中的 `crawler / match_live_stream` 记录,并手动执行 `python /app/scripts/docker_task_runner.py run match_live_stream ...`,确认 `la_match_live` 的 `play_url_m3u8` 已成功落库。 + ### 73. 彩票帖子 AI 分析图片先本地化再识别 - 状态:已完成 - 时间:2026-06-13 @@ -1499,3 +1523,8 @@ - 注意:2026149-2026150 期因 API 格式变更期间不可回溯(API 只返回当前期),已无法补回。 涉及模块: - `server/public/dongqiudi-crawler/lottery_community.py` + +## 四、避坑记录 + +- **Docker crawler 新增任务后,不能只同步源码不重建容器**:像 `match_live_stream.py` 这类新增动作,即使服务器目录里已经有文件,运行中的 `sport-era-crawler` 仍可能还是旧镜像;要同时检查容器内 `/app/.py`、`/app/config/crawler_tasks.yaml` 和 `/etc/cron.d/sport-era-crawler` 是否都已加载新任务,否则会出现“代码在服务器上、任务却永远不执行”的假象。 +- **Docker 外层 runner 与任务脚本内部不要重复抢同名锁**:`docker_task_runner.py` 已经按 action 加过锁,任务脚本内部如果再对同名 action 抢文件锁,会出现“任务日志连续成功、数据库完全没更新”的假成功症状;后续新增 Docker crawler 任务时,要先确认锁只保留一层,或显式区分 runner 锁和脚本独立执行锁。