feat: refactor agent manager, media library, and player UX

- Split admin users page into player/tier-1/tier-2 tabs with affiliation labels and context-specific create dialogs

- Add media library with uploaded_files migration, list/delete unused files API, and admin nav route

- Enforce player username format (alphanumeric 3-32) on frontend and backend via shared package

- Improve admin dialog/panel styling; refine player parlay and match bet card kickoff display

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-09 17:56:28 +08:00
parent d5e7c8edb3
commit df20444be9
27 changed files with 2136 additions and 563 deletions

View File

@@ -12,6 +12,7 @@ import { AuthService } from '../identity/auth.service';
import { SystemConfigService } from '../../shared/config/system-config.service';
import { Decimal } from '@prisma/client/runtime/library';
import { generateBatchNo } from '../../shared/common/decorators';
import { assertPlayerUsername } from '@thebet365/shared';
function dec(v: Decimal | null | undefined) {
return v?.toString() ?? '0';
@@ -475,6 +476,11 @@ export class AgentsService {
if (data.username !== undefined) {
const nextUsername = data.username.trim();
if (!nextUsername) throw new BadRequestException('账号名称不能为空');
try {
assertPlayerUsername(nextUsername);
} catch (e) {
throw new BadRequestException(e instanceof Error ? e.message : '玩家用户名格式无效');
}
if (nextUsername !== user.username) {
const taken = await this.prisma.user.findUnique({ where: { username: nextUsername } });
if (taken) throw new BadRequestException('账号名称已被占用');
@@ -531,6 +537,8 @@ export class AgentsService {
page?: number;
pageSize?: number;
keyword?: string;
status?: string;
level?: 1 | 2;
parentAgentId?: bigint;
}) {
const page = Math.max(1, params?.page ?? 1);
@@ -538,15 +546,26 @@ export class AgentsService {
const skip = (page - 1) * pageSize;
const where: Prisma.AgentProfileWhereInput = {};
if (params?.parentAgentId !== undefined) {
if (params?.level === 2) {
where.level = 2;
} else if (params?.level === 1) {
where.level = 1;
} else if (params?.parentAgentId !== undefined) {
where.parentAgentId = params.parentAgentId;
} else {
// Default: only show top-level agents (no parent)
where.parentAgentId = null;
}
const kw = params?.keyword?.trim();
const status = params?.status?.trim();
const userWhere: Prisma.UserWhereInput = {};
if (status && ['ACTIVE', 'SUSPENDED'].includes(status)) {
userWhere.status = status;
}
if (kw) {
where.user = { username: { contains: kw, mode: 'insensitive' } };
userWhere.username = { contains: kw, mode: 'insensitive' };
}
if (Object.keys(userWhere).length > 0) {
where.user = userWhere;
}
const [profiles, total] = await Promise.all([
@@ -592,6 +611,18 @@ export class AgentsService {
childAgentCounts.map((g) => [g.parentAgentId?.toString(), g._count._all]),
);
const parentAgentIds = [
...new Set(profiles.map((p) => p.parentAgentId).filter((id): id is bigint => id != null)),
];
const parentUsers =
parentAgentIds.length > 0
? await this.prisma.user.findMany({
where: { id: { in: parentAgentIds } },
select: { id: true, username: true },
})
: [];
const parentUsernameMap = new Map(parentUsers.map((u) => [u.id.toString(), u.username]));
const items = profiles.map((p) => {
const available = new Decimal(p.creditLimit).sub(p.usedCredit);
return {
@@ -602,6 +633,9 @@ export class AgentsService {
level: p.level,
status: p.status,
parentAgentId: p.parentAgentId?.toString() ?? null,
parentUsername: p.parentAgentId
? parentUsernameMap.get(p.parentAgentId.toString()) ?? null
: null,
creditLimit: p.creditLimit.toString(),
usedCredit: p.usedCredit.toString(),
availableCredit: available.toString(),
@@ -1235,6 +1269,12 @@ export class AgentsService {
}
}
try {
assertPlayerUsername(data.username);
} catch (e) {
throw new BadRequestException(e instanceof Error ? e.message : '玩家用户名格式无效');
}
const hash = await this.auth.hashPassword(data.password);
const locale = data.locale ?? 'zh-CN';