fix: restore backend runtime entrypoints
Some checks failed
2026 World Cup Quant Platform - Production Deployment / Code Quality, Security Gate & Testing (push) Failing after 2m44s
2026 World Cup Quant Platform - Production Deployment / Deploy to Production VM via Gitea CD (push) Has been skipped

This commit is contained in:
wooo
2026-06-18 13:00:28 +08:00
parent 3b9b8047e4
commit 489baecbe8
6 changed files with 423 additions and 0 deletions

View File

@@ -68,6 +68,29 @@ jobs:
python -m compileall -q platform/backend/app
fi
- name: Validate Backend Runtime Entrypoints
env:
PYTHONPATH: platform/backend
DATABASE_URL: postgresql+asyncpg://fifa_user:ci-placeholder-db-password@127.0.0.1:5432/fifa2026
REDIS_URL: redis://:ci-placeholder-redis-password@127.0.0.1:6379/0
run: |
python - <<'PY'
import importlib
modules = [
'app.main',
'app.analytics.worldcup_seed',
'app.analytics.crawler',
'app.analytics.news_worker',
'app.analytics.agent_review_worker',
'app.analytics.daily_card_calendar_worker',
'app.analytics.fixtures_worker',
]
for module in modules:
importlib.import_module(module)
print(f'OK import {module}')
PY
- name: Setup Node.js Environment
uses: actions/setup-node@v4
with:

View File

@@ -0,0 +1,55 @@
"""AI review worker placeholder with guarded cost-aware status."""
from __future__ import annotations
import asyncio
import json
import logging
import os
from datetime import datetime, timezone
from typing import Any
from redis.asyncio import Redis
LOGGER = logging.getLogger("fifa2026-agent-review-worker")
logging.basicConfig(level=logging.INFO)
REDIS_URL = os.getenv("REDIS_URL", "redis://fifa2026-redis:6379/0")
INTERVAL_SECONDS = max(300, int(os.getenv("AGENT_REVIEW_POLL_INTERVAL_SECONDS", "1800")))
STATUS_KEY = "ingestion:agent-review:last_run"
async def publish_status(payload: dict[str, Any]) -> None:
redis = Redis.from_url(REDIS_URL, decode_responses=True)
try:
await redis.set(STATUS_KEY, json.dumps(payload, ensure_ascii=False), ex=max(INTERVAL_SECONDS * 3, 900))
finally:
await redis.aclose()
async def run_once() -> dict[str, Any]:
payload = {
"status": "standby",
"worker": "agent_review_worker",
"run_at": datetime.now(timezone.utc).isoformat(),
"message": "AI 驗證器等待 Gemini/NemoTron 安全金鑰與成本閘門設定;目前不呼叫外部模型。",
}
await publish_status(payload)
LOGGER.info("%s", payload)
return payload
async def run_forever() -> None:
LOGGER.info("啟動 agent_review_workerinterval=%ss", INTERVAL_SECONDS)
while True:
try:
await run_once()
except Exception as exc: # pragma: no cover - worker loop must survive transient Redis errors
LOGGER.exception("agent_review_worker 本輪狀態寫入失敗:%s", exc)
await asyncio.sleep(INTERVAL_SECONDS)
if __name__ == "__main__":
if os.getenv("WORKER_ONCE") == "true":
print(asyncio.run(run_once()))
else:
asyncio.run(run_forever())

View File

@@ -0,0 +1,55 @@
"""Daily card calendar cache worker placeholder."""
from __future__ import annotations
import asyncio
import json
import logging
import os
from datetime import datetime, timezone
from typing import Any
from redis.asyncio import Redis
LOGGER = logging.getLogger("fifa2026-calendar-cache-worker")
logging.basicConfig(level=logging.INFO)
REDIS_URL = os.getenv("REDIS_URL", "redis://fifa2026-redis:6379/0")
INTERVAL_SECONDS = max(60, int(os.getenv("DAILY_CARD_CALENDAR_POLL_INTERVAL_SECONDS", "300")))
STATUS_KEY = "ingestion:daily-card-calendar:last_run"
async def publish_status(payload: dict[str, Any]) -> None:
redis = Redis.from_url(REDIS_URL, decode_responses=True)
try:
await redis.set(STATUS_KEY, json.dumps(payload, ensure_ascii=False), ex=max(INTERVAL_SECONDS * 3, 900))
finally:
await redis.aclose()
async def run_once() -> dict[str, Any]:
payload = {
"status": "standby",
"worker": "daily_card_calendar_worker",
"run_at": datetime.now(timezone.utc).isoformat(),
"message": "每日作戰室日期快取等待正式賽程來源回補;目前只回報排程健康狀態。",
}
await publish_status(payload)
LOGGER.info("%s", payload)
return payload
async def run_forever() -> None:
LOGGER.info("啟動 daily_card_calendar_workerinterval=%ss", INTERVAL_SECONDS)
while True:
try:
await run_once()
except Exception as exc: # pragma: no cover - worker loop must survive transient Redis errors
LOGGER.exception("daily_card_calendar_worker 本輪狀態寫入失敗:%s", exc)
await asyncio.sleep(INTERVAL_SECONDS)
if __name__ == "__main__":
if os.getenv("WORKER_ONCE") == "true":
print(asyncio.run(run_once()))
else:
asyncio.run(run_forever())

View File

@@ -0,0 +1,55 @@
"""Fixtures ingestion worker placeholder."""
from __future__ import annotations
import asyncio
import json
import logging
import os
from datetime import datetime, timezone
from typing import Any
from redis.asyncio import Redis
LOGGER = logging.getLogger("fifa2026-fixtures-worker")
logging.basicConfig(level=logging.INFO)
REDIS_URL = os.getenv("REDIS_URL", "redis://fifa2026-redis:6379/0")
INTERVAL_SECONDS = max(300, int(os.getenv("FIXTURES_POLL_INTERVAL_SECONDS", "21600")))
STATUS_KEY = "ingestion:fixtures:last_run"
async def publish_status(payload: dict[str, Any]) -> None:
redis = Redis.from_url(REDIS_URL, decode_responses=True)
try:
await redis.set(STATUS_KEY, json.dumps(payload, ensure_ascii=False), ex=max(INTERVAL_SECONDS * 3, 900))
finally:
await redis.aclose()
async def run_once() -> dict[str, Any]:
payload = {
"status": "standby",
"worker": "fixtures_worker",
"run_at": datetime.now(timezone.utc).isoformat(),
"message": "賽程同步器已啟動;正式來源未驗證前不覆寫既有賽程。",
}
await publish_status(payload)
LOGGER.info("%s", payload)
return payload
async def run_forever() -> None:
LOGGER.info("啟動 fixtures_workerinterval=%ss", INTERVAL_SECONDS)
while True:
try:
await run_once()
except Exception as exc: # pragma: no cover - worker loop must survive transient Redis errors
LOGGER.exception("fixtures_worker 本輪狀態寫入失敗:%s", exc)
await asyncio.sleep(INTERVAL_SECONDS)
if __name__ == "__main__":
if os.getenv("WORKER_ONCE") == "true":
print(asyncio.run(run_once()))
else:
asyncio.run(run_forever())

View File

@@ -0,0 +1,180 @@
"""Traditional Chinese localization helpers for public API payloads."""
from __future__ import annotations
from typing import Any
TEAM_NAMES = {
'Argentina': '阿根廷',
'Australia': '澳洲',
'Austria': '奧地利',
'Belgium': '比利時',
'Brazil': '巴西',
'Canada': '加拿大',
'Cape Verde': '維德角',
'Colombia': '哥倫比亞',
'Croatia': '克羅埃西亞',
'Curaçao': '古拉索',
'Czech Republic': '捷克',
'DR Congo': '剛果民主共和國',
'Ecuador': '厄瓜多',
'Egypt': '埃及',
'England': '英格蘭',
'France': '法國',
'Germany': '德國',
'Ghana': '迦納',
'Iran': '伊朗',
'Iraq': '伊拉克',
'Japan': '日本',
'Jordan': '約旦',
'Mexico': '墨西哥',
'Netherlands': '荷蘭',
'New Zealand': '紐西蘭',
'Norway': '挪威',
'Panama': '巴拿馬',
'Portugal': '葡萄牙',
'Qatar': '卡達',
'Saudi Arabia': '沙烏地阿拉伯',
'Senegal': '塞內加爾',
'South Africa': '南非',
'South Korea': '南韓',
'Spain': '西班牙',
'Sweden': '瑞典',
'Tunisia': '突尼西亞',
'Türkiye': '土耳其',
'Turkey': '土耳其',
'United States': '美國',
'USA': '美國',
'Uruguay': '烏拉圭',
'Uzbekistan': '烏茲別克',
}
COUNTRIES = {
'United States': '美國',
'USA': '美國',
'Canada': '加拿大',
'Mexico': '墨西哥',
}
CITIES = {
'New York': '紐約',
'New Jersey': '紐澤西',
'Los Angeles': '洛杉磯',
'Dallas': '達拉斯',
'Houston': '休士頓',
'Kansas City': '堪薩斯城',
'Miami': '邁阿密',
'Philadelphia': '費城',
'Seattle': '西雅圖',
'Atlanta': '亞特蘭大',
'Boston': '波士頓',
'San Francisco': '舊金山',
'Toronto': '多倫多',
'Vancouver': '溫哥華',
'Mexico City': '墨西哥城',
'Guadalajara': '瓜達拉哈拉',
'Monterrey': '蒙特雷',
}
VENUES = {
'MetLife Stadium': '大都會人壽體育場',
'SoFi Stadium': 'SoFi 體育場',
'AT&T Stadium': 'AT&T 體育場',
'NRG Stadium': 'NRG 體育場',
'Hard Rock Stadium': '硬石體育場',
'Lumen Field': '流明球場',
'Mercedes-Benz Stadium': '梅賽德斯-賓士體育場',
'Lincoln Financial Field': '林肯金融球場',
'Gillette Stadium': '吉列體育場',
'Levi's Stadium': '李維斯體育場',
'BMO Field': 'BMO 球場',
'BC Place': '卑詩體育館',
'Estadio Azteca': '阿茲特克體育場',
}
MARKETS = {
'1x2': '勝平負',
'h2h': '勝平負',
'h2h_3_way': '勝平負',
'asian_handicap': '亞洲讓球',
'spreads': '讓球盤',
'alternate_spreads': '讓球變體盤',
'ou': '大小球',
'totals': '大小球',
'alternate_totals': '大小球變體盤',
'btts': '雙方進球',
'draw_no_bet': '平手退回',
'team_total': '球隊大小球',
'team_totals': '球隊大小球',
'correct_score': '正確比分',
}
SELECTIONS = {
'home': '主隊',
'away': '客隊',
'draw': '平手',
'tie': '平手',
'over': '大分',
'under': '小分',
'yes': '',
'no': '',
}
STATUSES = {
'pre-match': '未開賽',
'scheduled': '未開賽',
'upcoming': '未開賽',
'in-play': '進行中',
'live': '進行中',
'finished': '已完賽',
'final': '已完賽',
'postponed': '延期',
'cancelled': '取消',
'canceled': '取消',
}
def _clean(value: Any) -> str:
return str(value or '').strip()
def _lookup(value: Any, mapping: dict[str, str]) -> str:
text = _clean(value)
if not text:
return '待確認'
return mapping.get(text, mapping.get(text.lower(), text))
def localize_team_name(value: Any) -> str:
return _lookup(value, TEAM_NAMES)
def localize_country(value: Any) -> str:
return _lookup(value, COUNTRIES)
def localize_city(value: Any) -> str:
return _lookup(value, CITIES)
def localize_venue_name(value: Any) -> str:
return _lookup(value, VENUES)
def localize_market_type(value: Any) -> str:
return _lookup(value, MARKETS)
def localize_selection(value: Any) -> str:
text = _clean(value)
if not text:
return '待確認'
lowered = text.lower()
return SELECTIONS.get(lowered, TEAM_NAMES.get(text, text))
def localize_status(value: Any) -> str:
text = _clean(value)
if not text:
return '待確認'
return STATUSES.get(text.lower().replace('_', '-'), text)

View File

@@ -0,0 +1,55 @@
"""News ingestion worker placeholder with explicit freshness status."""
from __future__ import annotations
import asyncio
import json
import logging
import os
from datetime import datetime, timezone
from typing import Any
from redis.asyncio import Redis
LOGGER = logging.getLogger("fifa2026-news-worker")
logging.basicConfig(level=logging.INFO)
REDIS_URL = os.getenv("REDIS_URL", "redis://fifa2026-redis:6379/0")
INTERVAL_SECONDS = max(60, int(os.getenv("NEWS_POLL_INTERVAL_SECONDS", "900")))
STATUS_KEY = "ingestion:news:last_run"
async def publish_status(payload: dict[str, Any]) -> None:
redis = Redis.from_url(REDIS_URL, decode_responses=True)
try:
await redis.set(STATUS_KEY, json.dumps(payload, ensure_ascii=False), ex=max(INTERVAL_SECONDS * 3, 900))
finally:
await redis.aclose()
async def run_once() -> dict[str, Any]:
payload = {
"status": "standby",
"worker": "news_worker",
"run_at": datetime.now(timezone.utc).isoformat(),
"message": "新聞來源尚未設定為正式授權來源;目前只回報新鮮度,不產生假新聞。",
}
await publish_status(payload)
LOGGER.info("%s", payload)
return payload
async def run_forever() -> None:
LOGGER.info("啟動 news_workerinterval=%ss", INTERVAL_SECONDS)
while True:
try:
await run_once()
except Exception as exc: # pragma: no cover - worker loop must survive transient Redis errors
LOGGER.exception("news_worker 本輪狀態寫入失敗:%s", exc)
await asyncio.sleep(INTERVAL_SECONDS)
if __name__ == "__main__":
if os.getenv("WORKER_ONCE") == "true":
print(asyncio.run(run_once()))
else:
asyncio.run(run_forever())