重构
This commit is contained in:
@@ -1,16 +1,20 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { isPreMatchKickoff, MarketType, resolveTranslationFallback } from '@thebet365/shared';
|
||||
import {
|
||||
defaultMarketName,
|
||||
defaultSelectionName,
|
||||
isPreMatchKickoff,
|
||||
isSettlementSupportedMarketType,
|
||||
PARLAY_MARKET_TYPES,
|
||||
resolveMarketText,
|
||||
resolveTranslationFallback,
|
||||
sanitizeLocalizedText,
|
||||
} from '@thebet365/shared';
|
||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
import { PrismaService } from '../../shared/prisma/prisma.service';
|
||||
import { appBadRequest, appNotFound } from '../../shared/common/app-error';
|
||||
|
||||
export type MatchBetStatsSummary = {
|
||||
betCount: number;
|
||||
totalStake: string;
|
||||
pendingCount: number;
|
||||
};
|
||||
import { MatchBetStatsService, type MatchBetStatsSummary } from './match-bet-stats.service';
|
||||
import type { ZhiboLeagueExport, ZhiboMatchExport, ZhiboMatchesBundleExport, ZhiboTeamExport } from './zhibo-match.types';
|
||||
import {
|
||||
leagueCodeFromExport,
|
||||
@@ -25,17 +29,11 @@ import {
|
||||
import { syncWc2026OutrightMarket } from './wc2026-outright.sync';
|
||||
import { OutrightService } from './outright.service';
|
||||
|
||||
export type { MatchBetStatsSummary } from './match-bet-stats.service';
|
||||
|
||||
const OUTRIGHT_PLACEHOLDER_CODE = 'OUT';
|
||||
|
||||
const PLAYER_PARLAY_MARKET_TYPES = [
|
||||
MarketType.FT_HANDICAP,
|
||||
MarketType.FT_OVER_UNDER,
|
||||
MarketType.FT_1X2,
|
||||
MarketType.FT_ODD_EVEN,
|
||||
MarketType.HT_HANDICAP,
|
||||
MarketType.HT_OVER_UNDER,
|
||||
MarketType.HT_1X2,
|
||||
] as const;
|
||||
const PLAYER_PARLAY_MARKET_TYPES = PARLAY_MARKET_TYPES;
|
||||
|
||||
export type ListPublishedOptions = {
|
||||
/** 响应中是否包含 markets(列表页默认 false,仅摘要) */
|
||||
@@ -49,6 +47,7 @@ export class MatchesService {
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
private outright: OutrightService,
|
||||
private matchBetStats: MatchBetStatsService,
|
||||
) {}
|
||||
|
||||
async createLeague(code: string, translations: Record<string, string>) {
|
||||
@@ -89,7 +88,6 @@ export class MatchesService {
|
||||
awayTeamId: bigint;
|
||||
startTime: Date;
|
||||
isHot?: boolean;
|
||||
correctScoreEnabled?: boolean;
|
||||
displayOrder?: number;
|
||||
createdBy?: bigint;
|
||||
status?: string;
|
||||
@@ -115,7 +113,6 @@ export class MatchesService {
|
||||
awayTeamId: data.awayTeamId,
|
||||
startTime: data.startTime,
|
||||
isHot: data.isHot ?? false,
|
||||
correctScoreEnabled: data.correctScoreEnabled ?? true,
|
||||
displayOrder: data.displayOrder ?? 0,
|
||||
createdBy: data.createdBy,
|
||||
status,
|
||||
@@ -624,59 +621,7 @@ export class MatchesService {
|
||||
async betStatsForMatches(
|
||||
matchIds: bigint[],
|
||||
): Promise<Map<string, MatchBetStatsSummary>> {
|
||||
const result = new Map<string, MatchBetStatsSummary>();
|
||||
if (!matchIds.length) return result;
|
||||
|
||||
const legs = await this.prisma.betSelection.findMany({
|
||||
where: { matchId: { in: matchIds } },
|
||||
select: {
|
||||
matchId: true,
|
||||
betId: true,
|
||||
bet: { select: { stake: true, status: true } },
|
||||
},
|
||||
});
|
||||
|
||||
const byMatch = new Map<
|
||||
string,
|
||||
Map<string, { stake: Decimal; status: string }>
|
||||
>();
|
||||
for (const leg of legs) {
|
||||
if (leg.matchId == null) continue;
|
||||
const mid = leg.matchId.toString();
|
||||
if (!byMatch.has(mid)) byMatch.set(mid, new Map());
|
||||
const bets = byMatch.get(mid)!;
|
||||
if (!bets.has(leg.betId.toString())) {
|
||||
bets.set(leg.betId.toString(), {
|
||||
stake: leg.bet.stake,
|
||||
status: leg.bet.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const id of matchIds) {
|
||||
const mid = id.toString();
|
||||
const bets = byMatch.get(mid);
|
||||
if (!bets) {
|
||||
result.set(mid, {
|
||||
betCount: 0,
|
||||
totalStake: '0',
|
||||
pendingCount: 0,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
let totalStake = new Decimal(0);
|
||||
let pendingCount = 0;
|
||||
for (const b of bets.values()) {
|
||||
totalStake = totalStake.add(b.stake);
|
||||
if (b.status === 'PENDING') pendingCount += 1;
|
||||
}
|
||||
result.set(mid, {
|
||||
betCount: bets.size,
|
||||
totalStake: totalStake.toString(),
|
||||
pendingCount,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
return this.matchBetStats.betStatsForMatches(matchIds);
|
||||
}
|
||||
|
||||
private async upsertTeamByCode(data: {
|
||||
@@ -718,7 +663,6 @@ export class MatchesService {
|
||||
awayTeamMs?: string;
|
||||
startTime: Date;
|
||||
isHot?: boolean;
|
||||
correctScoreEnabled?: boolean;
|
||||
displayOrder?: number;
|
||||
matchName?: string;
|
||||
stage?: string;
|
||||
@@ -833,7 +777,6 @@ export class MatchesService {
|
||||
awayTeamId: awayTeam.id,
|
||||
startTime: data.startTime,
|
||||
isHot: data.isHot ?? false,
|
||||
correctScoreEnabled: data.correctScoreEnabled ?? true,
|
||||
displayOrder: data.displayOrder ?? 0,
|
||||
createdBy: data.createdBy,
|
||||
status: 'DRAFT',
|
||||
@@ -881,7 +824,6 @@ export class MatchesService {
|
||||
status: match.status,
|
||||
isOutright: match.isOutright,
|
||||
isHot: match.isHot,
|
||||
correctScoreEnabled: match.correctScoreEnabled,
|
||||
displayOrder: match.displayOrder,
|
||||
startTime: match.startTime.toISOString(),
|
||||
leagueId: match.leagueId.toString(),
|
||||
@@ -909,21 +851,36 @@ export class MatchesService {
|
||||
htAway: scoreRow.htAwayScore ?? 0,
|
||||
ftHome: scoreRow.ftHomeScore ?? 0,
|
||||
ftAway: scoreRow.ftAwayScore ?? 0,
|
||||
homeCorners: scoreRow.homeCorners ?? null,
|
||||
awayCorners: scoreRow.awayCorners ?? null,
|
||||
homeYellowCards: scoreRow.homeYellowCards ?? null,
|
||||
awayYellowCards: scoreRow.awayYellowCards ?? null,
|
||||
homeRedCards: scoreRow.homeRedCards ?? null,
|
||||
awayRedCards: scoreRow.awayRedCards ?? null,
|
||||
homeCards: scoreRow.homeCards ?? null,
|
||||
awayCards: scoreRow.awayCards ?? null,
|
||||
winnerTeamId: scoreRow.winnerTeamId?.toString() ?? null,
|
||||
}
|
||||
: null,
|
||||
markets: markets.map((m) => ({
|
||||
id: m.id.toString(),
|
||||
marketType: m.marketType,
|
||||
marketKey: m.marketKey ?? m.marketType,
|
||||
lineKey: m.lineKey ?? null,
|
||||
period: m.period,
|
||||
lineValue: m.lineValue != null ? Number(m.lineValue) : null,
|
||||
paramsJson: m.paramsJson ?? null,
|
||||
status: m.status,
|
||||
showOnPlayer: m.showOnPlayer,
|
||||
promoLabel: m.promoLabel ?? '',
|
||||
promoLabelI18n: sanitizeLocalizedText(m.promoLabelI18n),
|
||||
nameI18n: sanitizeLocalizedText(m.nameI18n),
|
||||
sortOrder: m.sortOrder,
|
||||
selections: m.selections.map((s) => ({
|
||||
id: s.id.toString(),
|
||||
selectionCode: s.selectionCode,
|
||||
selectionName: s.selectionName,
|
||||
nameI18n: sanitizeLocalizedText(s.nameI18n),
|
||||
odds: Number(s.odds),
|
||||
status: s.status,
|
||||
sortOrder: s.sortOrder,
|
||||
@@ -949,7 +906,6 @@ export class MatchesService {
|
||||
groupName?: string;
|
||||
homeTeamLogoUrl?: string;
|
||||
awayTeamLogoUrl?: string;
|
||||
correctScoreEnabled?: boolean;
|
||||
updatedBy?: bigint;
|
||||
},
|
||||
) {
|
||||
@@ -1006,7 +962,6 @@ export class MatchesService {
|
||||
matchName,
|
||||
stage: data.stage !== undefined ? data.stage.trim() || null : match.stage,
|
||||
groupName: data.groupName !== undefined ? data.groupName.trim() || null : match.groupName,
|
||||
correctScoreEnabled: data.correctScoreEnabled ?? match.correctScoreEnabled,
|
||||
updatedBy: data.updatedBy,
|
||||
},
|
||||
});
|
||||
@@ -1261,7 +1216,6 @@ export class MatchesService {
|
||||
startTime: Date;
|
||||
status?: string;
|
||||
isHot?: boolean;
|
||||
correctScoreEnabled?: boolean;
|
||||
displayOrder?: number;
|
||||
matchName?: string | null;
|
||||
stage?: string | null;
|
||||
@@ -1270,10 +1224,18 @@ export class MatchesService {
|
||||
awayTeam?: { code: string; logoUrl?: string | null };
|
||||
league?: { logoUrl?: string | null };
|
||||
score?: {
|
||||
htHomeScore: number;
|
||||
htAwayScore: number;
|
||||
ftHomeScore: number;
|
||||
ftAwayScore: number;
|
||||
htHomeScore: number | null;
|
||||
htAwayScore: number | null;
|
||||
ftHomeScore: number | null;
|
||||
ftAwayScore: number | null;
|
||||
homeCorners?: number | null;
|
||||
awayCorners?: number | null;
|
||||
homeYellowCards?: number | null;
|
||||
awayYellowCards?: number | null;
|
||||
homeRedCards?: number | null;
|
||||
awayRedCards?: number | null;
|
||||
homeCards?: number | null;
|
||||
awayCards?: number | null;
|
||||
} | null;
|
||||
markets?: Array<Record<string, unknown>>;
|
||||
};
|
||||
@@ -1295,7 +1257,6 @@ export class MatchesService {
|
||||
awayTeamLogoUrl: m.awayTeam?.logoUrl ?? null,
|
||||
startTime: m.startTime.toISOString(),
|
||||
isHot: m.isHot ?? false,
|
||||
correctScoreEnabled: m.correctScoreEnabled ?? true,
|
||||
displayOrder: m.displayOrder ?? 0,
|
||||
matchName: m.matchName ?? null,
|
||||
stage: m.stage ?? null,
|
||||
@@ -1303,10 +1264,18 @@ export class MatchesService {
|
||||
status: m.status ?? 'PUBLISHED',
|
||||
score: m.score
|
||||
? {
|
||||
htHome: m.score.htHomeScore,
|
||||
htAway: m.score.htAwayScore,
|
||||
ftHome: m.score.ftHomeScore,
|
||||
ftAway: m.score.ftAwayScore,
|
||||
htHome: m.score.htHomeScore ?? null,
|
||||
htAway: m.score.htAwayScore ?? null,
|
||||
ftHome: m.score.ftHomeScore ?? null,
|
||||
ftAway: m.score.ftAwayScore ?? null,
|
||||
homeCorners: m.score.homeCorners ?? null,
|
||||
awayCorners: m.score.awayCorners ?? null,
|
||||
homeYellowCards: m.score.homeYellowCards ?? null,
|
||||
awayYellowCards: m.score.awayYellowCards ?? null,
|
||||
homeRedCards: m.score.homeRedCards ?? null,
|
||||
awayRedCards: m.score.awayRedCards ?? null,
|
||||
homeCards: m.score.homeCards ?? null,
|
||||
awayCards: m.score.awayCards ?? null,
|
||||
}
|
||||
: null,
|
||||
bettingOpen: this.isMatchBettingOpen({
|
||||
@@ -1327,29 +1296,49 @@ export class MatchesService {
|
||||
}),
|
||||
};
|
||||
if (m.markets && !options?.omitMarkets) {
|
||||
const csEnabled = m.correctScoreEnabled ?? true;
|
||||
const CORRECT_SCORE_TYPES = ['FT_CORRECT_SCORE', 'HT_CORRECT_SCORE', 'SH_CORRECT_SCORE'];
|
||||
return {
|
||||
...base,
|
||||
markets: m.markets
|
||||
.filter((market) => csEnabled || !CORRECT_SCORE_TYPES.includes(market.marketType as string))
|
||||
.filter(
|
||||
(market) =>
|
||||
((market.showOnPlayer as boolean | undefined) ?? true) &&
|
||||
isSettlementSupportedMarketType(market.marketType as string),
|
||||
)
|
||||
.map((market) => ({
|
||||
id: (market.id as bigint).toString(),
|
||||
marketType: market.marketType as string,
|
||||
period: market.period as string,
|
||||
status: (market.status as string) ?? 'OPEN',
|
||||
lineValue: market.lineValue != null ? Number(market.lineValue) : null,
|
||||
allowParlay: (market.allowParlay as boolean | undefined) ?? true,
|
||||
promoLabel: (market.promoLabel as string | null | undefined) ?? null,
|
||||
selections: ((market.selections as Array<Record<string, unknown>>) ?? []).map((s) => ({
|
||||
id: (s.id as bigint).toString(),
|
||||
selectionCode: s.selectionCode as string,
|
||||
selectionName: s.selectionName as string,
|
||||
status: (s.status as string) ?? 'OPEN',
|
||||
odds: Number(s.odds),
|
||||
oddsVersion: (s.oddsVersion as bigint).toString(),
|
||||
id: (market.id as bigint).toString(),
|
||||
marketType: market.marketType as string,
|
||||
marketKey: (market.marketKey as string | null | undefined) ?? (market.marketType as string),
|
||||
lineKey: (market.lineKey as string | null | undefined) ?? null,
|
||||
period: market.period as string,
|
||||
status: (market.status as string) ?? 'OPEN',
|
||||
lineValue: market.lineValue != null ? Number(market.lineValue) : null,
|
||||
allowSingle: (market.allowSingle as boolean | undefined) ?? true,
|
||||
allowParlay: (market.allowParlay as boolean | undefined) ?? true,
|
||||
marketDisplayName:
|
||||
resolveMarketText(market.nameI18n, locale, defaultMarketName(market.marketType as string, locale)) ||
|
||||
(market.marketType as string),
|
||||
promoLabel:
|
||||
resolveMarketText(
|
||||
market.promoLabelI18n,
|
||||
locale,
|
||||
(market.promoLabel as string | null | undefined) ?? '',
|
||||
) || null,
|
||||
selections: ((market.selections as Array<Record<string, unknown>>) ?? []).map((s) => ({
|
||||
id: (s.id as bigint).toString(),
|
||||
selectionCode: s.selectionCode as string,
|
||||
selectionName: s.selectionName as string,
|
||||
selectionDisplayName:
|
||||
resolveMarketText(
|
||||
s.nameI18n,
|
||||
locale,
|
||||
defaultSelectionName(market.marketType as string, s.selectionCode as string, locale) ||
|
||||
(s.selectionName as string),
|
||||
) || (s.selectionName as string),
|
||||
status: (s.status as string) ?? 'OPEN',
|
||||
odds: Number(s.odds),
|
||||
oddsVersion: (s.oddsVersion as bigint).toString(),
|
||||
})),
|
||||
})),
|
||||
})),
|
||||
};
|
||||
}
|
||||
return base;
|
||||
@@ -1392,7 +1381,7 @@ export class MatchesService {
|
||||
}
|
||||
|
||||
private playerMarketInclude = {
|
||||
where: { status: { in: ['OPEN', 'SUSPENDED', 'CLOSED'] } },
|
||||
where: { status: { in: ['OPEN', 'SUSPENDED', 'CLOSED'] }, showOnPlayer: true },
|
||||
include: {
|
||||
selections: {
|
||||
where: { status: { in: ['OPEN', 'SUSPENDED', 'CLOSED'] } },
|
||||
@@ -1404,7 +1393,7 @@ export class MatchesService {
|
||||
|
||||
/** 仅 status 字段,用于列表页计算 bettingOpen,不返回给客户端 */
|
||||
private playerMarketStatusInclude = {
|
||||
where: { status: { in: ['OPEN', 'SUSPENDED', 'CLOSED'] } },
|
||||
where: { status: { in: ['OPEN', 'SUSPENDED', 'CLOSED'] }, showOnPlayer: true },
|
||||
select: {
|
||||
status: true,
|
||||
selections: { select: { status: true } },
|
||||
@@ -1421,6 +1410,7 @@ export class MatchesService {
|
||||
|
||||
const marketWhere = {
|
||||
status: { in: ['OPEN', 'SUSPENDED', 'CLOSED'] },
|
||||
showOnPlayer: true,
|
||||
...(parlayMarketsOnly ? { marketType: { in: [...PLAYER_PARLAY_MARKET_TYPES] } } : {}),
|
||||
};
|
||||
|
||||
@@ -1576,8 +1566,8 @@ export class MatchesService {
|
||||
SH_CORRECT_SCORE: { 'zh-CN': '下半场波胆', 'en-US': '2H Correct Score', 'ms-MY': 'Skor Tepat PB2' },
|
||||
};
|
||||
const entry = labels[marketType];
|
||||
if (!entry) return marketType;
|
||||
return entry[locale] ?? entry['en-US'] ?? marketType;
|
||||
if (!entry) return defaultMarketName(marketType, locale);
|
||||
return entry[locale] ?? entry['en-US'] ?? defaultMarketName(marketType, locale);
|
||||
}
|
||||
|
||||
async enrichBetsForHistory(
|
||||
@@ -1594,6 +1584,7 @@ export class MatchesService {
|
||||
selections: Array<{
|
||||
matchId: bigint | null;
|
||||
marketType: string;
|
||||
marketNameSnapshot?: string | null;
|
||||
selectionNameSnapshot: string;
|
||||
odds: unknown;
|
||||
resultStatus?: string | null;
|
||||
@@ -1662,7 +1653,7 @@ export class MatchesService {
|
||||
const m = mid ? matchMeta.get(mid) : undefined;
|
||||
return {
|
||||
marketType: sel.marketType,
|
||||
marketLabel: this.marketLabelKey(sel.marketType, locale),
|
||||
marketLabel: sel.marketNameSnapshot || this.marketLabelKey(sel.marketType, locale),
|
||||
selectionName: sel.selectionNameSnapshot,
|
||||
odds: sel.odds,
|
||||
resultStatus: sel.resultStatus,
|
||||
|
||||
Reference in New Issue
Block a user