feat: WC2026 赛事 seed、生产上线初始化脚本与目录归档

重构 seed 为 WC2026 72 场小组赛与 48 强优胜盘;新增 production 模式仅保留 admin 与赛事示例;提供 prod-init-db 全量重置脚本;管理端 i18n 分包与赛事归档能力。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-12 18:17:00 +08:00
parent 8f14e85ebd
commit e7e938f261
94 changed files with 12332 additions and 976 deletions

View File

@@ -27,6 +27,7 @@ import { UsersService } from '../../domains/identity/users.service';
import { AgentsService } from '../../domains/agent/agents.service';
import { WalletService } from '../../domains/ledger/wallet.service';
import { MatchesService } from '../../domains/catalog/matches.service';
import { CatalogArchiveService } from '../../domains/catalog/catalog-archive.service';
import { OutrightService } from '../../domains/catalog/outright.service';
import { MarketsService } from '../../domains/odds/markets.service';
import { SettlementService } from '../../domains/settlement/settlement.service';
@@ -501,6 +502,10 @@ class CreatePlatformMatchDto {
@IsOptional()
@IsString()
awayTeamLogoUrl?: string;
@IsOptional()
@IsBoolean()
correctScoreEnabled?: boolean;
}
class UpdatePlatformMatchDto {
@@ -554,6 +559,10 @@ class UpdatePlatformMatchDto {
@IsOptional()
@IsString()
awayTeamLogoUrl?: string;
@IsOptional()
@IsBoolean()
correctScoreEnabled?: boolean;
}
class ReopenMatchDto {
@@ -562,6 +571,16 @@ class ReopenMatchDto {
startTime?: string;
}
class ArchiveMatchDto {
@IsOptional()
@IsBoolean()
force?: boolean;
@IsOptional()
@IsBoolean()
refundPendingBets?: boolean;
}
class BatchMatchOddsDto {
@IsArray()
updates!: OutrightOddsUpdateItemDto[];
@@ -924,6 +943,7 @@ export class AdminController {
private agents: AgentsService,
private wallet: WalletService,
private matches: MatchesService,
private catalogArchive: CatalogArchiveService,
private outright: OutrightService,
private markets: MarketsService,
private settlement: SettlementService,
@@ -948,6 +968,31 @@ export class AdminController {
return jsonResponse(overview);
}
@Get('users/page-init')
@RequirePermissions(P.agentsView)
async getUsersPageInit() {
const [
playerSettings,
bettingLimits,
hierarchySettings,
platformDirect,
agentLevelCounts,
] = await Promise.all([
this.systemConfig.getPlayerAccountSettings(),
this.bettingLimits.getLimits(),
this.systemConfig.getAgentHierarchySettings(),
this.systemConfig.getPlatformDirectCashbackSettings(),
this.agents.countAgentsByLevel(),
]);
return jsonResponse({
playerSettings,
bettingLimits,
hierarchySettings,
platformDirect,
agentLevelCounts,
});
}
@Get('users/settings/account')
@RequirePermissions(P.settings)
async getPlayerAccountSettings() {
@@ -1126,6 +1171,23 @@ export class AdminController {
return jsonResponse(detail);
}
@Delete('users/:id')
@RequirePermissions(P.usersCreate)
async deletePlayer(
@CurrentUser('id') operatorId: bigint,
@Param('id') id: string,
) {
await this.users.softDeletePlayer(BigInt(id));
await this.audit.log({
operatorId,
operatorType: 'ADMIN',
action: 'DELETE_PLAYER',
module: 'USERS',
targetId: id,
});
return jsonResponse({ deleted: true });
}
@Post('users')
@RequirePermissions(P.usersCreate)
async createPlayer(
@@ -1184,9 +1246,20 @@ export class AdminController {
@Get('agents/options')
@RequirePermissions(P.agentsView)
async listAgentOptions() {
async listAgentOptions(
@Query('keyword') keyword?: string,
@Query('limit') limit?: string,
) {
const take = Math.min(100, Math.max(1, parseInt(limit ?? '50', 10) || 50));
const kw = keyword?.trim();
const agents = await this.prisma.user.findMany({
where: { userType: 'AGENT', deletedAt: null },
where: {
userType: 'AGENT',
deletedAt: null,
...(kw
? { username: { contains: kw, mode: 'insensitive' as const } }
: {}),
},
select: {
id: true,
username: true,
@@ -1194,6 +1267,7 @@ export class AdminController {
parent: { select: { username: true } },
},
orderBy: [{ agentLevel: 'asc' }, { username: 'asc' }],
take,
});
return jsonResponse(
agents.map((a) => ({
@@ -1464,6 +1538,20 @@ export class AdminController {
return jsonResponse(league);
}
@Get('leagues/:leagueId/archive-preview')
@RequirePermissions(P.matches)
async getLeagueArchivePreview(@Param('leagueId') leagueId: string) {
const preview = await this.catalogArchive.getLeagueArchivePreview(BigInt(leagueId));
return jsonResponse(preview);
}
@Post('leagues/:leagueId/archive')
@RequirePermissions(P.matches)
async archiveLeague(@Param('leagueId') leagueId: string) {
const result = await this.catalogArchive.archiveLeague(BigInt(leagueId));
return jsonResponse(result);
}
@Get('leagues')
@RequirePermissions(P.matches, P.reports)
async listLeagues(
@@ -1499,13 +1587,17 @@ export class AdminController {
@Query('status') status?: string,
@Query('keyword') keyword?: string,
@Query('locale') locale?: string,
@Query('page') page?: string,
@Query('pageSize') pageSize?: string,
) {
const items = await this.matches.listAdminLeagueMatches(BigInt(leagueId), {
const result = await this.matches.listAdminLeagueMatches(BigInt(leagueId), {
status: status || undefined,
keyword: keyword || undefined,
locale: locale || undefined,
page: page ? Math.max(1, parseInt(page, 10) || 1) : 1,
pageSize: pageSize ? Math.min(100, Math.max(1, parseInt(pageSize, 10) || 20)) : 20,
});
return jsonResponse({ items });
return jsonResponse(result);
}
@Post('teams')
@@ -1581,6 +1673,7 @@ export class AdminController {
groupName: dto.groupName,
homeTeamLogoUrl: dto.homeTeamLogoUrl,
awayTeamLogoUrl: dto.awayTeamLogoUrl,
correctScoreEnabled: dto.correctScoreEnabled,
updatedBy: operatorId,
});
await this.outright.syncOutrightTeamsForLeagueIfExists(match.leagueId);
@@ -1594,6 +1687,28 @@ export class AdminController {
return jsonResponse({ deleted: true });
}
@Get('matches/:id/archive-preview')
@RequirePermissions(P.matches)
async getMatchArchivePreview(@Param('id') id: string) {
const preview = await this.catalogArchive.getMatchArchivePreview(BigInt(id));
return jsonResponse(preview);
}
@Post('matches/:id/archive')
@RequirePermissions(P.matches)
async archiveMatch(@Param('id') id: string, @Body() dto: ArchiveMatchDto) {
const matchId = BigInt(id);
const result = await this.catalogArchive.archiveMatch(matchId, {
force: dto.force === true,
});
let voidedCount = 0;
if (dto.refundPendingBets) {
const voided = await this.settlement.voidMatchBets(matchId);
voidedCount = voided.voidedCount;
}
return jsonResponse({ ...result, voidedCount });
}
@Post('matches')
@RequirePermissions(P.matches)
async createMatch(@CurrentUser('id') operatorId: bigint, @Body() dto: CreatePlatformMatchDto) {
@@ -1612,6 +1727,7 @@ export class AdminController {
awayTeamMs: dto.awayTeamMs,
startTime: new Date(dto.startTime),
isHot: dto.isHot,
correctScoreEnabled: dto.correctScoreEnabled,
displayOrder: dto.displayOrder,
matchName: dto.matchName,
stage: dto.stage,
@@ -1642,6 +1758,13 @@ export class AdminController {
return jsonResponse(match);
}
@Post('matches/:id/unpublish')
@RequirePermissions(P.matches)
async unpublishMatch(@Param('id') id: string) {
const match = await this.matches.unpublishMatch(BigInt(id));
return jsonResponse(match);
}
@Post('matches/:id/close')
@RequirePermissions(P.matches)
async closeMatch(@Param('id') id: string) {
@@ -1880,6 +2003,27 @@ export class AdminController {
return jsonResponse(data);
}
@Get('matches/:id/settlement/summary')
@RequirePermissions(P.settlement, P.reports)
async getMatchSettlementSummary(@Param('id') id: string) {
const data = await this.settlement.getMatchBetStatsSummary(BigInt(id));
return jsonResponse(data);
}
@Get('matches/:id/settlement/bets')
@RequirePermissions(P.settlement, P.reports)
async getMatchSettlementBets(
@Param('id') id: string,
@Query('page') page?: string,
@Query('pageSize') pageSize?: string,
) {
const data = await this.settlement.getMatchBetStatsBets(BigInt(id), {
page: page ? Math.max(1, parseInt(page, 10) || 1) : 1,
pageSize: pageSize ? Math.min(100, Math.max(1, parseInt(pageSize, 10) || 10)) : 10,
});
return jsonResponse(data);
}
@Get('matches/:id/settlement/stats')
@RequirePermissions(P.settlement, P.reports)
async getMatchSettlementStats(
@@ -2495,4 +2639,30 @@ export class AdminController {
);
return jsonResponse(result);
}
@Post('deposit-orders/:id/reopen')
@RequirePermissions(P.depositReview)
async reopenDepositOrder(
@CurrentUser('id') operatorId: bigint,
@Param('id') id: string,
) {
const result = await this.depositService.reopenDepositOrderForReview(
BigInt(id),
operatorId,
);
return jsonResponse(result);
}
@Delete('deposit-orders/:id')
@RequirePermissions(P.depositReview)
async deleteDepositOrder(
@CurrentUser('id') operatorId: bigint,
@Param('id') id: string,
) {
const result = await this.depositService.deleteDepositOrder(
BigInt(id),
operatorId,
);
return jsonResponse(result);
}
}

View File

@@ -3,6 +3,7 @@ import {
Get,
Post,
Put,
Delete,
Body,
Param,
Query,
@@ -176,9 +177,18 @@ export class AgentPortalController {
}
@Get('players')
async listPlayers(@CurrentUser('id') agentId: bigint) {
const players = await this.agents.getDirectPlayers(agentId);
return jsonResponse(players);
async listPlayers(
@CurrentUser('id') agentId: bigint,
@Query('page') page?: string,
@Query('pageSize') pageSize?: string,
) {
const result = await this.agents.getDirectPlayers(agentId, {
page: page ? Math.max(1, parseInt(page, 10) || 1) : 1,
pageSize: pageSize
? Math.min(100, Math.max(1, parseInt(pageSize, 10) || 20))
: 100,
});
return jsonResponse(result.items);
}
@Get('players/scoped')
@@ -261,6 +271,15 @@ export class AgentPortalController {
return jsonResponse(detail);
}
@Delete('players/:id')
async deletePlayer(
@CurrentUser('id') agentId: bigint,
@Param('id') playerId: string,
) {
await this.agents.deleteDirectPlayer(agentId, BigInt(playerId));
return jsonResponse({ deleted: true });
}
@Get('agents')
async listSubAgents(@CurrentUser('id') agentId: bigint, @CurrentUser('agentLevel') level: number) {
const maxLevel = await this.agents.getMaxAgentLevel();
@@ -401,13 +420,23 @@ export class AgentPortalController {
@CurrentUser('id') agentId: bigint,
@CurrentUser('agentLevel') level: number,
@Param('id') subAgentId: string,
@Query('page') page?: string,
@Query('pageSize') pageSize?: string,
) {
if (!(await this.canManageSubAgents(level))) {
return jsonResponse([]);
return jsonResponse({ items: [], total: 0, page: 1, pageSize: 20 });
}
await this.agents.assertDescendantAgent(agentId, BigInt(subAgentId));
const players = await this.agents.getPortalAgentDirectPlayers(agentId, BigInt(subAgentId));
return jsonResponse(players);
const result = await this.agents.getPortalAgentDirectPlayers(
agentId,
BigInt(subAgentId),
{
page: page ? Math.max(1, parseInt(page, 10) || 1) : 1,
pageSize: pageSize
? Math.min(100, Math.max(1, parseInt(pageSize, 10) || 20))
: 20,
},
);
return jsonResponse(result);
}
@Post('agents/:id/credit')