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:
@@ -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' };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user