This commit is contained in:
wchino
2026-06-13 17:38:25 +08:00
parent e7e938f261
commit 7b33d9f9fa
190 changed files with 23222 additions and 4336 deletions

View File

@@ -81,6 +81,14 @@ type AdminUploadUser = {
permissions?: string[];
};
function parseMatchStartTime(value: string): Date {
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
throw appBadRequest('MATCH_START_TIME_INVALID');
}
return date;
}
function uploadCategory(value?: string): UploadCategory {
const category = (value || 'contents').trim();
if (UPLOAD_CATEGORIES.includes(category as UploadCategory)) {
@@ -503,9 +511,6 @@ class CreatePlatformMatchDto {
@IsString()
awayTeamLogoUrl?: string;
@IsOptional()
@IsBoolean()
correctScoreEnabled?: boolean;
}
class UpdatePlatformMatchDto {
@@ -560,9 +565,6 @@ class UpdatePlatformMatchDto {
@IsString()
awayTeamLogoUrl?: string;
@IsOptional()
@IsBoolean()
correctScoreEnabled?: boolean;
}
class ReopenMatchDto {
@@ -591,6 +593,12 @@ class UpdateMarketDto {
@IsString()
promoLabel?: string | null;
@IsOptional()
promoLabelI18n?: Record<string, string> | null;
@IsOptional()
nameI18n?: Record<string, string> | null;
@IsOptional()
@IsString()
status?: string;
@@ -598,6 +606,10 @@ class UpdateMarketDto {
@IsOptional()
@IsNumber()
lineValue?: number | null;
@IsOptional()
@IsBoolean()
showOnPlayer?: boolean;
}
class UpdateSelectionDto {
@@ -605,6 +617,9 @@ class UpdateSelectionDto {
@IsString()
selectionName?: string;
@IsOptional()
nameI18n?: Record<string, string> | null;
@IsOptional()
@IsNumber()
@Min(1.01)
@@ -637,6 +652,38 @@ class ScoreDto {
@IsNumber()
ftAway?: number;
@IsOptional()
@IsNumber()
homeCorners?: number;
@IsOptional()
@IsNumber()
awayCorners?: number;
@IsOptional()
@IsNumber()
homeYellowCards?: number;
@IsOptional()
@IsNumber()
awayYellowCards?: number;
@IsOptional()
@IsNumber()
homeRedCards?: number;
@IsOptional()
@IsNumber()
awayRedCards?: number;
@IsOptional()
@IsNumber()
homeCards?: number;
@IsOptional()
@IsNumber()
awayCards?: number;
/** 冠军盘结算:获胜球队 ID */
@IsOptional()
@IsNumber()
@@ -674,6 +721,136 @@ class MarketTemplatesDto {
marketTypes!: string[];
}
class MarketDraftSelectionDto {
@IsOptional()
@IsString()
id?: string;
@IsString()
selectionCode!: string;
@IsOptional()
@IsString()
selectionName?: string;
@IsOptional()
nameI18n?: Record<string, string> | null;
@IsNumber()
@Min(1.01)
odds!: number;
@IsOptional()
@IsString()
status?: string;
@IsOptional()
@IsNumber()
sortOrder?: number;
}
class MarketDraftDto {
@IsOptional()
@IsString()
id?: string;
@IsString()
marketType!: string;
@IsOptional()
@IsString()
marketKey?: string | null;
@IsOptional()
@IsString()
lineKey?: string | null;
@IsOptional()
@IsString()
period?: string;
@IsOptional()
@IsNumber()
lineValue?: number | null;
@IsOptional()
paramsJson?: Record<string, unknown> | null;
@IsOptional()
@IsString()
status?: string;
@IsOptional()
@IsBoolean()
allowSingle?: boolean;
@IsOptional()
@IsBoolean()
allowParlay?: boolean;
@IsOptional()
@IsBoolean()
showOnPlayer?: boolean;
@IsOptional()
@IsNumber()
sortOrder?: number;
@IsOptional()
@IsString()
promoLabel?: string | null;
@IsOptional()
promoLabelI18n?: Record<string, string> | null;
@IsOptional()
nameI18n?: Record<string, string> | null;
@IsOptional()
@IsArray()
selections?: MarketDraftSelectionDto[];
}
class MarketTemplateSaveDto {
@IsOptional()
@IsString()
name?: string;
@IsOptional()
nameI18n?: Record<string, string> | null;
@IsOptional()
@IsString()
description?: string | null;
@IsOptional()
@IsBoolean()
isDefault?: boolean;
@IsOptional()
@IsString()
status?: string;
@IsOptional()
@IsNumber()
sortOrder?: number;
@IsOptional()
@IsArray()
items?: MarketDraftDto[];
}
class ApplyMarketTemplateDto {
@IsOptional()
@IsString()
templateId?: string;
}
class BulkMatchMarketsDto {
@IsArray()
markets!: MarketDraftDto[];
}
class UpdateOddsDto {
@IsNumber()
odds!: number;
@@ -892,6 +1069,38 @@ class ResettlePreviewDto {
@IsNumber()
ftAway?: number;
@IsOptional()
@IsNumber()
homeCorners?: number;
@IsOptional()
@IsNumber()
awayCorners?: number;
@IsOptional()
@IsNumber()
homeYellowCards?: number;
@IsOptional()
@IsNumber()
awayYellowCards?: number;
@IsOptional()
@IsNumber()
homeRedCards?: number;
@IsOptional()
@IsNumber()
awayRedCards?: number;
@IsOptional()
@IsNumber()
homeCards?: number;
@IsOptional()
@IsNumber()
awayCards?: number;
@IsOptional()
@IsString()
reason?: string;
@@ -1665,7 +1874,7 @@ export class AdminController {
awayTeamEn: dto.awayTeamEn,
awayTeamZh: dto.awayTeamZh,
awayTeamMs: dto.awayTeamMs,
startTime: new Date(dto.startTime),
startTime: parseMatchStartTime(dto.startTime),
isHot: dto.isHot,
displayOrder: dto.displayOrder,
matchName: dto.matchName,
@@ -1673,7 +1882,6 @@ export class AdminController {
groupName: dto.groupName,
homeTeamLogoUrl: dto.homeTeamLogoUrl,
awayTeamLogoUrl: dto.awayTeamLogoUrl,
correctScoreEnabled: dto.correctScoreEnabled,
updatedBy: operatorId,
});
await this.outright.syncOutrightTeamsForLeagueIfExists(match.leagueId);
@@ -1725,9 +1933,8 @@ export class AdminController {
awayTeamEn: dto.awayTeamEn,
awayTeamZh: dto.awayTeamZh,
awayTeamMs: dto.awayTeamMs,
startTime: new Date(dto.startTime),
startTime: parseMatchStartTime(dto.startTime),
isHot: dto.isHot,
correctScoreEnabled: dto.correctScoreEnabled,
displayOrder: dto.displayOrder,
matchName: dto.matchName,
stage: dto.stage,
@@ -1775,7 +1982,7 @@ export class AdminController {
@Post('matches/:id/reopen')
@RequirePermissions(P.matches)
async reopenMatch(@Param('id') id: string, @Body() dto: ReopenMatchDto) {
const startTime = dto.startTime ? new Date(dto.startTime) : undefined;
const startTime = dto.startTime ? parseMatchStartTime(dto.startTime) : undefined;
const match = await this.matches.reopenMatch(BigInt(id), startTime);
return jsonResponse(match);
}
@@ -1783,11 +1990,58 @@ export class AdminController {
@Post('matches/:id/cancel')
@RequirePermissions(P.matches)
async cancelMatch(@Param('id') id: string) {
await this.matches.cancelMatch(BigInt(id));
const voided = await this.settlement.voidMatchBets(BigInt(id));
const voided = await this.settlement.cancelMatchAndVoidBets(BigInt(id));
return jsonResponse(voided);
}
@Get('market-definitions')
@RequirePermissions(P.matches)
async listMarketDefinitions() {
return jsonResponse(this.markets.listMarketDefinitions());
}
@Get('market-templates')
@RequirePermissions(P.matches)
async listMarketTemplates() {
const templates = await this.markets.listTemplates();
return jsonResponse(templates);
}
@Post('market-templates')
@RequirePermissions(P.matches)
async createMarketTemplate(@Body() dto: MarketTemplateSaveDto) {
const template = await this.markets.createTemplate(dto);
return jsonResponse(template);
}
@Get('market-templates/:id')
@RequirePermissions(P.matches)
async getMarketTemplate(@Param('id') id: string) {
const template = await this.markets.getTemplate(BigInt(id));
return jsonResponse(template);
}
@Put('market-templates/:id')
@RequirePermissions(P.matches)
async updateMarketTemplate(@Param('id') id: string, @Body() dto: MarketTemplateSaveDto) {
const template = await this.markets.updateTemplate(BigInt(id), dto);
return jsonResponse(template);
}
@Post('market-templates/:id/duplicate')
@RequirePermissions(P.matches)
async duplicateMarketTemplate(@Param('id') id: string) {
const template = await this.markets.duplicateTemplate(BigInt(id));
return jsonResponse(template);
}
@Post('market-templates/:id/set-default')
@RequirePermissions(P.matches)
async setDefaultMarketTemplate(@Param('id') id: string) {
const template = await this.markets.setDefaultTemplate(BigInt(id));
return jsonResponse(template);
}
@Post('matches/:id/markets/templates')
@RequirePermissions(P.matches)
async generateTemplates(@Param('id') id: string, @Body() dto: MarketTemplatesDto) {
@@ -1795,6 +2049,27 @@ export class AdminController {
return jsonResponse(markets);
}
@Post('matches/:id/markets/apply-template')
@RequirePermissions(P.matches)
async applyMarketTemplate(@Param('id') id: string, @Body() dto: ApplyMarketTemplateDto) {
const result = await this.markets.applyTemplateToMatch(
BigInt(id),
dto.templateId ? BigInt(dto.templateId) : null,
);
return jsonResponse(result);
}
@Put('matches/:id/markets/bulk')
@RequirePermissions(P.matches)
async bulkSaveMatchMarkets(
@CurrentUser('id') operatorId: bigint,
@Param('id') id: string,
@Body() dto: BulkMatchMarketsDto,
) {
const result = await this.markets.saveMatchMarkets(BigInt(id), dto.markets, operatorId);
return jsonResponse(result);
}
@Put('matches/:id/odds')
@RequirePermissions(P.matches)
async batchUpdateMatchOdds(
@@ -1815,8 +2090,11 @@ export class AdminController {
async updateMarket(@Param('id') id: string, @Body() dto: UpdateMarketDto) {
const market = await this.markets.updateMarket(BigInt(id), {
promoLabel: dto.promoLabel,
promoLabelI18n: dto.promoLabelI18n,
nameI18n: dto.nameI18n,
status: dto.status,
lineValue: dto.lineValue,
showOnPlayer: dto.showOnPlayer,
});
return jsonResponse(market);
}
@@ -1832,6 +2110,7 @@ export class AdminController {
BigInt(id),
{
selectionName: dto.selectionName,
nameI18n: dto.nameI18n,
odds: dto.odds,
status: dto.status,
},
@@ -2057,6 +2336,16 @@ export class AdminController {
dto.ftAway ?? 0,
operatorId,
dto.winnerTeamId != null ? BigInt(dto.winnerTeamId) : undefined,
{
homeCorners: dto.homeCorners ?? null,
awayCorners: dto.awayCorners ?? null,
homeYellowCards: dto.homeYellowCards ?? null,
awayYellowCards: dto.awayYellowCards ?? null,
homeRedCards: dto.homeRedCards ?? null,
awayRedCards: dto.awayRedCards ?? null,
homeCards: dto.homeCards ?? null,
awayCards: dto.awayCards ?? null,
},
);
return jsonResponse(result);
}
@@ -2074,6 +2363,14 @@ export class AdminController {
htAway: dto?.htAway,
ftHome: dto?.ftHome,
ftAway: dto?.ftAway,
homeCorners: dto?.homeCorners,
awayCorners: dto?.awayCorners,
homeYellowCards: dto?.homeYellowCards,
awayYellowCards: dto?.awayYellowCards,
homeRedCards: dto?.homeRedCards,
awayRedCards: dto?.awayRedCards,
homeCards: dto?.homeCards,
awayCards: dto?.awayCards,
winnerTeamId: dto?.winnerTeamId != null ? BigInt(dto.winnerTeamId) : undefined,
page: dto?.page ? Math.max(1, dto.page) : 1,
pageSize: dto?.pageSize ? Math.min(100, Math.max(1, dto.pageSize)) : 10,
@@ -2123,6 +2420,14 @@ export class AdminController {
htAway: dto.htAway ?? 0,
ftHome: dto.ftHome ?? 0,
ftAway: dto.ftAway ?? 0,
homeCorners: dto.homeCorners ?? null,
awayCorners: dto.awayCorners ?? null,
homeYellowCards: dto.homeYellowCards ?? null,
awayYellowCards: dto.awayYellowCards ?? null,
homeRedCards: dto.homeRedCards ?? null,
awayRedCards: dto.awayRedCards ?? null,
homeCards: dto.homeCards ?? null,
awayCards: dto.awayCards ?? null,
},
operatorId,
dto.reason,