feat(admin,player,api): 公共管理与优胜冠军国旗、玩家端内容对接

新增公共内容 CRUD 与批量操作;公告滚动合并管理;优胜冠军内置国家选择与单行保存;玩家端统一 usePlayerHome 对接轮播与跑马灯。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-04 10:25:42 +08:00
parent 27580b2479
commit f76728dc3e
21 changed files with 1966 additions and 136 deletions

View File

@@ -138,6 +138,7 @@ export class OutrightService {
rank: sel.sortOrder + 1 || index + 1,
teamZh: teamZh || sel.selectionName,
teamEn: teamEn || sel.selectionName,
logoUrl: team?.logoUrl ?? null,
odds: sel.odds.toString(),
oddsVersion: sel.oddsVersion.toString(),
status: sel.status,
@@ -243,6 +244,7 @@ export class OutrightService {
teamZh: string;
teamEn: string;
odds: number;
logoUrl?: string;
},
) {
if (!data.teamCode?.trim()) {
@@ -256,10 +258,19 @@ export class OutrightService {
const market = await this.ensureOutrightMarket(match.id);
const code = data.teamCode.trim().toUpperCase();
const logoUrl =
data.logoUrl === undefined
? undefined
: data.logoUrl.trim()
? data.logoUrl.trim()
: null;
const team = await this.prisma.team.upsert({
where: { code },
create: { code },
update: {},
create: {
code,
...(logoUrl !== undefined ? { logoUrl } : {}),
},
update: logoUrl !== undefined ? { logoUrl } : {},
});
await this.upsertTeamTranslations(team.id, {
'zh-CN': data.teamZh.trim() || data.teamEn,
@@ -292,6 +303,77 @@ export class OutrightService {
return this.getForAdmin(matchId);
}
async updateSelectionTeam(
matchId: bigint,
selectionId: bigint,
data: {
teamCode?: string;
teamZh?: string;
teamEn?: string;
logoUrl?: string | null;
},
) {
const match = await this.getOutrightMatchOrThrow(matchId);
const market = await this.ensureOutrightMarket(match.id);
const sel = await this.prisma.marketSelection.findFirst({
where: { id: selectionId, marketId: market.id },
});
if (!sel) throw new NotFoundException('Selection not found');
const nextCode = data.teamCode?.trim().toUpperCase() || sel.selectionCode;
if (nextCode !== sel.selectionCode) {
const dup = await this.prisma.marketSelection.findFirst({
where: {
marketId: market.id,
selectionCode: nextCode,
id: { not: selectionId },
},
});
if (dup) {
throw new BadRequestException('Selection already exists for this team code');
}
await this.prisma.marketSelection.update({
where: { id: selectionId },
data: {
selectionCode: nextCode,
selectionName:
data.teamZh?.trim() || data.teamEn?.trim() || sel.selectionName,
},
});
} else if (data.teamZh?.trim() || data.teamEn?.trim()) {
await this.prisma.marketSelection.update({
where: { id: selectionId },
data: {
selectionName:
data.teamZh?.trim() || data.teamEn?.trim() || sel.selectionName,
},
});
}
const team = await this.prisma.team.upsert({
where: { code: nextCode },
create: { code: nextCode },
update: {},
});
if (data.teamZh !== undefined || data.teamEn !== undefined) {
await this.upsertTeamTranslations(team.id, {
'zh-CN': data.teamZh?.trim() || data.teamEn?.trim() || nextCode,
'en-US': data.teamEn?.trim() || data.teamZh?.trim() || nextCode,
});
}
if (data.logoUrl !== undefined) {
const logoUrl = data.logoUrl?.trim() ? data.logoUrl.trim() : null;
await this.prisma.team.update({
where: { id: team.id },
data: { logoUrl },
});
}
return this.getForAdmin(matchId);
}
async closeSelection(matchId: bigint, selectionId: bigint) {
const match = await this.getOutrightMatchOrThrow(matchId);
const market = await this.ensureOutrightMarket(match.id);
@@ -399,6 +481,7 @@ export class OutrightService {
id: sel.id.toString(),
teamCode: sel.selectionCode,
teamName,
logoUrl: team?.logoUrl ?? null,
odds: sel.odds.toString(),
oddsVersion: sel.oddsVersion.toString(),
};