feat(admin): 同步单赛事筛选与排序功能 (从 main 分支)

This commit is contained in:
2026-06-18 17:45:58 +08:00
parent dd7802c6f7
commit fa7b8cff25
6 changed files with 197 additions and 4 deletions

View File

@@ -1995,6 +1995,8 @@ export class AdminController {
@Query('locale') locale?: string,
@Query('page') page?: string,
@Query('pageSize') pageSize?: string,
@Query('hasBets') hasBets?: string,
@Query('orderBy') orderBy?: string,
) {
const result = await this.matches.listAdminLeagueMatches(BigInt(leagueId), {
status: status || undefined,
@@ -2002,6 +2004,8 @@ export class AdminController {
locale: locale || undefined,
page: page ? Math.max(1, parseInt(page, 10) || 1) : 1,
pageSize: pageSize ? Math.min(100, Math.max(1, parseInt(pageSize, 10) || 20)) : 20,
hasBets: hasBets || undefined,
orderBy: orderBy || undefined,
});
return jsonResponse(result);
}

View File

@@ -301,3 +301,89 @@ describe('MatchesService listUpcomingPublished', () => {
);
});
});
describe('MatchesService listAdminLeagueMatches', () => {
const leagueId = BigInt(1);
let prisma: {
match: { findMany: jest.Mock; count: jest.Mock };
entityTranslation: { findFirst: jest.Mock; findMany: jest.Mock };
};
let matchBetStats: { betStatsForMatches: jest.Mock };
let service: MatchesService;
beforeEach(() => {
prisma = {
match: { findMany: jest.fn(), count: jest.fn() },
entityTranslation: {
findFirst: jest.fn().mockResolvedValue(null),
findMany: jest.fn().mockResolvedValue([]),
},
};
matchBetStats = { betStatsForMatches: jest.fn() };
service = new MatchesService(
prisma as never,
{ syncWithLeaguePublished: jest.fn() } as never,
matchBetStats as never,
);
});
it('filters by hasBets = true', async () => {
const mockMatches = [
{ id: BigInt(10), startTime: new Date(), displayOrder: 0, status: 'PUBLISHED', isOutright: false, matchName: 'M1', homeTeamId: BigInt(2), awayTeamId: BigInt(3), homeTeam: { code: 'H1' }, awayTeam: { code: 'A1' } },
{ id: BigInt(11), startTime: new Date(), displayOrder: 0, status: 'PUBLISHED', isOutright: false, matchName: 'M2', homeTeamId: BigInt(4), awayTeamId: BigInt(5), homeTeam: { code: 'H2' }, awayTeam: { code: 'A2' } },
];
prisma.match.findMany.mockResolvedValue(mockMatches);
const statsMap = new Map();
statsMap.set('10', { betCount: 5, totalStake: '500.00', pendingCount: 0 });
statsMap.set('11', { betCount: 0, totalStake: '0.00', pendingCount: 0 });
matchBetStats.betStatsForMatches.mockResolvedValue(statsMap);
const result = await service.listAdminLeagueMatches(leagueId, {
hasBets: 'true',
});
expect(result.items).toHaveLength(1);
expect(result.items[0].id).toBe('10');
});
it('sorts by betCount', async () => {
const mockMatches = [
{ id: BigInt(10), startTime: new Date(), displayOrder: 0, status: 'PUBLISHED', isOutright: false, matchName: 'M1', homeTeamId: BigInt(2), awayTeamId: BigInt(3), homeTeam: { code: 'H1' }, awayTeam: { code: 'A1' } },
{ id: BigInt(11), startTime: new Date(), displayOrder: 0, status: 'PUBLISHED', isOutright: false, matchName: 'M2', homeTeamId: BigInt(4), awayTeamId: BigInt(5), homeTeam: { code: 'H2' }, awayTeam: { code: 'A2' } },
];
prisma.match.findMany.mockResolvedValue(mockMatches);
const statsMap = new Map();
statsMap.set('10', { betCount: 5, totalStake: '500.00', pendingCount: 0 });
statsMap.set('11', { betCount: 15, totalStake: '1500.00', pendingCount: 0 });
matchBetStats.betStatsForMatches.mockResolvedValue(statsMap);
const result = await service.listAdminLeagueMatches(leagueId, {
orderBy: 'betCount',
});
expect(result.items).toHaveLength(2);
expect(result.items[0].id).toBe('11');
expect(result.items[1].id).toBe('10');
});
it('sorts by totalStake', async () => {
const mockMatches = [
{ id: BigInt(10), startTime: new Date(), displayOrder: 0, status: 'PUBLISHED', isOutright: false, matchName: 'M1', homeTeamId: BigInt(2), awayTeamId: BigInt(3), homeTeam: { code: 'H1' }, awayTeam: { code: 'A1' } },
{ id: BigInt(11), startTime: new Date(), displayOrder: 0, status: 'PUBLISHED', isOutright: false, matchName: 'M2', homeTeamId: BigInt(4), awayTeamId: BigInt(5), homeTeam: { code: 'H2' }, awayTeam: { code: 'A2' } },
];
prisma.match.findMany.mockResolvedValue(mockMatches);
const statsMap = new Map();
statsMap.set('10', { betCount: 5, totalStake: '1200.00', pendingCount: 0 });
statsMap.set('11', { betCount: 15, totalStake: '800.00', pendingCount: 0 });
matchBetStats.betStatsForMatches.mockResolvedValue(statsMap);
const result = await service.listAdminLeagueMatches(leagueId, {
orderBy: 'totalStake',
});
expect(result.items).toHaveLength(2);
expect(result.items[0].id).toBe('10');
expect(result.items[1].id).toBe('11');
});
});

View File

@@ -556,6 +556,8 @@ export class MatchesService {
locale?: string;
page?: number;
pageSize?: number;
hasBets?: string;
orderBy?: string;
},
) {
const where: Prisma.MatchWhereInput = {
@@ -574,6 +576,70 @@ export class MatchesService {
}
const page = Math.max(1, opts.page ?? 1);
const pageSize = Math.min(100, Math.max(1, opts.pageSize ?? 20));
if (opts.hasBets === 'true' || opts.orderBy === 'betCount' || opts.orderBy === 'totalStake') {
const allRows = await this.prisma.match.findMany({
where,
include: { homeTeam: true, awayTeam: true },
});
const locale = opts.locale ?? 'zh-CN';
const betStatsMap = await this.betStatsForMatches(allRows.map((m) => m.id));
const itemsWithStats = await Promise.all(
allRows.map(async (m) => {
const [homeTeamName, awayTeamName] = await Promise.all([
this.getTranslation('TEAM', m.homeTeamId, locale),
this.getTranslation('TEAM', m.awayTeamId, locale),
]);
const raw = betStatsMap.get(m.id.toString());
const betCount = raw?.betCount ?? 0;
const totalStake = raw?.totalStake ?? '0';
const pendingBets = raw?.pendingCount ?? 0;
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 },
betCount,
totalStake,
pendingBets,
};
}),
);
let filteredItems = itemsWithStats;
if (opts.hasBets === 'true') {
filteredItems = itemsWithStats.filter((item) => item.betCount > 0);
}
if (opts.orderBy === 'betCount') {
filteredItems.sort((a, b) => b.betCount - a.betCount);
} else if (opts.orderBy === 'totalStake') {
filteredItems.sort((a, b) => {
const stakeA = parseFloat(a.totalStake);
const stakeB = parseFloat(b.totalStake);
return stakeB - stakeA;
});
} else {
filteredItems.sort((a, b) => {
if (a.displayOrder !== b.displayOrder) {
return a.displayOrder - b.displayOrder;
}
return new Date(b.startTime).getTime() - new Date(a.startTime).getTime();
});
}
const total = filteredItems.length;
const paginatedItems = filteredItems.slice((page - 1) * pageSize, page * pageSize);
return { items: paginatedItems, total, page, pageSize };
}
const [total, rows] = await Promise.all([
this.prisma.match.count({ where }),
this.prisma.match.findMany({