feat: 手动充值、邀请码注册与后台管理增强
新增玩家手动充值全流程(收款方式配置、充值下单/审核、钱包上分), 支持邀请码注册、邀请历史与专属返水率;完善后台代理/玩家管理与响应式操作栏, 并补充前台注册、充值页及多语言错误码。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { appForbidden, appUnauthorized } from '../../shared/common/app-error';
|
||||
import { Decimal } from '@prisma/client/runtime/library';
|
||||
import { appForbidden, appUnauthorized, appBadRequest, appNotFound } from '../../shared/common/app-error';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import { assertPlayerUsername } from '@thebet365/shared';
|
||||
import { PrismaService } from '../../shared/prisma/prisma.service';
|
||||
import { SystemConfigService } from '../../shared/config/system-config.service';
|
||||
import { InvitesService } from './invites.service';
|
||||
|
||||
const MAX_LOGIN_FAILS = 5;
|
||||
const LOCK_DURATION_MS = 15 * 60 * 1000;
|
||||
@@ -23,6 +26,7 @@ export class AuthService {
|
||||
private jwt: JwtService,
|
||||
private config: ConfigService,
|
||||
private systemConfig: SystemConfigService,
|
||||
private invites: InvitesService,
|
||||
) {}
|
||||
|
||||
/** 平台管理员 / 代理统一登录(按 userType 签发对应 JWT) */
|
||||
@@ -129,6 +133,125 @@ export class AuthService {
|
||||
};
|
||||
}
|
||||
|
||||
async resolveInviteSponsor(inviteCodeRaw?: string | null) {
|
||||
const resolved = await this.invites.resolveActiveInvite(inviteCodeRaw);
|
||||
return {
|
||||
sponsorId: resolved.sponsorId,
|
||||
parentId: resolved.parentId,
|
||||
inviteId: resolved.inviteId,
|
||||
};
|
||||
}
|
||||
|
||||
async registerPlayer(data: {
|
||||
username: string;
|
||||
password: string;
|
||||
inviteCode?: string;
|
||||
locale?: string;
|
||||
}) {
|
||||
const username = data.username.trim();
|
||||
if (!username) {
|
||||
throw appBadRequest('USERNAME_REQUIRED');
|
||||
}
|
||||
try {
|
||||
assertPlayerUsername(username);
|
||||
} catch {
|
||||
throw appBadRequest('USERNAME_FORMAT_INVALID');
|
||||
}
|
||||
if (!data.password || data.password.length < 8) {
|
||||
throw appBadRequest('PASSWORD_MIN_LENGTH');
|
||||
}
|
||||
|
||||
const { parentId, sponsorId, inviteId } = await this.resolveInviteSponsor(data.inviteCode);
|
||||
const inviteSponsorId = parentId == null && sponsorId != null ? sponsorId : null;
|
||||
|
||||
const existing = await this.prisma.user.findUnique({
|
||||
where: { username },
|
||||
select: { id: true },
|
||||
});
|
||||
if (existing) {
|
||||
throw appBadRequest('USERNAME_TAKEN');
|
||||
}
|
||||
|
||||
const hash = await this.hashPassword(data.password);
|
||||
const locale = data.locale?.trim() || 'zh-CN';
|
||||
|
||||
const user = await this.prisma.$transaction(async (tx) => {
|
||||
const created = await tx.user.create({
|
||||
data: {
|
||||
username,
|
||||
userType: 'PLAYER',
|
||||
parentId,
|
||||
inviteSponsorId,
|
||||
locale,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.userAuth.create({
|
||||
data: { userId: created.id, passwordHash: hash },
|
||||
});
|
||||
|
||||
await tx.wallet.create({
|
||||
data: { userId: created.id },
|
||||
});
|
||||
|
||||
await tx.userPreference.create({
|
||||
data: { userId: created.id, locale },
|
||||
});
|
||||
|
||||
if (inviteId) {
|
||||
await this.invites.recordRegistration(inviteId, created.id, tx);
|
||||
const invite = await tx.userInvite.findUnique({
|
||||
where: { id: inviteId },
|
||||
select: { cashbackRate: true },
|
||||
});
|
||||
if (invite?.cashbackRate != null && new Decimal(invite.cashbackRate).gt(0)) {
|
||||
await tx.cashbackRule.updateMany({
|
||||
where: { targetType: 'USER', targetId: created.id },
|
||||
data: { isActive: false },
|
||||
});
|
||||
await tx.cashbackRule.create({
|
||||
data: {
|
||||
name: `Player ${created.id.toString()}`,
|
||||
targetType: 'USER',
|
||||
targetId: created.id,
|
||||
rate: invite.cashbackRate,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return created;
|
||||
});
|
||||
|
||||
return this.login(username, data.password, 'player');
|
||||
}
|
||||
|
||||
async getInviteInfo(userId: bigint) {
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { inviteCode: true, userType: true, deletedAt: true },
|
||||
});
|
||||
if (!user || user.deletedAt) {
|
||||
throw appNotFound('USER_NOT_FOUND');
|
||||
}
|
||||
if (user.userType !== 'ADMIN' && user.userType !== 'AGENT') {
|
||||
throw appForbidden('ACCESS_DENIED_PORTAL');
|
||||
}
|
||||
return { inviteCode: user.inviteCode ?? null };
|
||||
}
|
||||
|
||||
async generateInviteCode(
|
||||
userId: bigint,
|
||||
userType: string,
|
||||
cashbackRate?: number,
|
||||
) {
|
||||
return this.invites.generateInviteCode(userId, {
|
||||
userType,
|
||||
cashbackRate: cashbackRate ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
async changePassword(userId: bigint, oldPassword: string, newPassword: string) {
|
||||
const auth = await this.prisma.userAuth.findUnique({ where: { userId } });
|
||||
if (!auth) throw appUnauthorized('USER_NOT_FOUND');
|
||||
|
||||
Reference in New Issue
Block a user