Files
thebet365/apps/api/scripts/build-wc2026-seed-json.mjs
2026-06-13 17:38:25 +08:00

223 lines
5.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 从 zhibo 导出的 world-cup-group-stage-matches.json 生成 seed 用裁剪 JSON 与球队映射 TS。
* 用法: node apps/api/scripts/build-wc2026-seed-json.mjs <源JSON路径>
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const apiRoot = path.resolve(__dirname, '..');
const seedDataDir = path.join(apiRoot, 'src/infrastructure/database/seed-data');
const outrightTeamsPath = path.join(apiRoot, 'src/domains/catalog/wc2026-outright-teams.ts');
const sourcePath = process.argv[2];
if (!sourcePath) {
console.error('用法: node build-wc2026-seed-json.mjs <源JSON路径>');
process.exit(1);
}
const raw = JSON.parse(fs.readFileSync(path.resolve(sourcePath), 'utf8'));
const outrightSrc = fs.readFileSync(outrightTeamsPath, 'utf8');
const outrightTeams = [...outrightSrc.matchAll(/code: '([^']+)', names: \{ 'zh-CN': '([^']+)', 'en-US': '([^']+)'/g)].map((m) => ({
code: m[1],
zh: m[2],
en: m[3],
}));
const CODE_TO_FLAG_ISO = {
ALG: 'dz',
ARG: 'ar',
AUS: 'au',
AUT: 'at',
BEL: 'be',
BIH: 'ba',
BRA: 'br',
CAN: 'ca',
CIV: 'ci',
COD: 'cd',
COL: 'co',
CPV: 'cv',
CRO: 'hr',
CUW: 'cw',
CZE: 'cz',
ECU: 'ec',
EGY: 'eg',
ENG: 'gb-eng',
ESP: 'es',
FRA: 'fr',
GER: 'de',
GHA: 'gh',
HAI: 'ht',
IRN: 'ir',
IRQ: 'iq',
JOR: 'jo',
JPN: 'jp',
KOR: 'kr',
KSA: 'sa',
MAR: 'ma',
MEX: 'mx',
NED: 'nl',
NOR: 'no',
NZL: 'nz',
PAN: 'pa',
PAR: 'py',
POR: 'pt',
QAT: 'qa',
RSA: 'za',
SCO: 'gb-sct',
SEN: 'sn',
SUI: 'ch',
SWE: 'se',
TUN: 'tn',
TUR: 'tr',
URU: 'uy',
USA: 'us',
UZB: 'uz',
};
function flagUrlForCode(code) {
const iso = CODE_TO_FLAG_ISO[code];
return iso ? `https://flagcdn.com/${iso}.svg` : '';
}
function pickNames(names) {
return {
zh: names?.zh ?? null,
en: names?.en ?? null,
zhTw: names?.zhTw ?? null,
vi: names?.vi ?? null,
km: names?.km ?? null,
ms: names?.ms ?? null,
};
}
function pickTeam(team) {
const code = resolveCanonicalCode(team);
return {
id: team.id,
name: team.name,
names: pickNames(team.names),
image: code ? flagUrlForCode(code) : '',
};
}
function slimMatch(m) {
return {
officialMatchNo: m.officialMatchNo,
stage: m.stage,
groupName: m.groupName,
liveMatchId: m.liveMatchId,
additionMatchId: m.additionMatchId,
channelId: m.channelId,
matchName: m.matchName,
league: { type: m.league.type, en: m.league.en, zh: m.league.zh },
kickoff: {
utcTimeStart: m.kickoff.utcTimeStart,
utcTimeStop: m.kickoff.utcTimeStop,
utcIso: m.kickoff.utcIso,
chinaTime: m.kickoff.chinaTime,
venueTime: m.kickoff.venueTime,
venueTimezone: m.kickoff.venueTimezone,
},
homeTeam: pickTeam(m.homeTeam),
awayTeam: pickTeam(m.awayTeam),
status: { state: m.status.state, isHot: 0 },
venue: {
names: pickNames(m.venue?.names),
city: pickNames(m.venue?.city),
},
sortOrder: m.sortOrder,
isPublished: m.isPublished,
};
}
function resolveCanonicalCode(team) {
const en = (team.name || team.names?.en || '').toLowerCase();
const zh = team.names?.zh || '';
const hit = outrightTeams.find(
(o) =>
o.en.toLowerCase() === en ||
o.zh === zh ||
(o.en === 'Turkey' && en.includes('türkiye')) ||
(o.en === 'Czech' && en === 'czechia') ||
(o.en === 'Bosnia' && en.includes('bosnia')) ||
(o.en === 'Ivory Coast' && en.includes('côte')) ||
(o.en === 'DR Congo' && en.includes('congo')) ||
(o.en === 'Curacao' && en.includes('cura')),
);
return hit?.code ?? null;
}
const matches = (raw.matches || []).map(slimMatch);
const bundle = { count: matches.length, matches };
const teamById = new Map();
for (const m of raw.matches || []) {
for (const t of [m.homeTeam, m.awayTeam]) {
if (t?.id != null) teamById.set(t.id, t);
}
}
const zhiboToCode = {};
const unmatched = [];
const missingFlagCodes = new Set();
for (const [id, team] of teamById) {
const code = resolveCanonicalCode(team);
if (code) {
zhiboToCode[id] = code;
const flagUrl = flagUrlForCode(code);
if (!flagUrl) {
missingFlagCodes.add(code);
}
} else {
unmatched.push({ id, name: team.name });
}
}
fs.mkdirSync(seedDataDir, { recursive: true });
const jsonOut = path.join(seedDataDir, 'wc2026-group-stage.json');
fs.writeFileSync(jsonOut, JSON.stringify(bundle, null, 2) + '\n', 'utf8');
const mapLines = Object.entries(zhiboToCode)
.sort(([a], [b]) => Number(a) - Number(b))
.map(([id, code]) => ` ${id}: '${code}',`)
.join('\n');
const flagIsoLines = Object.entries(CODE_TO_FLAG_ISO)
.sort(([a], [b]) => a.localeCompare(b))
.map(([code, iso]) => ` ${code}: '${iso}',`)
.join('\n');
const mapTs = `/** 由 build-wc2026-seed-json.mjs 生成 — zhibo externalId → WC2026 canonical code */
export const WC2026_ZIBO_ID_TO_CODE: Record<number, string> = {
${mapLines}
};
/** WC2026 默认国家/地区国旗FlagCDNseed 时写入 teams.logo_url */
export const WC2026_FLAG_ISO_BY_CODE: Record<string, string> = {
${flagIsoLines}
};
export const WC2026_TEAM_LOGO_BY_CODE: Record<string, string> = Object.fromEntries(
Object.entries(WC2026_FLAG_ISO_BY_CODE).map(([code, iso]) => [
code,
\`https://flagcdn.com/\${iso}.svg\`,
]),
);
`;
const mapOut = path.join(seedDataDir, 'wc2026-zhibo-team-map.ts');
fs.writeFileSync(mapOut, mapTs, 'utf8');
console.log(`Wrote ${jsonOut} (${matches.length} matches)`);
console.log(`Wrote ${mapOut} (${Object.keys(zhiboToCode).length} team mappings)`);
if (unmatched.length) {
console.warn('Unmatched teams:', unmatched);
process.exit(1);
}
if (missingFlagCodes.size) {
console.warn('Missing FlagCDN ISO mapping:', [...missingFlagCodes].sort());
process.exit(1);
}