feat(admin): 管理端列表分页、控制台图表与赛事导入

- 玩家/代理/赛事/注单/审计列表分页,默认每页 10 条,无页面滚动条布局

- ECharts 控制台概览、注单管理中文化与列宽优化

- zhibo 赛事字段迁移与导入,玩家编辑可改所属代理

- 管理端 API 分页与 dashboard 统计接口

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-03 13:49:31 +08:00
parent 2c356b2048
commit 80adc0e928
45 changed files with 6564 additions and 499 deletions

View File

@@ -1,4 +1,5 @@
import { Injectable, BadRequestException, ConflictException } from '@nestjs/common';
import { Injectable, BadRequestException, ConflictException, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../shared/prisma/prisma.service';
import { WalletService } from '../ledger/wallet.service';
import { Decimal } from '@prisma/client/runtime/library';
@@ -208,4 +209,157 @@ export class BetsService {
include: { selections: true },
});
}
private dec(v: Decimal | null | undefined) {
return v?.toString() ?? '0';
}
private formatBetListRow(
b: {
id: bigint;
betNo: string;
userId: bigint;
agentId: bigint | null;
betType: string;
stake: Decimal;
totalOdds: Decimal | null;
potentialReturn: Decimal | null;
actualReturn: Decimal;
status: string;
settlementStatus: string | null;
currency: string;
placedAt: Date;
settledAt: Date | null;
user: { id: bigint; username: string; parent: { username: string } | null };
_count: { selections: number };
},
) {
return {
id: b.id.toString(),
betNo: b.betNo,
userId: b.userId.toString(),
username: b.user.username,
parentUsername: b.user.parent?.username ?? null,
agentId: b.agentId?.toString() ?? null,
betType: b.betType,
stake: this.dec(b.stake),
totalOdds: b.totalOdds ? this.dec(b.totalOdds) : null,
potentialReturn: b.potentialReturn ? this.dec(b.potentialReturn) : null,
actualReturn: this.dec(b.actualReturn),
status: b.status,
settlementStatus: b.settlementStatus,
currency: b.currency,
placedAt: b.placedAt,
settledAt: b.settledAt,
selectionCount: b._count.selections,
};
}
async listBetsAdmin(params: {
page?: number;
pageSize?: number;
keyword?: string;
status?: string;
betType?: string;
placedFrom?: string;
placedTo?: string;
}) {
const page = Math.max(1, params.page ?? 1);
const pageSize = Math.min(Math.max(1, params.pageSize ?? 10), 100);
const skip = (page - 1) * pageSize;
const where: Prisma.BetWhereInput = {};
if (params.status) where.status = params.status;
if (params.betType) where.betType = params.betType;
if (params.placedFrom || params.placedTo) {
where.placedAt = {};
if (params.placedFrom) {
const from = new Date(params.placedFrom);
from.setHours(0, 0, 0, 0);
where.placedAt.gte = from;
}
if (params.placedTo) {
const to = new Date(params.placedTo);
to.setHours(23, 59, 59, 999);
where.placedAt.lte = to;
}
}
const kw = params.keyword?.trim();
if (kw) {
where.OR = [
{ betNo: { contains: kw, mode: 'insensitive' } },
{ user: { username: { contains: kw, mode: 'insensitive' } } },
];
}
const [items, total] = await Promise.all([
this.prisma.bet.findMany({
where,
include: {
user: {
select: {
id: true,
username: true,
parent: { select: { username: true } },
},
},
_count: { select: { selections: true } },
},
orderBy: { placedAt: 'desc' },
skip,
take: pageSize,
}),
this.prisma.bet.count({ where }),
]);
return {
items: items.map((b) => this.formatBetListRow(b)),
total,
page,
pageSize,
};
}
async getBetAdminDetail(betId: bigint) {
const bet = await this.prisma.bet.findUnique({
where: { id: betId },
include: {
user: {
select: {
id: true,
username: true,
parent: { select: { username: true } },
},
},
selections: { orderBy: { sortOrder: 'asc' } },
},
});
if (!bet) throw new NotFoundException('注单不存在');
return {
...this.formatBetListRow({
...bet,
_count: { selections: bet.selections.length },
}),
requestId: bet.requestId,
createdAt: bet.createdAt,
updatedAt: bet.updatedAt,
selections: bet.selections.map((s) => ({
id: s.id.toString(),
matchId: s.matchId?.toString() ?? null,
marketType: s.marketType,
period: s.period,
selectionName: s.selectionNameSnapshot,
handicapLine: s.handicapLine ? this.dec(s.handicapLine) : null,
totalLine: s.totalLine ? this.dec(s.totalLine) : null,
odds: this.dec(s.odds),
resultStatus: s.resultStatus,
effectiveOdds: s.effectiveOdds ? this.dec(s.effectiveOdds) : null,
sortOrder: s.sortOrder,
})),
};
}
}