Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b2e3197be | ||
|
|
c01bd53ea1 | ||
|
|
98d2a23589 | ||
|
|
33687e31b7 | ||
|
|
af8941e880 | ||
|
|
8ebe30a077 | ||
|
|
89d41de3dd | ||
|
|
42d243971e | ||
|
|
3a5d571e53 | ||
|
|
c9ac46faa7 | ||
|
|
3b61f0ba38 | ||
|
|
84a5fb43f5 | ||
|
|
ad597d0a79 | ||
|
|
76ef84aa8f | ||
|
|
b584a8f98e | ||
|
|
34ff61d1ee | ||
|
|
3362e1e072 | ||
|
|
66fd64ef0f | ||
|
|
6d2302875b | ||
|
|
299d508d7c | ||
|
|
f641276b9d | ||
|
|
1954948445 | ||
|
|
074aa7ab3e | ||
|
|
ffa60e6b26 | ||
|
|
9f85b559b0 | ||
|
|
354b100558 | ||
|
|
675246cde2 | ||
|
|
6081254d63 | ||
|
|
2de27db0e6 | ||
|
|
5a888a730e | ||
|
|
311c6c6224 | ||
|
|
204cb361c6 | ||
|
|
b1539d9baf | ||
|
|
6229b98076 | ||
|
|
33664adbe0 | ||
|
|
8c358dc376 | ||
|
|
bbb6e9ab69 | ||
|
|
a88764162d | ||
|
|
affb0a4002 | ||
|
|
7c260b1f99 | ||
|
|
61310de5a0 | ||
|
|
b8203e6168 | ||
|
|
a281b5fa8e | ||
|
|
0d985506ba | ||
|
|
2ec3b2dab5 | ||
|
|
27334d8589 | ||
|
|
51718e11f3 | ||
|
|
ee9a5a3d03 | ||
|
|
9994c71374 | ||
|
|
166740a74c | ||
|
|
f6d967869d | ||
|
|
980b019809 | ||
|
|
bb1a2ebb9e | ||
|
|
5435fe3686 | ||
|
|
a51ed4bb95 | ||
|
|
f654081e8f | ||
|
|
6722f6f7cc | ||
|
|
875a770c5a | ||
|
|
229b70c1be | ||
|
|
75a529be35 | ||
|
|
08c1affb14 | ||
|
|
4ab24a9ae8 | ||
|
|
173871cf6a | ||
|
|
a8a678a5f6 | ||
|
|
c59b7ade29 | ||
|
|
5f2e2e6c00 | ||
|
|
750ef91e29 | ||
|
|
a9281f27c7 | ||
|
|
ecef509037 | ||
|
|
11b06be9b1 | ||
|
|
bc9aef5a2f | ||
|
|
7ebd563134 | ||
|
|
cd9530686c | ||
|
|
80e0e6a8a6 | ||
|
|
8d528ba5fe | ||
|
|
b133fd36ee | ||
|
|
dbdb418e15 | ||
|
|
2496050ee7 | ||
|
|
ee4f2504bd | ||
|
|
5a8585d027 | ||
|
|
20f0b4b111 | ||
|
|
e501393b66 | ||
|
|
127475e539 | ||
|
|
ed1733e49d | ||
|
|
d246ab44e2 | ||
|
|
6b3bbab5ce | ||
|
|
93474019a4 | ||
|
|
50f0b6aff5 | ||
|
|
186521392d | ||
|
|
c3385b0e04 | ||
|
|
16b8d0365a | ||
|
|
0d656e8076 | ||
|
|
cd44cd6e47 | ||
|
|
7381097582 | ||
|
|
8dacaea803 | ||
|
|
30d768a3e6 | ||
|
|
4509279ac7 |
@@ -28,3 +28,7 @@ bin-release/
|
||||
**/.hbuilderx/
|
||||
**/unpackage/
|
||||
qa/
|
||||
.code-graph/
|
||||
./vscode/
|
||||
.tmp*
|
||||
.codex/
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
[ 9545ms] [WARNING] Manifest: property 'start_url' ignored, should be same origin as document. @ https://git.zones.sbnews.net/assets/site-manifest.json:0
|
||||
@@ -0,0 +1 @@
|
||||
[ 11298ms] [WARNING] Manifest: property 'start_url' ignored, should be same origin as document. @ https://git.zones.sbnews.net/assets/site-manifest.json:0
|
||||
@@ -0,0 +1,75 @@
|
||||
- generic [active] [ref=e1]:
|
||||
- generic [ref=e2]:
|
||||
- navigation "导航栏" [ref=e3]:
|
||||
- generic [ref=e4]:
|
||||
- link "首页" [ref=e5] [cursor=pointer]:
|
||||
- /url: /
|
||||
- link "探索" [ref=e7] [cursor=pointer]:
|
||||
- /url: /explore/repos
|
||||
- link "帮助" [ref=e8] [cursor=pointer]:
|
||||
- /url: https://docs.gitea.com
|
||||
- generic [ref=e9]:
|
||||
- link "注册" [ref=e10] [cursor=pointer]:
|
||||
- /url: /user/sign_up
|
||||
- link "登录" [ref=e14] [cursor=pointer]:
|
||||
- /url: /user/login
|
||||
- main "首页" [ref=e18]:
|
||||
- generic [ref=e20]:
|
||||
- img "Logo" [ref=e21]
|
||||
- generic [ref=e22]:
|
||||
- heading "HAHA" [level=1] [ref=e23]
|
||||
- heading "一款极易搭建的自托管 Git 服务" [level=2] [ref=e24]
|
||||
- generic [ref=e25]:
|
||||
- generic [ref=e26]:
|
||||
- heading "易安装" [level=1] [ref=e27]
|
||||
- paragraph [ref=e30]:
|
||||
- text: 直接
|
||||
- link "运行适合您平台的二进制文件" [ref=e31] [cursor=pointer]:
|
||||
- /url: https://docs.gitea.com/installation/install-from-binary
|
||||
- text: ,通过
|
||||
- link "Docker" [ref=e32] [cursor=pointer]:
|
||||
- /url: https://github.com/go-gitea/gitea/tree/master/docker
|
||||
- text: 部署,或使用
|
||||
- link "软件包" [ref=e33] [cursor=pointer]:
|
||||
- /url: https://docs.gitea.com/installation/install-from-package
|
||||
- text: 安装。
|
||||
- generic [ref=e34]:
|
||||
- heading "跨平台" [level=1] [ref=e35]
|
||||
- paragraph [ref=e38]:
|
||||
- text: 任何
|
||||
- link "Go 语言" [ref=e39] [cursor=pointer]:
|
||||
- /url: https://go.dev/
|
||||
- text: 支持的平台都可以运行 Gitea,包括 Windows、Mac、Linux 以及 ARM。挑一个您喜欢的就行!
|
||||
- generic [ref=e40]:
|
||||
- generic [ref=e41]:
|
||||
- heading "轻量级" [level=1] [ref=e42]
|
||||
- paragraph [ref=e45]: 一个廉价的树莓派就足以满足 Gitea 的最低系统硬件要求。最大程度节省您的服务器资源!
|
||||
- generic [ref=e46]:
|
||||
- heading "开源化" [level=1] [ref=e47]
|
||||
- paragraph [ref=e50]:
|
||||
- text: 所有代码都开源在
|
||||
- link "gitea.dev" [ref=e51] [cursor=pointer]:
|
||||
- /url: https://gitea.dev
|
||||
- text: ,还等什么?赶快加入我们来
|
||||
- link "共同发展" [ref=e52] [cursor=pointer]:
|
||||
- /url: https://github.com/go-gitea/gitea
|
||||
- text: 这个伟大的项目,成为贡献者吧!
|
||||
- group "页脚" [ref=e53]:
|
||||
- contentinfo "关于软件" [ref=e54]:
|
||||
- link "由 Gitea 强力驱动" [ref=e55] [cursor=pointer]:
|
||||
- /url: https://about.gitea.com
|
||||
- generic [ref=e56]: "版本: 1.27.1"
|
||||
- generic [ref=e57]:
|
||||
- text: "页面:"
|
||||
- strong [ref=e58]: 1ms
|
||||
- text: "模板:"
|
||||
- strong [ref=e59]: 1ms
|
||||
- group "链接" [ref=e60]:
|
||||
- menu [ref=e61] [cursor=pointer]:
|
||||
- generic [ref=e62]: Auto
|
||||
- menu [ref=e66] [cursor=pointer]:
|
||||
- generic [ref=e67]: 简体中文
|
||||
- link "许可证" [ref=e70] [cursor=pointer]:
|
||||
- /url: /assets/licenses.txt
|
||||
- link "API" [ref=e71] [cursor=pointer]:
|
||||
- /url: /api/swagger
|
||||
@@ -0,0 +1,75 @@
|
||||
- generic [active] [ref=f1e1]:
|
||||
- generic [ref=f1e2]:
|
||||
- navigation "导航栏" [ref=f1e3]:
|
||||
- generic [ref=f1e4]:
|
||||
- link "首页" [ref=f1e5] [cursor=pointer]:
|
||||
- /url: /
|
||||
- link "探索" [ref=f1e7] [cursor=pointer]:
|
||||
- /url: /explore/repos
|
||||
- link "帮助" [ref=f1e8] [cursor=pointer]:
|
||||
- /url: https://docs.gitea.com
|
||||
- generic [ref=f1e9]:
|
||||
- link "注册" [ref=f1e10] [cursor=pointer]:
|
||||
- /url: /user/sign_up
|
||||
- link "登录" [ref=f1e14] [cursor=pointer]:
|
||||
- /url: /user/login
|
||||
- main "首页" [ref=f1e18]:
|
||||
- generic [ref=f1e20]:
|
||||
- img "Logo" [ref=f1e21]
|
||||
- generic [ref=f1e22]:
|
||||
- heading "HAHA" [level=1] [ref=f1e23]
|
||||
- heading "一款极易搭建的自托管 Git 服务" [level=2] [ref=f1e24]
|
||||
- generic [ref=f1e25]:
|
||||
- generic [ref=f1e26]:
|
||||
- heading "易安装" [level=1] [ref=f1e27]
|
||||
- paragraph [ref=f1e30]:
|
||||
- text: 直接
|
||||
- link "运行适合您平台的二进制文件" [ref=f1e31] [cursor=pointer]:
|
||||
- /url: https://docs.gitea.com/installation/install-from-binary
|
||||
- text: ,通过
|
||||
- link "Docker" [ref=f1e32] [cursor=pointer]:
|
||||
- /url: https://github.com/go-gitea/gitea/tree/master/docker
|
||||
- text: 部署,或使用
|
||||
- link "软件包" [ref=f1e33] [cursor=pointer]:
|
||||
- /url: https://docs.gitea.com/installation/install-from-package
|
||||
- text: 安装。
|
||||
- generic [ref=f1e34]:
|
||||
- heading "跨平台" [level=1] [ref=f1e35]
|
||||
- paragraph [ref=f1e38]:
|
||||
- text: 任何
|
||||
- link "Go 语言" [ref=f1e39] [cursor=pointer]:
|
||||
- /url: https://go.dev/
|
||||
- text: 支持的平台都可以运行 Gitea,包括 Windows、Mac、Linux 以及 ARM。挑一个您喜欢的就行!
|
||||
- generic [ref=f1e40]:
|
||||
- generic [ref=f1e41]:
|
||||
- heading "轻量级" [level=1] [ref=f1e42]
|
||||
- paragraph [ref=f1e45]: 一个廉价的树莓派就足以满足 Gitea 的最低系统硬件要求。最大程度节省您的服务器资源!
|
||||
- generic [ref=f1e46]:
|
||||
- heading "开源化" [level=1] [ref=f1e47]
|
||||
- paragraph [ref=f1e50]:
|
||||
- text: 所有代码都开源在
|
||||
- link "gitea.dev" [ref=f1e51] [cursor=pointer]:
|
||||
- /url: https://gitea.dev
|
||||
- text: ,还等什么?赶快加入我们来
|
||||
- link "共同发展" [ref=f1e52] [cursor=pointer]:
|
||||
- /url: https://github.com/go-gitea/gitea
|
||||
- text: 这个伟大的项目,成为贡献者吧!
|
||||
- group "页脚" [ref=f1e53]:
|
||||
- contentinfo "关于软件" [ref=f1e54]:
|
||||
- link "由 Gitea 强力驱动" [ref=f1e55] [cursor=pointer]:
|
||||
- /url: https://about.gitea.com
|
||||
- generic [ref=f1e56]: "版本: 1.27.1"
|
||||
- generic [ref=f1e57]:
|
||||
- text: "页面:"
|
||||
- strong [ref=f1e58]: 3ms
|
||||
- text: "模板:"
|
||||
- strong [ref=f1e59]: 3ms
|
||||
- group "链接" [ref=f1e60]:
|
||||
- menu [ref=f1e61] [cursor=pointer]:
|
||||
- generic [ref=f1e62]: Auto
|
||||
- menu [ref=f1e66] [cursor=pointer]:
|
||||
- generic [ref=f1e67]: 简体中文
|
||||
- link "许可证" [ref=f1e70] [cursor=pointer]:
|
||||
- /url: /assets/licenses.txt
|
||||
- link "API" [ref=f1e71] [cursor=pointer]:
|
||||
- /url: /api/swagger
|
||||
@@ -35,7 +35,7 @@
|
||||
3. 相同或相似的功能必须封装为可复用的方法,避免重复代码。
|
||||
4. 查询字段、路由、菜单、配置前,优先以当前项目代码和 `sbnews.sql` / `docs/sql/` 中结构为准;如需线上核对,再按用户要求使用可用 MCP 或指定连接方式。
|
||||
5. 不通过 WSL sshpass/scp 上传代码到服务器;项目代码更新由用户自行通过 SFTP 等方式处理。
|
||||
6. 项目级文档(如协作规范、业务进度、设计稿映射、产品方案、实现流程、爬虫说明等)统一存放在项目根目录;仅子项目专属文档放在对应子目录。
|
||||
6. 新增项目级文档(如协作规范、业务进度、设计稿映射、产品方案、实现流程、爬虫说明等)统一存放在 `docs/`;根目录仅保留 `README.md`、`AGENTS.md`、`文档索引.md` 等入口文件。仅子项目专属的 README / AGENTS 等说明可放在对应子目录。
|
||||
|
||||
---
|
||||
|
||||
@@ -48,6 +48,13 @@
|
||||
| uniapp | `npm run dev:h5` | `npm run build:h5` |
|
||||
| pc | `npm run dev` | `npm run generate` / `npm run build` |
|
||||
|
||||
## 自动化执行边界
|
||||
|
||||
- 除非用户在当前任务中明确要求,否则不要自动执行构建、测试、提交代码或部署。
|
||||
- 完成代码修改后,可以说明建议执行的验证命令或部署命令,但不得自行运行。
|
||||
- 用户明确要求“构建”“测试”“提交”“部署”“上线”“发布”等操作时,才执行对应命令。
|
||||
- 对已经由用户给出的命令,可按用户原命令执行;不要额外扩展到未要求的构建、测试、提交或部署步骤。
|
||||
|
||||
## 数据库结构基准
|
||||
|
||||
- 数据库表前缀:`la_`。
|
||||
@@ -77,7 +84,7 @@
|
||||
|
||||
## 代码修改后验证与评审
|
||||
|
||||
除非用户特别说明不需要验证,代码修改完成后按改动类型选择验证方式:
|
||||
仅在用户明确要求验证、测试或构建时,代码修改完成后按改动类型选择验证方式;未明确要求时只列出建议命令,不自动执行:
|
||||
|
||||
### 后端接口修改
|
||||
|
||||
@@ -104,13 +111,24 @@
|
||||
| 5 | 命名一致性 | 字段、方法、路由命名与现有风格一致 |
|
||||
| 6 | 安全性 | SQL 拼接、用户输入、权限校验是否安全 |
|
||||
| 7 | 幂等性 | SQL 初始化必须可重复执行 |
|
||||
| 8 | 文档同步 | 功能完成后更新 `业务进度管理.md` |
|
||||
| 8 | 文档同步 | 功能完成后更新 `docs/业务进度管理.md` |
|
||||
|
||||
## 代码变更后部署
|
||||
|
||||
- 仅在用户明确要求部署、上线、发布或同步到测试服时,执行以下命令,将最新代码更新到测试服运行环境:
|
||||
|
||||
```powershell
|
||||
cd D:\www\gs-sport-era
|
||||
powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -Sudo -AutoIncremental -AutoIncrementalCommits 3 -RemoteDir /www/wwwroot/test-server.sbnews.net
|
||||
```
|
||||
|
||||
- 不要把部署视为代码变更后的默认必做步骤;未明确要求部署时,只提示可执行的部署命令。
|
||||
|
||||
## 设计稿映射
|
||||
|
||||
设计稿与页面/组件映射统一维护在:
|
||||
|
||||
- [设计稿映射.md](./设计稿映射.md)
|
||||
- [设计稿映射.md](./docs/设计稿映射.md)
|
||||
|
||||
开发或还原视觉前,先确认设计稿名称、页面路径、实现状态与备注。
|
||||
|
||||
@@ -118,10 +136,18 @@
|
||||
|
||||
当前业务进度、待办事项、避坑记录统一维护在:
|
||||
|
||||
- [业务进度管理.md](./业务进度管理.md)
|
||||
- [业务进度管理.md](./docs/业务进度管理.md)
|
||||
|
||||
开发或接手任务前,先查看该文档确认已完成事项、进行中事项、待办事项与避坑记录。
|
||||
|
||||
## 近期交接总结
|
||||
|
||||
- 当前真实开发目录以 `D:\www\gs-sport-era` 为准;Codex worktree(如 `C:\Users\Administrator\.codex\worktrees\...\gs-sport-era`)中的改动不会自动进入真实仓库、`git log` 或服务器运行目录。涉及可部署代码时,先确认改动已经同步到真实目录并形成清晰提交。
|
||||
- 正确的服务器运行目录是 `/www/wwwroot/test-server.sbnews.net`;`/www/wwwroot/api.sbnews.net` 不是当前项目的有效同步目录,不要再部署到该路径。排查“代码已更新但服务器没生效”时,本地真实仓库看 `git log`,服务器侧看文件内容、修改时间、容器内 `/app` 文件与定时任务日志。
|
||||
- Docker crawler 任务真实执行入口是 `sport-era-crawler` 容器。更新 `docker/crawler` 源码或 `config/crawler_tasks.yaml` 后,必须在服务器 `docker` 目录执行 `sudo docker compose -f docker-compose.crawler.yml build crawler` 和 `sudo docker compose -f docker-compose.crawler.yml up -d --no-deps crawler`;仅重启旧容器不能保证镜像内代码更新,必要时使用 `--no-cache` 重建。
|
||||
- 重启 crawler 后必须核验容器内代码和 cron,而不是只看宿主机源码:例如检查 `/app/match_live_stream.py`、`/app/config/crawler_tasks.yaml`,以及 `cat /etc/cron.d/* | grep match_live_stream`。
|
||||
- 雨燕直播间任务 `match_live_stream` 当前应直接同步 `all_live_rooms.json`,写入主播 `anchor_id`、`anchor_name`、`anchor_avatar`、`anchor_level` 以及直播间观看/关注/公告/简介字段;对应 SQL 为 `docs/sql/alter_match_live_anchor_fields.sql` 和 `docs/sql/insert_crontab_match_live_stream.sql`。上线后用 `la_match_live` 主播字段统计和 `la_crawler_task_log` 最近任务状态共同确认是否生效。
|
||||
|
||||
## 近期踩坑
|
||||
|
||||
- `Validate::only()` 只限制校验字段范围,不会自动过滤请求参数;新增字段落库前,要同时检查 validate、logic、model、保存白名单和数据库列是否同步。
|
||||
|
||||
@@ -14,11 +14,15 @@ Docker 只负责运行 Python 爬虫和容器内 cron。MySQL、Redis、Nginx
|
||||
配置源:
|
||||
|
||||
- Docker Compose:`docker/docker-compose.crawler.yml`
|
||||
- Compose 路径变量示例:`docker/.env.example`
|
||||
- 环境变量示例:`docker/.env.docker.example`
|
||||
- 任务清单:`server/public/dongqiudi-crawler/config/crawler_tasks.yaml`
|
||||
- 爬虫源码:`docker/crawler`
|
||||
- 任务清单:`docker/crawler/config/crawler_tasks.yaml`
|
||||
- 任务日志:管理后台 `系统设置 -> 系统维护 -> 爬虫任务日志`
|
||||
- 错误日志:管理后台 `系统设置 -> 系统维护 -> 爬虫错误日志`
|
||||
|
||||
`docker/docker-compose.crawler.yml` 默认使用 `./crawler` 作为构建上下文和 `logs/data` 挂载目录。如果服务器上的 `docker/.env` 已存在,需要确认里面是 `CRAWLER_DIR=./crawler`,或者删除该变量让 Compose 使用默认值。
|
||||
|
||||
禁用的定时任务不要放进 Docker 执行清单;如需临时停用某个 Docker 爬虫任务,将 `crawler_tasks.yaml` 对应任务的 `active` 改为 `false`,然后重建或重启 crawler 容器。
|
||||
|
||||
### 当前 Docker 定时任务列表
|
||||
@@ -62,7 +66,7 @@ Docker 只负责运行 Python 爬虫和容器内 cron。MySQL、Redis、Nginx
|
||||
|
||||
```bash
|
||||
ssh sbnews
|
||||
cd /www/wwwroot/test-server.sbnews.net/docker
|
||||
cd /www/wwwroot/api.sbnews.net/docker
|
||||
```
|
||||
|
||||
查看容器状态:
|
||||
@@ -123,7 +127,7 @@ sudo docker exec sport-era-crawler python -c "import scrapling, browserforge; pr
|
||||
|
||||
```powershell
|
||||
cd D:\www\sport-era
|
||||
powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -Sudo -RemoteDir /www/wwwroot/test-server.sbnews.net
|
||||
powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -Sudo -RemoteDir /www/wwwroot/api.sbnews.net
|
||||
```
|
||||
|
||||
如果 `sbnews` 配置在 `C:\Users\Administrator\.ssh\config` 中,默认会使用 Windows 的 `ssh.exe` 读取该配置。
|
||||
@@ -134,20 +138,29 @@ powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -Sudo -Remo
|
||||
|
||||
```powershell
|
||||
cd D:\www\sport-era
|
||||
powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -Sudo -RemoteDir /www/wwwroot/test-server.sbnews.net
|
||||
powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -Sudo -RemoteDir /www/wwwroot/api.sbnews.net
|
||||
```
|
||||
|
||||
全量模式默认只打包以下目录:
|
||||
全量模式默认以仓库根目录为源,只打包以下四个子目录:
|
||||
|
||||
```text
|
||||
server/app
|
||||
server/config
|
||||
server/extend
|
||||
server/public/dongqiudi-crawler
|
||||
server/route
|
||||
server/
|
||||
uniapp/
|
||||
admin/
|
||||
docker/
|
||||
```
|
||||
|
||||
默认会排除运行态或较大目录,例如 `.env`、`vendor`、`runtime`、`public/uploads`、`public/admin`、`public/mobile`、爬虫 `data/logs/venv`。如确实需要包含,可追加参数:
|
||||
也就是说,远端 `RemoteDir` 下最终会形成:
|
||||
|
||||
```text
|
||||
<RemoteDir>/server
|
||||
<RemoteDir>/uniapp
|
||||
<RemoteDir>/admin
|
||||
<RemoteDir>/docker
|
||||
<RemoteDir>/docker/crawler
|
||||
```
|
||||
|
||||
默认会排除运行态、密钥或较大目录,例如 `server/.env`、`docker/.env`、`docker/.env.docker`、`server/vendor`、`server/runtime`、`server/public/uploads`、`server/public/admin`、`server/public/mobile`、爬虫 `data/logs/venv`,以及 `uniapp/node_modules`、`uniapp/dist`、`admin/node_modules`、`admin/dist`。如确实需要包含,可追加参数:
|
||||
|
||||
```bash
|
||||
--include-env
|
||||
@@ -164,14 +177,14 @@ server/route
|
||||
|
||||
```powershell
|
||||
cd D:\www\sport-era
|
||||
powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -Sudo -From <old_commit> -To <new_commit> -RemoteDir /www/wwwroot/test-server.sbnews.net
|
||||
powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -Sudo -From <old_commit> -To <new_commit> -RemoteDir /www/wwwroot/api.sbnews.net
|
||||
```
|
||||
|
||||
示例:
|
||||
|
||||
```powershell
|
||||
cd D:\www\sport-era
|
||||
powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -Sudo -From abc1234 -To def5678 -RemoteDir /www/wwwroot/test-server.sbnews.net
|
||||
powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -Sudo -From abc1234 -To def5678 -RemoteDir /www/wwwroot/api.sbnews.net
|
||||
```
|
||||
|
||||
### 自动增量发布
|
||||
@@ -180,28 +193,28 @@ powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -Sudo -From
|
||||
|
||||
```powershell
|
||||
cd D:\www\sport-era
|
||||
powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -Sudo -AutoIncremental -RemoteDir /www/wwwroot/test-server.sbnews.net
|
||||
powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -Sudo -AutoIncremental -RemoteDir /www/wwwroot/api.sbnews.net
|
||||
```
|
||||
|
||||
如果需要一次发布最近 N 个提交的后端改动,可以追加 `-AutoIncrementalCommits <n>`,例如发布最近 3 个提交会对比 `HEAD~3` 和 `HEAD`:
|
||||
|
||||
```powershell
|
||||
cd D:\www\sport-era
|
||||
powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -Sudo -AutoIncremental -AutoIncrementalCommits 3 -RemoteDir /www/wwwroot/test-server.sbnews.net
|
||||
powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -Sudo -AutoIncremental -AutoIncrementalCommits 3 -RemoteDir /www/wwwroot/api.sbnews.net
|
||||
```
|
||||
|
||||
先演练不上传:
|
||||
|
||||
```powershell
|
||||
cd D:\www\sport-era
|
||||
powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -AutoIncremental -RemoteDir /www/wwwroot/test-server.sbnews.net -DryRun
|
||||
powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -AutoIncremental -RemoteDir /www/wwwroot/api.sbnews.net -DryRun
|
||||
```
|
||||
|
||||
脚本会在打包和上传前先检查 `server` 工作区;如果 `server` 内存在未提交、删除或未跟踪文件,会先自动提交这些变更,再继续计算增量范围。可以通过 `-AutoCommitMessage "提交说明"` 自定义这次自动提交的说明。若存在合并冲突,需要先手动解决。
|
||||
脚本会在打包和上传前先检查默认打包范围(`server` + `uniapp` + `admin` + `docker`);如果这些目录内存在未提交、删除或未跟踪文件,会先自动提交这些变更,再继续计算增量范围。可以通过 `-AutoCommitMessage "提交说明"` 自定义这次自动提交的说明。若存在合并冲突,需要先手动解决。
|
||||
|
||||
注意:自动增量只包含指定提交范围内已经提交到 Git 的差异,不会包含工作区未提交文件。需要指定任意起止提交时,继续使用 `-From <old_commit> -To <new_commit>`。
|
||||
|
||||
如果当前根目录 Git 只把 `server` 记录为 gitlink/submodule,且本机没有 `server/.git` 子仓库元数据,脚本无法拿到 `server` 内部的文件级差异;此时会自动降级为打包当前工作区默认的 5 个 server 目录,保证可以继续发布。
|
||||
如果当前根目录 Git 只把 `server`、`uniapp` 或 `admin` 记录为 gitlink/submodule,脚本无法拿到对应目录内部的文件级差异;此时会跳过该 gitlink 的自动提交,并把该目录从当前工作区按源码目录补进增量包,保证可以继续发布。
|
||||
|
||||
### 只演练不上传
|
||||
|
||||
@@ -209,7 +222,7 @@ powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -AutoIncrem
|
||||
|
||||
```powershell
|
||||
cd D:\www\sport-era
|
||||
powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -RemoteDir /www/wwwroot/test-server.sbnews.net -DryRun
|
||||
powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -RemoteDir /www/wwwroot/api.sbnews.net -DryRun
|
||||
```
|
||||
|
||||
脚本会在打包后打印 `update log`:增量模式包含 commit 范围、commit 摘要、变更文件和删除文件;全量模式包含当前最新 commit 和默认打包目录。
|
||||
@@ -222,17 +235,37 @@ powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -RemoteDir
|
||||
|
||||
```powershell
|
||||
cd D:\www\sport-era
|
||||
powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -Sudo -RemoteDir /www/wwwroot/test-server.sbnews.net -PostInstall "php think optimize:clear"
|
||||
powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -Sudo -RemoteDir /www/wwwroot/api.sbnews.net -PostInstall "php think optimize:clear"
|
||||
```
|
||||
|
||||
### 发布 uniapp 源码到服务器并在服务器构建
|
||||
|
||||
默认发布命令会把 `server/`、`uniapp/`、`admin/` 和 `docker/` 一起同步到远端根目录;如果需要在服务器端构建 `uniapp`,建议在远端根目录执行构建,再把 H5 产物写回同一套代码下的 `server/public/mobile`:
|
||||
|
||||
```powershell
|
||||
cd D:\www\sport-era
|
||||
powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 `
|
||||
-Sudo `
|
||||
-RemoteDir /www/wwwroot/api.sbnews.net `
|
||||
-UniappReleasePath ../server/public/mobile `
|
||||
-PostInstall "cd uniapp && npm install && npm run build:h5"
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `-RemoteDir` 指向服务器上的项目根目录,解压后里面会同时有 `server/`、`uniapp/`、`admin/` 和 `docker/`。
|
||||
- `-UniappReleasePath` 会通过环境变量 `UNIAPP_RELEASE_PATH` 传给 `uniapp/scripts/release.mjs`;这里传 `../server/public/mobile`,是因为构建命令会先 `cd uniapp`,再把 H5 产物回写到兄弟目录 `server/public/mobile`。
|
||||
- 服务器需要预先安装 `node >= 18.19.0` 和 `npm`,否则 `npm install` / `npm run build:h5` 无法执行。
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. `ssh: Could not resolve hostname sbnews`:检查 Windows 的 `C:\Users\Administrator\.ssh\config` 是否存在 `Host sbnews`,或直接用 `-HostName <服务器IP>`。
|
||||
2. PowerShell 禁止执行脚本:使用示例中的 `-ExecutionPolicy Bypass -File`,或以管理员身份调整当前用户执行策略。
|
||||
3. `tar: Cannot utime` 或 `Cannot change mode`:远端目录权限不足,请加 `-Sudo`。脚本会使用 `sudo tar` 覆盖解压,并默认跳过还原 owner/permission,减少权限冲突。
|
||||
4. 增量发布提示 commit 无效:确认 commit id 属于当前根目录 Git 可识别的历史。
|
||||
5. `server is tracked as a root gitlink`:脚本检测到根 Git 只记录了 `server` 指针,无法做文件级增量,会自动改为打包当前工作区默认 5 个 server 目录。
|
||||
5. `root gitlink include paths will be copied from working tree`:脚本检测到默认发布范围里仍有 gitlink/submodule 目录,会把这些目录从当前工作区源码补进增量包。
|
||||
6. 仍需使用 Bash 兼容版时,可继续运行 `bash ./scripts/deploy-server.sh ...`。
|
||||
7. 服务器运行 `uniapp` 报 `node: command not found` 或 `npm: command not found`:先在服务器安装 Node.js 18.19+ 与 npm,再执行 `-SourceDir uniapp` 的发布命令。
|
||||
|
||||
<a href="https://www.likeshop.cn"></a><br>
|
||||
<a href="https://www.likeshop.cn"></a><br>
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
INSERT INTO `la_community_tag` (`name`, `icon`, `sort`, `post_count`, `is_hot`, `status`, `create_time`)
|
||||
SELECT '世界杯', '', 90, 0, 1, 1, UNIX_TIMESTAMP()
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `la_community_tag` WHERE `name` = '世界杯'
|
||||
);
|
||||
|
||||
INSERT INTO `la_community_post` (`user_id`, `content`, `images`, `post_type`, `match_id`, `is_paid`, `price_points`, `free_content_len`, `status`, `create_time`, `update_time`)
|
||||
SELECT 1, '世界杯开赛氛围越来越浓了,大家觉得本届最大的黑马会是谁?', '[]', 0, 0, 0, 0, 100, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `la_community_post` WHERE `content` = '世界杯开赛氛围越来越浓了,大家觉得本届最大的黑马会是谁?'
|
||||
);
|
||||
|
||||
INSERT INTO `la_community_post` (`user_id`, `content`, `images`, `post_type`, `match_id`, `is_paid`, `price_points`, `free_content_len`, `status`, `create_time`, `update_time`)
|
||||
SELECT 1, '我更看好防守稳一点的球队,世界杯这种大赛还是先稳住再说。', '[]', 0, 0, 0, 0, 100, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `la_community_post` WHERE `content` = '我更看好防守稳一点的球队,世界杯这种大赛还是先稳住再说。'
|
||||
);
|
||||
|
||||
INSERT INTO `la_community_post` (`user_id`, `content`, `images`, `post_type`, `match_id`, `is_paid`, `price_points`, `free_content_len`, `status`, `create_time`, `update_time`)
|
||||
SELECT 1, '这届世界杯的球员状态起伏很大,关键还是看临场调整和替补深度。', '[]', 0, 0, 0, 0, 100, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `la_community_post` WHERE `content` = '这届世界杯的球员状态起伏很大,关键还是看临场调整和替补深度。'
|
||||
);
|
||||
|
||||
INSERT INTO `la_community_post_tag` (`post_id`, `tag_id`)
|
||||
SELECT p.id, t.id
|
||||
FROM `la_community_post` p
|
||||
JOIN `la_community_tag` t ON t.`name` = '世界杯'
|
||||
WHERE p.`content` IN (
|
||||
'世界杯开赛氛围越来越浓了,大家觉得本届最大的黑马会是谁?',
|
||||
'我更看好防守稳一点的球队,世界杯这种大赛还是先稳住再说。',
|
||||
'这届世界杯的球员状态起伏很大,关键还是看临场调整和替补深度。'
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE `post_id` = `post_id`;
|
||||
|
||||
UPDATE `la_community_tag` t
|
||||
SET `post_count` = (
|
||||
SELECT COUNT(*)
|
||||
FROM `la_community_post_tag` pt
|
||||
JOIN `la_community_post` p ON p.id = pt.post_id AND p.delete_time IS NULL
|
||||
WHERE pt.tag_id = t.id
|
||||
)
|
||||
WHERE t.`name` = '世界杯';
|
||||
@@ -1,29 +0,0 @@
|
||||
INSERT INTO `la_community_post` (`user_id`, `content`, `images`, `post_type`, `match_id`, `is_paid`, `price_points`, `free_content_len`, `status`, `create_time`, `update_time`)
|
||||
SELECT 1, '世界杯开赛氛围越来越浓了,大家觉得本届最大的黑马会是谁?', '[]', 0, 0, 0, 0, 100, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `la_community_post` WHERE `content` = '世界杯开赛氛围越来越浓了,大家觉得本届最大的黑马会是谁?'
|
||||
);
|
||||
|
||||
INSERT INTO `la_community_post` (`user_id`, `content`, `images`, `post_type`, `match_id`, `is_paid`, `price_points`, `free_content_len`, `status`, `create_time`, `update_time`)
|
||||
SELECT 1, '我更看好防守稳一点的球队,世界杯这种大赛还是先稳住再说。', '[]', 0, 0, 0, 0, 100, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `la_community_post` WHERE `content` = '我更看好防守稳一点的球队,世界杯这种大赛还是先稳住再说。'
|
||||
);
|
||||
|
||||
INSERT INTO `la_community_post` (`user_id`, `content`, `images`, `post_type`, `match_id`, `is_paid`, `price_points`, `free_content_len`, `status`, `create_time`, `update_time`)
|
||||
SELECT 1, '这届世界杯的球员状态起伏很大,关键还是看临场调整和替补深度。', '[]', 0, 0, 0, 0, 100, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `la_community_post` WHERE `content` = '这届世界杯的球员状态起伏很大,关键还是看临场调整和替补深度。'
|
||||
);
|
||||
|
||||
INSERT INTO `la_community_post_tag` (`post_id`, `tag_id`)
|
||||
SELECT p.id, 17
|
||||
FROM `la_community_post` p
|
||||
WHERE p.`content` IN (
|
||||
'世界杯开赛氛围越来越浓了,大家觉得本届最大的黑马会是谁?',
|
||||
'我更看好防守稳一点的球队,世界杯这种大赛还是先稳住再说。',
|
||||
'这届世界杯的球员状态起伏很大,关键还是看临场调整和替补深度。'
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE `post_id` = `post_id`;
|
||||
|
||||
UPDATE `la_community_tag` SET `post_count` = (SELECT COUNT(*) FROM `la_community_post_tag` WHERE tag_id = 17) WHERE id = 17;
|
||||
@@ -21,6 +21,25 @@ AI_KB_REDIS_DB=4
|
||||
DQD_LOG_LEVEL=INFO
|
||||
DQD_LOG_FILE=/app/logs/crawler.log
|
||||
|
||||
# Third-party match odds crawler. Keep the real account in docker/.env.docker only.
|
||||
ODDS_USERNAME=change-me
|
||||
ODDS_PASSWORD=change-me
|
||||
ODDS_BASE_URLS=https://205.201.0.120/,https://mos022.com/,https://hga039.com/,https://mos100.com/
|
||||
ODDS_SPORT_TYPES=FT,BK,ES,TN,VB,BM,TT,BS,SK,OP
|
||||
ODDS_SHOW_TYPES=live,today,early
|
||||
ODDS_VERIFY_TLS=0
|
||||
|
||||
# Titan007 public cup odds. Multiple cup URLs can be comma-separated.
|
||||
TITAN_ODDS_ENABLED=1
|
||||
TITAN_ODDS_URLS=https://zq.titan007.com/cn/CupMatch/75.html
|
||||
TITAN_ODDS_COMPANY_ID=3
|
||||
TITAN_ODDS_EURO_COMPANY_ID=545
|
||||
TITAN_ODDS_COMPANY_NAME=Crown
|
||||
TITAN_ODDS_SHOW_TYPE=early
|
||||
TITAN_ODDS_VERIFY_TLS=1
|
||||
TITAN_ODDS_TIMEOUT=20
|
||||
TITAN_ODDS_REQUEST_DELAY_SECONDS=0.2
|
||||
|
||||
# AI KB worker embedding config. Keep real key in docker/.env.docker only.
|
||||
EMBEDDING_API_KEY=change-me
|
||||
EMBEDDING_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
CRAWLER_DIR=./crawler
|
||||
@@ -1,4 +1,8 @@
|
||||
/data
|
||||
/log
|
||||
/logs
|
||||
.env
|
||||
.env.docker
|
||||
crawler/venv/
|
||||
crawler/logs/
|
||||
crawler/data/
|
||||
|
||||
@@ -3,8 +3,8 @@ __pycache__/
|
||||
**/__pycache__/
|
||||
*.pyc
|
||||
.pytest_cache/
|
||||
logs/*
|
||||
data/locks/*
|
||||
logs/
|
||||
data/
|
||||
test_output.txt
|
||||
tmp_*.py
|
||||
tmp_*.sql
|
||||
@@ -10,9 +10,9 @@ build/
|
||||
venv/
|
||||
env/
|
||||
.env
|
||||
logs/*.log
|
||||
data/*.json
|
||||
data/*.csv
|
||||
logs/
|
||||
data/
|
||||
test_output.txt
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
@@ -27,7 +27,8 @@ RUN python -m pip install --upgrade pip setuptools wheel \
|
||||
|
||||
COPY . /app
|
||||
|
||||
RUN mkdir -p /app/logs /app/data/locks /etc/sport-era-crawler \
|
||||
RUN sed -i 's/\r$//' /app/docker/entrypoint.sh \
|
||||
&& mkdir -p /app/logs /app/data/locks /etc/sport-era-crawler \
|
||||
&& chmod +x /app/docker/entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/app/docker/entrypoint.sh"]
|
||||
@@ -68,7 +68,7 @@ python main.py cron
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
dongqiudi-crawler/
|
||||
docker/crawler/
|
||||
├── config/
|
||||
│ └── settings.yaml # 配置文件
|
||||
├── src/
|
||||
@@ -25,7 +25,40 @@ DEFAULT_REQUEST_TIMEOUT = 180
|
||||
DEFAULT_RETRY_TIMES = 2
|
||||
DEFAULT_RETRY_SLEEP_MS = 800
|
||||
BOT_ACCOUNT_PREFIX = "ai_comment_"
|
||||
BOT_NICKNAME_PREFIX = "AI评论员"
|
||||
BOT_HANDLE_PREFIXES = [
|
||||
"看台",
|
||||
"深夜",
|
||||
"边线",
|
||||
"北区",
|
||||
"南看台",
|
||||
"东门",
|
||||
"西街",
|
||||
"慢热",
|
||||
"小酒馆",
|
||||
"补时",
|
||||
"角旗",
|
||||
"传中",
|
||||
"临场",
|
||||
"半场",
|
||||
"球门后",
|
||||
"旧球鞋",
|
||||
"赛点",
|
||||
"风向",
|
||||
"追球",
|
||||
"回防",
|
||||
]
|
||||
BOT_HANDLE_SUFFIXES = [
|
||||
"阿泽",
|
||||
"小顾",
|
||||
"老韩",
|
||||
"阿凯",
|
||||
"小唐",
|
||||
"阿林",
|
||||
"阿松",
|
||||
"阿北",
|
||||
"阿岳",
|
||||
"阿诚",
|
||||
]
|
||||
|
||||
DEFAULT_ARTICLE_PROMPT = (
|
||||
"你是一名专业、克制、真实的体育资讯评论员。请根据给定资讯内容生成1条中文评论,"
|
||||
@@ -592,15 +625,10 @@ class AiCommentRepository:
|
||||
for index in range(1, target_count + 1):
|
||||
persona = self._persona_for_index(index)
|
||||
account = f"{BOT_ACCOUNT_PREFIX}{index:04d}"
|
||||
nickname = self._build_bot_nickname(index, persona["label"])
|
||||
row = indexed.get(index)
|
||||
if row:
|
||||
if row["nickname"] != nickname:
|
||||
cursor.execute(
|
||||
f"UPDATE `{self.prefix}user` SET nickname=%s, update_time=%s WHERE id=%s",
|
||||
(nickname, now, row["id"]),
|
||||
)
|
||||
continue
|
||||
nickname = self._build_bot_nickname(index)
|
||||
password = self._create_password(self._random_password(), salt)
|
||||
sn = self._create_user_sn(cursor)
|
||||
cursor.execute(
|
||||
@@ -618,7 +646,8 @@ class AiCommentRepository:
|
||||
for row in rows:
|
||||
index = self._parse_account_index(row["account"])
|
||||
persona = self._persona_for_index(index)
|
||||
users.append(VirtualUser(int(row["id"]), row["account"], self._build_bot_nickname(index, persona["label"]), persona["key"]))
|
||||
nickname = str(row.get("nickname") or "").strip() or self._build_bot_nickname(index)
|
||||
users.append(VirtualUser(int(row["id"]), row["account"], nickname, persona["key"]))
|
||||
self._virtual_users_cache = list(users)
|
||||
return users[:target_count]
|
||||
|
||||
@@ -991,9 +1020,11 @@ class AiCommentRepository:
|
||||
return hashlib.md5(str(random.random()).encode("utf-8")).hexdigest()[:12]
|
||||
|
||||
@staticmethod
|
||||
def _build_bot_nickname(index: int, persona_label: str) -> str:
|
||||
number = f"{index:02d}" if index < 100 else str(index)
|
||||
return f"{BOT_NICKNAME_PREFIX}{number}·{persona_label}"
|
||||
def _build_bot_nickname(index: int) -> str:
|
||||
zero_based = max(0, index - 1)
|
||||
prefix = BOT_HANDLE_PREFIXES[zero_based % len(BOT_HANDLE_PREFIXES)]
|
||||
suffix = BOT_HANDLE_SUFFIXES[(zero_based // len(BOT_HANDLE_PREFIXES)) % len(BOT_HANDLE_SUFFIXES)]
|
||||
return f"{prefix}{suffix}"
|
||||
|
||||
@staticmethod
|
||||
def _persona_for_index(index: int) -> Dict[str, str]:
|
||||
@@ -154,3 +154,13 @@ tasks:
|
||||
action: ai_comment_dispatch
|
||||
cron: "*/10 * * * *"
|
||||
active: true
|
||||
- key: match_live_stream
|
||||
name: 赛事直播流与主播信息抓取
|
||||
action: match_live_stream
|
||||
cron: "*/1 * * * *"
|
||||
active: true
|
||||
- key: match_odds
|
||||
name: 第三方赛事赔率抓取
|
||||
action: match_odds
|
||||
cron: "*/5 * * * *"
|
||||
active: true
|
||||
@@ -0,0 +1,526 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- saved from url=(0046)https://38.11.29.1:50001/historys/hk/2026.html -->
|
||||
<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta name="applicable-device" content="pc,mobile">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<title>2026年香港搅珠记录</title>
|
||||
<meta name="keywords" content="">
|
||||
<meta name="description" content="">
|
||||
<link href="./2026年香港搅珠记录_files/style.css" rel="stylesheet" type="text/css">
|
||||
<link href="./2026年香港搅珠记录_files/kj.css" rel="stylesheet" type="text/css">
|
||||
<script type="text/javascript" src="./2026年香港搅珠记录_files/jquery.js.下载"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="cgi-tan-box-container" id="tipDialogContainer">
|
||||
<div class="cgi-tan-box">
|
||||
<div class="cgi-tan-content" id="tipDialogContainerInfo"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cgi-container">
|
||||
<script type="text/javascript" src="./2026年香港搅珠记录_files/menu-bottom-hk.js.下载"></script><div class="cgi-foot-links"> <ul class="clearfix"> <li><a id="fixedNavIndex" target="_blank" href="https://4.194.218.6:14155/#openkj"><span class="cfl1"></span>网址导航</a></li> <li><a id="fixedNavKjZs" href="https://38.11.29.1:50001/historys/mo/2026.html"><span class="cfl3"></span>[新]澳门记录</a></li> <li><a id="fixedNavKjZl" href="https://38.11.29.1:50001/historys/hk/2026.html" class="hover"><span class="cfl3"></span>香港记录</a></li> <li><a id="fixedNavTk" href="https://38.11.29.6:50010/historys/lam/2026.html"><span class="cfl3"></span>澳门记录</a></li> </ul></div>
|
||||
<div class="xz-black-bg selectYearDialog"></div>
|
||||
<header class="cgi-head">
|
||||
<a class="icon-back" href="javascript:history.back(-1)"></a>
|
||||
<span class="cgi-head-tit tit-center">2026年香港搅珠记录</span>
|
||||
<div id="btn-select-year" class="cgi-head-btn">选择年份</div>
|
||||
<div class="menu02 dn" id="menu" style="display: none;">
|
||||
<ul>
|
||||
<script type="text/javascript" src="./2026年香港搅珠记录_files/year-hk.js.下载"></script><li><a href="https://38.11.29.1:50001/historys/hk/2026.html">2026</a></li><li><a href="https://38.11.29.1:50001/historys/hk/2025.html">2025</a></li><li><a href="https://38.11.29.1:50001/historys/hk/2024.html">2024</a></li><li><a href="https://38.11.29.1:50001/historys/hk/2023.html">2023</a></li><li><a href="https://38.11.29.1:50001/historys/hk/2022.html">2022</a></li><li><a href="https://38.11.29.1:50001/historys/hk/2021.html">2021</a></li><li><a href="https://38.11.29.1:50001/historys/hk/2020.html">2020</a></li><li><a href="https://38.11.29.1:50001/historys/hk/2019.html">2019</a></li><li><a href="https://38.11.29.1:50001/historys/hk/2018.html">2018</a></li><li><a href="https://38.11.29.1:50001/historys/hk/2017.html">2017</a></li><li><a href="https://38.11.29.1:50001/historys/hk/2016.html">2016</a></li><li><a href="https://38.11.29.1:50001/historys/hk/2015.html">2015</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
$("#menu").hide();
|
||||
$("#btn-select-year").click(function() {
|
||||
$("#menu").toggle();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<div class="cgi-zs">
|
||||
<div class="tempWrap" style="overflow:hidden; position:relative;">
|
||||
<div class="bd" id="tabBox1-bd">
|
||||
<div class="con">
|
||||
<div class="kj-tit">香港⑥彩 第<font color="#fe0000">039</font>期 (2026年04月11日)</div>
|
||||
<div class="kj-box">
|
||||
<ul class="clearfix">
|
||||
<li><dl><dt class="ball-blue">14</dt><dd>蛇<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">42</dt><dd>牛<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">40</dt><dd>兔<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">11</dt><dd>猴<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">17</dt><dd>虎<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">28</dt><dd>兔<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li class="kj-jia"><dl><dt></dt><dd></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">02</dt><dd>蛇<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
</ul>
|
||||
</div><div class="kj-tit">香港⑥彩 第<font color="#fe0000">038</font>期 (2026年04月09日)</div>
|
||||
<div class="kj-box">
|
||||
<ul class="clearfix">
|
||||
<li><dl><dt class="ball-red">24</dt><dd>羊<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">43</dt><dd>鼠<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">45</dt><dd>狗<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">44</dt><dd>猪<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">13</dt><dd>马<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">16</dt><dd>兔<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li class="kj-jia"><dl><dt></dt><dd></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">40</dt><dd>兔<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
</ul>
|
||||
</div><div class="kj-tit">香港⑥彩 第<font color="#fe0000">037</font>期 (2026年04月07日)</div>
|
||||
<div class="kj-box">
|
||||
<ul class="clearfix">
|
||||
<li><dl><dt class="ball-green">33</dt><dd>狗<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">23</dt><dd>猴<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">25</dt><dd>马<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">34</dt><dd>鸡<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">29</dt><dd>虎<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">08</dt><dd>猪<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li class="kj-jia"><dl><dt></dt><dd></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">49</dt><dd>马<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
</ul>
|
||||
</div><div class="kj-tit">香港⑥彩 第<font color="#fe0000">036</font>期 (2026年04月04日)</div>
|
||||
<div class="kj-box">
|
||||
<ul class="clearfix">
|
||||
<li><dl><dt class="ball-green">32</dt><dd>猪<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">45</dt><dd>狗<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">40</dt><dd>兔<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">20</dt><dd>猪<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">35</dt><dd>猴<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">28</dt><dd>兔<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li class="kj-jia"><dl><dt></dt><dd></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">43</dt><dd>鼠<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
</ul>
|
||||
</div><div class="kj-tit">香港⑥彩 第<font color="#fe0000">035</font>期 (2026年03月31日)</div>
|
||||
<div class="kj-box">
|
||||
<ul class="clearfix">
|
||||
<li><dl><dt class="ball-blue">20</dt><dd>猪<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">32</dt><dd>猪<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">28</dt><dd>兔<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">09</dt><dd>狗<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">19</dt><dd>鼠<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">18</dt><dd>牛<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li class="kj-jia"><dl><dt></dt><dd></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">44</dt><dd>猪<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
</ul>
|
||||
</div><div class="kj-tit">香港⑥彩 第<font color="#fe0000">034</font>期 (2026年03月28日)</div>
|
||||
<div class="kj-box">
|
||||
<ul class="clearfix">
|
||||
<li><dl><dt class="ball-blue">42</dt><dd>牛<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">26</dt><dd>蛇<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">27</dt><dd>龙<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">11</dt><dd>猴<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">21</dt><dd>狗<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">45</dt><dd>狗<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
<li class="kj-jia"><dl><dt></dt><dd></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">19</dt><dd>鼠<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
</ul>
|
||||
</div><div class="kj-tit">香港⑥彩 第<font color="#fe0000">033</font>期 (2026年03月26日)</div>
|
||||
<div class="kj-box">
|
||||
<ul class="clearfix">
|
||||
<li><dl><dt class="ball-green">27</dt><dd>龙<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">12</dt><dd>羊<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">23</dt><dd>猴<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">28</dt><dd>兔<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">37</dt><dd>马<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">17</dt><dd>虎<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li class="kj-jia"><dl><dt></dt><dd></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">03</dt><dd>龙<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
</ul>
|
||||
</div><div class="kj-tit">香港⑥彩 第<font color="#fe0000">032</font>期 (2026年03月24日)</div>
|
||||
<div class="kj-box">
|
||||
<ul class="clearfix">
|
||||
<li><dl><dt class="ball-blue">26</dt><dd>蛇<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">06</dt><dd>牛<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">22</dt><dd>鸡<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">44</dt><dd>猪<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">49</dt><dd>马<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">09</dt><dd>狗<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li class="kj-jia"><dl><dt></dt><dd></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">07</dt><dd>鼠<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
</ul>
|
||||
</div><div class="kj-tit">香港⑥彩 第<font color="#fe0000">031</font>期 (2026年03月21日)</div>
|
||||
<div class="kj-box">
|
||||
<ul class="clearfix">
|
||||
<li><dl><dt class="ball-blue">09</dt><dd>狗<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">47</dt><dd>猴<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">18</dt><dd>牛<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">11</dt><dd>猴<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">05</dt><dd>虎<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">23</dt><dd>猴<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
<li class="kj-jia"><dl><dt></dt><dd></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">24</dt><dd>羊<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
</ul>
|
||||
</div><div class="kj-tit">香港⑥彩 第<font color="#fe0000">030</font>期 (2026年03月19日)</div>
|
||||
<div class="kj-box">
|
||||
<ul class="clearfix">
|
||||
<li><dl><dt class="ball-green">06</dt><dd>牛<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">08</dt><dd>猪<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">45</dt><dd>狗<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">02</dt><dd>蛇<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">41</dt><dd>虎<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">33</dt><dd>狗<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li class="kj-jia"><dl><dt></dt><dd></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">48</dt><dd>羊<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
</ul>
|
||||
</div><div class="kj-tit">香港⑥彩 第<font color="#fe0000">029</font>期 (2026年03月17日)</div>
|
||||
<div class="kj-box">
|
||||
<ul class="clearfix">
|
||||
<li><dl><dt class="ball-green">22</dt><dd>鸡<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">18</dt><dd>牛<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">45</dt><dd>狗<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">16</dt><dd>兔<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">28</dt><dd>兔<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">49</dt><dd>马<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li class="kj-jia"><dl><dt></dt><dd></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">13</dt><dd>马<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
</ul>
|
||||
</div><div class="kj-tit">香港⑥彩 第<font color="#fe0000">028</font>期 (2026年03月12日)</div>
|
||||
<div class="kj-box">
|
||||
<ul class="clearfix">
|
||||
<li><dl><dt class="ball-red">07</dt><dd>鼠<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">14</dt><dd>蛇<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">26</dt><dd>蛇<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">13</dt><dd>马<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">16</dt><dd>兔<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">30</dt><dd>牛<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
<li class="kj-jia"><dl><dt></dt><dd></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">34</dt><dd>鸡<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
</ul>
|
||||
</div><div class="kj-tit">香港⑥彩 第<font color="#fe0000">027</font>期 (2026年03月10日)</div>
|
||||
<div class="kj-box">
|
||||
<ul class="clearfix">
|
||||
<li><dl><dt class="ball-red">34</dt><dd>鸡<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">16</dt><dd>兔<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">25</dt><dd>马<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">02</dt><dd>蛇<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">35</dt><dd>猴<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">37</dt><dd>马<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li class="kj-jia"><dl><dt></dt><dd></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">49</dt><dd>马<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
</ul>
|
||||
</div><div class="kj-tit">香港⑥彩 第<font color="#fe0000">026</font>期 (2026年03月07日)</div>
|
||||
<div class="kj-box">
|
||||
<ul class="clearfix">
|
||||
<li><dl><dt class="ball-red">18</dt><dd>牛<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">12</dt><dd>羊<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">22</dt><dd>鸡<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">28</dt><dd>兔<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">15</dt><dd>龙<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">37</dt><dd>马<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li class="kj-jia"><dl><dt></dt><dd></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">31</dt><dd>鼠<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
</ul>
|
||||
</div><div class="kj-tit">香港⑥彩 第<font color="#fe0000">025</font>期 (2026年03月05日)</div>
|
||||
<div class="kj-box">
|
||||
<ul class="clearfix">
|
||||
<li><dl><dt class="ball-blue">04</dt><dd>兔<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">31</dt><dd>鼠<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">24</dt><dd>羊<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">18</dt><dd>牛<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">46</dt><dd>鸡<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">42</dt><dd>牛<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li class="kj-jia"><dl><dt></dt><dd></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">11</dt><dd>猴<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
</ul>
|
||||
</div><div class="kj-tit">香港⑥彩 第<font color="#fe0000">024</font>期 (2026年03月03日)</div>
|
||||
<div class="kj-box">
|
||||
<ul class="clearfix">
|
||||
<li><dl><dt class="ball-blue">20</dt><dd>猪<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">32</dt><dd>猪<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">31</dt><dd>鼠<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">44</dt><dd>猪<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">13</dt><dd>马<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">06</dt><dd>牛<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li class="kj-jia"><dl><dt></dt><dd></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">45</dt><dd>狗<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
</ul>
|
||||
</div><div class="kj-tit">香港⑥彩 第<font color="#fe0000">023</font>期 (2026年02月28日)</div>
|
||||
<div class="kj-box">
|
||||
<ul class="clearfix">
|
||||
<li><dl><dt class="ball-blue">47</dt><dd>猴<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">37</dt><dd>马<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">15</dt><dd>龙<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">46</dt><dd>鸡<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">39</dt><dd>龙<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">05</dt><dd>虎<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li class="kj-jia"><dl><dt></dt><dd></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">29</dt><dd>虎<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
</ul>
|
||||
</div><div class="kj-tit">香港⑥彩 第<font color="#fe0000">022</font>期 (2026年02月26日)</div>
|
||||
<div class="kj-box">
|
||||
<ul class="clearfix">
|
||||
<li><dl><dt class="ball-green">06</dt><dd>牛<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">19</dt><dd>鼠<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">38</dt><dd>蛇<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">15</dt><dd>龙<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">42</dt><dd>牛<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">13</dt><dd>马<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li class="kj-jia"><dl><dt></dt><dd></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">34</dt><dd>鸡<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
</ul>
|
||||
</div><div class="kj-tit">香港⑥彩 第<font color="#fe0000">021</font>期 (2026年02月24日)</div>
|
||||
<div class="kj-box">
|
||||
<ul class="clearfix">
|
||||
<li><dl><dt class="ball-blue">10</dt><dd>鸡<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">04</dt><dd>兔<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">03</dt><dd>龙<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">02</dt><dd>蛇<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">23</dt><dd>猴<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">13</dt><dd>马<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li class="kj-jia"><dl><dt></dt><dd></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">12</dt><dd>羊<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
</ul>
|
||||
</div><div class="kj-tit">香港⑥彩 第<font color="#fe0000">020</font>期 (2026年02月21日)</div>
|
||||
<div class="kj-box">
|
||||
<ul class="clearfix">
|
||||
<li><dl><dt class="ball-red">34</dt><dd>鸡<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">18</dt><dd>牛<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">35</dt><dd>猴<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">37</dt><dd>马<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">49</dt><dd>马<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">02</dt><dd>蛇<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li class="kj-jia"><dl><dt></dt><dd></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">33</dt><dd>狗<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
</ul>
|
||||
</div><div class="kj-tit">香港⑥彩 第<font color="#fe0000">019</font>期 (2026年02月15日)</div>
|
||||
<div class="kj-box">
|
||||
<ul class="clearfix">
|
||||
<li><dl><dt class="ball-red">08</dt><dd>狗<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">28</dt><dd>虎<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">46</dt><dd>猴<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">37</dt><dd>蛇<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">36</dt><dd>马<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">33</dt><dd>鸡<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li class="kj-jia"><dl><dt></dt><dd></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">04</dt><dd>虎<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
</ul>
|
||||
</div><div class="kj-tit">香港⑥彩 第<font color="#fe0000">018</font>期 (2026年02月12日)</div>
|
||||
<div class="kj-box">
|
||||
<ul class="clearfix">
|
||||
<li><dl><dt class="ball-blue">03</dt><dd>兔<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">46</dt><dd>猴<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">37</dt><dd>蛇<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">25</dt><dd>蛇<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">02</dt><dd>龙<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">14</dt><dd>龙<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
<li class="kj-jia"><dl><dt></dt><dd></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">10</dt><dd>猴<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
</ul>
|
||||
</div><div class="kj-tit">香港⑥彩 第<font color="#fe0000">017</font>期 (2026年02月10日)</div>
|
||||
<div class="kj-box">
|
||||
<ul class="clearfix">
|
||||
<li><dl><dt class="ball-green">39</dt><dd>兔<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">04</dt><dd>虎<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">26</dt><dd>龙<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">03</dt><dd>兔<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">18</dt><dd>鼠<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">14</dt><dd>龙<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
<li class="kj-jia"><dl><dt></dt><dd></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">40</dt><dd>虎<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
</ul>
|
||||
</div><div class="kj-tit">香港⑥彩 第<font color="#fe0000">016</font>期 (2026年02月07日)</div>
|
||||
<div class="kj-box">
|
||||
<ul class="clearfix">
|
||||
<li><dl><dt class="ball-green">32</dt><dd>狗<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">12</dt><dd>马<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">37</dt><dd>蛇<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">22</dt><dd>猴<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">44</dt><dd>狗<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">28</dt><dd>虎<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li class="kj-jia"><dl><dt></dt><dd></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">20</dt><dd>狗<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
</ul>
|
||||
</div><div class="kj-tit">香港⑥彩 第<font color="#fe0000">015</font>期 (2026年02月05日)</div>
|
||||
<div class="kj-box">
|
||||
<ul class="clearfix">
|
||||
<li><dl><dt class="ball-red">35</dt><dd>羊<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">24</dt><dd>马<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">36</dt><dd>马<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">09</dt><dd>鸡<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">01</dt><dd>蛇<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">17</dt><dd>牛<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li class="kj-jia"><dl><dt></dt><dd></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">42</dt><dd>鼠<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
</ul>
|
||||
</div><div class="kj-tit">香港⑥彩 第<font color="#fe0000">014</font>期 (2026年02月03日)</div>
|
||||
<div class="kj-box">
|
||||
<ul class="clearfix">
|
||||
<li><dl><dt class="ball-blue">42</dt><dd>鼠<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">39</dt><dd>兔<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">23</dt><dd>羊<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">27</dt><dd>兔<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">12</dt><dd>马<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">37</dt><dd>蛇<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li class="kj-jia"><dl><dt></dt><dd></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">05</dt><dd>牛<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
</ul>
|
||||
</div><div class="kj-tit">香港⑥彩 第<font color="#fe0000">013</font>期 (2026年01月31日)</div>
|
||||
<div class="kj-box">
|
||||
<ul class="clearfix">
|
||||
<li><dl><dt class="ball-red">30</dt><dd>鼠<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">35</dt><dd>羊<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">32</dt><dd>狗<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">36</dt><dd>马<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">25</dt><dd>蛇<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">47</dt><dd>羊<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li class="kj-jia"><dl><dt></dt><dd></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">46</dt><dd>猴<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
</ul>
|
||||
</div><div class="kj-tit">香港⑥彩 第<font color="#fe0000">012</font>期 (2026年01月29日)</div>
|
||||
<div class="kj-box">
|
||||
<ul class="clearfix">
|
||||
<li><dl><dt class="ball-blue">14</dt><dd>龙<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">44</dt><dd>狗<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">09</dt><dd>鸡<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">06</dt><dd>鼠<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">35</dt><dd>羊<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">12</dt><dd>马<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li class="kj-jia"><dl><dt></dt><dd></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">11</dt><dd>羊<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
</ul>
|
||||
</div><div class="kj-tit">香港⑥彩 第<font color="#fe0000">011</font>期 (2026年01月27日)</div>
|
||||
<div class="kj-box">
|
||||
<ul class="clearfix">
|
||||
<li><dl><dt class="ball-green">38</dt><dd>龙<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">47</dt><dd>羊<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">25</dt><dd>蛇<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">12</dt><dd>马<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">14</dt><dd>龙<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">07</dt><dd>猪<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li class="kj-jia"><dl><dt></dt><dd></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">15</dt><dd>兔<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
</ul>
|
||||
</div><div class="kj-tit">香港⑥彩 第<font color="#fe0000">010</font>期 (2026年01月24日)</div>
|
||||
<div class="kj-box">
|
||||
<ul class="clearfix">
|
||||
<li><dl><dt class="ball-green">16</dt><dd>虎<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">17</dt><dd>牛<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">48</dt><dd>马<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">22</dt><dd>猴<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">28</dt><dd>虎<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">06</dt><dd>鼠<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li class="kj-jia"><dl><dt></dt><dd></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">45</dt><dd>鸡<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
</ul>
|
||||
</div><div class="kj-tit">香港⑥彩 第<font color="#fe0000">009</font>期 (2026年01月22日)</div>
|
||||
<div class="kj-box">
|
||||
<ul class="clearfix">
|
||||
<li><dl><dt class="ball-blue">31</dt><dd>猪<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">04</dt><dd>虎<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">24</dt><dd>马<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">27</dt><dd>兔<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">15</dt><dd>兔<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">09</dt><dd>鸡<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li class="kj-jia"><dl><dt></dt><dd></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">45</dt><dd>鸡<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
</ul>
|
||||
</div><div class="kj-tit">香港⑥彩 第<font color="#fe0000">008</font>期 (2026年01月20日)</div>
|
||||
<div class="kj-box">
|
||||
<ul class="clearfix">
|
||||
<li><dl><dt class="ball-green">06</dt><dd>鼠<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">46</dt><dd>猴<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">44</dt><dd>狗<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">09</dt><dd>鸡<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">01</dt><dd>蛇<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">04</dt><dd>虎<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li class="kj-jia"><dl><dt></dt><dd></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">27</dt><dd>兔<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
</ul>
|
||||
</div><div class="kj-tit">香港⑥彩 第<font color="#fe0000">007</font>期 (2026年01月17日)</div>
|
||||
<div class="kj-box">
|
||||
<ul class="clearfix">
|
||||
<li><dl><dt class="ball-green">27</dt><dd>兔<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">12</dt><dd>马<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">42</dt><dd>鼠<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">05</dt><dd>牛<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">23</dt><dd>羊<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">15</dt><dd>兔<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li class="kj-jia"><dl><dt></dt><dd></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">46</dt><dd>猴<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
</ul>
|
||||
</div><div class="kj-tit">香港⑥彩 第<font color="#fe0000">006</font>期 (2026年01月15日)</div>
|
||||
<div class="kj-box">
|
||||
<ul class="clearfix">
|
||||
<li><dl><dt class="ball-green">44</dt><dd>狗<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">06</dt><dd>鼠<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">37</dt><dd>蛇<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">03</dt><dd>兔<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">38</dt><dd>龙<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">39</dt><dd>兔<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li class="kj-jia"><dl><dt></dt><dd></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">48</dt><dd>马<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
</ul>
|
||||
</div><div class="kj-tit">香港⑥彩 第<font color="#fe0000">005</font>期 (2026年01月13日)</div>
|
||||
<div class="kj-box">
|
||||
<ul class="clearfix">
|
||||
<li><dl><dt class="ball-green">38</dt><dd>龙<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">48</dt><dd>马<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">14</dt><dd>龙<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">49</dt><dd>蛇<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">24</dt><dd>马<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">34</dt><dd>猴<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li class="kj-jia"><dl><dt></dt><dd></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">27</dt><dd>兔<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
</ul>
|
||||
</div><div class="kj-tit">香港⑥彩 第<font color="#fe0000">004</font>期 (2026年01月10日)</div>
|
||||
<div class="kj-box">
|
||||
<ul class="clearfix">
|
||||
<li><dl><dt class="ball-blue">37</dt><dd>蛇<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">20</dt><dd>狗<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">22</dt><dd>猴<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">24</dt><dd>马<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">16</dt><dd>虎<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">03</dt><dd>兔<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li class="kj-jia"><dl><dt></dt><dd></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">42</dt><dd>鼠<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
</ul>
|
||||
</div><div class="kj-tit">香港⑥彩 第<font color="#fe0000">003</font>期 (2026年01月08日)</div>
|
||||
<div class="kj-box">
|
||||
<ul class="clearfix">
|
||||
<li><dl><dt class="ball-red">45</dt><dd>鸡<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">15</dt><dd>兔<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">46</dt><dd>猴<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">21</dt><dd>鸡<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">24</dt><dd>马<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">40</dt><dd>虎<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li class="kj-jia"><dl><dt></dt><dd></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">13</dt><dd>蛇<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
</ul>
|
||||
</div><div class="kj-tit">香港⑥彩 第<font color="#fe0000">002</font>期 (2026年01月06日)</div>
|
||||
<div class="kj-box">
|
||||
<ul class="clearfix">
|
||||
<li><dl><dt class="ball-red">12</dt><dd>马<font class="grey-txt">/</font><font class="wx-jin">金</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">02</dt><dd>龙<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">08</dt><dd>狗<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">19</dt><dd>猪<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">28</dt><dd>虎<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">36</dt><dd>马<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li class="kj-jia"><dl><dt></dt><dd></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">01</dt><dd>蛇<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
</ul>
|
||||
</div><div class="kj-tit">香港⑥彩 第<font color="#fe0000">001</font>期 (2026年01月03日)</div>
|
||||
<div class="kj-box">
|
||||
<ul class="clearfix">
|
||||
<li><dl><dt class="ball-blue">20</dt><dd>狗<font class="grey-txt">/</font><font class="wx-tu">土</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">16</dt><dd>虎<font class="grey-txt">/</font><font class="wx-mu">木</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">02</dt><dd>龙<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-green">21</dt><dd>鸡<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-red">13</dt><dd>蛇<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">10</dt><dd>猴<font class="grey-txt">/</font><font class="wx-huo">火</font></dd></dl></li>
|
||||
<li class="kj-jia"><dl><dt></dt><dd></dd></dl></li>
|
||||
<li><dl><dt class="ball-blue">14</dt><dd>龙<font class="grey-txt">/</font><font class="wx-shui">水</font></dd></dl></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="cgi-foot">© 2026 Hong Kong Lotto Reserved.</footer>
|
||||
<div class="cgi-foot-blank"></div>
|
||||
</div>
|
||||
|
||||
|
||||
</body></html>
|
||||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -19,7 +19,7 @@ from pathlib import Path
|
||||
env_file = Path("/etc/sport-era-crawler/env.sh")
|
||||
with env_file.open("w", encoding="utf-8") as fp:
|
||||
for key, value in sorted(os.environ.items()):
|
||||
if key.startswith("DQD_") or key in {"TZ", "PYTHONPATH", "PYTHONUNBUFFERED"}:
|
||||
if key.startswith(("DQD_", "ODDS_", "HG_ODDS_", "TITAN_ODDS_")) or key in {"TZ", "PYTHONPATH", "PYTHONUNBUFFERED"}:
|
||||
fp.write(f"export {key}={shlex.quote(value)}\n")
|
||||
env_file.chmod(0o600)
|
||||
PY
|
||||
@@ -30,6 +30,8 @@
|
||||
python main.py nba_news # 采集NBA新闻资讯
|
||||
python main.py cba_news # 采集CBA新闻资讯
|
||||
python main.py ai_comment_dispatch # Docker AI 评论调度
|
||||
python main.py match_live_stream # 抓取赛事直播线路播放流
|
||||
python main.py match_odds # 抓取第三方赛事赔率
|
||||
python main.py article_content_fetch # 抓取资讯文章正文内容
|
||||
python main.py all # 采集积分榜 + 赛程
|
||||
python main.py single <season_id> # 采集单个联赛积分榜
|
||||
@@ -59,6 +61,8 @@ TASK_ALERT_POLICY = {
|
||||
"lottery_draw": "saved_if_candidates",
|
||||
"lottery_draw_force": "saved_if_candidates",
|
||||
"ai_comment_dispatch": "saved_if_candidates",
|
||||
"match_live_stream": "saved_if_candidates",
|
||||
"match_odds": "saved_if_candidates",
|
||||
}
|
||||
|
||||
DEFAULT_ACTION_TIMEOUT = 600
|
||||
@@ -86,6 +90,8 @@ ACTION_TIMEOUTS = {
|
||||
"fifa_worldcup_news": 600,
|
||||
"dqd_worldcup": 600,
|
||||
"ai_comment_dispatch": 1800,
|
||||
"match_live_stream": 180,
|
||||
"match_odds": 300,
|
||||
}
|
||||
|
||||
|
||||
@@ -522,6 +528,47 @@ async def cmd_ai_comment_dispatch():
|
||||
return await asyncio.to_thread(run, db_config)
|
||||
|
||||
|
||||
async def cmd_match_live_stream():
|
||||
import asyncio
|
||||
from match_live_stream import run
|
||||
from src.core.config import load_config
|
||||
|
||||
cfg = load_config().database
|
||||
db_config = {
|
||||
"host": cfg.host,
|
||||
"port": cfg.port,
|
||||
"user": cfg.username,
|
||||
"password": cfg.password,
|
||||
"database": cfg.database,
|
||||
"charset": cfg.charset,
|
||||
"prefix": cfg.prefix,
|
||||
}
|
||||
return await asyncio.to_thread(run, db_config)
|
||||
|
||||
|
||||
async def cmd_match_odds():
|
||||
import asyncio
|
||||
from match_odds import run
|
||||
from src.core.config import load_config
|
||||
|
||||
cfg = load_config().database
|
||||
db_config = {
|
||||
"host": cfg.host,
|
||||
"port": cfg.port,
|
||||
"user": cfg.username,
|
||||
"password": cfg.password,
|
||||
"database": cfg.database,
|
||||
"charset": cfg.charset,
|
||||
"prefix": cfg.prefix,
|
||||
}
|
||||
result = await asyncio.to_thread(run, db_config)
|
||||
prefix = "✅" if result.get("success") else "❌"
|
||||
print(f"{prefix} {result.get('summary_text', '第三方赛事赔率抓取完成')}")
|
||||
if result.get("error"):
|
||||
print(f" {result['error']}")
|
||||
return result
|
||||
|
||||
|
||||
async def cmd_match_data():
|
||||
from src.scheduler.task_runner import TaskRunner
|
||||
async with TaskRunner() as runner:
|
||||
@@ -837,6 +884,8 @@ def main():
|
||||
"nba_news": cmd_nba_news,
|
||||
"cba_news": cmd_cba_news,
|
||||
"ai_comment_dispatch": cmd_ai_comment_dispatch,
|
||||
"match_live_stream": cmd_match_live_stream,
|
||||
"match_odds": cmd_match_odds,
|
||||
"article_content_fetch": cmd_article_content_fetch,
|
||||
"match_finish": cmd_match_finish,
|
||||
"live_detail": cmd_live_detail,
|
||||
@@ -0,0 +1,487 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import pymysql
|
||||
import requests
|
||||
|
||||
|
||||
DETAIL_URL_TEMPLATE = "https://json.ncctrials.com/room/{room_id}/detail.json?v={ts}"
|
||||
LIVE_ROOMS_URL_TEMPLATE = "https://json.ncctrials.com/all_live_rooms.json?v={ts}"
|
||||
YUYAN_SOURCE_SITE = "yuyan"
|
||||
YUYAN_SOURCE_DOMAIN = "https://yuyantv.cn"
|
||||
YUYAN_ROOM_URL_TEMPLATE = YUYAN_SOURCE_DOMAIN + "/pages/liveRoom.html?roomNum={room_num}"
|
||||
FETCH_BATCH_SIZE = 30
|
||||
FETCH_TIMEOUT = 15
|
||||
FETCH_ERROR_LIMIT = 500
|
||||
ROOM_ID_RE = re.compile(r"(?:/room/|[?&]roomNum=)(\d+)")
|
||||
DETAIL_JSONP_RE = re.compile(r"detail\((\{.*\})\)\s*;?\s*$", re.S)
|
||||
LIVE_ROOMS_JSONP_RE = re.compile(r"all_live_rooms\((\{.*\})\)\s*;?\s*$", re.S)
|
||||
DEFAULT_HEADERS = {
|
||||
"User-Agent": (
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||
"Chrome/137.0.0.0 Safari/537.36"
|
||||
),
|
||||
"Accept": "application/json,text/javascript,*/*;q=0.1",
|
||||
"Referer": "https://yuyantv.cn/liveType.html",
|
||||
}
|
||||
|
||||
|
||||
def _should_use_internal_lock() -> bool:
|
||||
flag = str(os.environ.get("DQD_RUNNING_UNDER_TASK_RUNNER") or "").strip().lower()
|
||||
return flag not in {"1", "true", "yes", "on"}
|
||||
|
||||
|
||||
def parse_match_live_detail_html(body: str) -> dict:
|
||||
match = DETAIL_JSONP_RE.search((body or "").strip())
|
||||
if not match:
|
||||
raise ValueError("detail jsonp not found")
|
||||
|
||||
payload = json.loads(match.group(1))
|
||||
stream = payload.get("data", {}).get("stream", {})
|
||||
parsed = {
|
||||
"play_url_flv": str(stream.get("flv") or "").strip(),
|
||||
"play_url_hd_flv": str(stream.get("hdFlv") or "").strip(),
|
||||
"play_url_m3u8": str(stream.get("m3u8") or "").strip(),
|
||||
"play_url_hd_m3u8": str(stream.get("hdM3u8") or "").strip(),
|
||||
}
|
||||
if not any(parsed.values()):
|
||||
raise ValueError("stream urls empty")
|
||||
return parsed
|
||||
|
||||
|
||||
def parse_yuyan_live_rooms_jsonp(body: str) -> List[Dict[str, Any]]:
|
||||
match = LIVE_ROOMS_JSONP_RE.search((body or "").strip())
|
||||
if not match:
|
||||
raise ValueError("live rooms jsonp not found")
|
||||
|
||||
payload = json.loads(match.group(1))
|
||||
if _safe_int(payload.get("code"), default=0) != 200:
|
||||
raise ValueError(str(payload.get("msg") or "live rooms request failed"))
|
||||
|
||||
data = payload.get("data") or {}
|
||||
groups = data.values() if isinstance(data, dict) else data
|
||||
rooms: List[Dict[str, Any]] = []
|
||||
seen_room_nums = set()
|
||||
for group in groups:
|
||||
if not isinstance(group, list):
|
||||
continue
|
||||
for room in group:
|
||||
if not isinstance(room, dict):
|
||||
continue
|
||||
room_num = str(room.get("roomNum") or "").strip()
|
||||
if not room_num or room_num in seen_room_nums:
|
||||
continue
|
||||
if _safe_int(room.get("liveStatus"), default=1) != 1:
|
||||
continue
|
||||
normalized = dict(room)
|
||||
normalized["roomNum"] = room_num
|
||||
rooms.append(normalized)
|
||||
seen_room_nums.add(room_num)
|
||||
return rooms
|
||||
|
||||
|
||||
def choose_next_fetch_at(now_ts: int, has_existing_stream: bool) -> int:
|
||||
return int(now_ts) + (600 if has_existing_stream else 60)
|
||||
|
||||
|
||||
def _safe_int(value: Any, default: int = 0) -> int:
|
||||
try:
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
|
||||
def _trim_error(message: Any) -> str:
|
||||
return str(message or "").strip()[:FETCH_ERROR_LIMIT]
|
||||
|
||||
|
||||
def _normalize_text(value: Any) -> str:
|
||||
return re.sub(r"\s+", " ", str(value or "")).strip()
|
||||
|
||||
|
||||
def _limit_text(value: Any, limit: int) -> str:
|
||||
return _normalize_text(value)[:limit]
|
||||
|
||||
|
||||
def _extract_anchor_info(room: Dict[str, Any]) -> Dict[str, Any]:
|
||||
anchor = room.get("anchor") if isinstance(room.get("anchor"), dict) else {}
|
||||
grow_dto = anchor.get("growDto") if isinstance(anchor.get("growDto"), dict) else {}
|
||||
avatar = str(anchor.get("cutOutIcon") or anchor.get("icon") or "").strip()
|
||||
return {
|
||||
"anchor_id": _safe_int(anchor.get("uid"), default=0),
|
||||
"anchor_name": _limit_text(anchor.get("nickName"), 100),
|
||||
"anchor_avatar": avatar[:500],
|
||||
"anchor_level": _limit_text(grow_dto.get("name"), 50),
|
||||
"room_view_count": _safe_int(room.get("viewCount"), default=0),
|
||||
"room_focus_count": _safe_int(room.get("focusCount"), default=0),
|
||||
"room_notice": _limit_text(room.get("notice"), 500),
|
||||
"room_detail": _limit_text(room.get("detail"), 255),
|
||||
}
|
||||
|
||||
|
||||
def _build_yuyan_room_source_url(room_num: Any) -> str:
|
||||
return YUYAN_ROOM_URL_TEMPLATE.format(room_num=str(room_num or "").strip())
|
||||
|
||||
|
||||
def _derive_room_id(source_url: str) -> str:
|
||||
match = ROOM_ID_RE.search(str(source_url or "").strip())
|
||||
if not match:
|
||||
raise ValueError("room id not found in source_url")
|
||||
return match.group(1)
|
||||
|
||||
|
||||
def _build_detail_url(source_url: str, now_ts: int) -> str:
|
||||
room_id = _derive_room_id(source_url)
|
||||
return DETAIL_URL_TEMPLATE.format(room_id=room_id, ts=int(now_ts))
|
||||
|
||||
|
||||
def _has_existing_stream(row: Dict[str, Any]) -> bool:
|
||||
return any(
|
||||
str(row.get(field) or "").strip()
|
||||
for field in ("play_url_m3u8", "play_url_hd_m3u8", "play_url_flv", "play_url_hd_flv")
|
||||
)
|
||||
|
||||
|
||||
def _fetch_stream_payload(source_url: str, now_ts: int) -> Dict[str, str]:
|
||||
detail_url = _build_detail_url(source_url, now_ts)
|
||||
response = requests.get(detail_url, timeout=FETCH_TIMEOUT, headers=DEFAULT_HEADERS)
|
||||
response.raise_for_status()
|
||||
return parse_match_live_detail_html(response.text)
|
||||
|
||||
|
||||
def _fetch_yuyan_live_rooms(now_ts: int) -> List[Dict[str, Any]]:
|
||||
url = LIVE_ROOMS_URL_TEMPLATE.format(ts=int(now_ts))
|
||||
response = requests.get(url, timeout=FETCH_TIMEOUT, headers=DEFAULT_HEADERS)
|
||||
response.raise_for_status()
|
||||
return parse_yuyan_live_rooms_jsonp(response.text)
|
||||
|
||||
|
||||
def _build_run_result(
|
||||
candidate_count: int,
|
||||
success_count: int,
|
||||
failed_count: int,
|
||||
room_count: int = 0,
|
||||
upserted_room_count: int = 0,
|
||||
stale_room_count: int = 0,
|
||||
) -> Dict[str, Any]:
|
||||
summary_text = (
|
||||
f"赛事直播流与主播信息抓取完成: 同步直播间 {room_count} 个, 入库 {upserted_room_count} 个, 下播 {stale_room_count} 个, 待处理 {candidate_count} 条, "
|
||||
f"成功 {success_count} 条, 失败 {failed_count} 条"
|
||||
)
|
||||
result = {
|
||||
"success": failed_count == 0,
|
||||
"room_count": room_count,
|
||||
"upserted_room_count": upserted_room_count,
|
||||
"stale_room_count": stale_room_count,
|
||||
"candidate_count": candidate_count,
|
||||
"saved_count": success_count,
|
||||
"count": success_count,
|
||||
"failed_count": failed_count,
|
||||
"summary_text": summary_text,
|
||||
}
|
||||
if failed_count > 0:
|
||||
result["error"] = f"赛事直播流与主播信息抓取存在失败线路: 失败 {failed_count} 条"
|
||||
return result
|
||||
|
||||
|
||||
class TaskFileLock:
|
||||
def __init__(self, name: str, lock_dir: Path | None = None):
|
||||
safe_name = "".join(ch if ch.isalnum() or ch in ("-", "_") else "_" for ch in str(name or "task"))
|
||||
self.lock_dir = Path(lock_dir or os.environ.get("DQD_LOCK_DIR") or (Path(__file__).resolve().parent / "data" / "locks"))
|
||||
self.path = self.lock_dir / f"{safe_name}.lock"
|
||||
self._fp = None
|
||||
self._locked = False
|
||||
|
||||
def acquire(self) -> bool:
|
||||
self.lock_dir.mkdir(parents=True, exist_ok=True)
|
||||
self._fp = open(self.path, "a+b")
|
||||
try:
|
||||
self._fp.seek(0)
|
||||
if os.name == "nt":
|
||||
import msvcrt
|
||||
|
||||
msvcrt.locking(self._fp.fileno(), msvcrt.LK_NBLCK, 1)
|
||||
else:
|
||||
import fcntl
|
||||
|
||||
fcntl.flock(self._fp.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
self._locked = True
|
||||
self._fp.seek(0)
|
||||
self._fp.truncate()
|
||||
self._fp.write(f"pid={os.getpid()} started_at={int(time.time())}\n".encode("utf-8"))
|
||||
self._fp.flush()
|
||||
return True
|
||||
except (BlockingIOError, OSError):
|
||||
self.release()
|
||||
return False
|
||||
|
||||
def release(self) -> None:
|
||||
if not self._fp:
|
||||
return
|
||||
try:
|
||||
if self._locked and os.name == "nt":
|
||||
import msvcrt
|
||||
|
||||
self._fp.seek(0)
|
||||
msvcrt.locking(self._fp.fileno(), msvcrt.LK_UNLCK, 1)
|
||||
elif self._locked:
|
||||
import fcntl
|
||||
|
||||
fcntl.flock(self._fp.fileno(), fcntl.LOCK_UN)
|
||||
finally:
|
||||
self._fp.close()
|
||||
self._fp = None
|
||||
self._locked = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class MatchLiveStreamRepository:
|
||||
db_config: Dict[str, Any]
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.prefix = str(self.db_config.get("prefix") or "la_")
|
||||
self.conn = pymysql.connect(
|
||||
host=self.db_config["host"],
|
||||
port=int(self.db_config["port"]),
|
||||
user=self.db_config["user"],
|
||||
password=self.db_config["password"],
|
||||
database=self.db_config["database"],
|
||||
charset=self.db_config.get("charset") or "utf8mb4",
|
||||
cursorclass=pymysql.cursors.DictCursor,
|
||||
autocommit=False,
|
||||
)
|
||||
|
||||
def close(self) -> None:
|
||||
self.conn.close()
|
||||
|
||||
def load_due_rows(self, now_ts: int, limit: int = FETCH_BATCH_SIZE) -> List[Dict[str, Any]]:
|
||||
sql = (
|
||||
f"SELECT id, match_id, title, source_url, play_url_flv, play_url_hd_flv, play_url_m3u8, play_url_hd_m3u8 "
|
||||
f"FROM `{self.prefix}match_live` "
|
||||
f"WHERE `delete_time` IS NULL AND `next_fetch_at` <= %s AND `play_type` <> 'html' "
|
||||
f"ORDER BY `next_fetch_at` ASC, `id` ASC LIMIT %s"
|
||||
)
|
||||
with self.conn.cursor() as cur:
|
||||
cur.execute(sql, (int(now_ts), int(limit)))
|
||||
return list(cur.fetchall() or [])
|
||||
|
||||
def upsert_live_rooms(self, rooms: List[Dict[str, Any]], now_ts: int) -> int:
|
||||
sql = (
|
||||
f"INSERT INTO `{self.prefix}match_live` ("
|
||||
"`match_id`, `title`, `source_url`, `play_type`, `iframe_url`, "
|
||||
"`source_site`, `source_domain`, `source_match_key`, `source_line_key`, `source_is_hot`, `source_match_time`, "
|
||||
"`anchor_id`, `anchor_name`, `anchor_avatar`, `anchor_level`, "
|
||||
"`room_view_count`, `room_focus_count`, `room_notice`, `room_detail`, "
|
||||
"`play_url_m3u8`, `play_url_hd_m3u8`, `play_url_flv`, `play_url_hd_flv`, "
|
||||
"`fetch_status`, `fetch_error`, `last_fetch_at`, `next_fetch_at`, `create_time`, `update_time`, `delete_time`"
|
||||
") VALUES ("
|
||||
+ ", ".join(["%s"] * 30)
|
||||
+ ") ON DUPLICATE KEY UPDATE "
|
||||
"`title`=VALUES(`title`), "
|
||||
"`play_type`=VALUES(`play_type`), "
|
||||
"`iframe_url`=VALUES(`iframe_url`), "
|
||||
"`source_site`=VALUES(`source_site`), "
|
||||
"`source_domain`=VALUES(`source_domain`), "
|
||||
"`source_match_key`=VALUES(`source_match_key`), "
|
||||
"`source_line_key`=VALUES(`source_line_key`), "
|
||||
"`source_is_hot`=VALUES(`source_is_hot`), "
|
||||
"`source_match_time`=VALUES(`source_match_time`), "
|
||||
"`anchor_id`=VALUES(`anchor_id`), "
|
||||
"`anchor_name`=VALUES(`anchor_name`), "
|
||||
"`anchor_avatar`=VALUES(`anchor_avatar`), "
|
||||
"`anchor_level`=VALUES(`anchor_level`), "
|
||||
"`room_view_count`=VALUES(`room_view_count`), "
|
||||
"`room_focus_count`=VALUES(`room_focus_count`), "
|
||||
"`room_notice`=VALUES(`room_notice`), "
|
||||
"`room_detail`=VALUES(`room_detail`), "
|
||||
"`next_fetch_at`=VALUES(`next_fetch_at`), "
|
||||
"`update_time`=VALUES(`update_time`), "
|
||||
"`delete_time`=NULL"
|
||||
)
|
||||
saved_count = 0
|
||||
with self.conn.cursor() as cur:
|
||||
for room in rooms:
|
||||
room_num = str(room.get("roomNum") or "").strip()
|
||||
if not room_num:
|
||||
continue
|
||||
title = _limit_text(room.get("title"), 255) or f"雨燕直播间 {room_num}"
|
||||
anchor_info = _extract_anchor_info(room)
|
||||
params = (
|
||||
0,
|
||||
title,
|
||||
_build_yuyan_room_source_url(room_num),
|
||||
"stream",
|
||||
"",
|
||||
YUYAN_SOURCE_SITE,
|
||||
YUYAN_SOURCE_DOMAIN,
|
||||
f"room:{room_num}",
|
||||
room_num[:100],
|
||||
1 if _safe_int(room.get("markType"), default=0) > 0 else 0,
|
||||
0,
|
||||
anchor_info["anchor_id"],
|
||||
anchor_info["anchor_name"],
|
||||
anchor_info["anchor_avatar"],
|
||||
anchor_info["anchor_level"],
|
||||
anchor_info["room_view_count"],
|
||||
anchor_info["room_focus_count"],
|
||||
anchor_info["room_notice"],
|
||||
anchor_info["room_detail"],
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
0,
|
||||
"",
|
||||
0,
|
||||
int(now_ts),
|
||||
int(now_ts),
|
||||
int(now_ts),
|
||||
None,
|
||||
)
|
||||
cur.execute(sql, params)
|
||||
saved_count += 1
|
||||
if saved_count > 0:
|
||||
self.conn.commit()
|
||||
return saved_count
|
||||
|
||||
def cleanup_stale_live_rooms(self, active_room_nums: List[str], now_ts: int) -> int:
|
||||
active_room_nums = [str(room_num).strip() for room_num in active_room_nums if str(room_num or "").strip()]
|
||||
params: List[Any] = [
|
||||
"直播间已下播或不在雨燕直播列表",
|
||||
int(now_ts),
|
||||
choose_next_fetch_at(now_ts, has_existing_stream=True),
|
||||
int(now_ts),
|
||||
int(now_ts),
|
||||
YUYAN_SOURCE_SITE,
|
||||
]
|
||||
where = [
|
||||
"`delete_time` IS NULL",
|
||||
"`source_site`=%s",
|
||||
"`play_type`='stream'",
|
||||
]
|
||||
if active_room_nums:
|
||||
placeholders = ", ".join(["%s"] * len(active_room_nums))
|
||||
where.append(f"`source_line_key` NOT IN ({placeholders})")
|
||||
params.extend(active_room_nums)
|
||||
|
||||
sql = (
|
||||
f"UPDATE `{self.prefix}match_live` SET "
|
||||
"`fetch_status`=2, `fetch_error`=%s, `last_fetch_at`=%s, `next_fetch_at`=%s, "
|
||||
"`update_time`=%s, `delete_time`=%s "
|
||||
"WHERE " + " AND ".join(where)
|
||||
)
|
||||
with self.conn.cursor() as cur:
|
||||
cur.execute(sql, tuple(params))
|
||||
stale_count = int(getattr(cur, "rowcount", 0) or 0)
|
||||
if stale_count > 0:
|
||||
self.conn.commit()
|
||||
return stale_count
|
||||
|
||||
def mark_success(self, row_id: int, stream_urls: Dict[str, str], now_ts: int) -> None:
|
||||
sql = (
|
||||
f"UPDATE `{self.prefix}match_live` SET "
|
||||
"`play_url_flv`=%s, `play_url_hd_flv`=%s, `play_url_m3u8`=%s, `play_url_hd_m3u8`=%s, "
|
||||
"`fetch_status`=1, `fetch_error`='', `last_fetch_at`=%s, `next_fetch_at`=%s, `update_time`=%s "
|
||||
"WHERE `id`=%s"
|
||||
)
|
||||
next_fetch_at = choose_next_fetch_at(now_ts, has_existing_stream=True)
|
||||
with self.conn.cursor() as cur:
|
||||
cur.execute(
|
||||
sql,
|
||||
(
|
||||
stream_urls["play_url_flv"],
|
||||
stream_urls["play_url_hd_flv"],
|
||||
stream_urls["play_url_m3u8"],
|
||||
stream_urls["play_url_hd_m3u8"],
|
||||
int(now_ts),
|
||||
next_fetch_at,
|
||||
int(now_ts),
|
||||
int(row_id),
|
||||
),
|
||||
)
|
||||
self.conn.commit()
|
||||
|
||||
def mark_failed(self, row_id: int, error_message: str, now_ts: int, has_existing_stream: bool) -> None:
|
||||
sql = (
|
||||
f"UPDATE `{self.prefix}match_live` SET "
|
||||
"`fetch_status`=2, `fetch_error`=%s, `last_fetch_at`=%s, `next_fetch_at`=%s, `update_time`=%s "
|
||||
"WHERE `id`=%s"
|
||||
)
|
||||
with self.conn.cursor() as cur:
|
||||
cur.execute(
|
||||
sql,
|
||||
(
|
||||
_trim_error(error_message),
|
||||
int(now_ts),
|
||||
choose_next_fetch_at(now_ts, has_existing_stream=has_existing_stream),
|
||||
int(now_ts),
|
||||
int(row_id),
|
||||
),
|
||||
)
|
||||
self.conn.commit()
|
||||
|
||||
|
||||
def run(db_config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
lock = None
|
||||
if _should_use_internal_lock():
|
||||
lock = TaskFileLock("match_live_stream")
|
||||
if not lock.acquire():
|
||||
return {
|
||||
"success": True,
|
||||
"candidate_count": 0,
|
||||
"saved_count": 0,
|
||||
"summary_text": "match_live_stream 已在执行中,跳过本次",
|
||||
}
|
||||
|
||||
repo = MatchLiveStreamRepository(db_config)
|
||||
now_ts = int(time.time())
|
||||
success_count = 0
|
||||
failed_count = 0
|
||||
room_count = 0
|
||||
upserted_room_count = 0
|
||||
stale_room_count = 0
|
||||
rows: List[Dict[str, Any]] = []
|
||||
|
||||
try:
|
||||
live_rooms = _fetch_yuyan_live_rooms(now_ts)
|
||||
room_count = len(live_rooms)
|
||||
upserted_room_count = repo.upsert_live_rooms(live_rooms, now_ts)
|
||||
stale_room_count = repo.cleanup_stale_live_rooms(
|
||||
[str(room.get("roomNum") or "").strip() for room in live_rooms],
|
||||
now_ts,
|
||||
)
|
||||
fetch_limit = max(FETCH_BATCH_SIZE, room_count)
|
||||
rows = repo.load_due_rows(now_ts, limit=fetch_limit)
|
||||
for row in rows:
|
||||
fetch_ts = int(time.time())
|
||||
has_existing_stream = _has_existing_stream(row)
|
||||
try:
|
||||
stream_urls = _fetch_stream_payload(str(row.get("source_url") or ""), fetch_ts)
|
||||
repo.mark_success(_safe_int(row.get("id")), stream_urls, fetch_ts)
|
||||
success_count += 1
|
||||
except Exception as exc: # noqa: BLE001
|
||||
repo.mark_failed(_safe_int(row.get("id")), str(exc), fetch_ts, has_existing_stream)
|
||||
failed_count += 1
|
||||
|
||||
candidate_count = len(rows)
|
||||
return _build_run_result(
|
||||
candidate_count,
|
||||
success_count,
|
||||
failed_count,
|
||||
room_count=room_count,
|
||||
upserted_room_count=upserted_room_count,
|
||||
stale_room_count=stale_room_count,
|
||||
)
|
||||
finally:
|
||||
repo.close()
|
||||
if lock is not None:
|
||||
lock.release()
|
||||
@@ -0,0 +1,725 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Iterable, List, Optional
|
||||
from urllib.parse import urljoin
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
import pymysql
|
||||
import requests
|
||||
import urllib3
|
||||
|
||||
from titan_odds import (
|
||||
DEFAULT_TITAN_COMPANY_ID,
|
||||
DEFAULT_TITAN_COMPANY_NAME,
|
||||
DEFAULT_TITAN_CUP_URLS,
|
||||
DEFAULT_TITAN_EURO_COMPANY_ID,
|
||||
TitanOddsClient,
|
||||
)
|
||||
|
||||
|
||||
DEFAULT_BASE_URLS = [
|
||||
"https://205.201.0.120/",
|
||||
"https://mos022.com/",
|
||||
"https://hga039.com/",
|
||||
"https://mos100.com/",
|
||||
]
|
||||
DEFAULT_SPORT_TYPES = ["FT", "BK", "ES", "TN", "VB", "BM", "TT", "BS", "SK", "OP"]
|
||||
DEFAULT_SHOW_TYPES = ["live", "today", "early"]
|
||||
SHOW_TYPE_RTYPE = {
|
||||
"live": "rb",
|
||||
"today": "r",
|
||||
"early": "r",
|
||||
}
|
||||
DEFAULT_USER_AGENT = (
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||
"Chrome/137.0.0.0 Safari/537.36"
|
||||
)
|
||||
REQUEST_DELAY_SECONDS = 0.4
|
||||
|
||||
|
||||
class OddsGameListUnavailable(Exception):
|
||||
"""Raised when the account/menu combination has no accessible game list."""
|
||||
|
||||
|
||||
def _split_env_list(value: str, default: Iterable[str]) -> List[str]:
|
||||
if not str(value or "").strip():
|
||||
return list(default)
|
||||
parts = re.split(r"[\s,]+", str(value).strip())
|
||||
return [item.strip() for item in parts if item.strip()]
|
||||
|
||||
|
||||
def _normalize_base_url(url: str) -> str:
|
||||
normalized = str(url or "").strip()
|
||||
if not normalized:
|
||||
raise ValueError("empty odds base url")
|
||||
if not normalized.startswith(("http://", "https://")):
|
||||
normalized = "https://" + normalized
|
||||
return normalized.rstrip("/") + "/"
|
||||
|
||||
|
||||
def _bool_env(value: Any, default: bool = False) -> bool:
|
||||
if value is None or value == "":
|
||||
return default
|
||||
return str(value).strip().lower() in {"1", "true", "yes", "on", "y"}
|
||||
|
||||
|
||||
def _safe_int(value: Any, default: int = 0) -> int:
|
||||
try:
|
||||
return int(str(value).strip())
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
|
||||
def _safe_float(value: Any, default: float = 0.0) -> float:
|
||||
try:
|
||||
return float(str(value).strip())
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
|
||||
def _text(raw: Dict[str, str], key: str) -> str:
|
||||
return str(raw.get(key) or "").strip()
|
||||
|
||||
|
||||
def _limit(value: Any, size: int) -> str:
|
||||
return re.sub(r"\s+", " ", str(value or "")).strip()[:size]
|
||||
|
||||
|
||||
def _dump_json(value: Any) -> str:
|
||||
return json.dumps(value, ensure_ascii=False, separators=(",", ":"), default=str)
|
||||
|
||||
|
||||
def _extract_version(html: str) -> str:
|
||||
match = re.search(r"top\.ver\s*=\s*'([^']+)'", html or "")
|
||||
if not match:
|
||||
raise ValueError("odds site version not found")
|
||||
return match.group(1)
|
||||
|
||||
|
||||
def _parse_login_xml(body: str) -> Dict[str, str]:
|
||||
root = ET.fromstring((body or "").strip())
|
||||
status = (root.findtext("status") or "").strip()
|
||||
msg = (root.findtext("msg") or "").strip()
|
||||
uid = (root.findtext("uid") or "").strip()
|
||||
if status != "200" or not uid:
|
||||
code_message = (root.findtext("code_message") or root.findtext("msg") or "login failed").strip()
|
||||
raise ValueError(f"odds login failed: {code_message or msg or status}")
|
||||
return {child.tag: (child.text or "").strip() for child in list(root)}
|
||||
|
||||
|
||||
def _skippable_game_list_reason(body: str) -> str:
|
||||
prefix = re.sub(r"\s+", " ", str(body or "").strip())[:120]
|
||||
if prefix.startswith("CheckEMNU"):
|
||||
return "菜单不可用"
|
||||
if prefix.startswith("coupon_date error"):
|
||||
return "早盘日期不可用"
|
||||
return ""
|
||||
|
||||
|
||||
def _build_markets(raw: Dict[str, str]) -> Dict[str, Any]:
|
||||
return {
|
||||
"handicap": {
|
||||
"line": _text(raw, "RATIO_R"),
|
||||
"home": _text(raw, "IOR_RH"),
|
||||
"away": _text(raw, "IOR_RC"),
|
||||
"strong": _text(raw, "STRONG"),
|
||||
},
|
||||
"total": {
|
||||
"over_line": _text(raw, "RATIO_OUO"),
|
||||
"under_line": _text(raw, "RATIO_OUU"),
|
||||
"over": _text(raw, "IOR_OUC"),
|
||||
"under": _text(raw, "IOR_OUH"),
|
||||
},
|
||||
"moneyline": {
|
||||
"home": _text(raw, "IOR_MH"),
|
||||
"away": _text(raw, "IOR_MC"),
|
||||
"draw": _text(raw, "IOR_MN"),
|
||||
},
|
||||
"first_half": {
|
||||
"handicap_line": _text(raw, "RATIO_HR"),
|
||||
"handicap_home": _text(raw, "IOR_HRH"),
|
||||
"handicap_away": _text(raw, "IOR_HRC"),
|
||||
"total_over_line": _text(raw, "RATIO_HOUO"),
|
||||
"total_under_line": _text(raw, "RATIO_HOUU"),
|
||||
"total_over": _text(raw, "IOR_HOUC"),
|
||||
"total_under": _text(raw, "IOR_HOUH"),
|
||||
"home_win": _text(raw, "IOR_HMH"),
|
||||
"away_win": _text(raw, "IOR_HMC"),
|
||||
"draw": _text(raw, "IOR_HMN"),
|
||||
},
|
||||
"both_teams_to_score": {
|
||||
"yes": _text(raw, "IOR_TSY"),
|
||||
"no": _text(raw, "IOR_TSN"),
|
||||
},
|
||||
"kickoff": {
|
||||
"home": _text(raw, "IOR_TKH"),
|
||||
"away": _text(raw, "IOR_TKC"),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def parse_game_list_xml(
|
||||
body: str,
|
||||
*,
|
||||
source_site: str,
|
||||
source_url: str,
|
||||
sport_type: str,
|
||||
show_type: str,
|
||||
odds_type: str,
|
||||
) -> List[Dict[str, Any]]:
|
||||
root = ET.fromstring((body or "").strip())
|
||||
items: List[Dict[str, Any]] = []
|
||||
ec_nodes = root.findall(".//ec")
|
||||
if not ec_nodes:
|
||||
ec_nodes = [root]
|
||||
|
||||
for ec_node in ec_nodes:
|
||||
ec_id = str(ec_node.attrib.get("id") or "").replace("ec", "", 1)
|
||||
for game in ec_node.findall(".//game"):
|
||||
raw = {child.tag: (child.text or "").strip() for child in list(game)}
|
||||
source_match_id = _text(raw, "GID") or str(game.attrib.get("id") or "").strip()
|
||||
if not source_match_id:
|
||||
continue
|
||||
if not _text(raw, "ECID") and ec_id:
|
||||
raw["ECID"] = ec_id
|
||||
markets = _build_markets(raw)
|
||||
item = {
|
||||
"source_site": _limit(source_site, 50),
|
||||
"source_url": _limit(source_url, 255),
|
||||
"source_match_id": _limit(source_match_id, 64),
|
||||
"source_parent_id": _limit(_text(raw, "GIDM"), 64),
|
||||
"source_league_id": _limit(_text(raw, "LID"), 64),
|
||||
"source_event_id": _limit(_text(raw, "ECID") or ec_id, 64),
|
||||
"sport_type": _limit(str(sport_type).upper(), 20),
|
||||
"show_type": _limit(str(show_type).lower(), 20),
|
||||
"odds_type": _limit(str(odds_type).upper(), 20),
|
||||
"league_name": _limit(_text(raw, "LEAGUE"), 150),
|
||||
"match_time_text": _limit(_text(raw, "DATETIME"), 50),
|
||||
"home_team_id": _limit(_text(raw, "TEAM_H_ID"), 64),
|
||||
"away_team_id": _limit(_text(raw, "TEAM_C_ID"), 64),
|
||||
"home_team": _limit(_text(raw, "TEAM_H"), 150),
|
||||
"away_team": _limit(_text(raw, "TEAM_C"), 150),
|
||||
"strong_side": _limit(_text(raw, "STRONG"), 10),
|
||||
"more_count": _safe_int(_text(raw, "MORE")),
|
||||
"is_running": 1 if _text(raw, "IS_RB").upper() == "Y" or str(show_type).lower() == "live" else 0,
|
||||
"handicap_line": _limit(markets["handicap"]["line"], 32),
|
||||
"handicap_home_odds": _limit(markets["handicap"]["home"], 32),
|
||||
"handicap_away_odds": _limit(markets["handicap"]["away"], 32),
|
||||
"total_line": _limit(markets["total"]["over_line"], 32),
|
||||
"total_over_odds": _limit(markets["total"]["over"], 32),
|
||||
"total_under_odds": _limit(markets["total"]["under"], 32),
|
||||
"home_win_odds": _limit(markets["moneyline"]["home"], 32),
|
||||
"draw_odds": _limit(markets["moneyline"]["draw"], 32),
|
||||
"away_win_odds": _limit(markets["moneyline"]["away"], 32),
|
||||
"half_handicap_line": _limit(markets["first_half"]["handicap_line"], 32),
|
||||
"half_home_win_odds": _limit(markets["first_half"]["home_win"], 32),
|
||||
"half_draw_odds": _limit(markets["first_half"]["draw"], 32),
|
||||
"half_away_win_odds": _limit(markets["first_half"]["away_win"], 32),
|
||||
"raw_data": raw,
|
||||
"markets": markets,
|
||||
}
|
||||
items.append(item)
|
||||
return items
|
||||
|
||||
|
||||
@dataclass
|
||||
class OddsLoginSession:
|
||||
base_url: str
|
||||
transform_url: str
|
||||
version: str
|
||||
uid: str
|
||||
ltype: str
|
||||
odds_type: str
|
||||
|
||||
|
||||
class HgOddsClient:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
base_url: str,
|
||||
username: str,
|
||||
password: str,
|
||||
user_agent: str = DEFAULT_USER_AGENT,
|
||||
timeout: int = 20,
|
||||
verify_tls: bool = False,
|
||||
langx: str = "zh-cn",
|
||||
odds_type: str = "H",
|
||||
) -> None:
|
||||
self.base_url = _normalize_base_url(base_url)
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.user_agent = user_agent
|
||||
self.timeout = int(timeout)
|
||||
self.verify_tls = bool(verify_tls)
|
||||
self.langx = langx
|
||||
self.odds_type = odds_type
|
||||
self.session = requests.Session()
|
||||
self.session.verify = self.verify_tls
|
||||
self.session.headers.update(
|
||||
{
|
||||
"User-Agent": self.user_agent,
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
"Referer": self.base_url,
|
||||
}
|
||||
)
|
||||
if not self.verify_tls:
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
self.login_session: Optional[OddsLoginSession] = None
|
||||
|
||||
def login(self) -> OddsLoginSession:
|
||||
bootstrap = self.session.post(
|
||||
self.base_url,
|
||||
data={"detection": "Y", "sub_doubleLogin": "", "isapp": "", "q": "", "appversion": ""},
|
||||
timeout=self.timeout,
|
||||
)
|
||||
bootstrap.raise_for_status()
|
||||
version = _extract_version(bootstrap.text)
|
||||
transform_url = urljoin(self.base_url, "transform_nl.php") + f"?ver={version}"
|
||||
user_agent_b64 = base64.b64encode(self.user_agent.encode("utf-8")).decode("ascii")
|
||||
login_response = self.session.post(
|
||||
transform_url,
|
||||
data={
|
||||
"p": "chk_login",
|
||||
"langx": self.langx,
|
||||
"ver": version,
|
||||
"username": self.username,
|
||||
"password": self.password,
|
||||
"app": "N",
|
||||
"auto": "EDACFD",
|
||||
"userAgent": user_agent_b64,
|
||||
},
|
||||
timeout=self.timeout,
|
||||
)
|
||||
login_response.raise_for_status()
|
||||
parsed = _parse_login_xml(login_response.text)
|
||||
self.login_session = OddsLoginSession(
|
||||
base_url=self.base_url,
|
||||
transform_url=transform_url,
|
||||
version=version,
|
||||
uid=parsed["uid"],
|
||||
ltype=parsed.get("ltype") or "3",
|
||||
odds_type=parsed.get("odd_f_type") or self.odds_type,
|
||||
)
|
||||
return self.login_session
|
||||
|
||||
def fetch_game_list(self, sport_type: str, show_type: str) -> List[Dict[str, Any]]:
|
||||
login_session = self.login_session or self.login()
|
||||
normalized_show_type = str(show_type).lower()
|
||||
normalized_sport_type = str(sport_type).upper()
|
||||
now_ms = int(time.time() * 1000)
|
||||
response = self.session.post(
|
||||
login_session.transform_url,
|
||||
data={
|
||||
"uid": login_session.uid,
|
||||
"ver": login_session.version,
|
||||
"langx": self.langx,
|
||||
"p": "get_game_list",
|
||||
"p3type": "",
|
||||
"date": "",
|
||||
"gtype": normalized_sport_type.lower(),
|
||||
"showtype": normalized_show_type,
|
||||
"rtype": SHOW_TYPE_RTYPE.get(normalized_show_type, "r"),
|
||||
"ltype": login_session.ltype,
|
||||
"filter": normalized_sport_type,
|
||||
"cupFantasy": "N",
|
||||
"sorttype": "L",
|
||||
"specialClick": "",
|
||||
"isFantasy": "N",
|
||||
"ts": str(now_ms),
|
||||
"chgSortTS": str(now_ms),
|
||||
},
|
||||
timeout=self.timeout,
|
||||
)
|
||||
response.raise_for_status()
|
||||
body = response.text or ""
|
||||
skip_reason = _skippable_game_list_reason(body)
|
||||
if skip_reason:
|
||||
raise OddsGameListUnavailable(skip_reason)
|
||||
if not body.lstrip().startswith("<"):
|
||||
prefix = re.sub(r"\s+", " ", body.strip())[:120]
|
||||
raise ValueError(f"unexpected odds response: {prefix}")
|
||||
return parse_game_list_xml(
|
||||
body,
|
||||
source_site="hg",
|
||||
source_url=login_session.base_url,
|
||||
sport_type=normalized_sport_type,
|
||||
show_type=normalized_show_type,
|
||||
odds_type=login_session.odds_type,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class MatchOddsRepository:
|
||||
db_config: Dict[str, Any]
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.prefix = str(self.db_config.get("prefix") or "la_")
|
||||
self.conn = pymysql.connect(
|
||||
host=self.db_config["host"],
|
||||
port=int(self.db_config["port"]),
|
||||
user=self.db_config["user"],
|
||||
password=self.db_config["password"],
|
||||
database=self.db_config["database"],
|
||||
charset=self.db_config.get("charset") or "utf8mb4",
|
||||
cursorclass=pymysql.cursors.DictCursor,
|
||||
autocommit=False,
|
||||
)
|
||||
|
||||
def close(self) -> None:
|
||||
self.conn.close()
|
||||
|
||||
def ensure_table(self) -> None:
|
||||
sql = f"""
|
||||
CREATE TABLE IF NOT EXISTS `{self.prefix}match_odds` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`source_site` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '赔率来源',
|
||||
`source_url` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '来源线路',
|
||||
`source_match_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '来源比赛ID',
|
||||
`source_parent_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '来源主比赛ID',
|
||||
`source_league_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '来源联赛ID',
|
||||
`source_event_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '来源事件ID',
|
||||
`sport_type` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '运动类型',
|
||||
`show_type` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '盘口类型 live/today/early',
|
||||
`odds_type` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '赔率格式',
|
||||
`league_name` VARCHAR(150) NOT NULL DEFAULT '' COMMENT '联赛名称',
|
||||
`match_time_text` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '来源比赛时间文本',
|
||||
`home_team_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '主队来源ID',
|
||||
`away_team_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '客队来源ID',
|
||||
`home_team` VARCHAR(150) NOT NULL DEFAULT '' COMMENT '主队名称',
|
||||
`away_team` VARCHAR(150) NOT NULL DEFAULT '' COMMENT '客队名称',
|
||||
`strong_side` VARCHAR(10) NOT NULL DEFAULT '' COMMENT '让球强弱方',
|
||||
`more_count` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '更多玩法数量',
|
||||
`is_running` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '是否滚球',
|
||||
`handicap_line` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '让球盘口',
|
||||
`handicap_home_odds` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '主队让球赔率',
|
||||
`handicap_away_odds` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '客队让球赔率',
|
||||
`total_line` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '大小球盘口',
|
||||
`total_over_odds` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '大球赔率',
|
||||
`total_under_odds` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '小球赔率',
|
||||
`home_win_odds` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '主胜赔率',
|
||||
`draw_odds` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '平局赔率',
|
||||
`away_win_odds` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '客胜赔率',
|
||||
`half_handicap_line` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '上半场让球盘口',
|
||||
`half_home_win_odds` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '上半场主胜赔率',
|
||||
`half_draw_odds` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '上半场平局赔率',
|
||||
`half_away_win_odds` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '上半场客胜赔率',
|
||||
`create_time` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`raw_data` JSON NULL COMMENT '来源原始game节点',
|
||||
`markets_json` JSON NULL COMMENT '规范化盘口JSON',
|
||||
`update_time` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_source_match` (`source_site`, `source_match_id`),
|
||||
KEY `idx_sport_show` (`sport_type`, `show_type`),
|
||||
KEY `idx_league` (`source_league_id`),
|
||||
KEY `idx_update_time` (`update_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='第三方赛事赔率表'
|
||||
"""
|
||||
with self.conn.cursor() as cur:
|
||||
cur.execute(sql)
|
||||
self.conn.commit()
|
||||
|
||||
def upsert_latest(self, items: List[Dict[str, Any]], now_ts: Optional[int] = None) -> int:
|
||||
if not items:
|
||||
return 0
|
||||
now_ts = int(now_ts or time.time())
|
||||
sql = (
|
||||
f"INSERT INTO `{self.prefix}match_odds` ("
|
||||
"`source_site`, `source_url`, `source_match_id`, `source_parent_id`, `source_league_id`, `source_event_id`, "
|
||||
"`sport_type`, `show_type`, `odds_type`, `league_name`, `match_time_text`, "
|
||||
"`home_team_id`, `away_team_id`, `home_team`, `away_team`, `strong_side`, `more_count`, `is_running`, "
|
||||
"`handicap_line`, `handicap_home_odds`, `handicap_away_odds`, "
|
||||
"`total_line`, `total_over_odds`, `total_under_odds`, "
|
||||
"`home_win_odds`, `draw_odds`, `away_win_odds`, "
|
||||
"`half_handicap_line`, `half_home_win_odds`, `half_draw_odds`, `half_away_win_odds`, "
|
||||
"`create_time`, `raw_data`, `markets_json`, `update_time`"
|
||||
") VALUES ("
|
||||
+ ", ".join(["%s"] * 35)
|
||||
+ ") ON DUPLICATE KEY UPDATE "
|
||||
"`source_url`=VALUES(`source_url`), "
|
||||
"`source_parent_id`=VALUES(`source_parent_id`), "
|
||||
"`source_league_id`=VALUES(`source_league_id`), "
|
||||
"`source_event_id`=VALUES(`source_event_id`), "
|
||||
"`sport_type`=VALUES(`sport_type`), "
|
||||
"`show_type`=VALUES(`show_type`), "
|
||||
"`odds_type`=VALUES(`odds_type`), "
|
||||
"`league_name`=VALUES(`league_name`), "
|
||||
"`match_time_text`=VALUES(`match_time_text`), "
|
||||
"`home_team_id`=VALUES(`home_team_id`), "
|
||||
"`away_team_id`=VALUES(`away_team_id`), "
|
||||
"`home_team`=VALUES(`home_team`), "
|
||||
"`away_team`=VALUES(`away_team`), "
|
||||
"`strong_side`=VALUES(`strong_side`), "
|
||||
"`more_count`=VALUES(`more_count`), "
|
||||
"`is_running`=VALUES(`is_running`), "
|
||||
"`handicap_line`=VALUES(`handicap_line`), "
|
||||
"`handicap_home_odds`=VALUES(`handicap_home_odds`), "
|
||||
"`handicap_away_odds`=VALUES(`handicap_away_odds`), "
|
||||
"`total_line`=VALUES(`total_line`), "
|
||||
"`total_over_odds`=VALUES(`total_over_odds`), "
|
||||
"`total_under_odds`=VALUES(`total_under_odds`), "
|
||||
"`home_win_odds`=VALUES(`home_win_odds`), "
|
||||
"`draw_odds`=VALUES(`draw_odds`), "
|
||||
"`away_win_odds`=VALUES(`away_win_odds`), "
|
||||
"`half_handicap_line`=VALUES(`half_handicap_line`), "
|
||||
"`half_home_win_odds`=VALUES(`half_home_win_odds`), "
|
||||
"`half_draw_odds`=VALUES(`half_draw_odds`), "
|
||||
"`half_away_win_odds`=VALUES(`half_away_win_odds`), "
|
||||
"`raw_data`=VALUES(`raw_data`), "
|
||||
"`markets_json`=VALUES(`markets_json`), "
|
||||
"`update_time`=VALUES(`update_time`)"
|
||||
)
|
||||
saved_count = 0
|
||||
with self.conn.cursor() as cur:
|
||||
for item in items:
|
||||
params = (
|
||||
item["source_site"],
|
||||
item["source_url"],
|
||||
item["source_match_id"],
|
||||
item["source_parent_id"],
|
||||
item["source_league_id"],
|
||||
item["source_event_id"],
|
||||
item["sport_type"],
|
||||
item["show_type"],
|
||||
item["odds_type"],
|
||||
item["league_name"],
|
||||
item["match_time_text"],
|
||||
item["home_team_id"],
|
||||
item["away_team_id"],
|
||||
item["home_team"],
|
||||
item["away_team"],
|
||||
item["strong_side"],
|
||||
int(item["more_count"]),
|
||||
int(item["is_running"]),
|
||||
item["handicap_line"],
|
||||
item["handicap_home_odds"],
|
||||
item["handicap_away_odds"],
|
||||
item["total_line"],
|
||||
item["total_over_odds"],
|
||||
item["total_under_odds"],
|
||||
item["home_win_odds"],
|
||||
item["draw_odds"],
|
||||
item["away_win_odds"],
|
||||
item["half_handicap_line"],
|
||||
item["half_home_win_odds"],
|
||||
item["half_draw_odds"],
|
||||
item["half_away_win_odds"],
|
||||
now_ts,
|
||||
_dump_json(item["raw_data"]),
|
||||
_dump_json(item["markets"]),
|
||||
now_ts,
|
||||
)
|
||||
cur.execute(sql, params)
|
||||
saved_count += 1
|
||||
self.conn.commit()
|
||||
return saved_count
|
||||
|
||||
|
||||
def _build_result(
|
||||
*,
|
||||
success: bool,
|
||||
candidate_count: int,
|
||||
saved_count: int,
|
||||
source_url: str = "",
|
||||
skipped_count: int = 0,
|
||||
errors: Optional[List[str]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
errors = errors or []
|
||||
summary_text = (
|
||||
f"第三方赛事赔率抓取完成: 来源 {source_url or '-'}, 待写入 {candidate_count} 场, "
|
||||
f"入库 {saved_count} 场, 跳过 {skipped_count} 项, 失败 {len(errors)} 项"
|
||||
)
|
||||
result = {
|
||||
"success": bool(success),
|
||||
"candidate_count": int(candidate_count),
|
||||
"saved_count": int(saved_count),
|
||||
"count": int(saved_count),
|
||||
"source_url": source_url,
|
||||
"skipped_count": int(skipped_count),
|
||||
"failed_count": len(errors),
|
||||
"summary_text": summary_text,
|
||||
}
|
||||
if errors:
|
||||
result["error"] = "; ".join(errors[:5])
|
||||
return result
|
||||
|
||||
|
||||
def run(db_config: Dict[str, Any], env: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
|
||||
env = env if env is not None else os.environ
|
||||
items: List[Dict[str, Any]] = []
|
||||
fetch_errors: List[str] = []
|
||||
source_urls: List[str] = []
|
||||
successful_sources = 0
|
||||
skipped_count = 0
|
||||
|
||||
username = (env.get("ODDS_USERNAME") or env.get("HG_ODDS_USERNAME") or "").strip()
|
||||
password = (env.get("ODDS_PASSWORD") or env.get("HG_ODDS_PASSWORD") or "").strip()
|
||||
if username and password:
|
||||
base_urls = [
|
||||
_normalize_base_url(url)
|
||||
for url in _split_env_list(env.get("ODDS_BASE_URLS", ""), DEFAULT_BASE_URLS)
|
||||
]
|
||||
sport_types = [
|
||||
item.upper()
|
||||
for item in _split_env_list(env.get("ODDS_SPORT_TYPES", ""), DEFAULT_SPORT_TYPES)
|
||||
]
|
||||
show_types = [
|
||||
item.lower()
|
||||
for item in _split_env_list(env.get("ODDS_SHOW_TYPES", ""), DEFAULT_SHOW_TYPES)
|
||||
]
|
||||
timeout = _safe_int(env.get("ODDS_TIMEOUT"), 20)
|
||||
verify_tls = _bool_env(env.get("ODDS_VERIFY_TLS"), default=False)
|
||||
langx = (env.get("ODDS_LANGX") or "zh-cn").strip()
|
||||
odds_type = (env.get("ODDS_TYPE") or "H").strip().upper()
|
||||
request_delay = _safe_float(
|
||||
env.get("ODDS_REQUEST_DELAY_SECONDS"),
|
||||
REQUEST_DELAY_SECONDS,
|
||||
)
|
||||
|
||||
client: Optional[HgOddsClient] = None
|
||||
login_errors: List[str] = []
|
||||
for base_url in base_urls:
|
||||
try:
|
||||
candidate = HgOddsClient(
|
||||
base_url=base_url,
|
||||
username=username,
|
||||
password=password,
|
||||
timeout=timeout,
|
||||
verify_tls=verify_tls,
|
||||
langx=langx,
|
||||
odds_type=odds_type,
|
||||
)
|
||||
candidate.login()
|
||||
client = candidate
|
||||
break
|
||||
except Exception as exc: # noqa: BLE001
|
||||
login_errors.append(f"hg {base_url}: {exc}")
|
||||
|
||||
if client is None:
|
||||
fetch_errors.extend(login_errors)
|
||||
else:
|
||||
successful_sources += 1
|
||||
source_urls.append(client.base_url)
|
||||
for sport_type in sport_types:
|
||||
for show_type in show_types:
|
||||
try:
|
||||
items.extend(client.fetch_game_list(sport_type, show_type))
|
||||
except OddsGameListUnavailable:
|
||||
skipped_count += 1
|
||||
except Exception as exc: # noqa: BLE001
|
||||
fetch_errors.append(f"hg {sport_type}/{show_type}: {exc}")
|
||||
if request_delay > 0:
|
||||
time.sleep(request_delay)
|
||||
|
||||
titan_enabled = _bool_env(env.get("TITAN_ODDS_ENABLED"), default=True)
|
||||
if titan_enabled:
|
||||
titan_urls = [
|
||||
str(url).strip()
|
||||
for url in _split_env_list(
|
||||
env.get("TITAN_ODDS_URLS", ""),
|
||||
DEFAULT_TITAN_CUP_URLS,
|
||||
)
|
||||
]
|
||||
titan_client = TitanOddsClient(
|
||||
timeout=_safe_int(env.get("TITAN_ODDS_TIMEOUT"), 20),
|
||||
verify_tls=_bool_env(env.get("TITAN_ODDS_VERIFY_TLS"), default=True),
|
||||
company_id=_safe_int(
|
||||
env.get("TITAN_ODDS_COMPANY_ID"),
|
||||
DEFAULT_TITAN_COMPANY_ID,
|
||||
),
|
||||
euro_company_id=_safe_int(
|
||||
env.get("TITAN_ODDS_EURO_COMPANY_ID"),
|
||||
DEFAULT_TITAN_EURO_COMPANY_ID,
|
||||
),
|
||||
company_name=(
|
||||
env.get("TITAN_ODDS_COMPANY_NAME")
|
||||
or DEFAULT_TITAN_COMPANY_NAME
|
||||
).strip(),
|
||||
show_type=(env.get("TITAN_ODDS_SHOW_TYPE") or "early").strip().lower(),
|
||||
request_delay=_safe_float(
|
||||
env.get("TITAN_ODDS_REQUEST_DELAY_SECONDS"),
|
||||
0.2,
|
||||
),
|
||||
)
|
||||
for titan_url in titan_urls:
|
||||
try:
|
||||
items.extend(titan_client.fetch_cup(titan_url))
|
||||
successful_sources += 1
|
||||
source_urls.append(titan_url)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
fetch_errors.append(f"titan007 {titan_url}: {exc}")
|
||||
titan_request_delay = _safe_float(
|
||||
env.get("TITAN_ODDS_REQUEST_DELAY_SECONDS"),
|
||||
0.2,
|
||||
)
|
||||
if titan_request_delay > 0:
|
||||
time.sleep(titan_request_delay)
|
||||
|
||||
if successful_sources == 0:
|
||||
if not username or not password:
|
||||
fetch_errors.append("ODDS_USERNAME/ODDS_PASSWORD 未配置")
|
||||
return _build_result(
|
||||
success=False,
|
||||
candidate_count=len(items),
|
||||
saved_count=0,
|
||||
source_url=",".join(dict.fromkeys(source_urls)),
|
||||
skipped_count=skipped_count,
|
||||
errors=fetch_errors or ["没有可用的赔率来源"],
|
||||
)
|
||||
|
||||
repo = None
|
||||
source_url = ",".join(dict.fromkeys(source_urls))
|
||||
try:
|
||||
repo = MatchOddsRepository(db_config)
|
||||
repo.ensure_table()
|
||||
saved_count = repo.upsert_latest(items, now_ts=int(time.time()))
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return _build_result(
|
||||
success=False,
|
||||
candidate_count=len(items),
|
||||
saved_count=0,
|
||||
source_url=source_url,
|
||||
skipped_count=skipped_count,
|
||||
errors=fetch_errors + [f"database: {exc}"],
|
||||
)
|
||||
finally:
|
||||
if repo is not None:
|
||||
repo.close()
|
||||
|
||||
return _build_result(
|
||||
success=True,
|
||||
candidate_count=len(items),
|
||||
saved_count=saved_count,
|
||||
source_url=source_url,
|
||||
skipped_count=skipped_count,
|
||||
errors=fetch_errors,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from src.core.config import load_config
|
||||
|
||||
cfg = load_config().database
|
||||
print(
|
||||
json.dumps(
|
||||
run(
|
||||
{
|
||||
"host": cfg.host,
|
||||
"port": cfg.port,
|
||||
"user": cfg.username,
|
||||
"password": cfg.password,
|
||||
"database": cfg.database,
|
||||
"charset": cfg.charset,
|
||||
"prefix": cfg.prefix,
|
||||
}
|
||||
),
|
||||
ensure_ascii=False,
|
||||
)
|
||||
)
|
||||
@@ -259,6 +259,7 @@ def run_main_action(action: str, timeout: int, output_callback: Callable[[str],
|
||||
start = time.monotonic()
|
||||
env = os.environ.copy()
|
||||
env["PYTHONUNBUFFERED"] = "1"
|
||||
env["DQD_RUNNING_UNDER_TASK_RUNNER"] = "1"
|
||||
proc = subprocess.Popen(
|
||||
[sys.executable, "main.py", action],
|
||||
cwd=str(APP_DIR),
|
||||
@@ -0,0 +1,21 @@
|
||||
-- 爬虫错误日志表
|
||||
CREATE TABLE IF NOT EXISTS `la_crawl_error_log` (
|
||||
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
`task_name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '任务名称(schedule/standings/match_finish等)',
|
||||
`request_url` VARCHAR(1000) NOT NULL DEFAULT '' COMMENT '请求URL',
|
||||
`request_method` VARCHAR(10) NOT NULL DEFAULT 'GET' COMMENT '请求方法',
|
||||
`request_params` TEXT COMMENT '请求参数(JSON)',
|
||||
`http_status` INT NOT NULL DEFAULT 0 COMMENT 'HTTP状态码(0=网络异常)',
|
||||
`error_type` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '错误类型(NetworkError/404/timeout等)',
|
||||
`error_message` TEXT COMMENT '错误信息',
|
||||
`response_body` TEXT COMMENT '响应内容(截断)',
|
||||
`channel` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '请求通道(api/tls/playwright/page)',
|
||||
`notified` TINYINT NOT NULL DEFAULT 0 COMMENT '是否已发送邮件通知(0=未通知,1=已通知)',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
KEY `idx_notified` (`notified`),
|
||||
KEY `idx_task_created` (`task_name`, `created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='爬虫请求错误日志';
|
||||
|
||||
-- 定时任务:错误报告邮件(每5分钟)
|
||||
INSERT INTO `la_dev_crontab` (`name`, `type`, `command`, `params`, `status`, `expression`, `create_time`, `update_time`)
|
||||
VALUES ('爬虫错误报告邮件', 1, 'crawler', 'error_report', 1, '*/5 * * * *', UNIX_TIMESTAMP(), UNIX_TIMESTAMP());
|
||||
@@ -0,0 +1,20 @@
|
||||
CREATE TABLE IF NOT EXISTS `la_lottery_draw_result` (
|
||||
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
|
||||
`code` varchar(30) NOT NULL DEFAULT '' COMMENT '彩种编码(关联la_lottery_game.code)',
|
||||
`issue` varchar(30) NOT NULL DEFAULT '' COMMENT '期号',
|
||||
`draw_time` datetime DEFAULT NULL COMMENT '开奖时间',
|
||||
`draw_code` varchar(255) NOT NULL DEFAULT '' COMMENT '开奖号码(逗号分隔)',
|
||||
`draw_ext` varchar(500) NOT NULL DEFAULT '' COMMENT '扩展号码(台湾宾果等)',
|
||||
`status` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '开奖状态 1已开奖 4停售等',
|
||||
`idx` int unsigned NOT NULL DEFAULT 0 COMMENT '当日期数索引(index)',
|
||||
`next_issue` varchar(30) NOT NULL DEFAULT '' COMMENT '下期期号',
|
||||
`next_time` datetime DEFAULT NULL COMMENT '下期开奖时间',
|
||||
`trend` json DEFAULT NULL COMMENT '走势数据(JSON)',
|
||||
`s_time` datetime DEFAULT NULL COMMENT '数据同步时间(API sTime)',
|
||||
`create_time` int unsigned NOT NULL DEFAULT 0,
|
||||
`update_time` int unsigned NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_code_issue` (`code`, `issue`),
|
||||
KEY `idx_code_draw_time` (`code`, `draw_time`),
|
||||
KEY `idx_draw_time` (`draw_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='彩种开奖结果表';
|
||||
@@ -0,0 +1,2 @@
|
||||
INSERT INTO `la_dev_crontab` (`name`, `type`, `command`, `params`, `status`, `expression`, `create_time`, `update_time`)
|
||||
VALUES ('超时比赛收尾', 1, 'crawler', 'match_finish', 1, '*/1 * * * *', UNIX_TIMESTAMP(), UNIX_TIMESTAMP());
|
||||