feat: 世界杯48强夺冠盘、管理端调赔与项目文档

- 固定48强基准数据、同步种子与后台世界杯夺冠页

- 补全 user_preferences 迁移文件;新增启动指南与默认数据说明

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-03 16:19:36 +08:00
parent 3b739982a1
commit 95abbcb470
17 changed files with 1157 additions and 92 deletions

View 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 };
}