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/
|
**/.hbuilderx/
|
||||||
**/unpackage/
|
**/unpackage/
|
||||||
qa/
|
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. 相同或相似的功能必须封装为可复用的方法,避免重复代码。
|
3. 相同或相似的功能必须封装为可复用的方法,避免重复代码。
|
||||||
4. 查询字段、路由、菜单、配置前,优先以当前项目代码和 `sbnews.sql` / `docs/sql/` 中结构为准;如需线上核对,再按用户要求使用可用 MCP 或指定连接方式。
|
4. 查询字段、路由、菜单、配置前,优先以当前项目代码和 `sbnews.sql` / `docs/sql/` 中结构为准;如需线上核对,再按用户要求使用可用 MCP 或指定连接方式。
|
||||||
5. 不通过 WSL sshpass/scp 上传代码到服务器;项目代码更新由用户自行通过 SFTP 等方式处理。
|
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` |
|
| uniapp | `npm run dev:h5` | `npm run build:h5` |
|
||||||
| pc | `npm run dev` | `npm run generate` / `npm run build` |
|
| pc | `npm run dev` | `npm run generate` / `npm run build` |
|
||||||
|
|
||||||
|
## 自动化执行边界
|
||||||
|
|
||||||
|
- 除非用户在当前任务中明确要求,否则不要自动执行构建、测试、提交代码或部署。
|
||||||
|
- 完成代码修改后,可以说明建议执行的验证命令或部署命令,但不得自行运行。
|
||||||
|
- 用户明确要求“构建”“测试”“提交”“部署”“上线”“发布”等操作时,才执行对应命令。
|
||||||
|
- 对已经由用户给出的命令,可按用户原命令执行;不要额外扩展到未要求的构建、测试、提交或部署步骤。
|
||||||
|
|
||||||
## 数据库结构基准
|
## 数据库结构基准
|
||||||
|
|
||||||
- 数据库表前缀:`la_`。
|
- 数据库表前缀:`la_`。
|
||||||
@@ -77,7 +84,7 @@
|
|||||||
|
|
||||||
## 代码修改后验证与评审
|
## 代码修改后验证与评审
|
||||||
|
|
||||||
除非用户特别说明不需要验证,代码修改完成后按改动类型选择验证方式:
|
仅在用户明确要求验证、测试或构建时,代码修改完成后按改动类型选择验证方式;未明确要求时只列出建议命令,不自动执行:
|
||||||
|
|
||||||
### 后端接口修改
|
### 后端接口修改
|
||||||
|
|
||||||
@@ -104,13 +111,24 @@
|
|||||||
| 5 | 命名一致性 | 字段、方法、路由命名与现有风格一致 |
|
| 5 | 命名一致性 | 字段、方法、路由命名与现有风格一致 |
|
||||||
| 6 | 安全性 | SQL 拼接、用户输入、权限校验是否安全 |
|
| 6 | 安全性 | SQL 拼接、用户输入、权限校验是否安全 |
|
||||||
| 7 | 幂等性 | 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、保存白名单和数据库列是否同步。
|
- `Validate::only()` 只限制校验字段范围,不会自动过滤请求参数;新增字段落库前,要同时检查 validate、logic、model、保存白名单和数据库列是否同步。
|
||||||
|
|||||||
@@ -14,11 +14,15 @@ Docker 只负责运行 Python 爬虫和容器内 cron。MySQL、Redis、Nginx
|
|||||||
配置源:
|
配置源:
|
||||||
|
|
||||||
- Docker Compose:`docker/docker-compose.crawler.yml`
|
- Docker Compose:`docker/docker-compose.crawler.yml`
|
||||||
|
- Compose 路径变量示例:`docker/.env.example`
|
||||||
- 环境变量示例:`docker/.env.docker.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 执行清单;如需临时停用某个 Docker 爬虫任务,将 `crawler_tasks.yaml` 对应任务的 `active` 改为 `false`,然后重建或重启 crawler 容器。
|
||||||
|
|
||||||
### 当前 Docker 定时任务列表
|
### 当前 Docker 定时任务列表
|
||||||
@@ -62,7 +66,7 @@ Docker 只负责运行 Python 爬虫和容器内 cron。MySQL、Redis、Nginx
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
ssh sbnews
|
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
|
```powershell
|
||||||
cd D:\www\sport-era
|
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` 读取该配置。
|
如果 `sbnews` 配置在 `C:\Users\Administrator\.ssh\config` 中,默认会使用 Windows 的 `ssh.exe` 读取该配置。
|
||||||
@@ -134,20 +138,29 @@ powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -Sudo -Remo
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
cd D:\www\sport-era
|
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
|
```text
|
||||||
server/app
|
server/
|
||||||
server/config
|
uniapp/
|
||||||
server/extend
|
admin/
|
||||||
server/public/dongqiudi-crawler
|
docker/
|
||||||
server/route
|
|
||||||
```
|
```
|
||||||
|
|
||||||
默认会排除运行态或较大目录,例如 `.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
|
```bash
|
||||||
--include-env
|
--include-env
|
||||||
@@ -164,14 +177,14 @@ server/route
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
cd D:\www\sport-era
|
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
|
```powershell
|
||||||
cd D:\www\sport-era
|
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
|
```powershell
|
||||||
cd D:\www\sport-era
|
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`:
|
如果需要一次发布最近 N 个提交的后端改动,可以追加 `-AutoIncrementalCommits <n>`,例如发布最近 3 个提交会对比 `HEAD~3` 和 `HEAD`:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
cd D:\www\sport-era
|
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
|
```powershell
|
||||||
cd D:\www\sport-era
|
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 的差异,不会包含工作区未提交文件。需要指定任意起止提交时,继续使用 `-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
|
```powershell
|
||||||
cd D:\www\sport-era
|
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 和默认打包目录。
|
脚本会在打包后打印 `update log`:增量模式包含 commit 范围、commit 摘要、变更文件和删除文件;全量模式包含当前最新 commit 和默认打包目录。
|
||||||
@@ -222,17 +235,37 @@ powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -RemoteDir
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
cd D:\www\sport-era
|
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>`。
|
1. `ssh: Could not resolve hostname sbnews`:检查 Windows 的 `C:\Users\Administrator\.ssh\config` 是否存在 `Host sbnews`,或直接用 `-HostName <服务器IP>`。
|
||||||
2. PowerShell 禁止执行脚本:使用示例中的 `-ExecutionPolicy Bypass -File`,或以管理员身份调整当前用户执行策略。
|
2. PowerShell 禁止执行脚本:使用示例中的 `-ExecutionPolicy Bypass -File`,或以管理员身份调整当前用户执行策略。
|
||||||
3. `tar: Cannot utime` 或 `Cannot change mode`:远端目录权限不足,请加 `-Sudo`。脚本会使用 `sudo tar` 覆盖解压,并默认跳过还原 owner/permission,减少权限冲突。
|
3. `tar: Cannot utime` 或 `Cannot change mode`:远端目录权限不足,请加 `-Sudo`。脚本会使用 `sudo tar` 覆盖解压,并默认跳过还原 owner/permission,减少权限冲突。
|
||||||
4. 增量发布提示 commit 无效:确认 commit id 属于当前根目录 Git 可识别的历史。
|
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 ...`。
|
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>
|
||||||
<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_LEVEL=INFO
|
||||||
DQD_LOG_FILE=/app/logs/crawler.log
|
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.
|
# AI KB worker embedding config. Keep real key in docker/.env.docker only.
|
||||||
EMBEDDING_API_KEY=change-me
|
EMBEDDING_API_KEY=change-me
|
||||||
EMBEDDING_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode
|
EMBEDDING_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
CRAWLER_DIR=./crawler
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
/data
|
/data
|
||||||
/log
|
/log
|
||||||
|
/logs
|
||||||
.env
|
.env
|
||||||
.env.docker
|
.env.docker
|
||||||
|
crawler/venv/
|
||||||
|
crawler/logs/
|
||||||
|
crawler/data/
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ __pycache__/
|
|||||||
**/__pycache__/
|
**/__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
logs/*
|
logs/
|
||||||
data/locks/*
|
data/
|
||||||
test_output.txt
|
test_output.txt
|
||||||
tmp_*.py
|
tmp_*.py
|
||||||
tmp_*.sql
|
tmp_*.sql
|
||||||
@@ -10,9 +10,9 @@ build/
|
|||||||
venv/
|
venv/
|
||||||
env/
|
env/
|
||||||
.env
|
.env
|
||||||
logs/*.log
|
logs/
|
||||||
data/*.json
|
data/
|
||||||
data/*.csv
|
test_output.txt
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
*.swp
|
*.swp
|
||||||
@@ -27,7 +27,8 @@ RUN python -m pip install --upgrade pip setuptools wheel \
|
|||||||
|
|
||||||
COPY . /app
|
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
|
&& chmod +x /app/docker/entrypoint.sh
|
||||||
|
|
||||||
ENTRYPOINT ["/app/docker/entrypoint.sh"]
|
ENTRYPOINT ["/app/docker/entrypoint.sh"]
|
||||||
@@ -68,7 +68,7 @@ python main.py cron
|
|||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
```
|
```
|
||||||
dongqiudi-crawler/
|
docker/crawler/
|
||||||
├── config/
|
├── config/
|
||||||
│ └── settings.yaml # 配置文件
|
│ └── settings.yaml # 配置文件
|
||||||
├── src/
|
├── src/
|
||||||
@@ -25,7 +25,40 @@ DEFAULT_REQUEST_TIMEOUT = 180
|
|||||||
DEFAULT_RETRY_TIMES = 2
|
DEFAULT_RETRY_TIMES = 2
|
||||||
DEFAULT_RETRY_SLEEP_MS = 800
|
DEFAULT_RETRY_SLEEP_MS = 800
|
||||||
BOT_ACCOUNT_PREFIX = "ai_comment_"
|
BOT_ACCOUNT_PREFIX = "ai_comment_"
|
||||||
BOT_NICKNAME_PREFIX = "AI评论员"
|
BOT_HANDLE_PREFIXES = [
|
||||||
|
"看台",
|
||||||
|
"深夜",
|
||||||
|
"边线",
|
||||||
|
"北区",
|
||||||
|
"南看台",
|
||||||
|
"东门",
|
||||||
|
"西街",
|
||||||
|
"慢热",
|
||||||
|
"小酒馆",
|
||||||
|
"补时",
|
||||||
|
"角旗",
|
||||||
|
"传中",
|
||||||
|
"临场",
|
||||||
|
"半场",
|
||||||
|
"球门后",
|
||||||
|
"旧球鞋",
|
||||||
|
"赛点",
|
||||||
|
"风向",
|
||||||
|
"追球",
|
||||||
|
"回防",
|
||||||
|
]
|
||||||
|
BOT_HANDLE_SUFFIXES = [
|
||||||
|
"阿泽",
|
||||||
|
"小顾",
|
||||||
|
"老韩",
|
||||||
|
"阿凯",
|
||||||
|
"小唐",
|
||||||
|
"阿林",
|
||||||
|
"阿松",
|
||||||
|
"阿北",
|
||||||
|
"阿岳",
|
||||||
|
"阿诚",
|
||||||
|
]
|
||||||
|
|
||||||
DEFAULT_ARTICLE_PROMPT = (
|
DEFAULT_ARTICLE_PROMPT = (
|
||||||
"你是一名专业、克制、真实的体育资讯评论员。请根据给定资讯内容生成1条中文评论,"
|
"你是一名专业、克制、真实的体育资讯评论员。请根据给定资讯内容生成1条中文评论,"
|
||||||
@@ -592,15 +625,10 @@ class AiCommentRepository:
|
|||||||
for index in range(1, target_count + 1):
|
for index in range(1, target_count + 1):
|
||||||
persona = self._persona_for_index(index)
|
persona = self._persona_for_index(index)
|
||||||
account = f"{BOT_ACCOUNT_PREFIX}{index:04d}"
|
account = f"{BOT_ACCOUNT_PREFIX}{index:04d}"
|
||||||
nickname = self._build_bot_nickname(index, persona["label"])
|
|
||||||
row = indexed.get(index)
|
row = indexed.get(index)
|
||||||
if row:
|
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
|
continue
|
||||||
|
nickname = self._build_bot_nickname(index)
|
||||||
password = self._create_password(self._random_password(), salt)
|
password = self._create_password(self._random_password(), salt)
|
||||||
sn = self._create_user_sn(cursor)
|
sn = self._create_user_sn(cursor)
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
@@ -618,7 +646,8 @@ class AiCommentRepository:
|
|||||||
for row in rows:
|
for row in rows:
|
||||||
index = self._parse_account_index(row["account"])
|
index = self._parse_account_index(row["account"])
|
||||||
persona = self._persona_for_index(index)
|
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)
|
self._virtual_users_cache = list(users)
|
||||||
return users[:target_count]
|
return users[:target_count]
|
||||||
|
|
||||||
@@ -991,9 +1020,11 @@ class AiCommentRepository:
|
|||||||
return hashlib.md5(str(random.random()).encode("utf-8")).hexdigest()[:12]
|
return hashlib.md5(str(random.random()).encode("utf-8")).hexdigest()[:12]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _build_bot_nickname(index: int, persona_label: str) -> str:
|
def _build_bot_nickname(index: int) -> str:
|
||||||
number = f"{index:02d}" if index < 100 else str(index)
|
zero_based = max(0, index - 1)
|
||||||
return f"{BOT_NICKNAME_PREFIX}{number}·{persona_label}"
|
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
|
@staticmethod
|
||||||
def _persona_for_index(index: int) -> Dict[str, str]:
|
def _persona_for_index(index: int) -> Dict[str, str]:
|
||||||
@@ -154,3 +154,13 @@ tasks:
|
|||||||
action: ai_comment_dispatch
|
action: ai_comment_dispatch
|
||||||
cron: "*/10 * * * *"
|
cron: "*/10 * * * *"
|
||||||
active: true
|
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")
|
env_file = Path("/etc/sport-era-crawler/env.sh")
|
||||||
with env_file.open("w", encoding="utf-8") as fp:
|
with env_file.open("w", encoding="utf-8") as fp:
|
||||||
for key, value in sorted(os.environ.items()):
|
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")
|
fp.write(f"export {key}={shlex.quote(value)}\n")
|
||||||
env_file.chmod(0o600)
|
env_file.chmod(0o600)
|
||||||
PY
|
PY
|
||||||
@@ -30,6 +30,8 @@
|
|||||||
python main.py nba_news # 采集NBA新闻资讯
|
python main.py nba_news # 采集NBA新闻资讯
|
||||||
python main.py cba_news # 采集CBA新闻资讯
|
python main.py cba_news # 采集CBA新闻资讯
|
||||||
python main.py ai_comment_dispatch # Docker AI 评论调度
|
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 article_content_fetch # 抓取资讯文章正文内容
|
||||||
python main.py all # 采集积分榜 + 赛程
|
python main.py all # 采集积分榜 + 赛程
|
||||||
python main.py single <season_id> # 采集单个联赛积分榜
|
python main.py single <season_id> # 采集单个联赛积分榜
|
||||||
@@ -59,6 +61,8 @@ TASK_ALERT_POLICY = {
|
|||||||
"lottery_draw": "saved_if_candidates",
|
"lottery_draw": "saved_if_candidates",
|
||||||
"lottery_draw_force": "saved_if_candidates",
|
"lottery_draw_force": "saved_if_candidates",
|
||||||
"ai_comment_dispatch": "saved_if_candidates",
|
"ai_comment_dispatch": "saved_if_candidates",
|
||||||
|
"match_live_stream": "saved_if_candidates",
|
||||||
|
"match_odds": "saved_if_candidates",
|
||||||
}
|
}
|
||||||
|
|
||||||
DEFAULT_ACTION_TIMEOUT = 600
|
DEFAULT_ACTION_TIMEOUT = 600
|
||||||
@@ -86,6 +90,8 @@ ACTION_TIMEOUTS = {
|
|||||||
"fifa_worldcup_news": 600,
|
"fifa_worldcup_news": 600,
|
||||||
"dqd_worldcup": 600,
|
"dqd_worldcup": 600,
|
||||||
"ai_comment_dispatch": 1800,
|
"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)
|
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():
|
async def cmd_match_data():
|
||||||
from src.scheduler.task_runner import TaskRunner
|
from src.scheduler.task_runner import TaskRunner
|
||||||
async with TaskRunner() as runner:
|
async with TaskRunner() as runner:
|
||||||
@@ -837,6 +884,8 @@ def main():
|
|||||||
"nba_news": cmd_nba_news,
|
"nba_news": cmd_nba_news,
|
||||||
"cba_news": cmd_cba_news,
|
"cba_news": cmd_cba_news,
|
||||||
"ai_comment_dispatch": cmd_ai_comment_dispatch,
|
"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,
|
"article_content_fetch": cmd_article_content_fetch,
|
||||||
"match_finish": cmd_match_finish,
|
"match_finish": cmd_match_finish,
|
||||||
"live_detail": cmd_live_detail,
|
"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()
|
start = time.monotonic()
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
env["PYTHONUNBUFFERED"] = "1"
|
env["PYTHONUNBUFFERED"] = "1"
|
||||||
|
env["DQD_RUNNING_UNDER_TASK_RUNNER"] = "1"
|
||||||
proc = subprocess.Popen(
|
proc = subprocess.Popen(
|
||||||
[sys.executable, "main.py", action],
|
[sys.executable, "main.py", action],
|
||||||
cwd=str(APP_DIR),
|
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());
|
||||||