feat: 世界杯48强夺冠盘、管理端调赔与项目文档
- 固定48强基准数据、同步种子与后台世界杯夺冠页 - 补全 user_preferences 迁移文件;新增启动指南与默认数据说明 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
179
apps/api/src/domains/catalog/wc2026-outright.sync.ts
Normal file
179
apps/api/src/domains/catalog/wc2026-outright.sync.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import type { PrismaClient } from '@prisma/client';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
import {
|
||||
WC2026_LEAGUE_CODE,
|
||||
WC2026_OUTRIGHT_TEAMS,
|
||||
type Wc2026OutrightTeam,
|
||||
} from './wc2026-outright-teams';
|
||||
|
||||
const PLACEHOLDER_TEAM_CODE = 'OUT';
|
||||
|
||||
const CANONICAL_CODES = new Set(WC2026_OUTRIGHT_TEAMS.map((t) => t.code));
|
||||
|
||||
export type Wc2026OutrightSyncOptions = {
|
||||
/** true:赔率/队名/排序与 wc2026-outright-teams.ts 完全一致,并关闭不在表内的选项 */
|
||||
forceCanonical?: boolean;
|
||||
};
|
||||
|
||||
function oddsEqual(a: Decimal | number, b: number) {
|
||||
return Number(a) === b;
|
||||
}
|
||||
|
||||
async function upsertTeamTranslations(
|
||||
prisma: PrismaClient,
|
||||
teamId: bigint,
|
||||
names: Wc2026OutrightTeam['names'],
|
||||
) {
|
||||
for (const [locale, value] of Object.entries(names)) {
|
||||
await prisma.entityTranslation.upsert({
|
||||
where: {
|
||||
entityType_entityId_locale_fieldName: {
|
||||
entityType: 'TEAM',
|
||||
entityId: teamId,
|
||||
locale,
|
||||
fieldName: 'name',
|
||||
},
|
||||
},
|
||||
create: {
|
||||
entityType: 'TEAM',
|
||||
entityId: teamId,
|
||||
locale,
|
||||
fieldName: 'name',
|
||||
value,
|
||||
},
|
||||
update: { value },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function upsertTeam(prisma: PrismaClient, entry: Wc2026OutrightTeam) {
|
||||
const team = await prisma.team.upsert({
|
||||
where: { code: entry.code },
|
||||
create: { code: entry.code },
|
||||
update: {},
|
||||
});
|
||||
await upsertTeamTranslations(prisma, team.id, entry.names);
|
||||
return team;
|
||||
}
|
||||
|
||||
/** 确保 WC2026 夺冠盘存在;forceCanonical 时与基准表完全一致 */
|
||||
export async function syncWc2026OutrightMarket(
|
||||
prisma: PrismaClient,
|
||||
options: Wc2026OutrightSyncOptions = {},
|
||||
) {
|
||||
const forceCanonical = options.forceCanonical ?? false;
|
||||
const league = await prisma.league.findUnique({ where: { code: WC2026_LEAGUE_CODE } });
|
||||
if (!league) {
|
||||
throw new Error(`League ${WC2026_LEAGUE_CODE} not found — run seedSportsDemo first`);
|
||||
}
|
||||
|
||||
const placeholder = await upsertTeam(prisma, {
|
||||
rank: 0,
|
||||
code: PLACEHOLDER_TEAM_CODE,
|
||||
names: { 'zh-CN': '冠军盘', 'en-US': 'Outright' },
|
||||
defaultOdds: 1,
|
||||
});
|
||||
|
||||
for (const entry of WC2026_OUTRIGHT_TEAMS) {
|
||||
await upsertTeam(prisma, entry);
|
||||
}
|
||||
|
||||
let match = await prisma.match.findFirst({
|
||||
where: { leagueId: league.id, isOutright: true, deletedAt: null },
|
||||
});
|
||||
if (!match) {
|
||||
match = await prisma.match.create({
|
||||
data: {
|
||||
leagueId: league.id,
|
||||
homeTeamId: placeholder.id,
|
||||
awayTeamId: placeholder.id,
|
||||
isOutright: true,
|
||||
matchName: '2026 FIFA World Cup Winner',
|
||||
startTime: new Date('2027-07-01T00:00:00Z'),
|
||||
status: 'PUBLISHED',
|
||||
publishTime: new Date(),
|
||||
isHot: true,
|
||||
displayOrder: 0,
|
||||
},
|
||||
});
|
||||
} else if (match.status === 'DRAFT') {
|
||||
match = await prisma.match.update({
|
||||
where: { id: match.id },
|
||||
data: { status: 'PUBLISHED', publishTime: match.publishTime ?? new Date() },
|
||||
});
|
||||
}
|
||||
|
||||
let market = await prisma.market.findFirst({
|
||||
where: { matchId: match.id, marketType: 'OUTRIGHT_WINNER' },
|
||||
include: { selections: true },
|
||||
});
|
||||
if (!market) {
|
||||
market = await prisma.market.create({
|
||||
data: {
|
||||
matchId: match.id,
|
||||
marketType: 'OUTRIGHT_WINNER',
|
||||
period: 'OUTRIGHT',
|
||||
allowSingle: true,
|
||||
allowParlay: false,
|
||||
sortOrder: 1,
|
||||
status: 'OPEN',
|
||||
},
|
||||
include: { selections: true },
|
||||
});
|
||||
}
|
||||
|
||||
const existingByCode = new Map(market.selections.map((s) => [s.selectionCode, s]));
|
||||
|
||||
for (const entry of WC2026_OUTRIGHT_TEAMS) {
|
||||
const sortOrder = entry.rank - 1;
|
||||
const existing = existingByCode.get(entry.code);
|
||||
if (!existing) {
|
||||
await prisma.marketSelection.create({
|
||||
data: {
|
||||
marketId: market.id,
|
||||
selectionCode: entry.code,
|
||||
selectionName: entry.names['zh-CN'],
|
||||
odds: entry.defaultOdds,
|
||||
sortOrder,
|
||||
status: 'OPEN',
|
||||
},
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const updateData: {
|
||||
selectionName: string;
|
||||
sortOrder: number;
|
||||
status: string;
|
||||
odds?: number;
|
||||
oddsVersion?: bigint;
|
||||
} = {
|
||||
selectionName: entry.names['zh-CN'],
|
||||
sortOrder,
|
||||
status: 'OPEN',
|
||||
};
|
||||
|
||||
if (forceCanonical && !oddsEqual(existing.odds, entry.defaultOdds)) {
|
||||
updateData.odds = entry.defaultOdds;
|
||||
updateData.oddsVersion = existing.oddsVersion + BigInt(1);
|
||||
}
|
||||
|
||||
await prisma.marketSelection.update({
|
||||
where: { id: existing.id },
|
||||
data: updateData,
|
||||
});
|
||||
}
|
||||
|
||||
if (forceCanonical) {
|
||||
for (const sel of market.selections) {
|
||||
if (CANONICAL_CODES.has(sel.selectionCode)) continue;
|
||||
if (sel.selectionCode === PLACEHOLDER_TEAM_CODE) continue;
|
||||
await prisma.marketSelection.update({
|
||||
where: { id: sel.id },
|
||||
data: { status: 'CLOSED' },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { matchId: match.id, marketId: market.id };
|
||||
}
|
||||
Reference in New Issue
Block a user