feat(admin,api,player): 赛事分组管理、盘口独立页与多语言展示优化

- 管理端按联赛展示单场,新增赛事/单场流程与列表展开状态保持

- 盘口赔率迁至独立页面,保存按钮仅在有修改时高亮

- API 新增联赛列表与子场查询,按 locale 返回队名并修复编译

- 波胆其它选项与促销标签等 i18n 补齐,文案更易懂
This commit is contained in:
2026-06-04 16:25:03 +08:00
parent c68abadceb
commit cc737e2924
39 changed files with 3330 additions and 378 deletions

View File

@@ -39,6 +39,7 @@ import {
MinLength,
IsIn,
Min,
ValidateIf,
} from 'class-validator';
import type { ZhiboMatchExport, ZhiboMatchesBundleExport } from '../../domains/catalog/zhibo-match.types';
@@ -207,25 +208,63 @@ class DepositDto {
remark?: string;
}
class CreatePlatformMatchDto {
class CreatePlatformLeagueDto {
@IsString()
leagueEn!: string;
@IsString()
leagueZh!: string;
@IsOptional()
@IsString()
leagueMs?: string;
@IsOptional()
@IsString()
logoUrl?: string;
@IsOptional()
@IsNumber()
displayOrder?: number;
}
class CreatePlatformMatchDto {
@IsOptional()
@IsString()
leagueId?: string;
@ValidateIf((o: CreatePlatformMatchDto) => !o.leagueId)
@IsString()
leagueEn?: string;
@ValidateIf((o: CreatePlatformMatchDto) => !o.leagueId)
@IsString()
leagueZh?: string;
@IsOptional()
@IsString()
leagueMs?: string;
@IsString()
homeTeamEn!: string;
@IsString()
homeTeamZh!: string;
@IsOptional()
@IsString()
homeTeamMs?: string;
@IsString()
awayTeamEn!: string;
@IsString()
awayTeamZh!: string;
@IsOptional()
@IsString()
awayTeamMs?: string;
@IsString()
startTime!: string;
@@ -236,6 +275,64 @@ class CreatePlatformMatchDto {
@IsOptional()
@IsNumber()
displayOrder?: number;
@IsOptional()
@IsString()
matchName?: string;
@IsOptional()
@IsString()
stage?: string;
@IsOptional()
@IsString()
groupName?: string;
@IsOptional()
@IsString()
leagueLogoUrl?: string;
@IsOptional()
@IsString()
homeTeamLogoUrl?: string;
@IsOptional()
@IsString()
awayTeamLogoUrl?: string;
}
class BatchMatchOddsDto {
@IsArray()
updates!: OutrightOddsUpdateItemDto[];
}
class UpdateMarketDto {
@IsOptional()
@IsString()
promoLabel?: string | null;
@IsOptional()
@IsString()
status?: string;
@IsOptional()
@IsNumber()
lineValue?: number | null;
}
class UpdateSelectionDto {
@IsOptional()
@IsString()
selectionName?: string;
@IsOptional()
@IsNumber()
@Min(1.01)
odds?: number;
@IsOptional()
@IsString()
status?: string;
}
function isZhiboBundlePayload(body: unknown): body is ZhiboMatchesBundleExport {
@@ -704,11 +801,58 @@ export class AdminController {
}
@Post('leagues')
async createLeague(@Body() dto: { code: string; translations: Record<string, string> }) {
const league = await this.matches.createLeague(dto.code, dto.translations);
async createLeague(
@Body() dto: CreatePlatformLeagueDto | { code: string; translations: Record<string, string> },
) {
if ('leagueZh' in dto || 'leagueEn' in dto) {
const body = dto as CreatePlatformLeagueDto;
const league = await this.matches.createPlatformLeague({
leagueEn: body.leagueEn,
leagueZh: body.leagueZh,
leagueMs: body.leagueMs,
logoUrl: body.logoUrl,
displayOrder: body.displayOrder,
});
return jsonResponse(league);
}
const legacy = dto as { code: string; translations: Record<string, string> };
const league = await this.matches.createLeague(legacy.code, legacy.translations);
return jsonResponse(league);
}
@Get('leagues')
async listLeagues(
@Query('page') page?: string,
@Query('pageSize') pageSize?: string,
@Query('status') status?: string,
@Query('keyword') keyword?: string,
) {
const p = Math.max(1, page ? parseInt(page, 10) : 1);
const size = Math.min(Math.max(1, pageSize ? parseInt(pageSize, 10) : 10), 100);
const result = await this.matches.listAdminLeagues({
page: p,
pageSize: size,
status: status || undefined,
keyword: keyword || undefined,
});
return jsonResponse(result);
}
@Get('leagues/:leagueId/matches')
async listLeagueMatches(
@Param('leagueId') leagueId: string,
@Query('status') status?: string,
@Query('keyword') keyword?: string,
@Query('locale') locale?: string,
) {
const items = await this.matches.listAdminLeagueMatches(BigInt(leagueId), {
status: status || undefined,
keyword: keyword || undefined,
locale: locale || undefined,
});
return jsonResponse({ items });
}
@Post('teams')
async createTeam(@Body() dto: { code: string; translations: Record<string, string> }) {
const team = await this.matches.createTeam(dto.code, dto.translations);
@@ -764,15 +908,24 @@ export class AdminController {
@Body() dto: CreatePlatformMatchDto,
) {
const match = await this.matches.updatePlatformMatch(BigInt(id), {
leagueEn: dto.leagueEn,
leagueZh: dto.leagueZh,
leagueEn: dto.leagueEn ?? '',
leagueZh: dto.leagueZh ?? '',
leagueMs: dto.leagueMs,
homeTeamEn: dto.homeTeamEn,
homeTeamZh: dto.homeTeamZh,
homeTeamMs: dto.homeTeamMs,
awayTeamEn: dto.awayTeamEn,
awayTeamZh: dto.awayTeamZh,
awayTeamMs: dto.awayTeamMs,
startTime: new Date(dto.startTime),
isHot: dto.isHot,
displayOrder: dto.displayOrder,
matchName: dto.matchName,
stage: dto.stage,
groupName: dto.groupName,
leagueLogoUrl: dto.leagueLogoUrl,
homeTeamLogoUrl: dto.homeTeamLogoUrl,
awayTeamLogoUrl: dto.awayTeamLogoUrl,
updatedBy: operatorId,
});
return jsonResponse(match);
@@ -787,15 +940,25 @@ export class AdminController {
@Post('matches')
async createMatch(@CurrentUser('id') operatorId: bigint, @Body() dto: CreatePlatformMatchDto) {
const match = await this.matches.createPlatformMatch({
leagueEn: dto.leagueEn,
leagueZh: dto.leagueZh,
leagueId: dto.leagueId ? BigInt(dto.leagueId) : undefined,
leagueEn: dto.leagueEn ?? '',
leagueZh: dto.leagueZh ?? '',
leagueMs: dto.leagueMs,
homeTeamEn: dto.homeTeamEn,
homeTeamZh: dto.homeTeamZh,
homeTeamMs: dto.homeTeamMs,
awayTeamEn: dto.awayTeamEn,
awayTeamZh: dto.awayTeamZh,
awayTeamMs: dto.awayTeamMs,
startTime: new Date(dto.startTime),
isHot: dto.isHot,
displayOrder: dto.displayOrder,
matchName: dto.matchName,
stage: dto.stage,
groupName: dto.groupName,
leagueLogoUrl: dto.leagueLogoUrl,
homeTeamLogoUrl: dto.homeTeamLogoUrl,
awayTeamLogoUrl: dto.awayTeamLogoUrl,
createdBy: operatorId,
});
return jsonResponse(match);
@@ -835,6 +998,48 @@ export class AdminController {
return jsonResponse(markets);
}
@Put('matches/:id/odds')
async batchUpdateMatchOdds(
@CurrentUser('id') operatorId: bigint,
@Param('id') id: string,
@Body() dto: BatchMatchOddsDto,
) {
const updates = dto.updates.map((u) => ({
selectionId: BigInt(u.selectionId),
odds: u.odds,
}));
const results = await this.markets.batchUpdateOdds(updates, operatorId);
return jsonResponse({ matchId: id, updated: results.length });
}
@Patch('markets/:id')
async updateMarket(@Param('id') id: string, @Body() dto: UpdateMarketDto) {
const market = await this.markets.updateMarket(BigInt(id), {
promoLabel: dto.promoLabel,
status: dto.status,
lineValue: dto.lineValue,
});
return jsonResponse(market);
}
@Patch('selections/:id')
async updateSelection(
@CurrentUser('id') operatorId: bigint,
@Param('id') id: string,
@Body() dto: UpdateSelectionDto,
) {
const selection = await this.markets.updateSelection(
BigInt(id),
{
selectionName: dto.selectionName,
odds: dto.odds,
status: dto.status,
},
operatorId,
);
return jsonResponse(selection);
}
@Put('selections/:id/odds')
async updateOdds(
@CurrentUser('id') operatorId: bigint,