关于 thscore-football-api
thscore-football-api 是面向 THScore Football API 集成的开发技能,提供 TypeScript 类型、端点示例和实施规则。它涵盖实时比分、赛程、统计、赔率及历史数据,并强调缓存、错误处理和 API 密钥保护。
主要功能
- 01提供 TypeScript 类型和端点示例
- 02说明实时比分、赛程及比赛统计集成
- 03涵盖联赛资料、排名和射手榜
- 04介绍赔率及历史数据使用场景
- 05指导缓存、错误处理与密钥安全
适用场景
- 01构建 THScore API 客户端或 HTTP 封装
- 02开发实时比分、比赛动画或积分榜页面
- 03同步历史比赛数据并设计缓存
- 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/月) | 1 | iFrame/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 |