feat(admin,api,player): 赛事分组管理、盘口独立页与多语言展示优化
- 管理端按联赛展示单场,新增赛事/单场流程与列表展开状态保持 - 盘口赔率迁至独立页面,保存按钮仅在有修改时高亮 - API 新增联赛列表与子场查询,按 locale 返回队名并修复编译 - 波胆其它选项与促销标签等 i18n 补齐,文案更易懂
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "leagues" ADD COLUMN "logo_url" VARCHAR(500);
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "markets" ADD COLUMN "promo_label" VARCHAR(100);
|
||||
@@ -219,6 +219,7 @@ model League {
|
||||
id BigInt @id @default(autoincrement())
|
||||
sportType String @default("FOOTBALL") @map("sport_type") @db.VarChar(20)
|
||||
code String @unique @db.VarChar(64)
|
||||
logoUrl String? @map("logo_url") @db.VarChar(500)
|
||||
displayOrder Int @default(0) @map("display_order")
|
||||
isActive Boolean @default(true) @map("is_active")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
@@ -330,6 +331,7 @@ model Market {
|
||||
allowSingle Boolean @default(true) @map("allow_single")
|
||||
allowParlay Boolean @default(true) @map("allow_parlay")
|
||||
sortOrder Int @default(0) @map("sort_order")
|
||||
promoLabel String? @map("promo_label") @db.VarChar(100)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
MinLength,
|
||||
IsIn,
|
||||
Min,
|
||||
ValidateIf,
|
||||
} from 'class-validator';
|
||||
import type { ZhiboMatchExport, ZhiboMatchesBundleExport } from '../../domains/catalog/zhibo-match.types';
|
||||
|
||||
@@ -207,25 +208,63 @@ class DepositDto {
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
class CreatePlatformMatchDto {
|
||||
class CreatePlatformLeagueDto {
|
||||
@IsString()
|
||||
leagueEn!: string;
|
||||
|
||||
@IsString()
|
||||
leagueZh!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
leagueMs?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
logoUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
displayOrder?: number;
|
||||
}
|
||||
|
||||
class CreatePlatformMatchDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
leagueId?: string;
|
||||
|
||||
@ValidateIf((o: CreatePlatformMatchDto) => !o.leagueId)
|
||||
@IsString()
|
||||
leagueEn?: string;
|
||||
|
||||
@ValidateIf((o: CreatePlatformMatchDto) => !o.leagueId)
|
||||
@IsString()
|
||||
leagueZh?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
leagueMs?: string;
|
||||
|
||||
@IsString()
|
||||
homeTeamEn!: string;
|
||||
|
||||
@IsString()
|
||||
homeTeamZh!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
homeTeamMs?: string;
|
||||
|
||||
@IsString()
|
||||
awayTeamEn!: string;
|
||||
|
||||
@IsString()
|
||||
awayTeamZh!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
awayTeamMs?: string;
|
||||
|
||||
@IsString()
|
||||
startTime!: string;
|
||||
|
||||
@@ -236,6 +275,64 @@ class CreatePlatformMatchDto {
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
displayOrder?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
matchName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
stage?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
groupName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
leagueLogoUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
homeTeamLogoUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
awayTeamLogoUrl?: string;
|
||||
}
|
||||
|
||||
class BatchMatchOddsDto {
|
||||
@IsArray()
|
||||
updates!: OutrightOddsUpdateItemDto[];
|
||||
}
|
||||
|
||||
class UpdateMarketDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
promoLabel?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
status?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
lineValue?: number | null;
|
||||
}
|
||||
|
||||
class UpdateSelectionDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
selectionName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(1.01)
|
||||
odds?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
status?: string;
|
||||
}
|
||||
|
||||
function isZhiboBundlePayload(body: unknown): body is ZhiboMatchesBundleExport {
|
||||
@@ -704,11 +801,58 @@ export class AdminController {
|
||||
}
|
||||
|
||||
@Post('leagues')
|
||||
async createLeague(@Body() dto: { code: string; translations: Record<string, string> }) {
|
||||
const league = await this.matches.createLeague(dto.code, dto.translations);
|
||||
async createLeague(
|
||||
@Body() dto: CreatePlatformLeagueDto | { code: string; translations: Record<string, string> },
|
||||
) {
|
||||
if ('leagueZh' in dto || 'leagueEn' in dto) {
|
||||
const body = dto as CreatePlatformLeagueDto;
|
||||
const league = await this.matches.createPlatformLeague({
|
||||
leagueEn: body.leagueEn,
|
||||
leagueZh: body.leagueZh,
|
||||
leagueMs: body.leagueMs,
|
||||
logoUrl: body.logoUrl,
|
||||
displayOrder: body.displayOrder,
|
||||
});
|
||||
return jsonResponse(league);
|
||||
}
|
||||
const legacy = dto as { code: string; translations: Record<string, string> };
|
||||
const league = await this.matches.createLeague(legacy.code, legacy.translations);
|
||||
return jsonResponse(league);
|
||||
}
|
||||
|
||||
@Get('leagues')
|
||||
async listLeagues(
|
||||
@Query('page') page?: string,
|
||||
@Query('pageSize') pageSize?: string,
|
||||
@Query('status') status?: string,
|
||||
@Query('keyword') keyword?: string,
|
||||
) {
|
||||
const p = Math.max(1, page ? parseInt(page, 10) : 1);
|
||||
const size = Math.min(Math.max(1, pageSize ? parseInt(pageSize, 10) : 10), 100);
|
||||
const result = await this.matches.listAdminLeagues({
|
||||
page: p,
|
||||
pageSize: size,
|
||||
status: status || undefined,
|
||||
keyword: keyword || undefined,
|
||||
});
|
||||
return jsonResponse(result);
|
||||
}
|
||||
|
||||
@Get('leagues/:leagueId/matches')
|
||||
async listLeagueMatches(
|
||||
@Param('leagueId') leagueId: string,
|
||||
@Query('status') status?: string,
|
||||
@Query('keyword') keyword?: string,
|
||||
@Query('locale') locale?: string,
|
||||
) {
|
||||
const items = await this.matches.listAdminLeagueMatches(BigInt(leagueId), {
|
||||
status: status || undefined,
|
||||
keyword: keyword || undefined,
|
||||
locale: locale || undefined,
|
||||
});
|
||||
return jsonResponse({ items });
|
||||
}
|
||||
|
||||
@Post('teams')
|
||||
async createTeam(@Body() dto: { code: string; translations: Record<string, string> }) {
|
||||
const team = await this.matches.createTeam(dto.code, dto.translations);
|
||||
@@ -764,15 +908,24 @@ export class AdminController {
|
||||
@Body() dto: CreatePlatformMatchDto,
|
||||
) {
|
||||
const match = await this.matches.updatePlatformMatch(BigInt(id), {
|
||||
leagueEn: dto.leagueEn,
|
||||
leagueZh: dto.leagueZh,
|
||||
leagueEn: dto.leagueEn ?? '',
|
||||
leagueZh: dto.leagueZh ?? '',
|
||||
leagueMs: dto.leagueMs,
|
||||
homeTeamEn: dto.homeTeamEn,
|
||||
homeTeamZh: dto.homeTeamZh,
|
||||
homeTeamMs: dto.homeTeamMs,
|
||||
awayTeamEn: dto.awayTeamEn,
|
||||
awayTeamZh: dto.awayTeamZh,
|
||||
awayTeamMs: dto.awayTeamMs,
|
||||
startTime: new Date(dto.startTime),
|
||||
isHot: dto.isHot,
|
||||
displayOrder: dto.displayOrder,
|
||||
matchName: dto.matchName,
|
||||
stage: dto.stage,
|
||||
groupName: dto.groupName,
|
||||
leagueLogoUrl: dto.leagueLogoUrl,
|
||||
homeTeamLogoUrl: dto.homeTeamLogoUrl,
|
||||
awayTeamLogoUrl: dto.awayTeamLogoUrl,
|
||||
updatedBy: operatorId,
|
||||
});
|
||||
return jsonResponse(match);
|
||||
@@ -787,15 +940,25 @@ export class AdminController {
|
||||
@Post('matches')
|
||||
async createMatch(@CurrentUser('id') operatorId: bigint, @Body() dto: CreatePlatformMatchDto) {
|
||||
const match = await this.matches.createPlatformMatch({
|
||||
leagueEn: dto.leagueEn,
|
||||
leagueZh: dto.leagueZh,
|
||||
leagueId: dto.leagueId ? BigInt(dto.leagueId) : undefined,
|
||||
leagueEn: dto.leagueEn ?? '',
|
||||
leagueZh: dto.leagueZh ?? '',
|
||||
leagueMs: dto.leagueMs,
|
||||
homeTeamEn: dto.homeTeamEn,
|
||||
homeTeamZh: dto.homeTeamZh,
|
||||
homeTeamMs: dto.homeTeamMs,
|
||||
awayTeamEn: dto.awayTeamEn,
|
||||
awayTeamZh: dto.awayTeamZh,
|
||||
awayTeamMs: dto.awayTeamMs,
|
||||
startTime: new Date(dto.startTime),
|
||||
isHot: dto.isHot,
|
||||
displayOrder: dto.displayOrder,
|
||||
matchName: dto.matchName,
|
||||
stage: dto.stage,
|
||||
groupName: dto.groupName,
|
||||
leagueLogoUrl: dto.leagueLogoUrl,
|
||||
homeTeamLogoUrl: dto.homeTeamLogoUrl,
|
||||
awayTeamLogoUrl: dto.awayTeamLogoUrl,
|
||||
createdBy: operatorId,
|
||||
});
|
||||
return jsonResponse(match);
|
||||
@@ -835,6 +998,48 @@ export class AdminController {
|
||||
return jsonResponse(markets);
|
||||
}
|
||||
|
||||
@Put('matches/:id/odds')
|
||||
async batchUpdateMatchOdds(
|
||||
@CurrentUser('id') operatorId: bigint,
|
||||
@Param('id') id: string,
|
||||
@Body() dto: BatchMatchOddsDto,
|
||||
) {
|
||||
const updates = dto.updates.map((u) => ({
|
||||
selectionId: BigInt(u.selectionId),
|
||||
odds: u.odds,
|
||||
}));
|
||||
const results = await this.markets.batchUpdateOdds(updates, operatorId);
|
||||
return jsonResponse({ matchId: id, updated: results.length });
|
||||
}
|
||||
|
||||
@Patch('markets/:id')
|
||||
async updateMarket(@Param('id') id: string, @Body() dto: UpdateMarketDto) {
|
||||
const market = await this.markets.updateMarket(BigInt(id), {
|
||||
promoLabel: dto.promoLabel,
|
||||
status: dto.status,
|
||||
lineValue: dto.lineValue,
|
||||
});
|
||||
return jsonResponse(market);
|
||||
}
|
||||
|
||||
@Patch('selections/:id')
|
||||
async updateSelection(
|
||||
@CurrentUser('id') operatorId: bigint,
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateSelectionDto,
|
||||
) {
|
||||
const selection = await this.markets.updateSelection(
|
||||
BigInt(id),
|
||||
{
|
||||
selectionName: dto.selectionName,
|
||||
odds: dto.odds,
|
||||
status: dto.status,
|
||||
},
|
||||
operatorId,
|
||||
);
|
||||
return jsonResponse(selection);
|
||||
}
|
||||
|
||||
@Put('selections/:id/odds')
|
||||
async updateOdds(
|
||||
@CurrentUser('id') operatorId: bigint,
|
||||
|
||||
@@ -193,46 +193,284 @@ export class MatchesService {
|
||||
return null;
|
||||
}
|
||||
|
||||
async createPlatformMatch(data: {
|
||||
async createPlatformLeague(data: {
|
||||
leagueEn: string;
|
||||
leagueZh: string;
|
||||
leagueMs?: string;
|
||||
logoUrl?: string;
|
||||
displayOrder?: number;
|
||||
}) {
|
||||
const leagueEn = data.leagueEn.trim();
|
||||
const leagueZh = data.leagueZh.trim();
|
||||
if (!leagueEn && !leagueZh) {
|
||||
throw new BadRequestException('请填写赛事名称(中文或英文至少一项)');
|
||||
}
|
||||
const league = await this.upsertLeagueFromZhiboExport({
|
||||
type: 'FOOTBALL',
|
||||
en: leagueEn || leagueZh,
|
||||
zh: leagueZh || leagueEn,
|
||||
});
|
||||
if (data.leagueMs?.trim()) {
|
||||
await this.upsertEntityTranslations('LEAGUE', league.id, {
|
||||
'ms-MY': data.leagueMs.trim(),
|
||||
});
|
||||
}
|
||||
const updates: { logoUrl?: string; displayOrder?: number } = {};
|
||||
if (data.logoUrl?.trim()) updates.logoUrl = data.logoUrl.trim();
|
||||
if (data.displayOrder != null) updates.displayOrder = data.displayOrder;
|
||||
if (Object.keys(updates).length) {
|
||||
await this.prisma.league.update({ where: { id: league.id }, data: updates });
|
||||
}
|
||||
const [en, zh, ms] = await Promise.all([
|
||||
this.getTranslationExact('LEAGUE', league.id, 'en-US'),
|
||||
this.getTranslationExact('LEAGUE', league.id, 'zh-CN'),
|
||||
this.getTranslationExact('LEAGUE', league.id, 'ms-MY'),
|
||||
]);
|
||||
const fresh = await this.prisma.league.findUniqueOrThrow({ where: { id: league.id } });
|
||||
return {
|
||||
id: fresh.id.toString(),
|
||||
code: fresh.code,
|
||||
logoUrl: fresh.logoUrl,
|
||||
displayOrder: fresh.displayOrder,
|
||||
leagueEn: en,
|
||||
leagueZh: zh,
|
||||
leagueMs: ms,
|
||||
};
|
||||
}
|
||||
|
||||
async listAdminLeagues(opts: {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
keyword?: string;
|
||||
status?: string;
|
||||
}) {
|
||||
const skip = (opts.page - 1) * opts.pageSize;
|
||||
const kw = opts.keyword?.trim();
|
||||
let idFilter: bigint[] | undefined;
|
||||
|
||||
if (kw || opts.status) {
|
||||
const ids = new Set<bigint>();
|
||||
if (kw) {
|
||||
const trRows = await this.prisma.entityTranslation.findMany({
|
||||
where: {
|
||||
entityType: 'LEAGUE',
|
||||
fieldName: 'name',
|
||||
value: { contains: kw, mode: 'insensitive' },
|
||||
},
|
||||
select: { entityId: true },
|
||||
});
|
||||
for (const r of trRows) ids.add(r.entityId);
|
||||
}
|
||||
const matchWhere: Prisma.MatchWhereInput = {
|
||||
deletedAt: null,
|
||||
isOutright: false,
|
||||
};
|
||||
if (opts.status) matchWhere.status = opts.status;
|
||||
if (kw) {
|
||||
matchWhere.OR = [
|
||||
{ matchName: { contains: kw, mode: 'insensitive' } },
|
||||
{ homeTeam: { code: { contains: kw, mode: 'insensitive' } } },
|
||||
{ awayTeam: { code: { contains: kw, mode: 'insensitive' } } },
|
||||
];
|
||||
}
|
||||
const matchLeagues = await this.prisma.match.findMany({
|
||||
where: matchWhere,
|
||||
select: { leagueId: true },
|
||||
distinct: ['leagueId'],
|
||||
});
|
||||
for (const m of matchLeagues) ids.add(m.leagueId);
|
||||
idFilter = [...ids];
|
||||
if (!idFilter.length) {
|
||||
return { items: [], total: 0, page: opts.page, pageSize: opts.pageSize };
|
||||
}
|
||||
}
|
||||
|
||||
const where: Prisma.LeagueWhereInput = { deletedAt: null };
|
||||
if (idFilter) where.id = { in: idFilter };
|
||||
|
||||
const [leagues, total] = await Promise.all([
|
||||
this.prisma.league.findMany({
|
||||
where,
|
||||
orderBy: [{ displayOrder: 'asc' }, { id: 'desc' }],
|
||||
skip,
|
||||
take: opts.pageSize,
|
||||
}),
|
||||
this.prisma.league.count({ where }),
|
||||
]);
|
||||
|
||||
const items = await Promise.all(
|
||||
leagues.map(async (league) => {
|
||||
const [leagueEn, leagueZh, leagueMs, matchCount] = await Promise.all([
|
||||
this.getTranslationExact('LEAGUE', league.id, 'en-US'),
|
||||
this.getTranslationExact('LEAGUE', league.id, 'zh-CN'),
|
||||
this.getTranslationExact('LEAGUE', league.id, 'ms-MY'),
|
||||
this.prisma.match.count({
|
||||
where: {
|
||||
leagueId: league.id,
|
||||
deletedAt: null,
|
||||
isOutright: false,
|
||||
...(opts.status ? { status: opts.status } : {}),
|
||||
},
|
||||
}),
|
||||
]);
|
||||
return {
|
||||
id: league.id.toString(),
|
||||
code: league.code,
|
||||
logoUrl: league.logoUrl,
|
||||
displayOrder: league.displayOrder,
|
||||
leagueEn,
|
||||
leagueZh,
|
||||
leagueMs,
|
||||
matchCount,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return { items, total, page: opts.page, pageSize: opts.pageSize };
|
||||
}
|
||||
|
||||
async listAdminLeagueMatches(
|
||||
leagueId: bigint,
|
||||
opts: { status?: string; keyword?: string; locale?: string },
|
||||
) {
|
||||
const where: Prisma.MatchWhereInput = {
|
||||
leagueId,
|
||||
deletedAt: null,
|
||||
isOutright: false,
|
||||
};
|
||||
if (opts.status) where.status = opts.status;
|
||||
const kw = opts.keyword?.trim();
|
||||
if (kw) {
|
||||
where.OR = [
|
||||
{ matchName: { contains: kw, mode: 'insensitive' } },
|
||||
{ homeTeam: { code: { contains: kw, mode: 'insensitive' } } },
|
||||
{ awayTeam: { code: { contains: kw, mode: 'insensitive' } } },
|
||||
];
|
||||
}
|
||||
const items = await this.prisma.match.findMany({
|
||||
where,
|
||||
include: { homeTeam: true, awayTeam: true },
|
||||
orderBy: [{ displayOrder: 'asc' }, { startTime: 'desc' }],
|
||||
});
|
||||
const locale = opts.locale ?? 'zh-CN';
|
||||
return Promise.all(
|
||||
items.map(async (m) => {
|
||||
const [homeTeamName, awayTeamName] = await Promise.all([
|
||||
this.getTranslation('TEAM', m.homeTeamId, locale),
|
||||
this.getTranslation('TEAM', m.awayTeamId, locale),
|
||||
]);
|
||||
return {
|
||||
id: m.id.toString(),
|
||||
status: m.status,
|
||||
isOutright: m.isOutright,
|
||||
isHot: m.isHot,
|
||||
displayOrder: m.displayOrder,
|
||||
startTime: m.startTime,
|
||||
matchName: m.matchName,
|
||||
homeTeamName,
|
||||
awayTeamName,
|
||||
homeTeam: { code: m.homeTeam.code },
|
||||
awayTeam: { code: m.awayTeam.code },
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async createPlatformMatch(data: {
|
||||
leagueId?: bigint;
|
||||
leagueEn?: string;
|
||||
leagueZh?: string;
|
||||
leagueMs?: string;
|
||||
homeTeamZh: string;
|
||||
homeTeamEn: string;
|
||||
homeTeamMs?: string;
|
||||
awayTeamZh: string;
|
||||
awayTeamEn: string;
|
||||
awayTeamMs?: string;
|
||||
startTime: Date;
|
||||
isHot?: boolean;
|
||||
displayOrder?: number;
|
||||
matchName?: string;
|
||||
stage?: string;
|
||||
groupName?: string;
|
||||
leagueLogoUrl?: string;
|
||||
homeTeamLogoUrl?: string;
|
||||
awayTeamLogoUrl?: string;
|
||||
createdBy?: bigint;
|
||||
}) {
|
||||
const homeEn = data.homeTeamEn.trim();
|
||||
const homeZh = data.homeTeamZh.trim();
|
||||
const homeMs = data.homeTeamMs?.trim() ?? '';
|
||||
const awayEn = data.awayTeamEn.trim();
|
||||
const awayZh = data.awayTeamZh.trim();
|
||||
if ((!homeEn && !homeZh) || (!awayEn && !awayZh)) {
|
||||
throw new BadRequestException('请填写主客队中英文名至少各一项');
|
||||
const awayMs = data.awayTeamMs?.trim() ?? '';
|
||||
if ((!homeEn && !homeZh && !homeMs) || (!awayEn && !awayZh && !awayMs)) {
|
||||
throw new BadRequestException('请填写主客队名称(中文、英文或马来文至少一项)');
|
||||
}
|
||||
|
||||
const league = await this.upsertLeagueFromZhiboExport({
|
||||
type: 'FOOTBALL',
|
||||
en: data.leagueEn.trim(),
|
||||
zh: data.leagueZh.trim(),
|
||||
});
|
||||
let league;
|
||||
if (data.leagueId) {
|
||||
league = await this.prisma.league.findFirst({
|
||||
where: { id: data.leagueId, deletedAt: null },
|
||||
});
|
||||
if (!league) throw new NotFoundException('赛事不存在');
|
||||
} else {
|
||||
const leagueEn = data.leagueEn?.trim() ?? '';
|
||||
const leagueZh = data.leagueZh?.trim() ?? '';
|
||||
const leagueMs = data.leagueMs?.trim() ?? '';
|
||||
if (!leagueEn && !leagueZh && !leagueMs) {
|
||||
throw new BadRequestException('请填写赛事名称(中文、英文或马来文至少一项)');
|
||||
}
|
||||
league = await this.upsertLeagueFromZhiboExport({
|
||||
type: 'FOOTBALL',
|
||||
en: leagueEn || leagueZh || leagueMs,
|
||||
zh: leagueZh || leagueEn || leagueMs,
|
||||
});
|
||||
if (leagueMs) {
|
||||
await this.upsertEntityTranslations('LEAGUE', league.id, {
|
||||
'ms-MY': leagueMs,
|
||||
});
|
||||
}
|
||||
if (data.leagueLogoUrl?.trim()) {
|
||||
await this.prisma.league.update({
|
||||
where: { id: league.id },
|
||||
data: { logoUrl: data.leagueLogoUrl.trim() },
|
||||
});
|
||||
}
|
||||
}
|
||||
const [homeTeam, awayTeam] = await Promise.all([
|
||||
this.upsertTeamFromZhiboExport({
|
||||
id: null,
|
||||
name: homeEn || homeZh,
|
||||
names: { zh: homeZh || null, en: homeEn || null, zhTw: '', vi: null, km: null, ms: null },
|
||||
image: '',
|
||||
name: homeEn || homeZh || homeMs,
|
||||
names: {
|
||||
zh: homeZh || null,
|
||||
en: homeEn || null,
|
||||
zhTw: '',
|
||||
vi: null,
|
||||
km: null,
|
||||
ms: homeMs || null,
|
||||
},
|
||||
image: data.homeTeamLogoUrl?.trim() || '',
|
||||
}),
|
||||
this.upsertTeamFromZhiboExport({
|
||||
id: null,
|
||||
name: awayEn || awayZh,
|
||||
names: { zh: awayZh || null, en: awayEn || null, zhTw: '', vi: null, km: null, ms: null },
|
||||
image: '',
|
||||
name: awayEn || awayZh || awayMs,
|
||||
names: {
|
||||
zh: awayZh || null,
|
||||
en: awayEn || null,
|
||||
zhTw: '',
|
||||
vi: null,
|
||||
km: null,
|
||||
ms: awayMs || null,
|
||||
},
|
||||
image: data.awayTeamLogoUrl?.trim() || '',
|
||||
}),
|
||||
]);
|
||||
|
||||
const matchName =
|
||||
data.matchName?.trim() ||
|
||||
`${homeEn || homeZh || homeMs} - ${awayEn || awayZh || awayMs}`;
|
||||
|
||||
return this.createMatch({
|
||||
leagueId: league.id,
|
||||
homeTeamId: homeTeam.id,
|
||||
@@ -243,7 +481,9 @@ export class MatchesService {
|
||||
createdBy: data.createdBy,
|
||||
status: 'DRAFT',
|
||||
zhibo: {
|
||||
matchName: `${homeEn || homeZh} - ${awayEn || awayZh}`,
|
||||
matchName,
|
||||
stage: data.stage?.trim() || undefined,
|
||||
groupName: data.groupName?.trim() || undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -251,7 +491,7 @@ export class MatchesService {
|
||||
private async requireAdminMatch(matchId: bigint) {
|
||||
const match = await this.prisma.match.findFirst({
|
||||
where: { id: matchId, deletedAt: null },
|
||||
include: { homeTeam: true, awayTeam: true },
|
||||
include: { homeTeam: true, awayTeam: true, league: true },
|
||||
});
|
||||
if (!match) throw new NotFoundException('赛事不存在');
|
||||
return match;
|
||||
@@ -259,27 +499,66 @@ export class MatchesService {
|
||||
|
||||
async getAdminMatchDetail(matchId: bigint) {
|
||||
const match = await this.requireAdminMatch(matchId);
|
||||
const [leagueEn, leagueZh, homeEn, homeZh, awayEn, awayZh] = await Promise.all([
|
||||
this.getTranslation('LEAGUE', match.leagueId, 'en-US'),
|
||||
this.getTranslation('LEAGUE', match.leagueId, 'zh-CN'),
|
||||
this.getTranslation('TEAM', match.homeTeamId, 'en-US'),
|
||||
this.getTranslation('TEAM', match.homeTeamId, 'zh-CN'),
|
||||
this.getTranslation('TEAM', match.awayTeamId, 'en-US'),
|
||||
this.getTranslation('TEAM', match.awayTeamId, 'zh-CN'),
|
||||
const markets = await this.prisma.market.findMany({
|
||||
where: { matchId },
|
||||
include: { selections: { orderBy: { sortOrder: 'asc' } } },
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
});
|
||||
const [leagueEn, leagueZh, leagueMs, homeEn, homeZh, homeMs, awayEn, awayZh, awayMs] =
|
||||
await Promise.all([
|
||||
this.getTranslationExact('LEAGUE', match.leagueId, 'en-US'),
|
||||
this.getTranslationExact('LEAGUE', match.leagueId, 'zh-CN'),
|
||||
this.getTranslationExact('LEAGUE', match.leagueId, 'ms-MY'),
|
||||
this.getTranslationExact('TEAM', match.homeTeamId, 'en-US'),
|
||||
this.getTranslationExact('TEAM', match.homeTeamId, 'zh-CN'),
|
||||
this.getTranslationExact('TEAM', match.homeTeamId, 'ms-MY'),
|
||||
this.getTranslationExact('TEAM', match.awayTeamId, 'en-US'),
|
||||
this.getTranslationExact('TEAM', match.awayTeamId, 'zh-CN'),
|
||||
this.getTranslationExact('TEAM', match.awayTeamId, 'ms-MY'),
|
||||
]);
|
||||
return {
|
||||
id: match.id.toString(),
|
||||
status: match.status,
|
||||
isOutright: match.isOutright,
|
||||
isHot: match.isHot,
|
||||
displayOrder: match.displayOrder,
|
||||
startTime: match.startTime.toISOString(),
|
||||
leagueId: match.leagueId.toString(),
|
||||
leagueCode: match.league.code,
|
||||
leagueEn,
|
||||
leagueZh,
|
||||
leagueMs,
|
||||
leagueLogoUrl: match.league.logoUrl ?? '',
|
||||
homeTeamEn: homeEn,
|
||||
homeTeamZh: homeZh,
|
||||
homeTeamMs: homeMs,
|
||||
homeTeamCode: match.homeTeam.code,
|
||||
homeTeamLogoUrl: match.homeTeam.logoUrl ?? '',
|
||||
awayTeamEn: awayEn,
|
||||
awayTeamZh: awayZh,
|
||||
awayTeamMs: awayMs,
|
||||
awayTeamCode: match.awayTeam.code,
|
||||
awayTeamLogoUrl: match.awayTeam.logoUrl ?? '',
|
||||
matchName: match.matchName ?? '',
|
||||
stage: match.stage ?? '',
|
||||
groupName: match.groupName ?? '',
|
||||
markets: markets.map((m) => ({
|
||||
id: m.id.toString(),
|
||||
marketType: m.marketType,
|
||||
period: m.period,
|
||||
lineValue: m.lineValue != null ? Number(m.lineValue) : null,
|
||||
status: m.status,
|
||||
promoLabel: m.promoLabel ?? '',
|
||||
sortOrder: m.sortOrder,
|
||||
selections: m.selections.map((s) => ({
|
||||
id: s.id.toString(),
|
||||
selectionCode: s.selectionCode,
|
||||
selectionName: s.selectionName,
|
||||
odds: Number(s.odds),
|
||||
status: s.status,
|
||||
sortOrder: s.sortOrder,
|
||||
})),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -288,13 +567,22 @@ export class MatchesService {
|
||||
data: {
|
||||
leagueEn: string;
|
||||
leagueZh: string;
|
||||
leagueMs?: string;
|
||||
homeTeamZh: string;
|
||||
homeTeamEn: string;
|
||||
homeTeamMs?: string;
|
||||
awayTeamZh: string;
|
||||
awayTeamEn: string;
|
||||
awayTeamMs?: string;
|
||||
startTime: Date;
|
||||
isHot?: boolean;
|
||||
displayOrder?: number;
|
||||
matchName?: string;
|
||||
stage?: string;
|
||||
groupName?: string;
|
||||
leagueLogoUrl?: string;
|
||||
homeTeamLogoUrl?: string;
|
||||
awayTeamLogoUrl?: string;
|
||||
updatedBy?: bigint;
|
||||
},
|
||||
) {
|
||||
@@ -306,23 +594,55 @@ export class MatchesService {
|
||||
throw new BadRequestException('当前状态不可编辑');
|
||||
}
|
||||
|
||||
const matchName = `${data.homeTeamEn.trim() || data.homeTeamZh.trim()} - ${data.awayTeamEn.trim() || data.awayTeamZh.trim()}`;
|
||||
const matchName =
|
||||
data.matchName?.trim() ||
|
||||
`${data.homeTeamEn.trim() || data.homeTeamZh.trim() || data.homeTeamMs?.trim() || ''} - ${data.awayTeamEn.trim() || data.awayTeamZh.trim() || data.awayTeamMs?.trim() || ''}`;
|
||||
|
||||
await Promise.all([
|
||||
this.upsertEntityTranslations('LEAGUE', match.leagueId, {
|
||||
'zh-CN': data.leagueZh.trim(),
|
||||
'en-US': data.leagueEn.trim(),
|
||||
'ms-MY': (data.leagueMs ?? '').trim(),
|
||||
}),
|
||||
this.upsertEntityTranslations('TEAM', match.homeTeamId, {
|
||||
'zh-CN': data.homeTeamZh.trim(),
|
||||
'en-US': data.homeTeamEn.trim(),
|
||||
'ms-MY': (data.homeTeamMs ?? '').trim(),
|
||||
}),
|
||||
this.upsertEntityTranslations('TEAM', match.awayTeamId, {
|
||||
'zh-CN': data.awayTeamZh.trim(),
|
||||
'en-US': data.awayTeamEn.trim(),
|
||||
'ms-MY': (data.awayTeamMs ?? '').trim(),
|
||||
}),
|
||||
]);
|
||||
|
||||
const logoUpdates: Promise<unknown>[] = [];
|
||||
if (data.leagueLogoUrl !== undefined) {
|
||||
logoUpdates.push(
|
||||
this.prisma.league.update({
|
||||
where: { id: match.leagueId },
|
||||
data: { logoUrl: data.leagueLogoUrl.trim() || null },
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (data.homeTeamLogoUrl !== undefined) {
|
||||
logoUpdates.push(
|
||||
this.prisma.team.update({
|
||||
where: { id: match.homeTeamId },
|
||||
data: { logoUrl: data.homeTeamLogoUrl.trim() || null },
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (data.awayTeamLogoUrl !== undefined) {
|
||||
logoUpdates.push(
|
||||
this.prisma.team.update({
|
||||
where: { id: match.awayTeamId },
|
||||
data: { logoUrl: data.awayTeamLogoUrl.trim() || null },
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (logoUpdates.length) await Promise.all(logoUpdates);
|
||||
|
||||
return this.prisma.match.update({
|
||||
where: { id: matchId },
|
||||
data: {
|
||||
@@ -330,6 +650,8 @@ export class MatchesService {
|
||||
isHot: data.isHot ?? match.isHot,
|
||||
displayOrder: data.displayOrder ?? match.displayOrder,
|
||||
matchName,
|
||||
stage: data.stage !== undefined ? data.stage.trim() || null : match.stage,
|
||||
groupName: data.groupName !== undefined ? data.groupName.trim() || null : match.groupName,
|
||||
updatedBy: data.updatedBy,
|
||||
},
|
||||
});
|
||||
@@ -484,6 +806,13 @@ export class MatchesService {
|
||||
});
|
||||
}
|
||||
|
||||
private async getTranslationExact(entityType: string, entityId: bigint, locale: string) {
|
||||
const row = await this.prisma.entityTranslation.findFirst({
|
||||
where: { entityType, entityId, locale, fieldName: 'name' },
|
||||
});
|
||||
return row?.value ?? '';
|
||||
}
|
||||
|
||||
async getTranslation(entityType: string, entityId: bigint, locale: string) {
|
||||
const translations = await this.prisma.entityTranslation.findMany({
|
||||
where: { entityType, entityId },
|
||||
@@ -500,25 +829,63 @@ export class MatchesService {
|
||||
leagueId: bigint;
|
||||
homeTeamId: bigint;
|
||||
awayTeamId: bigint;
|
||||
homeTeam?: { code: string };
|
||||
awayTeam?: { code: string };
|
||||
markets?: unknown[];
|
||||
startTime: Date;
|
||||
status?: string;
|
||||
isHot?: boolean;
|
||||
displayOrder?: number;
|
||||
matchName?: string | null;
|
||||
stage?: string | null;
|
||||
groupName?: string | null;
|
||||
homeTeam?: { code: string; logoUrl?: string | null };
|
||||
awayTeam?: { code: string; logoUrl?: string | null };
|
||||
league?: { logoUrl?: string | null };
|
||||
markets?: Array<Record<string, unknown>>;
|
||||
};
|
||||
const [leagueName, homeName, awayName] = await Promise.all([
|
||||
this.getTranslation('LEAGUE', m.leagueId, locale),
|
||||
this.getTranslation('TEAM', m.homeTeamId, locale),
|
||||
this.getTranslation('TEAM', m.awayTeamId, locale),
|
||||
]);
|
||||
return {
|
||||
...match,
|
||||
const base = {
|
||||
id: m.id.toString(),
|
||||
leagueId: m.leagueId.toString(),
|
||||
leagueName,
|
||||
leagueLogoUrl: m.league?.logoUrl ?? null,
|
||||
homeTeamName: homeName,
|
||||
awayTeamName: awayName,
|
||||
homeTeamCode: m.homeTeam?.code ?? '',
|
||||
awayTeamCode: m.awayTeam?.code ?? '',
|
||||
homeTeamLogoUrl: m.homeTeam?.logoUrl ?? null,
|
||||
awayTeamLogoUrl: m.awayTeam?.logoUrl ?? null,
|
||||
startTime: m.startTime.toISOString(),
|
||||
isHot: m.isHot ?? false,
|
||||
displayOrder: m.displayOrder ?? 0,
|
||||
matchName: m.matchName ?? null,
|
||||
stage: m.stage ?? null,
|
||||
groupName: m.groupName ?? null,
|
||||
status: m.status ?? 'PUBLISHED',
|
||||
};
|
||||
if (m.markets) {
|
||||
return {
|
||||
...base,
|
||||
markets: m.markets.map((market) => ({
|
||||
id: (market.id as bigint).toString(),
|
||||
marketType: market.marketType as string,
|
||||
period: market.period as string,
|
||||
lineValue: market.lineValue != null ? Number(market.lineValue) : null,
|
||||
allowParlay: (market.allowParlay as boolean | undefined) ?? true,
|
||||
promoLabel: (market.promoLabel as string | null | undefined) ?? null,
|
||||
selections: ((market.selections as Array<Record<string, unknown>>) ?? []).map((s) => ({
|
||||
id: (s.id as bigint).toString(),
|
||||
selectionCode: s.selectionCode as string,
|
||||
selectionName: s.selectionName as string,
|
||||
odds: Number(s.odds),
|
||||
oddsVersion: (s.oddsVersion as bigint).toString(),
|
||||
})),
|
||||
})),
|
||||
};
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
async listPublished(locale = 'en-US', leagueId?: bigint) {
|
||||
@@ -528,27 +895,37 @@ export class MatchesService {
|
||||
status: 'PUBLISHED',
|
||||
isOutright: false,
|
||||
sportType: 'FOOTBALL',
|
||||
deletedAt: null,
|
||||
startTime: { gt: now },
|
||||
...(leagueId ? { leagueId } : {}),
|
||||
},
|
||||
include: {
|
||||
league: true,
|
||||
homeTeam: true,
|
||||
awayTeam: true,
|
||||
markets: {
|
||||
where: { status: 'OPEN' },
|
||||
include: { selections: { where: { status: 'OPEN' } } },
|
||||
include: { selections: { where: { status: 'OPEN' }, orderBy: { sortOrder: 'asc' } } },
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
},
|
||||
},
|
||||
orderBy: [{ isHot: 'desc' }, { startTime: 'asc' }],
|
||||
orderBy: [{ isHot: 'desc' }, { displayOrder: 'asc' }, { startTime: 'asc' }],
|
||||
});
|
||||
|
||||
return Promise.all(matches.map((m) => this.enrichMatch(m, locale)));
|
||||
}
|
||||
|
||||
async getMatchDetail(matchId: bigint, locale = 'en-US') {
|
||||
const match = await this.prisma.match.findUnique({
|
||||
where: { id: matchId },
|
||||
const match = await this.prisma.match.findFirst({
|
||||
where: {
|
||||
id: matchId,
|
||||
deletedAt: null,
|
||||
sportType: 'FOOTBALL',
|
||||
isOutright: false,
|
||||
status: { in: ['PUBLISHED', 'CLOSED'] },
|
||||
},
|
||||
include: {
|
||||
league: true,
|
||||
homeTeam: true,
|
||||
awayTeam: true,
|
||||
markets: {
|
||||
@@ -560,9 +937,6 @@ export class MatchesService {
|
||||
},
|
||||
});
|
||||
if (!match) throw new NotFoundException('Match not found');
|
||||
if (match.sportType !== 'FOOTBALL') {
|
||||
throw new NotFoundException('Match not found');
|
||||
}
|
||||
return this.enrichMatch(match, locale);
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,22 @@ export class MarketsService {
|
||||
return created;
|
||||
}
|
||||
|
||||
private formatHandicapName(side: 'home' | 'away', line: number, half = false) {
|
||||
const sideLabel = side === 'home' ? '主队' : '客队';
|
||||
const value = side === 'home' ? line : -line;
|
||||
const lineText = value > 0 ? `+${value}` : `${value}`;
|
||||
return half ? `半场${sideLabel} ${lineText}` : `${sideLabel} ${lineText}`;
|
||||
}
|
||||
|
||||
private formatOuName(side: 'over' | 'under', line: number, half = false) {
|
||||
const sideLabel = side === 'over' ? '大' : '小';
|
||||
return half ? `半场${sideLabel} ${line}` : `${sideLabel} ${line}`;
|
||||
}
|
||||
|
||||
private formatScoreName(code: string) {
|
||||
return code.replace('SCORE_', '').replace('_', '-');
|
||||
}
|
||||
|
||||
private getMarketConfig(marketType: string) {
|
||||
const configs: Record<string, {
|
||||
period: string;
|
||||
@@ -62,9 +78,9 @@ export class MarketsService {
|
||||
allowParlay: true,
|
||||
sortOrder: 1,
|
||||
selections: [
|
||||
{ code: 'HOME', name: 'Home', odds: 2.5 },
|
||||
{ code: 'DRAW', name: 'Draw', odds: 3.2 },
|
||||
{ code: 'AWAY', name: 'Away', odds: 2.8 },
|
||||
{ code: 'HOME', name: '主胜', odds: 2.5 },
|
||||
{ code: 'DRAW', name: '和', odds: 3.2 },
|
||||
{ code: 'AWAY', name: '客胜', odds: 2.8 },
|
||||
],
|
||||
},
|
||||
HT_1X2: {
|
||||
@@ -72,9 +88,9 @@ export class MarketsService {
|
||||
allowParlay: true,
|
||||
sortOrder: 5,
|
||||
selections: [
|
||||
{ code: 'HOME', name: 'HT Home', odds: 3.0 },
|
||||
{ code: 'DRAW', name: 'HT Draw', odds: 2.0 },
|
||||
{ code: 'AWAY', name: 'HT Away', odds: 3.5 },
|
||||
{ code: 'HOME', name: '半场主胜', odds: 3.0 },
|
||||
{ code: 'DRAW', name: '半场和', odds: 2.0 },
|
||||
{ code: 'AWAY', name: '半场客胜', odds: 3.5 },
|
||||
],
|
||||
},
|
||||
FT_HANDICAP: {
|
||||
@@ -83,8 +99,8 @@ export class MarketsService {
|
||||
allowParlay: true,
|
||||
sortOrder: 2,
|
||||
selections: [
|
||||
{ code: 'HOME', name: 'Home -0.5', odds: 1.9 },
|
||||
{ code: 'AWAY', name: 'Away +0.5', odds: 1.9 },
|
||||
{ code: 'HOME', name: this.formatHandicapName('home', -0.5), odds: 1.9 },
|
||||
{ code: 'AWAY', name: this.formatHandicapName('away', -0.5), odds: 1.9 },
|
||||
],
|
||||
},
|
||||
HT_HANDICAP: {
|
||||
@@ -93,8 +109,8 @@ export class MarketsService {
|
||||
allowParlay: true,
|
||||
sortOrder: 6,
|
||||
selections: [
|
||||
{ code: 'HOME', name: 'HT Home -0.5', odds: 1.9 },
|
||||
{ code: 'AWAY', name: 'HT Away +0.5', odds: 1.9 },
|
||||
{ code: 'HOME', name: this.formatHandicapName('home', -0.5, true), odds: 1.9 },
|
||||
{ code: 'AWAY', name: this.formatHandicapName('away', -0.5, true), odds: 1.9 },
|
||||
],
|
||||
},
|
||||
FT_OVER_UNDER: {
|
||||
@@ -103,8 +119,8 @@ export class MarketsService {
|
||||
allowParlay: true,
|
||||
sortOrder: 3,
|
||||
selections: [
|
||||
{ code: 'OVER', name: 'Over 2.5', odds: 1.85 },
|
||||
{ code: 'UNDER', name: 'Under 2.5', odds: 1.95 },
|
||||
{ code: 'OVER', name: this.formatOuName('over', 2.5), odds: 1.85 },
|
||||
{ code: 'UNDER', name: this.formatOuName('under', 2.5), odds: 1.95 },
|
||||
],
|
||||
},
|
||||
HT_OVER_UNDER: {
|
||||
@@ -113,8 +129,8 @@ export class MarketsService {
|
||||
allowParlay: true,
|
||||
sortOrder: 7,
|
||||
selections: [
|
||||
{ code: 'OVER', name: 'HT Over 1.5', odds: 2.0 },
|
||||
{ code: 'UNDER', name: 'HT Under 1.5', odds: 1.75 },
|
||||
{ code: 'OVER', name: this.formatOuName('over', 1.5, true), odds: 2.0 },
|
||||
{ code: 'UNDER', name: this.formatOuName('under', 1.5, true), odds: 1.75 },
|
||||
],
|
||||
},
|
||||
FT_ODD_EVEN: {
|
||||
@@ -122,8 +138,8 @@ export class MarketsService {
|
||||
allowParlay: true,
|
||||
sortOrder: 4,
|
||||
selections: [
|
||||
{ code: 'ODD', name: 'Odd', odds: 1.9 },
|
||||
{ code: 'EVEN', name: 'Even', odds: 1.9 },
|
||||
{ code: 'ODD', name: '单', odds: 1.9 },
|
||||
{ code: 'EVEN', name: '双', odds: 1.9 },
|
||||
],
|
||||
},
|
||||
FT_CORRECT_SCORE: {
|
||||
@@ -132,7 +148,7 @@ export class MarketsService {
|
||||
sortOrder: 8,
|
||||
selections: FT_CORRECT_SCORE_TEMPLATE.map((code) => ({
|
||||
code,
|
||||
name: code.replace('SCORE_', '').replace('_', '-') || code,
|
||||
name: this.formatScoreName(code),
|
||||
odds: 8.0,
|
||||
})),
|
||||
},
|
||||
@@ -142,7 +158,7 @@ export class MarketsService {
|
||||
sortOrder: 9,
|
||||
selections: HT_CORRECT_SCORE_TEMPLATE.map((code) => ({
|
||||
code,
|
||||
name: code.replace('SCORE_', '').replace('_', '-') || code,
|
||||
name: this.formatScoreName(code),
|
||||
odds: 6.0,
|
||||
})),
|
||||
},
|
||||
@@ -152,7 +168,7 @@ export class MarketsService {
|
||||
sortOrder: 10,
|
||||
selections: HT_CORRECT_SCORE_TEMPLATE.map((code) => ({
|
||||
code,
|
||||
name: code.replace('SCORE_', '').replace('_', '-') || code,
|
||||
name: this.formatScoreName(code),
|
||||
odds: 6.0,
|
||||
})),
|
||||
},
|
||||
@@ -206,4 +222,45 @@ export class MarketsService {
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
async updateMarket(
|
||||
marketId: bigint,
|
||||
data: { promoLabel?: string | null; status?: string; lineValue?: number | null },
|
||||
) {
|
||||
const market = await this.prisma.market.findUnique({ where: { id: marketId } });
|
||||
if (!market) throw new NotFoundException('Market not found');
|
||||
|
||||
return this.prisma.market.update({
|
||||
where: { id: marketId },
|
||||
data: {
|
||||
...(data.promoLabel !== undefined ? { promoLabel: data.promoLabel?.trim() || null } : {}),
|
||||
...(data.status !== undefined ? { status: data.status } : {}),
|
||||
...(data.lineValue !== undefined ? { lineValue: data.lineValue } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async updateSelection(
|
||||
selectionId: bigint,
|
||||
data: { selectionName?: string; odds?: number; status?: string },
|
||||
operatorId?: bigint,
|
||||
) {
|
||||
const selection = await this.prisma.marketSelection.findUnique({
|
||||
where: { id: selectionId },
|
||||
});
|
||||
if (!selection) throw new NotFoundException('Selection not found');
|
||||
|
||||
if (data.odds != null) {
|
||||
if (!operatorId) throw new BadRequestException('Operator required for odds update');
|
||||
return this.updateOdds(selectionId, data.odds, operatorId);
|
||||
}
|
||||
|
||||
return this.prisma.marketSelection.update({
|
||||
where: { id: selectionId },
|
||||
data: {
|
||||
...(data.selectionName !== undefined ? { selectionName: data.selectionName.trim() } : {}),
|
||||
...(data.status !== undefined ? { status: data.status } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user