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