-
-
-
- {{ t('user.hint.no_agent') }}
+
+
+ {{ t('user.type.player') }}
+ {{ t('user.type.tier1_agent') }}
+
+ {{ t('user.hint.account_type') }}
+
+
+
+
+
+ {{ t('user.hint.no_agent') }}
+
+
+
+
+
+ {{ t('agent.hint.credit_limit') }}
+
+
+
+ {{ t('agent.hint.cashback_example') }}
+
+
-
-
- {{ t('user.hint.initial_balance') }}
-
-
-
-
+
+
+
+ {{ t('user.hint.initial_balance') }}
+
+
+
+
+
{{ t('common.cancel') }}
diff --git a/apps/admin/src/views/agent-form.ts b/apps/admin/src/views/agent-form.ts
index d7540ca..3934600 100644
--- a/apps/admin/src/views/agent-form.ts
+++ b/apps/admin/src/views/agent-form.ts
@@ -1,9 +1,17 @@
import { FormValidationError } from '../i18n/form-validation';
-export interface AgentCreateForm {
+export interface PromotableUserOption {
+ id: string;
username: string;
- password: string;
- confirmPassword: string;
+ status: string;
+ parentId: string | null;
+ parentUsername: string | null;
+ phone: string | null;
+ email: string | null;
+}
+
+export interface AgentCreateForm {
+ userId: string;
creditLimit: number;
cashbackRate: number;
phone: string;
@@ -54,9 +62,7 @@ export interface AgentDetail extends AgentRow {
export function emptyAgentCreateForm(): AgentCreateForm {
return {
- username: '',
- password: 'Agent@123',
- confirmPassword: 'Agent@123',
+ userId: '',
creditLimit: 50000,
cashbackRate: 0,
phone: '',
@@ -82,14 +88,20 @@ export function editFormFromAgentDetail(d: AgentDetail): AgentEditForm {
};
}
+export function applyPromotableUserToForm(
+ form: AgentCreateForm,
+ user: PromotableUserOption,
+): void {
+ form.userId = user.id;
+ form.phone = user.phone ?? '';
+ form.email = user.email ?? '';
+}
+
export function buildCreateAgentPayload(form: AgentCreateForm) {
- if (!form.username.trim()) throw new FormValidationError('err.username_required');
- if (form.password.length < 8) throw new FormValidationError('err.password_min');
- if (form.password !== form.confirmPassword) throw new FormValidationError('err.password_mismatch');
+ if (!form.userId) throw new FormValidationError('err.user_required');
if (form.creditLimit < 0) throw new FormValidationError('err.credit_negative');
return {
- username: form.username.trim(),
- password: form.password,
+ userId: form.userId,
creditLimit: form.creditLimit,
cashbackRate: form.cashbackRate,
phone: form.phone.trim() || undefined,
diff --git a/apps/admin/src/views/user-form.ts b/apps/admin/src/views/user-form.ts
index 1535ccf..9eb3e56 100644
--- a/apps/admin/src/views/user-form.ts
+++ b/apps/admin/src/views/user-form.ts
@@ -9,6 +9,10 @@ export interface PlayerCreateForm {
email: string;
initialDeposit: number;
remark: string;
+ /** 创建为一级代理(非玩家) */
+ asTier1Agent: boolean;
+ creditLimit: number;
+ cashbackRate: number;
}
export interface PlayerEditForm {
@@ -63,6 +67,9 @@ export function emptyPlayerCreateForm(): PlayerCreateForm {
email: '',
initialDeposit: 0,
remark: '',
+ asTier1Agent: false,
+ creditLimit: 50000,
+ cashbackRate: 0,
};
}
@@ -110,6 +117,20 @@ export function buildCreatePlayerPayload(form: PlayerCreateForm) {
if (!form.username.trim()) throw new FormValidationError('err.username_required');
if (form.password.length < 8) throw new FormValidationError('err.password_min');
if (form.password !== form.confirmPassword) throw new FormValidationError('err.password_mismatch');
+ if (form.asTier1Agent) {
+ if (form.parentId) throw new FormValidationError('err.agent_no_parent');
+ if (form.initialDeposit > 0) throw new FormValidationError('err.agent_no_initial_deposit');
+ if (form.creditLimit < 0) throw new FormValidationError('err.credit_negative');
+ return {
+ username: form.username.trim(),
+ password: form.password,
+ phone: form.phone.trim() || undefined,
+ email: form.email.trim() || undefined,
+ asTier1Agent: true,
+ creditLimit: form.creditLimit,
+ cashbackRate: form.cashbackRate,
+ };
+ }
return {
username: form.username.trim(),
password: form.password,
diff --git a/apps/admin/vite.config.ts b/apps/admin/vite.config.ts
index 655a82e..b5f0e4a 100644
--- a/apps/admin/vite.config.ts
+++ b/apps/admin/vite.config.ts
@@ -4,6 +4,10 @@ import { resolve } from 'path';
export default defineConfig({
plugins: [vue()],
+ resolve: {
+ // 避免 src 内遗留的 .js 抢先于 .ts/.vue 被解析(曾导致 i18n 文案缺失)
+ extensions: ['.mjs', '.ts', '.mts', '.tsx', '.vue', '.js', '.jsx', '.json'],
+ },
publicDir: resolve(__dirname, '../../packages/shared/public'),
server: {
port: 5174,
diff --git a/apps/api/src/applications/admin/admin.controller.ts b/apps/api/src/applications/admin/admin.controller.ts
index d21275a..f9f8757 100644
--- a/apps/api/src/applications/admin/admin.controller.ts
+++ b/apps/api/src/applications/admin/admin.controller.ts
@@ -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,
diff --git a/apps/api/src/domains/agent/agents.service.ts b/apps/api/src/domains/agent/agents.service.ts
index 94ee917..287e947 100644
--- a/apps/api/src/domains/agent/agents.service.ts
+++ b/apps/api/src/domains/agent/agents.service.ts
@@ -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 } });
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2fa0b86..13b98da 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -25,6 +25,9 @@ importers:
vue-echarts:
specifier: ^8.0.1
version: 8.0.1(echarts@6.1.0)(vue@3.5.35(typescript@5.7.3))
+ vue-i18n:
+ specifier: ^11.1.1
+ version: 11.4.4(vue@3.5.35(typescript@5.7.3))
vue-router:
specifier: ^4.5.0
version: 4.6.4(vue@3.5.35(typescript@5.7.3))