重构 API 为 8 领域 + 应用层架构
将后端模块拆分为 domains、applications、shared 三层,结算计算器移入 domain 纯函数目录,API 路径与测试保持不变。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
203
apps/api/src/domains/odds/markets.service.ts
Normal file
203
apps/api/src/domains/odds/markets.service.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import { Injectable, BadRequestException, NotFoundException } from '@nestjs/common';
|
||||
import { PrismaService } from '../../shared/prisma/prisma.service';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
import {
|
||||
FT_CORRECT_SCORE_TEMPLATE,
|
||||
HT_CORRECT_SCORE_TEMPLATE,
|
||||
} from '../settlement/domain/settlement-calculator';
|
||||
|
||||
@Injectable()
|
||||
export class MarketsService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async generateTemplates(matchId: bigint, marketTypes: string[]) {
|
||||
const match = await this.prisma.match.findUnique({ where: { id: matchId } });
|
||||
if (!match) throw new NotFoundException('Match not found');
|
||||
|
||||
const created = [];
|
||||
|
||||
for (const marketType of marketTypes) {
|
||||
const existing = await this.prisma.market.findFirst({
|
||||
where: { matchId, marketType },
|
||||
});
|
||||
if (existing) continue;
|
||||
|
||||
const config = this.getMarketConfig(marketType);
|
||||
const market = await this.prisma.market.create({
|
||||
data: {
|
||||
matchId,
|
||||
marketType,
|
||||
period: config.period,
|
||||
lineValue: config.lineValue,
|
||||
allowSingle: true,
|
||||
allowParlay: config.allowParlay,
|
||||
sortOrder: config.sortOrder,
|
||||
selections: {
|
||||
create: config.selections.map((s, i) => ({
|
||||
selectionCode: s.code,
|
||||
selectionName: s.name,
|
||||
odds: s.odds ?? 1.01,
|
||||
sortOrder: i,
|
||||
})),
|
||||
},
|
||||
},
|
||||
include: { selections: true },
|
||||
});
|
||||
created.push(market);
|
||||
}
|
||||
|
||||
return created;
|
||||
}
|
||||
|
||||
private getMarketConfig(marketType: string) {
|
||||
const configs: Record<string, {
|
||||
period: string;
|
||||
lineValue?: number;
|
||||
allowParlay: boolean;
|
||||
sortOrder: number;
|
||||
selections: Array<{ code: string; name: string; odds?: number }>;
|
||||
}> = {
|
||||
FT_1X2: {
|
||||
period: 'FT',
|
||||
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 },
|
||||
],
|
||||
},
|
||||
HT_1X2: {
|
||||
period: 'HT',
|
||||
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 },
|
||||
],
|
||||
},
|
||||
FT_HANDICAP: {
|
||||
period: 'FT',
|
||||
lineValue: -0.5,
|
||||
allowParlay: true,
|
||||
sortOrder: 2,
|
||||
selections: [
|
||||
{ code: 'HOME', name: 'Home -0.5', odds: 1.9 },
|
||||
{ code: 'AWAY', name: 'Away +0.5', odds: 1.9 },
|
||||
],
|
||||
},
|
||||
HT_HANDICAP: {
|
||||
period: 'HT',
|
||||
lineValue: -0.5,
|
||||
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 },
|
||||
],
|
||||
},
|
||||
FT_OVER_UNDER: {
|
||||
period: 'FT',
|
||||
lineValue: 2.5,
|
||||
allowParlay: true,
|
||||
sortOrder: 3,
|
||||
selections: [
|
||||
{ code: 'OVER', name: 'Over 2.5', odds: 1.85 },
|
||||
{ code: 'UNDER', name: 'Under 2.5', odds: 1.95 },
|
||||
],
|
||||
},
|
||||
HT_OVER_UNDER: {
|
||||
period: 'HT',
|
||||
lineValue: 1.5,
|
||||
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 },
|
||||
],
|
||||
},
|
||||
FT_ODD_EVEN: {
|
||||
period: 'FT',
|
||||
allowParlay: true,
|
||||
sortOrder: 4,
|
||||
selections: [
|
||||
{ code: 'ODD', name: 'Odd', odds: 1.9 },
|
||||
{ code: 'EVEN', name: 'Even', odds: 1.9 },
|
||||
],
|
||||
},
|
||||
FT_CORRECT_SCORE: {
|
||||
period: 'FT',
|
||||
allowParlay: true,
|
||||
sortOrder: 8,
|
||||
selections: FT_CORRECT_SCORE_TEMPLATE.map((code) => ({
|
||||
code,
|
||||
name: code.replace('SCORE_', '').replace('_', '-') || code,
|
||||
odds: 8.0,
|
||||
})),
|
||||
},
|
||||
HT_CORRECT_SCORE: {
|
||||
period: 'HT',
|
||||
allowParlay: true,
|
||||
sortOrder: 9,
|
||||
selections: HT_CORRECT_SCORE_TEMPLATE.map((code) => ({
|
||||
code,
|
||||
name: code.replace('SCORE_', '').replace('_', '-') || code,
|
||||
odds: 6.0,
|
||||
})),
|
||||
},
|
||||
SH_CORRECT_SCORE: {
|
||||
period: 'SH',
|
||||
allowParlay: true,
|
||||
sortOrder: 10,
|
||||
selections: HT_CORRECT_SCORE_TEMPLATE.map((code) => ({
|
||||
code,
|
||||
name: code.replace('SCORE_', '').replace('_', '-') || code,
|
||||
odds: 6.0,
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
const config = configs[marketType];
|
||||
if (!config) throw new BadRequestException(`Unknown market type: ${marketType}`);
|
||||
return config;
|
||||
}
|
||||
|
||||
async updateOdds(selectionId: bigint, newOdds: number, operatorId: bigint) {
|
||||
const selection = await this.prisma.marketSelection.findUnique({
|
||||
where: { id: selectionId },
|
||||
});
|
||||
if (!selection) throw new NotFoundException('Selection not found');
|
||||
if (newOdds <= 1) throw new BadRequestException('Odds must be > 1.00');
|
||||
|
||||
const newVersion = selection.oddsVersion + BigInt(1);
|
||||
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
await tx.oddsChangeLog.create({
|
||||
data: {
|
||||
selectionId,
|
||||
oldOdds: selection.odds,
|
||||
newOdds,
|
||||
oddsVersion: newVersion,
|
||||
changedBy: operatorId,
|
||||
},
|
||||
});
|
||||
|
||||
return tx.marketSelection.update({
|
||||
where: { id: selectionId },
|
||||
data: { odds: newOdds, oddsVersion: newVersion },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async batchUpdateOdds(
|
||||
updates: Array<{ selectionId: bigint; odds: number }>,
|
||||
operatorId: bigint,
|
||||
) {
|
||||
const results = [];
|
||||
for (const u of updates) {
|
||||
results.push(await this.updateOdds(u.selectionId, u.odds, operatorId));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user