feat(player): 注册账号、登录双模式与移动端性能优化

注册必填 7-32 位账号,手机号区号/本地号分存;登录默认账号模式并支持切换手机号登录;Player i18n 拆包与赛事接口优化。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-12 10:56:51 +08:00
parent 83f0f380c5
commit 312c3c5816
35 changed files with 1944 additions and 1394 deletions

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common';
import { isPreMatchKickoff, resolveTranslationFallback } from '@thebet365/shared';
import { isPreMatchKickoff, MarketType, resolveTranslationFallback } from '@thebet365/shared';
import { Cron, CronExpression } from '@nestjs/schedule';
import { Prisma } from '@prisma/client';
import { Decimal } from '@prisma/client/runtime/library';
@@ -26,6 +26,23 @@ import { syncWc2026OutrightMarket } from './wc2026-outright.sync';
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;
export type ListPublishedOptions = {
/** 响应中是否包含 markets列表页默认 false仅摘要 */
includeMarkets?: boolean;
/** 仅返回串关玩法 markets需 includeMarkets=true */
parlayMarketsOnly?: boolean;
};
@Injectable()
export class MatchesService {
constructor(private prisma: PrismaService) {}
@@ -1158,7 +1175,11 @@ export class MatchesService {
return resolveTranslationFallback(map, locale);
}
async enrichMatch(match: Record<string, unknown>, locale: string) {
async enrichMatch(
match: Record<string, unknown>,
locale: string,
options?: { omitMarkets?: boolean },
) {
const m = match as {
id: bigint;
leagueId: bigint;
@@ -1230,7 +1251,7 @@ export class MatchesService {
}>,
}),
};
if (m.markets) {
if (m.markets && !options?.omitMarkets) {
return {
...base,
markets: m.markets.map((market) => ({
@@ -1302,7 +1323,41 @@ export class MatchesService {
orderBy: { sortOrder: 'asc' as const },
};
async listPublished(locale = 'en-US', leagueId?: bigint) {
/** 仅 status 字段,用于列表页计算 bettingOpen不返回给客户端 */
private playerMarketStatusInclude = {
where: { status: { in: ['OPEN', 'SUSPENDED', 'CLOSED'] } },
select: {
status: true,
selections: { select: { status: true } },
},
};
async listPublished(
locale = 'en-US',
leagueId?: bigint,
options?: ListPublishedOptions,
) {
const includeMarkets = options?.includeMarkets ?? true;
const parlayMarketsOnly = options?.parlayMarketsOnly ?? false;
const marketWhere = {
status: { in: ['OPEN', 'SUSPENDED', 'CLOSED'] },
...(parlayMarketsOnly ? { marketType: { in: [...PLAYER_PARLAY_MARKET_TYPES] } } : {}),
};
const marketsRelation = includeMarkets
? {
where: marketWhere,
include: {
selections: {
where: { status: { in: ['OPEN', 'SUSPENDED', 'CLOSED'] } },
orderBy: { sortOrder: 'asc' as const },
},
},
orderBy: { sortOrder: 'asc' as const },
}
: this.playerMarketStatusInclude;
const matches = await this.prisma.match.findMany({
where: {
status: { in: ['PUBLISHED', 'CLOSED', 'PENDING_SETTLEMENT', 'SETTLED'] },
@@ -1317,12 +1372,22 @@ export class MatchesService {
homeTeam: true,
awayTeam: true,
score: true,
markets: this.playerMarketInclude,
markets: marketsRelation,
},
orderBy: [{ isHot: 'desc' }, { displayOrder: 'asc' }, { startTime: 'asc' }],
});
return Promise.all(matches.map((m) => this.enrichMatch(m, locale)));
const enriched = await Promise.all(
matches.map((m) => this.enrichMatch(m, locale, { omitMarkets: !includeMarkets })),
);
if (!includeMarkets || !parlayMarketsOnly) return enriched;
return enriched.filter(
(m) =>
Array.isArray((m as { markets?: unknown[] }).markets) &&
(m as { markets: unknown[] }).markets.length > 0,
);
}
async getMatchDetail(matchId: bigint, locale = 'en-US') {