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,10 @@
import { Injectable, BadRequestException, ForbiddenException } from '@nestjs/common';
import {
Injectable,
BadRequestException,
ForbiddenException,
NotFoundException,
} from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../shared/prisma/prisma.service';
import { WalletService } from '../ledger/wallet.service';
import { AuthService } from '../identity/auth.service';
@@ -147,6 +153,211 @@ export class AgentsService {
return { success: true };
}
async listAgentsAdmin(params?: {
page?: number;
pageSize?: number;
keyword?: 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.AgentProfileWhereInput = {};
const kw = params?.keyword?.trim();
if (kw) {
where.user = { username: { contains: kw, mode: 'insensitive' } };
}
const [profiles, total] = await Promise.all([
this.prisma.agentProfile.findMany({
where,
include: {
user: { include: { preferences: true } },
},
orderBy: { createdAt: 'desc' },
skip,
take: pageSize,
}),
this.prisma.agentProfile.count({ where }),
]);
const agentIds = profiles.map((p) => p.userId);
const playerCounts =
agentIds.length > 0
? await this.prisma.user.groupBy({
by: ['parentId'],
where: {
userType: 'PLAYER',
parentId: { in: agentIds },
deletedAt: null,
},
_count: { _all: true },
})
: [];
const countMap = new Map(
playerCounts.map((g) => [g.parentId?.toString(), g._count._all]),
);
const items = profiles.map((p) => {
const available = new Decimal(p.creditLimit).sub(p.usedCredit);
return {
id: p.id.toString(),
userId: p.userId.toString(),
username: p.user.username,
userStatus: p.user.status,
level: p.level,
status: p.status,
parentAgentId: p.parentAgentId?.toString() ?? null,
creditLimit: p.creditLimit.toString(),
usedCredit: p.usedCredit.toString(),
availableCredit: available.toString(),
directPlayerLiability: p.directPlayerLiability.toString(),
childAgentExposure: p.childAgentExposure.toString(),
cashbackRate: p.cashbackRate.toString(),
directPlayerCount: countMap.get(p.userId.toString()) ?? 0,
phone: p.user.preferences?.phone ?? null,
email: p.user.preferences?.email ?? null,
locale: p.user.locale,
createdAt: p.createdAt,
updatedAt: p.updatedAt,
};
});
return { items, total, page, pageSize };
}
async getAgentAdminDetail(agentId: bigint) {
const profile = await this.prisma.agentProfile.findUnique({
where: { userId: agentId },
include: { user: { include: { preferences: true, auth: true } } },
});
if (!profile) throw new NotFoundException('代理不存在');
const [directPlayerCount, recentCredits] = await Promise.all([
this.prisma.user.count({
where: { parentId: agentId, userType: 'PLAYER', deletedAt: null },
}),
this.prisma.agentCreditTransaction.findMany({
where: { agentId },
orderBy: { createdAt: 'desc' },
take: 10,
}),
]);
const available = new Decimal(profile.creditLimit).sub(profile.usedCredit);
let parentUsername: string | null = null;
if (profile.parentAgentId) {
const parent = await this.prisma.user.findUnique({
where: { id: profile.parentAgentId },
select: { username: true },
});
parentUsername = parent?.username ?? null;
}
return {
id: profile.id.toString(),
userId: profile.userId.toString(),
username: profile.user.username,
userStatus: profile.user.status,
level: profile.level,
status: profile.status,
parentAgentId: profile.parentAgentId?.toString() ?? null,
parentUsername,
creditLimit: profile.creditLimit.toString(),
usedCredit: profile.usedCredit.toString(),
availableCredit: available.toString(),
directPlayerLiability: profile.directPlayerLiability.toString(),
childAgentExposure: profile.childAgentExposure.toString(),
cashbackRate: profile.cashbackRate.toString(),
directPlayerCount,
phone: profile.user.preferences?.phone ?? null,
email: profile.user.preferences?.email ?? null,
locale: profile.user.locale,
lastLoginAt: profile.user.auth?.lastLoginAt ?? null,
createdAt: profile.createdAt,
updatedAt: profile.updatedAt,
recentCreditTransactions: recentCredits.map((t) => ({
id: t.id.toString(),
transactionType: t.transactionType,
amount: t.amount.toString(),
creditBefore: t.creditBefore.toString(),
creditAfter: t.creditAfter.toString(),
remark: t.remark,
createdAt: t.createdAt,
})),
};
}
async updateAgentAdmin(
agentId: bigint,
data: {
status?: string;
locale?: string;
phone?: string;
email?: string;
cashbackRate?: number;
},
) {
const profile = await this.prisma.agentProfile.findUnique({
where: { userId: agentId },
include: { user: true },
});
if (!profile) throw new NotFoundException('代理不存在');
if (data.status && !['ACTIVE', 'SUSPENDED'].includes(data.status)) {
throw new BadRequestException('无效状态');
}
if (data.status) {
await this.prisma.$transaction([
this.prisma.user.update({
where: { id: agentId },
data: { status: data.status },
}),
this.prisma.agentProfile.update({
where: { userId: agentId },
data: { status: data.status },
}),
]);
}
if (data.locale) {
await this.prisma.user.update({
where: { id: agentId },
data: { locale: data.locale },
});
}
if (data.cashbackRate !== undefined) {
await this.prisma.agentProfile.update({
where: { userId: agentId },
data: { cashbackRate: data.cashbackRate },
});
}
if (data.phone !== undefined || data.email !== undefined || data.locale) {
const phone = data.phone !== undefined ? data.phone?.trim() || null : undefined;
const email = data.email !== undefined ? data.email?.trim() || null : undefined;
await this.prisma.userPreference.upsert({
where: { userId: agentId },
create: {
userId: agentId,
locale: data.locale ?? profile.user.locale,
phone: phone ?? null,
email: email ?? null,
},
update: {
...(data.locale ? { locale: data.locale } : {}),
...(phone !== undefined ? { phone } : {}),
...(email !== undefined ? { email } : {}),
},
});
}
return this.getAgentAdminDetail(agentId);
}
async createAgent(
operatorId: bigint,
data: {
@@ -155,6 +366,10 @@ export class AgentsService {
level: number;
parentAgentId?: bigint;
creditLimit?: number;
locale?: string;
phone?: string;
email?: string;
cashbackRate?: number;
},
) {
if (data.level === 2 && !data.parentAgentId) {
@@ -164,12 +379,14 @@ export class AgentsService {
const hash = await this.auth.hashPassword(data.password);
return this.prisma.$transaction(async (tx) => {
const locale = data.locale ?? 'zh-CN';
const user = await tx.user.create({
data: {
username: data.username,
userType: 'AGENT',
parentId: data.parentAgentId,
agentLevel: data.level,
locale,
},
});
@@ -177,12 +394,22 @@ export class AgentsService {
data: { userId: user.id, passwordHash: hash },
});
await tx.userPreference.create({
data: {
userId: user.id,
locale,
phone: data.phone?.trim() || null,
email: data.email?.trim() || null,
},
});
await tx.agentProfile.create({
data: {
userId: user.id,
level: data.level,
parentAgentId: data.parentAgentId,
creditLimit: data.creditLimit ?? 0,
cashbackRate: data.cashbackRate ?? 0,
},
});
@@ -215,38 +442,81 @@ export class AgentsService {
async createPlayer(
operatorId: bigint,
data: { username: string; password: string; parentId: bigint },
data: {
username: string;
password: string;
parentId?: bigint;
locale?: string;
phone?: string;
email?: string;
initialDeposit?: number;
depositRemark?: string;
depositRequestId?: string;
},
) {
const hash = await this.auth.hashPassword(data.password);
let parentId: bigint | null = null;
if (data.parentId != null) {
const parent = await this.prisma.user.findUnique({ where: { id: data.parentId } });
if (!parent || parent.userType !== 'AGENT') {
throw new BadRequestException('上级必须为代理账号');
}
parentId = data.parentId;
}
return this.prisma.$transaction(async (tx) => {
const user = await tx.user.create({
const hash = await this.auth.hashPassword(data.password);
const locale = data.locale ?? 'zh-CN';
const user = await this.prisma.$transaction(async (tx) => {
const created = await tx.user.create({
data: {
username: data.username,
userType: 'PLAYER',
parentId: data.parentId,
parentId,
locale,
},
});
await tx.userAuth.create({
data: { userId: user.id, passwordHash: hash },
data: { userId: created.id, passwordHash: hash },
});
await tx.wallet.create({
data: { userId: user.id },
data: { userId: created.id },
});
await tx.userPreference.create({
data: { userId: user.id },
data: {
userId: created.id,
locale,
phone: data.phone?.trim() || null,
email: data.email?.trim() || null,
},
});
const parent = await tx.user.findUnique({ where: { id: data.parentId } });
if (parent?.userType === 'AGENT') {
await this.recalculateUsedCredit(data.parentId);
}
return user;
return created;
});
if (parentId) {
await this.recalculateUsedCredit(parentId);
}
const initial = data.initialDeposit ?? 0;
if (initial > 0) {
const requestId =
data.depositRequestId ?? `admin-create-${user.id}-${Date.now()}`;
await this.wallet.deposit(
user.id,
initial,
operatorId,
data.depositRemark ?? '开户初始余额',
requestId,
);
if (parentId) {
await this.recalculateUsedCredit(parentId);
}
}
return user;
}
async getDirectPlayers(agentId: bigint) {