feat(admin): 从已有玩家升级代理、修复 i18n 与过期 .js 冲突

- 新建一级代理改为选择已有玩家;新建用户可选一级代理

- 操作日志/注单等扁平 key 翻译;清理 src 内误生成 .js,Vite 优先解析 .ts

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-03 15:42:15 +08:00
parent cbfa18d1d3
commit 3b739982a1
27 changed files with 625 additions and 165 deletions

View File

@@ -88,6 +88,20 @@ class CreatePlayerAdminDto {
@IsOptional()
@IsString()
remark?: string;
/** 创建为一级代理(非玩家) */
@IsOptional()
asTier1Agent?: boolean;
@IsOptional()
@IsNumber()
@Min(0)
creditLimit?: number;
@IsOptional()
@IsNumber()
@Min(0)
cashbackRate?: number;
}
class UpdatePlayerAdminDto {
@@ -114,21 +128,14 @@ class UpdatePlayerAdminDto {
}
class CreateAgentAdminDto {
/** 已有玩家用户 ID升级为一级代理 */
@IsString()
username!: string;
@IsString()
@MinLength(8)
password!: string;
userId!: string;
@IsNumber()
@Min(0)
creditLimit!: number;
@IsOptional()
@IsString()
locale?: string;
@IsOptional()
@IsString()
phone?: string;
@@ -331,18 +338,41 @@ export class AdminController {
initialDeposit: dto.initialDeposit,
depositRemark: dto.remark,
depositRequestId: `create-player-${dto.username}-${Date.now()}`,
asTier1Agent: dto.asTier1Agent,
creditLimit: dto.creditLimit,
cashbackRate: dto.cashbackRate,
});
await this.audit.log({
operatorId,
operatorType: 'ADMIN',
action: 'CREATE_PLAYER',
module: 'USERS',
action: dto.asTier1Agent ? 'CREATE_AGENT' : 'CREATE_PLAYER',
module: dto.asTier1Agent ? 'AGENTS' : 'USERS',
targetId: user.id.toString(),
});
if (dto.asTier1Agent) {
const detail = await this.agents.getAgentAdminDetail(user.id);
return jsonResponse(detail);
}
const detail = await this.users.getPlayerAdminDetail(user.id);
return jsonResponse(detail);
}
@Get('users/promotable-for-agent')
async listPromotableForAgent(@Query('keyword') keyword?: string) {
const rows = await this.agents.listPromotablePlayers(keyword);
return jsonResponse(
rows.map((u) => ({
id: u.id.toString(),
username: u.username,
status: u.status,
parentId: u.parentId?.toString() ?? null,
parentUsername: u.parent?.username ?? null,
phone: u.preferences?.phone ?? null,
email: u.preferences?.email ?? null,
})),
);
}
@Get('agents/options')
async listAgentOptions() {
const agents = await this.prisma.user.findMany({
@@ -397,12 +427,8 @@ export class AdminController {
@CurrentUser('id') operatorId: bigint,
@Body() dto: CreateAgentAdminDto,
) {
const user = await this.agents.createAgent(operatorId, {
username: dto.username,
password: dto.password,
level: 1,
const user = await this.agents.promotePlayerToTier1Agent(BigInt(dto.userId), {
creditLimit: dto.creditLimit,
locale: dto.locale,
phone: dto.phone,
email: dto.email,
cashbackRate: dto.cashbackRate,

View File

@@ -358,6 +358,113 @@ export class AgentsService {
return this.getAgentAdminDetail(agentId);
}
/** 可升级为一级代理的玩家(尚无代理档案) */
async listPromotablePlayers(keyword?: string) {
const q = keyword?.trim();
return this.prisma.user.findMany({
where: {
userType: 'PLAYER',
deletedAt: null,
agentProfile: null,
...(q
? { username: { contains: q, mode: 'insensitive' } }
: {}),
},
select: {
id: true,
username: true,
status: true,
parentId: true,
preferences: { select: { phone: true, email: true } },
parent: { select: { username: true } },
},
orderBy: { id: 'desc' },
take: 50,
});
}
/** 将已有玩家账号升级为一级代理(不新建用户) */
async promotePlayerToTier1Agent(
userId: bigint,
data: {
creditLimit: number;
cashbackRate?: number;
phone?: string;
email?: string;
},
) {
const user = await this.prisma.user.findUnique({
where: { id: userId },
include: { agentProfile: true, preferences: true },
});
if (!user || user.deletedAt) {
throw new NotFoundException('用户不存在');
}
if (user.userType !== 'PLAYER') {
throw new BadRequestException('仅玩家账号可设为代理');
}
if (user.agentProfile) {
throw new BadRequestException('该用户已是代理');
}
const oldParentId = user.parentId;
const phone =
data.phone !== undefined
? data.phone.trim() || null
: user.preferences?.phone ?? null;
const email =
data.email !== undefined
? data.email.trim() || null
: user.preferences?.email ?? null;
await this.prisma.$transaction(async (tx) => {
await tx.user.update({
where: { id: userId },
data: {
userType: 'AGENT',
agentLevel: 1,
parentId: null,
},
});
if (user.preferences) {
await tx.userPreference.update({
where: { userId },
data: { phone, email },
});
} else {
await tx.userPreference.create({
data: {
userId,
locale: user.locale,
phone,
email,
},
});
}
await tx.agentProfile.create({
data: {
userId,
level: 1,
parentAgentId: null,
creditLimit: data.creditLimit,
cashbackRate: data.cashbackRate ?? 0,
},
});
await tx.agentClosure.create({
data: { ancestorId: userId, descendantId: userId, depth: 0 },
});
});
if (oldParentId) {
await this.recalculateUsedCredit(oldParentId);
}
return this.prisma.user.findUnique({ where: { id: userId } });
}
async createAgent(
operatorId: bigint,
data: {
@@ -455,8 +562,30 @@ export class AgentsService {
initialDeposit?: number;
depositRemark?: string;
depositRequestId?: string;
asTier1Agent?: boolean;
creditLimit?: number;
cashbackRate?: number;
},
) {
if (data.asTier1Agent) {
if (data.parentId != null) {
throw new BadRequestException('一级代理不可设置上级玩家');
}
if (data.initialDeposit && data.initialDeposit > 0) {
throw new BadRequestException('设为代理时请使用授信额度,勿填玩家初始余额');
}
return this.createAgent(operatorId, {
username: data.username,
password: data.password,
level: 1,
creditLimit: data.creditLimit ?? 0,
cashbackRate: data.cashbackRate ?? 0,
locale: data.locale,
phone: data.phone,
email: data.email,
});
}
let parentId: bigint | null = null;
if (data.parentId != null) {
const parent = await this.prisma.user.findUnique({ where: { id: data.parentId } });