feat(admin): 管理端列表分页、控制台图表与赛事导入
- 玩家/代理/赛事/注单/审计列表分页,默认每页 10 条,无页面滚动条布局 - ECharts 控制台概览、注单管理中文化与列宽优化 - zhibo 赛事字段迁移与导入,玩家编辑可改所属代理 - 管理端 API 分页与 dashboard 统计接口 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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,
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user