feat: 世界杯48强夺冠盘、管理端调赔与项目文档
- 固定48强基准数据、同步种子与后台世界杯夺冠页 - 补全 user_preferences 迁移文件;新增启动指南与默认数据说明 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
108
apps/api/src/domains/catalog/outright.service.ts
Normal file
108
apps/api/src/domains/catalog/outright.service.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '../../shared/prisma/prisma.service';
|
||||
import { MarketsService } from '../odds/markets.service';
|
||||
import { WC2026_OUTRIGHT_TEAMS } from './wc2026-outright-teams';
|
||||
import { syncWc2026OutrightMarket } from './wc2026-outright.sync';
|
||||
|
||||
const CANONICAL_CODES = new Set(WC2026_OUTRIGHT_TEAMS.map((t) => t.code));
|
||||
|
||||
@Injectable()
|
||||
export class OutrightService {
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
private markets: MarketsService,
|
||||
) {}
|
||||
|
||||
async getWc2026ForAdmin() {
|
||||
const { matchId, marketId } = await syncWc2026OutrightMarket(this.prisma, {
|
||||
forceCanonical: false,
|
||||
});
|
||||
|
||||
const match = await this.prisma.match.findUniqueOrThrow({ where: { id: matchId } });
|
||||
const [leagueZh, leagueEn, market] = await Promise.all([
|
||||
this.getTranslation('LEAGUE', match.leagueId, 'zh-CN'),
|
||||
this.getTranslation('LEAGUE', match.leagueId, 'en-US'),
|
||||
this.prisma.market.findUniqueOrThrow({
|
||||
where: { id: marketId },
|
||||
include: {
|
||||
selections: { orderBy: { sortOrder: 'asc' } },
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const teamByCode = new Map(
|
||||
WC2026_OUTRIGHT_TEAMS.map((t) => [t.code, t]),
|
||||
);
|
||||
|
||||
const canonicalSelections = market.selections.filter((sel) =>
|
||||
CANONICAL_CODES.has(sel.selectionCode),
|
||||
);
|
||||
|
||||
const selections = await Promise.all(
|
||||
canonicalSelections.map(async (sel) => {
|
||||
const meta = teamByCode.get(sel.selectionCode);
|
||||
const team = await this.prisma.team.findUnique({ where: { code: sel.selectionCode } });
|
||||
const teamZh = team
|
||||
? await this.getTranslation('TEAM', team.id, 'zh-CN')
|
||||
: sel.selectionName;
|
||||
const teamEn = team
|
||||
? await this.getTranslation('TEAM', team.id, 'en-US')
|
||||
: sel.selectionName;
|
||||
return {
|
||||
id: sel.id.toString(),
|
||||
teamCode: sel.selectionCode,
|
||||
rank: meta?.rank ?? sel.sortOrder + 1,
|
||||
teamZh,
|
||||
teamEn,
|
||||
odds: sel.odds.toString(),
|
||||
oddsVersion: sel.oddsVersion.toString(),
|
||||
status: sel.status,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
selections.sort((a, b) => a.rank - b.rank);
|
||||
|
||||
return {
|
||||
matchId: matchId.toString(),
|
||||
marketId: marketId.toString(),
|
||||
matchStatus: match.status,
|
||||
marketStatus: market.status,
|
||||
leagueZh,
|
||||
leagueEn,
|
||||
selections,
|
||||
expectedCount: WC2026_OUTRIGHT_TEAMS.length,
|
||||
};
|
||||
}
|
||||
|
||||
async applyWc2026Canonical() {
|
||||
await syncWc2026OutrightMarket(this.prisma, { forceCanonical: true });
|
||||
return this.getWc2026ForAdmin();
|
||||
}
|
||||
|
||||
async updateWc2026Odds(
|
||||
updates: Array<{ selectionId: string; odds: number }>,
|
||||
operatorId: bigint,
|
||||
) {
|
||||
const results = await this.markets.batchUpdateOdds(
|
||||
updates.map((u) => ({ selectionId: BigInt(u.selectionId), odds: u.odds })),
|
||||
operatorId,
|
||||
);
|
||||
return results.map((r) => ({
|
||||
id: r.id.toString(),
|
||||
odds: r.odds.toString(),
|
||||
oddsVersion: r.oddsVersion.toString(),
|
||||
}));
|
||||
}
|
||||
|
||||
private async getTranslation(
|
||||
entityType: string,
|
||||
entityId: bigint,
|
||||
locale: string,
|
||||
): Promise<string> {
|
||||
const row = await this.prisma.entityTranslation.findFirst({
|
||||
where: { entityType, entityId, locale, fieldName: 'name' },
|
||||
});
|
||||
return row?.value ?? '';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user