feat: split admin dashboard, improve match ops, and player closed-match UX
Admin: add match/player overview sub-nav; refine settlement flow and league match management UI; improve action button enabled/disabled styles; enhance logo upload and outright odds sync. API: expose matchPhase/bettingOpen for closed matches; league publish guards; settlement preview with auto score save; outright team auto-sync. Player: watermark for closed/settled states; keep match and bet details visible; remove default login credentials. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -373,6 +373,10 @@ class CreatePlatformLeagueDto {
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
displayOrder?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
class CreatePlatformMatchDto {
|
||||
@@ -456,6 +460,59 @@ class CreatePlatformMatchDto {
|
||||
awayTeamLogoUrl?: string;
|
||||
}
|
||||
|
||||
class UpdatePlatformMatchDto {
|
||||
@IsString()
|
||||
homeTeamEn!: string;
|
||||
|
||||
@IsString()
|
||||
homeTeamZh!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
homeTeamMs?: string;
|
||||
|
||||
@IsString()
|
||||
awayTeamEn!: string;
|
||||
|
||||
@IsString()
|
||||
awayTeamZh!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
awayTeamMs?: string;
|
||||
|
||||
@IsString()
|
||||
startTime!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isHot?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
displayOrder?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
matchName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
stage?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
groupName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
homeTeamLogoUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
awayTeamLogoUrl?: string;
|
||||
}
|
||||
|
||||
class BatchMatchOddsDto {
|
||||
@IsArray()
|
||||
updates!: OutrightOddsUpdateItemDto[];
|
||||
@@ -518,6 +575,16 @@ class ScoreDto {
|
||||
winnerTeamId?: number;
|
||||
}
|
||||
|
||||
class SettlementPreviewDto extends ScoreDto {
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
page?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
/* 智能比分推荐已关闭
|
||||
class SmartScoreSuggestDto {
|
||||
@IsOptional()
|
||||
@@ -1252,6 +1319,7 @@ export class AdminController {
|
||||
leagueMs: body.leagueMs,
|
||||
logoUrl: body.logoUrl,
|
||||
displayOrder: body.displayOrder,
|
||||
isActive: body.isActive,
|
||||
});
|
||||
return jsonResponse(league);
|
||||
}
|
||||
@@ -1260,6 +1328,23 @@ export class AdminController {
|
||||
return jsonResponse(league);
|
||||
}
|
||||
|
||||
@Put('leagues/:leagueId')
|
||||
@RequirePermissions(P.matches)
|
||||
async updateLeague(
|
||||
@Param('leagueId') leagueId: string,
|
||||
@Body() dto: CreatePlatformLeagueDto,
|
||||
) {
|
||||
const league = await this.matches.updatePlatformLeague(BigInt(leagueId), {
|
||||
leagueEn: dto.leagueEn,
|
||||
leagueZh: dto.leagueZh,
|
||||
leagueMs: dto.leagueMs,
|
||||
logoUrl: dto.logoUrl,
|
||||
displayOrder: dto.displayOrder,
|
||||
isActive: dto.isActive,
|
||||
});
|
||||
return jsonResponse(league);
|
||||
}
|
||||
|
||||
@Get('leagues')
|
||||
@RequirePermissions(P.matches, P.reports)
|
||||
async listLeagues(
|
||||
@@ -1360,12 +1445,9 @@ export class AdminController {
|
||||
async updateMatch(
|
||||
@CurrentUser('id') operatorId: bigint,
|
||||
@Param('id') id: string,
|
||||
@Body() dto: CreatePlatformMatchDto,
|
||||
@Body() dto: UpdatePlatformMatchDto,
|
||||
) {
|
||||
const match = await this.matches.updatePlatformMatch(BigInt(id), {
|
||||
leagueEn: dto.leagueEn ?? '',
|
||||
leagueZh: dto.leagueZh ?? '',
|
||||
leagueMs: dto.leagueMs,
|
||||
homeTeamEn: dto.homeTeamEn,
|
||||
homeTeamZh: dto.homeTeamZh,
|
||||
homeTeamMs: dto.homeTeamMs,
|
||||
@@ -1378,11 +1460,11 @@ export class AdminController {
|
||||
matchName: dto.matchName,
|
||||
stage: dto.stage,
|
||||
groupName: dto.groupName,
|
||||
leagueLogoUrl: dto.leagueLogoUrl,
|
||||
homeTeamLogoUrl: dto.homeTeamLogoUrl,
|
||||
awayTeamLogoUrl: dto.awayTeamLogoUrl,
|
||||
updatedBy: operatorId,
|
||||
});
|
||||
await this.outright.syncOutrightTeamsForLeagueIfExists(match.leagueId);
|
||||
return jsonResponse(match);
|
||||
}
|
||||
|
||||
@@ -1420,6 +1502,7 @@ export class AdminController {
|
||||
awayTeamLogoUrl: dto.awayTeamLogoUrl,
|
||||
createdBy: operatorId,
|
||||
});
|
||||
await this.outright.syncOutrightTeamsForLeagueIfExists(match.leagueId);
|
||||
return jsonResponse(match);
|
||||
}
|
||||
|
||||
@@ -1712,9 +1795,27 @@ export class AdminController {
|
||||
async settlementPreview(
|
||||
@CurrentUser('id') operatorId: bigint,
|
||||
@Param('id') id: string,
|
||||
@Body() dto?: { page?: number; pageSize?: number },
|
||||
@Body() dto?: SettlementPreviewDto,
|
||||
) {
|
||||
const preview = await this.settlement.previewSettlement(BigInt(id), operatorId, {
|
||||
const matchId = BigInt(id);
|
||||
const hasScore =
|
||||
dto?.htHome !== undefined ||
|
||||
dto?.htAway !== undefined ||
|
||||
dto?.ftHome !== undefined ||
|
||||
dto?.ftAway !== undefined ||
|
||||
dto?.winnerTeamId !== undefined;
|
||||
if (hasScore) {
|
||||
await this.settlement.recordScore(
|
||||
matchId,
|
||||
dto!.htHome ?? 0,
|
||||
dto!.htAway ?? 0,
|
||||
dto!.ftHome ?? 0,
|
||||
dto!.ftAway ?? 0,
|
||||
operatorId,
|
||||
dto!.winnerTeamId != null ? BigInt(dto!.winnerTeamId) : undefined,
|
||||
);
|
||||
}
|
||||
const preview = await this.settlement.previewSettlement(matchId, operatorId, {
|
||||
page: dto?.page ? Math.max(1, dto.page) : 1,
|
||||
pageSize: dto?.pageSize ? Math.min(100, Math.max(1, dto.pageSize)) : 10,
|
||||
});
|
||||
@@ -1980,6 +2081,24 @@ export class AdminController {
|
||||
return jsonResponse({ ok: true });
|
||||
}
|
||||
|
||||
@Delete('uploads/by-url')
|
||||
@RequirePermissions(P.content, P.matches)
|
||||
async deleteFileByUrl(@Body() body: { url: string }) {
|
||||
const { url } = body;
|
||||
if (!url || typeof url !== 'string') throw new BadRequestException('url is required');
|
||||
|
||||
const record = await this.prisma.uploadedFile.findFirst({ where: { url } });
|
||||
if (!record) return jsonResponse({ ok: true, note: 'not_found' });
|
||||
|
||||
const root = getUploadRoot();
|
||||
try {
|
||||
await unlink(join(root, record.category, record.filename));
|
||||
} catch { /* already gone */ }
|
||||
await this.prisma.uploadedFile.delete({ where: { id: record.id } });
|
||||
|
||||
return jsonResponse({ ok: true });
|
||||
}
|
||||
|
||||
private async getUsedFileUrls(): Promise<Set<string>> {
|
||||
const [ctRows, leagueRows, teamRows, prefRows] = await Promise.all([
|
||||
this.prisma.contentTranslation.findMany({ select: { imageUrl: true } }),
|
||||
|
||||
@@ -98,14 +98,14 @@ class TransferDto {
|
||||
|
||||
@IsString()
|
||||
requestId!: string;
|
||||
}
|
||||
|
||||
class CreditDto extends TransferDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
class CreditDto extends TransferDto {}
|
||||
|
||||
class UpdateSubAgentDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@@ -249,7 +249,13 @@ export class AgentPortalController {
|
||||
@Param('id') playerId: string,
|
||||
@Body() dto: TransferDto,
|
||||
) {
|
||||
const result = await this.agents.depositToPlayer(agentId, BigInt(playerId), dto.amount, dto.requestId);
|
||||
const result = await this.agents.depositToPlayer(
|
||||
agentId,
|
||||
BigInt(playerId),
|
||||
dto.amount,
|
||||
dto.requestId,
|
||||
dto.remark,
|
||||
);
|
||||
return jsonResponse(result);
|
||||
}
|
||||
|
||||
@@ -259,7 +265,13 @@ export class AgentPortalController {
|
||||
@Param('id') playerId: string,
|
||||
@Body() dto: TransferDto,
|
||||
) {
|
||||
const result = await this.agents.withdrawFromPlayer(agentId, BigInt(playerId), dto.amount, dto.requestId);
|
||||
const result = await this.agents.withdrawFromPlayer(
|
||||
agentId,
|
||||
BigInt(playerId),
|
||||
dto.amount,
|
||||
dto.requestId,
|
||||
dto.remark,
|
||||
);
|
||||
return jsonResponse(result);
|
||||
}
|
||||
|
||||
@@ -335,10 +347,7 @@ export class AgentPortalController {
|
||||
const p = Math.max(1, page ? parseInt(page, 10) : 1);
|
||||
const size = Math.min(Math.max(1, pageSize ? parseInt(pageSize, 10) : 10), 100);
|
||||
const skip = (p - 1) * size;
|
||||
const descendants = await this.prisma.agentClosure.findMany({
|
||||
where: { ancestorId: agentId },
|
||||
});
|
||||
const agentIds = descendants.map((d) => d.descendantId);
|
||||
const agentIds = await this.agents.getSubtreeAgentIds(agentId);
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.bet.findMany({
|
||||
|
||||
Reference in New Issue
Block a user