重构 API 为 8 领域 + 应用层架构
将后端模块拆分为 domains、applications、shared 三层,结算计算器移入 domain 纯函数目录,API 路径与测试保持不变。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
8
apps/api/src/domains/catalog/matches.module.ts
Normal file
8
apps/api/src/domains/catalog/matches.module.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MatchesService } from './matches.service';
|
||||
|
||||
@Module({
|
||||
providers: [MatchesService],
|
||||
exports: [MatchesService],
|
||||
})
|
||||
export class MatchesModule {}
|
||||
161
apps/api/src/domains/catalog/matches.service.ts
Normal file
161
apps/api/src/domains/catalog/matches.service.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||
import { PrismaService } from '../../shared/prisma/prisma.service';
|
||||
|
||||
@Injectable()
|
||||
export class MatchesService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async createLeague(code: string, translations: Record<string, string>) {
|
||||
const league = await this.prisma.league.create({ data: { code } });
|
||||
for (const [locale, value] of Object.entries(translations)) {
|
||||
await this.prisma.entityTranslation.create({
|
||||
data: {
|
||||
entityType: 'LEAGUE',
|
||||
entityId: league.id,
|
||||
locale,
|
||||
fieldName: 'name',
|
||||
value,
|
||||
},
|
||||
});
|
||||
}
|
||||
return league;
|
||||
}
|
||||
|
||||
async createTeam(code: string, translations: Record<string, string>) {
|
||||
const team = await this.prisma.team.create({ data: { code } });
|
||||
for (const [locale, value] of Object.entries(translations)) {
|
||||
await this.prisma.entityTranslation.create({
|
||||
data: {
|
||||
entityType: 'TEAM',
|
||||
entityId: team.id,
|
||||
locale,
|
||||
fieldName: 'name',
|
||||
value,
|
||||
},
|
||||
});
|
||||
}
|
||||
return team;
|
||||
}
|
||||
|
||||
async createMatch(data: {
|
||||
leagueId: bigint;
|
||||
homeTeamId: bigint;
|
||||
awayTeamId: bigint;
|
||||
startTime: Date;
|
||||
isHot?: boolean;
|
||||
createdBy?: bigint;
|
||||
}) {
|
||||
return this.prisma.match.create({
|
||||
data: {
|
||||
leagueId: data.leagueId,
|
||||
homeTeamId: data.homeTeamId,
|
||||
awayTeamId: data.awayTeamId,
|
||||
startTime: data.startTime,
|
||||
isHot: data.isHot ?? false,
|
||||
createdBy: data.createdBy,
|
||||
status: 'DRAFT',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async publishMatch(matchId: bigint) {
|
||||
return this.prisma.match.update({
|
||||
where: { id: matchId },
|
||||
data: { status: 'PUBLISHED', publishTime: new Date() },
|
||||
});
|
||||
}
|
||||
|
||||
async closeMatch(matchId: bigint) {
|
||||
return this.prisma.match.update({
|
||||
where: { id: matchId },
|
||||
data: { status: 'CLOSED', closeTime: new Date() },
|
||||
});
|
||||
}
|
||||
|
||||
async cancelMatch(matchId: bigint) {
|
||||
return this.prisma.match.update({
|
||||
where: { id: matchId },
|
||||
data: { status: 'CANCELLED' },
|
||||
});
|
||||
}
|
||||
|
||||
async getTranslation(entityType: string, entityId: bigint, locale: string) {
|
||||
const translations = await this.prisma.entityTranslation.findMany({
|
||||
where: { entityType, entityId },
|
||||
});
|
||||
const map = Object.fromEntries(
|
||||
translations.filter((t) => t.fieldName === 'name').map((t) => [t.locale, t.value]),
|
||||
);
|
||||
return map[locale] || map['en-US'] || map['zh-CN'] || Object.values(map)[0] || '';
|
||||
}
|
||||
|
||||
async enrichMatch(match: Record<string, unknown>, locale: string) {
|
||||
const m = match as {
|
||||
id: bigint;
|
||||
leagueId: bigint;
|
||||
homeTeamId: bigint;
|
||||
awayTeamId: bigint;
|
||||
league?: unknown;
|
||||
homeTeam?: unknown;
|
||||
awayTeam?: unknown;
|
||||
markets?: 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,
|
||||
leagueName,
|
||||
homeTeamName: homeName,
|
||||
awayTeamName: awayName,
|
||||
};
|
||||
}
|
||||
|
||||
async listPublished(locale = 'en-US', leagueId?: bigint) {
|
||||
const matches = await this.prisma.match.findMany({
|
||||
where: {
|
||||
status: 'PUBLISHED',
|
||||
...(leagueId ? { leagueId } : {}),
|
||||
},
|
||||
include: {
|
||||
markets: {
|
||||
where: { status: 'OPEN' },
|
||||
include: { selections: { where: { status: 'OPEN' } } },
|
||||
},
|
||||
},
|
||||
orderBy: [{ isHot: 'desc' }, { 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 },
|
||||
include: {
|
||||
markets: {
|
||||
include: { selections: true },
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
},
|
||||
score: true,
|
||||
},
|
||||
});
|
||||
if (!match) throw new NotFoundException('Match not found');
|
||||
return this.enrichMatch(match, locale);
|
||||
}
|
||||
|
||||
@Cron(CronExpression.EVERY_MINUTE)
|
||||
async autoCloseMatches() {
|
||||
const now = new Date();
|
||||
await this.prisma.match.updateMany({
|
||||
where: {
|
||||
status: 'PUBLISHED',
|
||||
startTime: { lte: now },
|
||||
},
|
||||
data: { status: 'CLOSED', closeTime: now },
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user