feat: WC2026 赛事 seed、生产上线初始化脚本与目录归档

重构 seed 为 WC2026 72 场小组赛与 48 强优胜盘;新增 production 模式仅保留 admin 与赛事示例;提供 prod-init-db 全量重置脚本;管理端 i18n 分包与赛事归档能力。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-12 18:17:00 +08:00
parent 8f14e85ebd
commit e7e938f261
94 changed files with 12332 additions and 976 deletions

View File

@@ -66,6 +66,7 @@ export class OutrightService {
m.status,
market,
openCount,
league.isActive,
);
const [titleZh, titleEn, titleMs] = await Promise.all([
this.getOutrightTitle(m.id, 'zh-CN'),
@@ -141,6 +142,7 @@ export class OutrightService {
: sel.selectionName;
return {
id: sel.id.toString(),
teamId: team?.id.toString() ?? null,
teamCode: sel.selectionCode,
rank: sel.sortOrder + 1 || index + 1,
teamZh: teamZh || sel.selectionName,
@@ -159,6 +161,11 @@ export class OutrightService {
fullMarket.selections.filter(
(s) => s.selectionCode !== PLACEHOLDER_TEAM_CODE,
),
league.isActive,
);
const unsettledFixtureCount = await this.countUnsettledLeagueFixtures(
match.leagueId,
);
const [titleZh, titleEn, titleMs] = await Promise.all([
@@ -178,6 +185,8 @@ export class OutrightService {
titleEn: titleEn || match.matchName || '',
titleMs,
status: match.status,
leagueIsPublished: league.isActive,
unsettledFixtureCount,
marketId: fullMarket.id.toString(),
marketStatus: fullMarket.status,
canImportCanonical: league.code === WC2026_LEAGUE_CODE,
@@ -191,15 +200,16 @@ export class OutrightService {
/** 按联赛获取或创建冠军盘,并从单场赛程同步参赛队伍 */
async getOrCreateAndSyncForLeague(leagueId: bigint) {
const league = await this.prisma.league.findUnique({
where: { id: leagueId },
});
if (!league) throw appNotFound('LEAGUE_NOT_FOUND');
let match = await this.prisma.match.findFirst({
where: { leagueId, isOutright: true, deletedAt: null },
orderBy: { id: 'asc' },
});
if (!match) {
const league = await this.prisma.league.findUnique({
where: { id: leagueId },
});
if (!league) throw appNotFound('LEAGUE_NOT_FOUND');
const [leagueZh, leagueEn, leagueMs] = await Promise.all([
this.getTranslation('LEAGUE', leagueId, 'zh-CN'),
this.getTranslation('LEAGUE', leagueId, 'en-US'),
@@ -210,12 +220,17 @@ export class OutrightService {
titleZh: leagueZh || league.code,
titleEn: leagueEn || league.code,
titleMs: leagueMs || undefined,
status: 'DRAFT',
status: league.isActive ? 'PUBLISHED' : 'DRAFT',
});
match = await this.prisma.match.findFirstOrThrow({
where: { leagueId, isOutright: true, deletedAt: null },
orderBy: { id: 'asc' },
});
} else {
await this.syncOutrightStatusWithLeague(match, league);
match = await this.prisma.match.findFirstOrThrow({
where: { id: match.id },
});
}
const sync = await this.syncSelectionsFromLeagueFixtures(match.id);
const data = await this.getForAdmin(match.id);
@@ -226,6 +241,47 @@ export class OutrightService {
};
}
/** 联赛发布后同步冠军盘状态(随联赛发布,无需单独发布) */
async syncWithLeaguePublished(leagueId: bigint) {
const league = await this.prisma.league.findUnique({
where: { id: leagueId },
});
if (!league?.isActive) return;
const existing = await this.prisma.match.findFirst({
where: { leagueId, isOutright: true, deletedAt: null },
orderBy: { id: 'asc' },
});
if (!existing) {
await this.getOrCreateAndSyncForLeague(leagueId);
return;
}
await this.syncOutrightStatusWithLeague(existing, league);
}
/** 联赛下尚未结算/取消的单场数量(不含冠军盘) */
async countUnsettledLeagueFixtures(leagueId: bigint): Promise<number> {
return this.prisma.match.count({
where: {
leagueId,
isOutright: false,
deletedAt: null,
status: { notIn: ['SETTLED', 'CANCELLED'] },
},
});
}
private async syncOutrightStatusWithLeague(
match: { id: bigint; status: string },
league: { isActive: boolean },
) {
if (!league.isActive || match.status !== 'DRAFT') return;
await this.prisma.match.update({
where: { id: match.id },
data: { status: 'PUBLISHED', publishTime: new Date() },
});
}
/** 若联赛已有冠军盘,则从单场同步球队(不自动创建冠军盘) */
async syncOutrightTeamsForLeagueIfExists(leagueId: bigint) {
const match = await this.prisma.match.findFirst({
@@ -635,6 +691,7 @@ export class OutrightService {
isOutright: true,
sportType: 'FOOTBALL',
deletedAt: null,
league: { isActive: true, deletedAt: null },
},
include: {
markets: {
@@ -804,18 +861,28 @@ export class OutrightService {
matchStatus: string,
market: { status: string } | null | undefined,
selections: Array<{ selectionCode: string; status: string }>,
leagueIsActive = true,
): { playerVisible: boolean; playerHiddenReason: string | null } {
const openCount = selections.filter(
(s) => s.status === 'OPEN' && s.selectionCode !== PLACEHOLDER_TEAM_CODE,
).length;
return this.playerVisibilityByCounts(matchStatus, market, openCount);
return this.playerVisibilityByCounts(
matchStatus,
market,
openCount,
leagueIsActive,
);
}
private playerVisibilityByCounts(
matchStatus: string,
market: { status: string } | null | undefined,
openSelectionCount: number,
leagueIsActive = true,
): { playerVisible: boolean; playerHiddenReason: string | null } {
if (!leagueIsActive) {
return { playerVisible: false, playerHiddenReason: 'LEAGUE_INACTIVE' };
}
if (matchStatus !== 'PUBLISHED') {
return { playerVisible: false, playerHiddenReason: 'NOT_PUBLISHED' };
}