重构 API 为 8 领域 + 应用层架构

将后端模块拆分为 domains、applications、shared 三层,结算计算器移入 domain 纯函数目录,API 路径与测试保持不变。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-02 14:48:41 +08:00
parent 14e49374ac
commit 4c92157299
47 changed files with 169 additions and 138 deletions

View 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;
}
}