feat(player): 完善 H5 投注端与 API 演示数据
- 球赛/串关/优胜冠军、赛事详情、历史投注与个人资料编辑 - 固定顶栏、公告与底栏,仅内容区滚动 - 底部导航与站点 favicon 使用 logo,登录页精简 - API 种子、冠军盘与历史注单增强 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -96,9 +96,8 @@ export class MatchesService {
|
||||
leagueId: bigint;
|
||||
homeTeamId: bigint;
|
||||
awayTeamId: bigint;
|
||||
league?: unknown;
|
||||
homeTeam?: unknown;
|
||||
awayTeam?: unknown;
|
||||
homeTeam?: { code: string };
|
||||
awayTeam?: { code: string };
|
||||
markets?: unknown[];
|
||||
};
|
||||
const [leagueName, homeName, awayName] = await Promise.all([
|
||||
@@ -108,9 +107,13 @@ export class MatchesService {
|
||||
]);
|
||||
return {
|
||||
...match,
|
||||
id: m.id.toString(),
|
||||
leagueId: m.leagueId.toString(),
|
||||
leagueName,
|
||||
homeTeamName: homeName,
|
||||
awayTeamName: awayName,
|
||||
homeTeamCode: m.homeTeam?.code ?? '',
|
||||
awayTeamCode: m.awayTeam?.code ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -118,9 +121,12 @@ export class MatchesService {
|
||||
const matches = await this.prisma.match.findMany({
|
||||
where: {
|
||||
status: 'PUBLISHED',
|
||||
isOutright: false,
|
||||
...(leagueId ? { leagueId } : {}),
|
||||
},
|
||||
include: {
|
||||
homeTeam: true,
|
||||
awayTeam: true,
|
||||
markets: {
|
||||
where: { status: 'OPEN' },
|
||||
include: { selections: { where: { status: 'OPEN' } } },
|
||||
@@ -136,8 +142,11 @@ export class MatchesService {
|
||||
const match = await this.prisma.match.findUnique({
|
||||
where: { id: matchId },
|
||||
include: {
|
||||
homeTeam: true,
|
||||
awayTeam: true,
|
||||
markets: {
|
||||
include: { selections: true },
|
||||
where: { status: 'OPEN' },
|
||||
include: { selections: { where: { status: 'OPEN' }, orderBy: { sortOrder: 'asc' } } },
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
},
|
||||
score: true,
|
||||
@@ -147,12 +156,180 @@ export class MatchesService {
|
||||
return this.enrichMatch(match, locale);
|
||||
}
|
||||
|
||||
async listOutrights(locale = 'en-US') {
|
||||
const matches = await this.prisma.match.findMany({
|
||||
where: { status: 'PUBLISHED', isOutright: true },
|
||||
include: {
|
||||
markets: {
|
||||
where: { marketType: 'OUTRIGHT_WINNER', status: 'OPEN' },
|
||||
include: {
|
||||
selections: {
|
||||
where: { status: 'OPEN' },
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: [{ displayOrder: 'asc' }, { startTime: 'asc' }],
|
||||
});
|
||||
|
||||
const results = [];
|
||||
for (const match of matches) {
|
||||
const leagueName = await this.getTranslation('LEAGUE', match.leagueId, locale);
|
||||
const market = match.markets[0];
|
||||
if (!market) continue;
|
||||
|
||||
const selections = await Promise.all(
|
||||
market.selections.map(async (sel) => {
|
||||
const teamCode = sel.selectionCode.replace(/^TEAM_/, '');
|
||||
const team = await this.prisma.team.findUnique({ where: { code: teamCode } });
|
||||
const teamName = team
|
||||
? await this.getTranslation('TEAM', team.id, locale)
|
||||
: sel.selectionName;
|
||||
return {
|
||||
id: sel.id.toString(),
|
||||
teamCode,
|
||||
teamName,
|
||||
odds: sel.odds.toString(),
|
||||
oddsVersion: sel.oddsVersion.toString(),
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
results.push({
|
||||
id: match.id.toString(),
|
||||
leagueId: match.leagueId.toString(),
|
||||
leagueName,
|
||||
title: `*${leagueName} 冠军`,
|
||||
marketId: market.id.toString(),
|
||||
selections,
|
||||
});
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private marketLabelKey(marketType: string): string {
|
||||
const keys: Record<string, string> = {
|
||||
FT_1X2: '全场独赢',
|
||||
FT_HANDICAP: '全场让球',
|
||||
FT_OVER_UNDER: '全场大小',
|
||||
FT_ODD_EVEN: '全场单双',
|
||||
HT_1X2: '半场独赢',
|
||||
HT_HANDICAP: '半场让球',
|
||||
HT_OVER_UNDER: '半场大小',
|
||||
OUTRIGHT_WINNER: '冠军',
|
||||
FT_CORRECT_SCORE: '波胆',
|
||||
HT_CORRECT_SCORE: '上半场波胆',
|
||||
SH_CORRECT_SCORE: '下半场波胆',
|
||||
};
|
||||
return keys[marketType] ?? marketType;
|
||||
}
|
||||
|
||||
async enrichBetsForHistory(
|
||||
bets: Array<{
|
||||
betNo: string;
|
||||
betType: string;
|
||||
stake: unknown;
|
||||
totalOdds: unknown;
|
||||
potentialReturn: unknown;
|
||||
actualReturn: unknown;
|
||||
status: string;
|
||||
placedAt: Date;
|
||||
selections: Array<{
|
||||
matchId: bigint | null;
|
||||
marketType: string;
|
||||
selectionNameSnapshot: string;
|
||||
odds: unknown;
|
||||
resultStatus?: string | null;
|
||||
}>;
|
||||
}>,
|
||||
locale: string,
|
||||
) {
|
||||
const matchIds = [
|
||||
...new Set(
|
||||
bets.flatMap((b) =>
|
||||
b.selections.map((s) => s.matchId).filter((id): id is bigint => id != null),
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
const matches =
|
||||
matchIds.length > 0
|
||||
? await this.prisma.match.findMany({
|
||||
where: { id: { in: matchIds } },
|
||||
include: { homeTeam: true, awayTeam: true },
|
||||
})
|
||||
: [];
|
||||
|
||||
const matchMeta = new Map<
|
||||
string,
|
||||
{ leagueName: string; matchTitle: string; isOutright: boolean }
|
||||
>();
|
||||
|
||||
for (const m of matches) {
|
||||
const [leagueName, homeName, awayName] = await Promise.all([
|
||||
this.getTranslation('LEAGUE', m.leagueId, locale),
|
||||
this.getTranslation('TEAM', m.homeTeamId, locale),
|
||||
this.getTranslation('TEAM', m.awayTeamId, locale),
|
||||
]);
|
||||
matchMeta.set(m.id.toString(), {
|
||||
leagueName,
|
||||
matchTitle: m.isOutright ? leagueName : `${homeName} vs ${awayName}`,
|
||||
isOutright: m.isOutright,
|
||||
});
|
||||
}
|
||||
|
||||
return bets.map((bet) => {
|
||||
const firstMatchId = bet.selections.find((s) => s.matchId)?.matchId?.toString();
|
||||
const meta = firstMatchId ? matchMeta.get(firstMatchId) : undefined;
|
||||
const isParlay = bet.betType === 'PARLAY' || bet.selections.length > 1;
|
||||
|
||||
const legs = bet.selections.map((sel) => {
|
||||
const mid = sel.matchId?.toString();
|
||||
const m = mid ? matchMeta.get(mid) : undefined;
|
||||
return {
|
||||
marketType: sel.marketType,
|
||||
marketLabel: this.marketLabelKey(sel.marketType),
|
||||
selectionName: sel.selectionNameSnapshot,
|
||||
odds: sel.odds,
|
||||
resultStatus: sel.resultStatus,
|
||||
matchTitle: m?.matchTitle ?? sel.selectionNameSnapshot,
|
||||
leagueName: m?.leagueName ?? '',
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
betNo: bet.betNo,
|
||||
betType: bet.betType,
|
||||
stake: bet.stake,
|
||||
totalOdds: bet.totalOdds,
|
||||
potentialReturn: bet.potentialReturn,
|
||||
actualReturn: bet.actualReturn,
|
||||
status: bet.status,
|
||||
placedAt: bet.placedAt,
|
||||
leagueName: isParlay
|
||||
? 'Parlay'
|
||||
: meta?.leagueName ?? legs[0]?.leagueName ?? '',
|
||||
legCount: bet.selections.length,
|
||||
matchTitle: isParlay
|
||||
? ''
|
||||
: meta?.matchTitle ?? legs[0]?.matchTitle ?? bet.betNo,
|
||||
pickLabel: isParlay
|
||||
? ''
|
||||
: `${legs[0]?.marketLabel ?? ''}: ${legs[0]?.selectionName ?? ''}`,
|
||||
legs,
|
||||
isParlay,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@Cron(CronExpression.EVERY_MINUTE)
|
||||
async autoCloseMatches() {
|
||||
const now = new Date();
|
||||
await this.prisma.match.updateMany({
|
||||
where: {
|
||||
status: 'PUBLISHED',
|
||||
isOutright: false,
|
||||
startTime: { lte: now },
|
||||
},
|
||||
data: { status: 'CLOSED', closeTime: now },
|
||||
|
||||
Reference in New Issue
Block a user