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

@@ -14,12 +14,9 @@ import {
toVenueJson,
translationsFromZhiboNames,
} from './zhibo-match.mapper';
import { WC2026_OUTRIGHT_TEAMS } from './wc2026-outright-teams';
import { syncWc2026OutrightMarket } from './wc2026-outright.sync';
const WC2026_OUTRIGHT_RANK = new Map(
WC2026_OUTRIGHT_TEAMS.map((t) => [t.code, t.rank]),
);
const WC2026_OUTRIGHT_CODES = new Set(WC2026_OUTRIGHT_TEAMS.map((t) => t.code));
const OUTRIGHT_PLACEHOLDER_CODE = 'OUT';
@Injectable()
export class MatchesService {
@@ -570,8 +567,19 @@ export class MatchesService {
}
async listOutrights(locale = 'en-US') {
try {
await syncWc2026OutrightMarket(this.prisma, { forceCanonical: false });
} catch {
/* 联赛未 seed 时忽略,仍返回已有数据 */
}
const matches = await this.prisma.match.findMany({
where: { status: 'PUBLISHED', isOutright: true, sportType: 'FOOTBALL' },
where: {
status: 'PUBLISHED',
isOutright: true,
sportType: 'FOOTBALL',
deletedAt: null,
},
include: {
markets: {
where: { marketType: 'OUTRIGHT_WINNER', status: 'OPEN' },
@@ -592,27 +600,28 @@ export class MatchesService {
const market = match.markets[0];
if (!market) continue;
const selections = (
await Promise.all(
market.selections
.filter((sel) => WC2026_OUTRIGHT_CODES.has(sel.selectionCode))
.map(async (sel) => {
const teamCode = sel.selectionCode.replace(/^TEAM_/, '');
const team = await this.prisma.team.findUnique({ where: { code: teamCode } });
const teamName = team
? await this.getTranslation('TEAM', team.id, locale)
: sel.selectionName;
return {
id: sel.id.toString(),
teamCode,
teamName,
rank: WC2026_OUTRIGHT_RANK.get(teamCode) ?? 999,
odds: sel.odds.toString(),
oddsVersion: sel.oddsVersion.toString(),
};
}),
)
).sort((a, b) => a.rank - b.rank);
const selections = await Promise.all(
market.selections
.filter((sel) => sel.selectionCode !== OUTRIGHT_PLACEHOLDER_CODE)
.map(async (sel) => {
const team = await this.prisma.team.findUnique({
where: { code: sel.selectionCode },
});
const teamName = team
? await this.getTranslation('TEAM', team.id, locale)
: sel.selectionName;
return {
id: sel.id.toString(),
teamCode: sel.selectionCode,
teamName,
rank: sel.sortOrder + 1,
odds: sel.odds.toString(),
oddsVersion: sel.oddsVersion.toString(),
};
}),
);
if (selections.length === 0) continue;
results.push({
id: match.id.toString(),