feat(admin,api,player): 赛事分组管理、盘口独立页与多语言展示优化
- 管理端按联赛展示单场,新增赛事/单场流程与列表展开状态保持 - 盘口赔率迁至独立页面,保存按钮仅在有修改时高亮 - API 新增联赛列表与子场查询,按 locale 返回队名并修复编译 - 波胆其它选项与促销标签等 i18n 补齐,文案更易懂
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user