feat(admin,player,api): 优胜冠军通用管理与界面精简

管理端新增冠军盘列表/编辑、展开懒加载与 ECharts 修复;各列表页去掉重复标题。玩家端支持多赛事冠军盘、分批加载与语言切换刷新。API 扩展 outright CRUD 与列表性能优化。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-04 09:17:01 +08:00
parent 9b63d67e7c
commit 27580b2479
39 changed files with 2250 additions and 578 deletions

View File

@@ -260,6 +260,52 @@ class BatchOutrightOddsDto {
updates!: OutrightOddsUpdateItemDto[];
}
class CreateOutrightDto {
@IsString()
leagueId!: string;
@IsString()
titleZh!: string;
@IsString()
titleEn!: string;
@IsOptional()
@IsString()
status?: string;
}
class UpdateOutrightDto {
@IsOptional()
@IsString()
status?: string;
@IsOptional()
@IsString()
matchName?: string;
@IsOptional()
isHot?: boolean;
@IsOptional()
displayOrder?: number;
}
class AddOutrightSelectionDto {
@IsString()
teamCode!: string;
@IsString()
teamZh!: string;
@IsString()
teamEn!: string;
@IsNumber()
@Min(1.01)
odds!: number;
}
class CashbackPreviewDto {
@IsString()
periodStart!: string;
@@ -648,24 +694,111 @@ export class AdminController {
return jsonResponse(selection);
}
@Get('outrights/wc2026')
async getWc2026Outright() {
const data = await this.outright.getWc2026ForAdmin();
@Get('outrights')
async listOutrights() {
const data = await this.outright.listForAdmin();
return jsonResponse(data);
}
@Get('outrights/leagues')
async listOutrightLeagues() {
const data = await this.outright.listLeagueOptions();
return jsonResponse(data);
}
@Post('outrights')
async createOutright(@Body() dto: CreateOutrightDto) {
const data = await this.outright.createForAdmin({
leagueId: BigInt(dto.leagueId),
titleZh: dto.titleZh,
titleEn: dto.titleEn,
status: dto.status,
});
return jsonResponse(data);
}
@Post('outrights/import/wc2026')
async importWc2026Outright() {
const data = await this.outright.importWc2026Canonical();
return jsonResponse(data);
}
/** @deprecated */
@Get('outrights/wc2026')
async getWc2026OutrightLegacy() {
const list = await this.outright.listForAdmin();
const wc = list.find((e) => e.leagueCode === 'WC2026');
if (!wc) throw new BadRequestException('WC2026 outright not found — run import');
return jsonResponse(await this.outright.getForAdmin(BigInt(wc.id)));
}
/** @deprecated */
@Put('outrights/wc2026/odds')
async updateWc2026OutrightOdds(
async updateWc2026OutrightOddsLegacy(
@CurrentUser('id') operatorId: bigint,
@Body() dto: BatchOutrightOddsDto,
) {
const data = await this.outright.updateWc2026Odds(dto.updates, operatorId);
const list = await this.outright.listForAdmin();
const wc = list.find((e) => e.leagueCode === 'WC2026');
if (!wc) throw new BadRequestException('WC2026 outright not found');
return jsonResponse(
await this.outright.batchUpdateOdds(BigInt(wc.id), dto.updates, operatorId),
);
}
/** @deprecated */
@Post('outrights/wc2026/apply-canonical')
async applyWc2026CanonicalLegacy() {
return jsonResponse(await this.outright.importWc2026Canonical());
}
@Get('outrights/:matchId')
async getOutright(@Param('matchId') matchId: string) {
const data = await this.outright.getForAdmin(BigInt(matchId));
return jsonResponse(data);
}
@Post('outrights/wc2026/apply-canonical')
async applyWc2026Canonical() {
const data = await this.outright.applyWc2026Canonical();
@Put('outrights/:matchId')
async updateOutright(
@Param('matchId') matchId: string,
@Body() dto: UpdateOutrightDto,
) {
const data = await this.outright.updateForAdmin(BigInt(matchId), dto);
return jsonResponse(data);
}
@Put('outrights/:matchId/odds')
async updateOutrightOdds(
@CurrentUser('id') operatorId: bigint,
@Param('matchId') matchId: string,
@Body() dto: BatchOutrightOddsDto,
) {
const data = await this.outright.batchUpdateOdds(
BigInt(matchId),
dto.updates,
operatorId,
);
return jsonResponse(data);
}
@Post('outrights/:matchId/selections')
async addOutrightSelection(
@Param('matchId') matchId: string,
@Body() dto: AddOutrightSelectionDto,
) {
const data = await this.outright.addSelection(BigInt(matchId), dto);
return jsonResponse(data);
}
@Delete('outrights/:matchId/selections/:selectionId')
async removeOutrightSelection(
@Param('matchId') matchId: string,
@Param('selectionId') selectionId: string,
) {
const data = await this.outright.closeSelection(
BigInt(matchId),
BigInt(selectionId),
);
return jsonResponse(data);
}