mcpField
T
thscore-football-api

thscore-football-api

github.com/silkyland开发158 次安装34 周前更新
代码仓库

关于 thscore-football-api

thscore-football-api 是面向 THScore Football API 集成的开发技能,提供 TypeScript 类型、端点示例和实施规则。它涵盖实时比分、赛程、统计、赔率及历史数据,并强调缓存、错误处理和 API 密钥保护。

主要功能

  1. 01提供 TypeScript 类型和端点示例
  2. 02说明实时比分、赛程及比赛统计集成
  3. 03涵盖联赛资料、排名和射手榜
  4. 04介绍赔率及历史数据使用场景
  5. 05指导缓存、错误处理与密钥安全

适用场景

  1. 01构建 THScore API 客户端或 HTTP 封装
  2. 02开发实时比分、比赛动画或积分榜页面
  3. 03同步历史比赛数据并设计缓存
  4. 04排查 API 密钥、限流和响应格式问题

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/silkyland/football-api-skill --skill thscore-football-api

安装后在 Claude Code 中输入/thscore-football-api确认已加载。

附带文件types.ts

详细介绍

THScore Football API – 开发者技能

关于如何使用 THScore Football API 的全面最佳实践和参考指南。此技能提供 TypeScript 类型定义、端点存根、规则和完整代码示例,旨在让 AI Agent 辅助的集成更加可靠,满足生产环境要求。


适用场景

在以下情况下参考此技能:

  • 设置 THScore API 客户端或 HTTP 封装
  • 获取实时比分、比赛时间线或进行中比赛的统计数据
  • 获取联赛/杯赛参考数据(国家、联赛 ID、规则)
  • 展示足球实时动画(iFrame/WebView 嵌入)
  • 构建积分榜、最佳射手榜或 FIFA 排名页面
  • 集成亚洲让球盘、欧洲赔率或 1×2 赔率数据源
  • 构建定时任务,同步历史比赛数据
  • 设计体育数据的缓存层或数据库架构
  • 调试 API 密钥错误、超出速率限制或响应格式错误的问题

可用产品套餐和端点集

套餐API 数量主要功能
Live Data($599/月)22实时比分、赛程、赛事、阵容、实时文字、资料
Stats($399/月)13比赛分析、球员统计、积分榜、最佳射手、FIFA 排名
Odds—18 家博彩公司、亚洲让球盘、欧洲赔率、1×2 赔率
Odds Pro—200 多家欧洲博彩公司
Betfair—Betfair 交易所数据
Live Animation($1699/月)1iFrame/WebView 动画,42 种赛事
Historical Database—过往赛季、历史比赛数据
Common API∞所有付费套餐共用:联赛和杯赛资料、国家、博彩公司

基础 URL 和身份验证

http://www.thscore.info/<API_PATH>?api_key=<YOUR_API_KEY>

对于位于亚洲的服务端(延迟更低):

http://api2.thscore.info/<API_PATH>?api_key=<YOUR_API_KEY>

始终将 api_key 存储在环境变量中,切勿硬编码。


TypeScript 类型(types.ts)

// ==========================================================
// types.ts
// THScore Football API – Complete TypeScript Type Definitions
// ==========================================================

// ── Common / Shared ────────────────────────────────────────

/** Unix timestamp (seconds) */
type UnixTimestamp = number;

/** ISO 3166-1 alpha-2 country code */
type CountryCode = string;

/** Match state integer returned by Livescores */
export enum MatchState {
  NotStarted    = 0,
  FirstHalf     = 1,
  HalfTime      = 2,
  SecondHalf    = 3,
  ExtraTimeFirst = 4,
  ExtraTimeHT   = 5,
  ExtraTimeSecond = 6,
  PenaltyShootout = 7,
  Finished      = -1,
  Postponed     = -11,
  Cancelled     = -12,
  TBD           = -13,
  Abandoned     = -14,
  Suspended     = -15,
  Delayed       = -16,
  Interrupted   = -17,
  ExtraTime     = -18,
  Walkover      = -19,
}

/** Match state labels (use to display to end users) */
export const MatchStateLabel: Record<MatchState, string> = {
  [MatchState.NotStarted]:       "Not Started",
  [MatchState.FirstHalf]:        "1st Half",
  [MatchState.HalfTime]:         "Half Time",
  [MatchState.SecondHalf]:       "2nd Half",
  [MatchState.ExtraTimeFirst]:   "Extra Time 1st",
  [MatchState.ExtraTimeHT]:      "Extra Time HT",
  [MatchState.ExtraTimeSecond]:  "Extra Time 2nd",
  [MatchState.PenaltyShootout]:  "Penalties",
  [MatchState.Finished]:         "Finished",
  [MatchState.Postponed]:        "Postponed",
  [MatchState.Cancelled]:        "Cancelled",
  [MatchState.TBD]:              "TBD",
  [MatchState.Abandoned]:        "Abandoned",
  [MatchState.Suspended]:        "Suspended",
  [MatchState.Delayed]:          "Delayed",
  [MatchState.Interrupted]:      "Interrupted",
  [MatchState.ExtraTime]:        "Extra Time",
  [MatchState.Walkover]:         "Walkover",
};

// ── Common API ─────────────────────────────────────────────
// GET /football_th/league/basic.aspx (1 hr limit / 1 day recommended)

export interface LeagueBasic {
  leagueId:      number;
  leagueName:    string;
  leagueNameEn:  string;
  countryId:     number;
  countryName:   string;
  countryNameEn: string;
  leagueLogo:    string;  // URL
  countryLogo:   string;  // URL
  type:          number;  // 1 = League, 2 = Cup, 3 = International
  curSeasonId:   number;
}

export interface LeagueBasicResponse {
  code:    number;
  message: string;
  results: LeagueBasic[];
}

// GET /football_th/league.aspx (30 min limit / 1 day recommended)

export interface LeagueFull extends LeagueBasic {
  seasonId:     number;
  seasonName:   string;
  startDate:    string;   // "YYYY-MM-DD"
  endDate:      string;   // "YYYY-MM-DD"
  primaryColor: string;   // Hex
  secondaryColor: string; // Hex
  officialSite: string;   // URL
  totalTeams:   number;
}

// GET /football_th/league.aspx?cmd=rule
// Returns league rules (round-robin format, stages, etc.)

export interface LeagueRule {
  leagueId:   number;
  seasonId:   number;
  info:       string; // HTML or plain text rules
}

// ── Live Data Plan ─────────────────────────────────────────

// GET /football_th/livescores.aspx (10 sec limit / 1 min recommended)

export interface LiveMatch {
  matchId:      number;
  leagueId:     number;
  leagueName:   string;
  homeTeamId:   number;
  homeTeamName: string;
  awayTeamId:   number;
  awayTeamName: string;
  homeScore:    number;
  awayScore:    number;
  homeHalfScore: number;
  awayHalfScore: number;
  state:        MatchState;
  matchTime:    UnixTimestamp;  // UTC kickoff time
  elapsed:      number;         // minutes elapsed (0 if not started)
  homeRed:      number;
  awayRed:      number;
  homeYellow:   number;
  awayYellow:   number;
  homeCorner:   number;
  awayCorner:   number;
  round:        string;
  hasAnimation: boolean;        // true if live animation is available
  animationMatchId?: number;    // use in animation iFrame URL
}

export interface LivescoresResponse {
  code:    number;
  message: string;
  results: LiveMatch[];
}

// GET /football_th/schedule/results.aspx (60 sec limit / 6 hr recommended)

export interface ScheduledMatch {
  matchId:      number;
  leagueId:     number;
  leagueName:   string;
  homeTeamId:   number;
  homeTeamName: string;
  awayTeamId:   number;
  awayTeamName: string;
  homeScore:    number | null; // null = future match
  awayScore:    number | null;
  state:        MatchState;
  matchTime:    UnixTimestamp;
  round:        string;
}

// GET /football_th/matchevent.aspx (60 sec limit)
// Events include: goals, bookings, substitutions

export enum EventType {
  Goal        = 1,
  OwnGoal     = 2,
  YellowCard  = 3,
  RedCard     = 4,
  YellowRed   = 5,
  Substitution = 6,
  Penalty     = 7,
  MissedPenalty = 8,
}

export interface MatchEvent {
  matchId:   number;
  type:      EventType;
  minute:    number;
  addedTime: number;          // stoppage time minutes
  teamId:    number;
  playerId:  number;
  playerName: string;
  relatedPlayerId?:   number; // e.g. player who was substituted
  relatedPlayerName?: string;
}

// GET /football_th/matchlineup.aspx

export interface Player {
  playerId:   number;
  playerName: string;
  shirtNumber: number;
  position:   string;  // "GK" | "DF" | "MF" | "FW"
  isStarting: boolean;
  isCaptain:  boolean;
  countryCode: CountryCode;
}

export interface MatchLineup {
  matchId:      number;
  homeTeamId:   number;
  awayTeamId:   number;
  homeLineup:   Player[];
  awayLineup:   Player[];
  homeSubs:     Player[];
  awaySubs:     Player[];
  homeFormation: string;  // e.g. "4-3-3"
  awayFormation: string;
  homeCoach:    string;
  awayCoach:    string;
}

// ── Profile (Live Data Plan) ────────────────────────────────
// GET /football_th/league.aspx   → LeagueFull (see above)
// GET /football_th/team.aspx

export interface Team {
  teamId:      number;
  teamName:    string;
  teamNameEn:  string;
  teamLogo:    string;  // URL
  countryId:   number;
  countryName: string;
  stadium:     string;
  capacity:    number;
  founded:     number;  // Year
  website:     string;
}

// GET /football_th/player.aspx

export interface PlayerProfile {
  playerId:    number;
  playerName:  string;
  playerNameEn: string;
  photo:       string;  // URL
  nationality: CountryCode;
  position:    string;
  dateOfBirth: string;  // "YYYY-MM-DD"
  height:      number;  // cm
  weight:      number;  // kg
  currentTeamId: number;
}

// ── Stats Plan ─────────────────────────────────────────────
// GET /football_th/league/table.aspx (doc: /doc/docs_id=43)

export interface StandingRow {
  rank:          number;
  teamId:        number;
  teamName:      string;
  played:        number;
  won:           number;
  drawn:         number;
  lost:          number;
  goalsFor:      number;
  goalsAgainst:  number;
  goalDifference: number;
  points:        number;
  form:          string[];  // last 5 results: "W" | "D" | "L"
  winRate:       number;    // percentage 0–100
  drawRate:      number;
  loseRate:      number;
}

export interface StandingsResponse {
  code:      number;
  leagueId:  number;
  seasonId:  number;
  results:   StandingRow[];
}

// GET /football_th/topscorer.aspx (doc: /doc/docs_id=39)

export interface TopScorer {
  rank:       number;
  playerId:   number;
  playerName: string;
  teamId:     number;
  teamName:   string;
  goals:      number;
  fieldGoals: number;
  penalties:  number;
  assists:    number;
  appearances: number;
}

// GET /football_th/fifaranking.aspx (doc: /doc/docs_id=47)

export interface FifaRankingEntry {
  rank:          number;
  previousRank:  number;
  teamId:        number;
  teamName:      string;
  countryCode:   CountryCode;
  points:        number;
  previousPoints: number;
  rankChange:    number;  // positive = up, negative = down
}

// ── Odds ───────────────────────────────────────────────────

export interface AsianHandicapOdds {
  matchId:     number;
  bookmaker:   string;
  home:        number;  // odds for home side
  handicap:    number;  // e.g. -0.5
  away:        number;
  updatedAt:   UnixTimestamp;
}

export interface EuropeanOdds {
  matchId:   number;
  bookmaker: string;
  home:      number;
  draw:      number;
  away:      number;
  updatedAt: UnixTimestamp;
}

// ── Live Animation ─────────────────────────────────────────
// (no API response type—it is an iFrame/WebView URL)

export interface AnimationEmbedParams {
  matchId:   number;
  accessKey: string;
}

export function buildAnimationUrl(params: AnimationEmbedParams): string {
  return `https://www.isportslive8.com/football/detail.html?matchId=${params.matchId}&accessKey=${params.accessKey}`;
}

按优先级划分的规则类别

优先级类别前缀影响
1身份验证和路由auth-严重
2速率限制和缓存limit-严重
3实时动画——何时及如何使用anim-高
4特定端点的最佳实践ep-高
5错误处理和弹性error-高
6数据规范化和类型data-中
7安全性security-严重

规则

1. 身份验证和路由(严重)
auth-env-key – 将 API 密钥存储在环境变量中

❌ 错误:

const url = `http://www.thscore.info/football_th/livescores.aspx?api_key=abc123`;

✅ 正确:

const API_KEY = process.env.THSCORE_API_KEY;
if (!API_KEY) throw new Error("THSCORE_API_KEY is not set");
const url = `http://www.thscore.info/football_th/livescores.aspx?api_key=${API_KEY}`;
auth-asia-server – 亚洲地区的服务端使用 api2.thscore.info
// config.ts
export const THSCORE_HOST =
  process.env.THSCORE_REGION === "asia"
    ? "api2.thscore.info"
    : "www.thscore.info";

export function buildUrl(path: string, params: Record<string, string> = {}): string {
  const base = `http://${THSCORE_HOST}${path}`;
  const query = new URLSearchParams({
    api_key: process.env.THSCORE_API_KEY!,
    ...params,
  });
  return `${base}?${query.toString()}`;
}

2. 速率限制和缓存(严重)
端点硬性限制建议
Livescores for Today每 10 秒/次调用每 1 分钟/次调用
Schedule & Results每 60 秒/次调用每 6 小时/次调用
League & Cup Profile (Basic)每 1 小时/次调用每 1 天/次调用
League & Cup Profile (Full)每 30 分钟/次调用每 1 天/次调用
Standings—每 1 小时/次调用
Top Scorers—每 1 天/次调用
FIFA Ranking—每 1 周/次调用
limit-tiered-cache – 根据端点类型使用合适的 TTL
// cache.service.ts (using Redis)
import { Redis } from "ioredis";
const redis = new Redis(process.env.REDIS_URL!);

async function cachedFetch<T>(key: string, ttlSeconds: number, fetcher: () => Promise<T>): Promise<T> {
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached) as T;

  const data = await fetcher();
  await redis.setex(key, ttlSeconds, JSON.stringify(data));
  return data;
}

// Usage examples:
// Livescores — cache 60 seconds
const livescores = await cachedFetch("thscore:livescores", 60, fetchLivescores);

// League list — cache 24 hours
const leagues = await cachedFetch("thscore:leagues", 86400, fetchLeagues);

// Standings — cache 1 hour
const standings = await cachedFetch(`thscore:standings:${leagueId}`, 3600, () => fetchStandings(leagueId));
limit-background-sync – 切勿在 HTTP 请求处理程序中轮询 API
// BAD: polling inside a route handler
app.get("/api/livescores", async (req, res) => {
  const data = await fetch(buildUrl("/football_th/livescores.aspx")); // ← blocks users
  res.json(await data.json());
});

// GOOD: route handler reads from cache; a cron job populates it
app.get("/api/livescores", async (req, res) => {
  const cached = await redis.get("thscore:livescores");
  res.json(cached ? JSON.parse(cached) : { code: 503, message: "Data not ready yet" });
});

// cron job (every 60 seconds)
cron.schedule("* * * * *", async () => {
  const data = await fetchLivescores();
  await redis.setex("thscore:livescores", 90, JSON.stringify(data));
});

3. 实时动画——何时及如何使用(高)
anim-when – 实时动画数据的可用时机

实时动画仅在比赛进行中且处于以下某种活跃状态时可用:

import { MatchState } from "./types";

const ANIMATION_STATES: MatchState[] = [
  MatchState.FirstHalf,
  MatchState.HalfTime,
  MatchState.SecondHalf,
  MatchState.ExtraTimeFirst,
  MatchState.ExtraTimeHT,
  MatchState.ExtraTimeSecond,
  MatchState.PenaltyShootout,
];

export function isAnimationAvailable(match: LiveMatch): boolean {
  return ANIMATION_STATES.includes(match.state) && match.hasAnimation === true;
}

检查 Livescores 响应中的 hasAnimation: true。不要为已安排或已结束的比赛嵌入 iFrame——动画 URL 将返回空白页。

anim-embed-html – 在页面中嵌入动画(PC/H5 使用 iFrame)

<div id="animation-container" style="display:none">
  <iframe
    id="match-animation"
    src=""
    width="100%"
    height="480"
    frameborder="0"
    allowfullscreen
    loading="lazy"
  ></iframe>
</div>
// match-detail.ts
import { buildAnimationUrl, isAnimationAvailable, LiveMatch } from "./types";

function renderAnimation(match: LiveMatch): void {
  const container = document.getElementById("animation-container")!;
  const iframe = document.getElementById("match-animation") as HTMLIFrameElement;

  if (!isAnimationAvailable(match)) {
    container.style.display = "none";
    return;
  }

  const url = buildAnimationUrl({
    matchId:   match.animationMatchId ?? match.matchId,
    accessKey: process.env.THSCORE_ANIMATION_ACCESS_KEY!,
  });

  iframe.src = url;
  container.style.display = "block";
}
anim-webview-mobile – 在 WebView 中加载动画(React Native)
// AnimationPlayer.tsx
import React from "react";
import { WebView } from "react-native-webview";
import { buildAnimationUrl } from "../types";

interface Props {
  matchId: number;
  accessKey: string;
}

export const AnimationPlayer: React.FC<Props> = ({ matchId, accessKey }) => {
  const url = buildAnimationUrl({ matchId, accessKey });
  return (
    <WebView
      source={{ uri: url }}
      style={{ flex: 1 }}
      allowsFullscreenVideo
      mediaPlaybackRequiresUserAction={false}
    />
  );
};
anim-domain-whitelist – 在控制面板中注册域名
  • 通过 iFrame 嵌入前,请前往 控制台面板 → 动画 → 域名白名单。
  • 在使用 WebView 的 APP 平台上,无需添加白名单,可直接访问 URL。

4. 特定端点的最佳实践(高优先级)
ep-league-basic – 联赛与杯赛资料(基本)

端点: GET /football_th/league/basic.aspx 调用频率: 硬性限制为每小时 1 次 · 建议每天调用 1 次 套餐: 所有付费足球套餐(Stats、Live Data、Odds、Odds Pro、Betfair)

此端点返回的内容:

  • leagueId – 用于关联实时比分、赛程、积分榜和赔率数据。
  • leagueName / leagueNameEn – 泰语和英语显示名称。
  • countryId / countryName – 按国家对联赛分组。
  • leagueLogo / countryLogo – CDN 图片 URL。
  • type – 1 = 联赛,2 = 杯赛,3 = 国际赛事。
  • curSeasonId – 当前赛季;调用统计或积分榜端点时使用。
  • 覆盖 140 多个国家的 1,600 多个联赛和杯赛。

✅ 最佳实践:

// league-service.ts
import { LeagueBasic, LeagueBasicResponse } from "./types";
import { buildUrl, cachedFetch } from "./utils";

/** Fetch and cache the full league catalogue for 24 hours. */
export async function getLeagues(): Promise<LeagueBasic[]> {
  return cachedFetch<LeagueBasic[]>("thscore:leagues:basic", 86400, async () => {
    const res = await fetch(buildUrl("/football_th/league/basic.aspx"));
    const json: LeagueBasicResponse = await res.json();
    if (json.code !== 0) throw new Error(`API error: ${json.message}`);
    return json.results;
  });
}

/** Group leagues by country for a country-selector UI */
export function groupByCountry(leagues: LeagueBasic[]): Record<string, LeagueBasic[]> {
  return leagues.reduce((acc, league) => {
    const key = league.countryNameEn;
    if (!acc[key]) acc[key] = [];
    acc[key].push(league);
    return acc;
  }, {} as Record<string, LeagueBasic[]>);
}

/** Filter for Cups only */
export function getCups(leagues: LeagueBasic[]): LeagueBasic[] {
  return leagues.filter(l => l.type === 2);
}

/** Find league by name (case-insensitive) */
export function findLeague(leagues: LeagueBasic[], name: string): LeagueBasic | undefined {
  const q = name.toLowerCase();
  return leagues.find(l => l.leagueNameEn.toLowerCase().includes(q));
}
ep-league-full – 联赛与杯赛资料(完整)

端点: GET /football_th/league.aspx 调用频率: 硬性限制为每 30 分钟 1 次 · 建议每天调用 1 次 套餐: 仅限 Live Data

基本资料之外的其他字段:

  • seasonId、seasonName – 当前赛季详情(例如,“2024-2025”)
  • startDate、endDate – 赛季日期范围
  • primaryColor、secondaryColor – 用于 UI 主题的品牌颜色
  • totalTeams – 参赛球队数量
  • officialSite – 联赛官方网站

还支持: GET /football_th/league.aspx?cmd=rule
以纯文本/HTML 格式返回联赛规则,可用于联赛信息页面。

ep-livescores – 今日实时比分

端点: GET /football_th/livescores.aspx 调用频率: 硬性限制为每 10 秒 1 次 · 建议每分钟调用 1 次 套餐: Live Data

建议使用的关键响应字段:

  • state → 映射到 MatchState 枚举
  • elapsed → 显示实时比赛计时
  • homeScore、awayScore、homeHalfScore、awayHalfScore → 比分卡
  • homeCorner、awayCorner → 角球数
  • homeRed、awayRed → 红牌标记
  • hasAnimation → 控制是否显示动画嵌入按钮
// livescores.service.ts
import { LivescoresResponse, LiveMatch, MatchState, MatchStateLabel } from "./types";
import { buildUrl, cachedFetch } from "./utils";

export async function getLivescores(): Promise<LiveMatch[]> {
  return cachedFetch<LiveMatch[]>("thscore:livescores", 60, async () => {
    const res = await fetch(buildUrl("/football_th/livescores.aspx"));
    const json: LivescoresResponse = await res.json();
    if (json.code !== 0) throw new Error(`[Livescores] API error ${json.code}: ${json.message}`);
    return json.results;
  });
}

/** Returns only currently in-play matches */
export function getInPlayMatches(matches: LiveMatch[]): LiveMatch[] {
  const inPlayStates = [
    MatchState.FirstHalf, MatchState.HalfTime,
    MatchState.SecondHalf, MatchState.ExtraTimeFirst,
    MatchState.ExtraTimeHT, MatchState.ExtraTimeSecond,
    MatchState.PenaltyShootout,
  ];
  return matches.filter(m => inPlayStates.includes(m.state));
}

/** Returns a display-friendly score string e.g. "2 - 1 (1 - 0)" */
export function formatScore(match: LiveMatch): string {
  return `${match.homeScore} - ${match.awayScore} (HT: ${match.homeHalfScore} - ${match.awayHalfScore})`;
}

/** Display-friendly status with elapsed minutes */
export function formatStatus(match: LiveMatch): string {
  const label = MatchStateLabel[match.state] ?? "Unknown";
  if (match.elapsed > 0 && match.state === MatchState.FirstHalf || match.state === MatchState.SecondHalf) {
    return `${label} ${match.elapsed}'`;
  }
  return label;
}
ep-standings – 联赛与杯赛积分榜

端点: GET /football_th/league/table.aspx(参见文档 #43) 套餐: Stats

建议显示的响应字段:

  • rank – 积分榜排名
  • played、won、drawn、lost – 比赛记录
  • goalsFor、goalsAgainst、goalDifference – 进球统计
  • points – 总积分
  • form – 最近 5 场比赛结果数组 ["W","W","D","L","W"]
  • winRate、drawRate、loseRate – 百分比统计(适用于分析标签页)
// standings.service.ts
import { StandingRow, StandingsResponse } from "./types";
import { buildUrl, cachedFetch } from "./utils";

export async function getStandings(leagueId: number, seasonId: number): Promise<StandingRow[]> {
  const key = `thscore:standings:${leagueId}:${seasonId}`;
  return cachedFetch(key, 3600, async () => {
    const res = await fetch(buildUrl("/football_th/league/table.aspx", {
      leagueId: String(leagueId),
      seasonId: String(seasonId),
    }));
    const json: StandingsResponse = await res.json();
    if (json.code !== 0) throw new Error(`[Standings] API error: ${json.code}`);
    return json.results;
  });
}

/** Colour-code a form result for UI rendering */
export function formBadgeColor(result: string): string {
  return result === "W" ? "green" : result === "D" ? "gray" : "red";
}

5. 错误处理与韧性(高优先级)
error-retry-backoff – 对暂时性错误使用指数退避重试
// utils/http.ts
export async function fetchWithRetry(url: string, maxRetries = 3): Promise<Response> {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const controller = new AbortController();
      const timeout = setTimeout(() => controller.abort(), 8000); // 8s timeout

      const res = await fetch(url, { signal: controller.signal });
      clearTimeout(timeout);

      if (res.ok) return res;
      if (res.status >= 500) throw new Error(`Server error ${res.status}`);
      throw new Error(`Client error ${res.status}: ${res.statusText}`);
    } catch (err) {
      if (attempt === maxRetries - 1) throw err;
      const delay = 1000 * Math.pow(2, attempt); // 1s, 2s, 4s
      console.warn(`[THScore] Attempt ${attempt + 1} failed. Retrying in ${delay}ms...`);
      await new Promise(r => setTimeout(r, delay));
    }
  }
  throw new Error("Unreachable");
}
error-graceful-degrade – API 不可用时显示过期数据
export async function getLivescoresSafe(): Promise<LiveMatch[]> {
  try {
    return await getLivescores();
  } catch (err) {
    console.error("[THScore] Livescores unavailable, serving stale cache:", err);
    const stale = await redis.get("thscore:livescores:stale");
    return stale ? JSON.parse(stale) : [];
  }
}

// On successful fetch, always persist a "stale" backup
async function fetchAndPersist(): Promise<void> {
  const data = await getLivescores();
  await redis.set("thscore:livescores:stale", JSON.stringify(data)); // no TTL = forever
  await redis.setex("thscore:livescores", 60, JSON.stringify(data));
}

6. 数据规范化与类型(中优先级)
data-map-on-ingestion – 接收数据时立即将 API 数据结构转换为内部类型

切勿将原始 THScore JSON 直接传递给 UI 或数据库。务必通过有类型的转换器进行映射。

// mappers.ts
import { LiveMatch, MatchState } from "./types";

/** Raw shape from API (untrusted, not typed) */
interface RawMatch {
  matchId: number;
  state: number;
  homeName: string;
  awayName: string;
  homeScore: number;
  awayScore: number;
  hHalf: number;
  aHalf: number;
  // ... more raw fields
}

export function mapRawMatch(raw: RawMatch): LiveMatch {
  return {
    matchId:       raw.matchId,
    homeTeamName:  raw.homeName,
    awayTeamName:  raw.awayName,
    homeScore:     raw.homeScore ?? 0,
    awayScore:     raw.awayScore ?? 0,
    homeHalfScore: raw.hHalf ?? 0,
    awayHalfScore: raw.aHalf ?? 0,
    state:         raw.state as MatchState,
    hasAnimation:  Boolean(raw.animId),
    animationMatchId: raw.animId ?? undefined,
    // ... map remaining fields
  } as LiveMatch;
}

7. 安全(严重)
security-proxy-all – 始终通过后端代理

THScore 不支持 CORS。任何浏览器直接向 thscore.info 发出的请求都会被阻止。你的 API 密钥也会暴露在网络日志中。

✅ 架构:

Browser / App
     │
     ▼
Your Backend (Node / PHP / Python)   ← holds THSCORE_API_KEY
     │
     ▼
api2.thscore.info / www.thscore.info
// routes/football.ts (Express example)
router.get("/livescores", async (req, res) => {
  const data = await getLivescoresSafe();
  res.json(data); // ← never expose raw API key to client
});

官方文档参考

主题URL
入门指南https://www.thscore.info/doc/docs.shtml
足球 API 首页https://www.thscore.info/doc/docs_id=42.shtml
联赛与杯赛资料(基本)/football_th/league/basic.aspx
联赛与杯赛资料(完整)/football_th/league.aspx
今日即时比分doc/docs_id=20 · /football_th/livescores.aspx
赛程与赛果doc/docs_id=21 · /football_th/schedule/results.aspx
赛事与统计(比赛)doc/docs_id=15
首发阵容与伤病doc/docs_id=17
联赛与杯赛积分榜doc/docs_id=43
射手榜doc/docs_id=39
FIFA 排名doc/docs_id=47
直播动画配置doc/docs_id=242
动画直播 URL 演示https://www.isportslive8.com
所有足球联赛列表https://www.thscore.info/LeagueTh.aspx

评论

支持 Markdown