feat(player): 接入创蓝短信手机注册与登录页优化

新增 SMS 验证码注册、8 国手机号选择与 Redis 频控;优化登录/注册 UI 及图形验证码样式。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-12 10:25:59 +08:00
parent 168aecfd5c
commit db28390be9
39 changed files with 1521 additions and 107 deletions

View File

@@ -8,6 +8,8 @@ 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';
import { SmsService } from './sms/sms.service';
import { normalizePhone, resolvePlayerLoginUsername } from './sms/phone.util';
const MAX_LOGIN_FAILS = 5;
const LOCK_DURATION_MS = 15 * 60 * 1000;
@@ -27,6 +29,7 @@ export class AuthService {
private config: ConfigService,
private systemConfig: SystemConfigService,
private invites: InvitesService,
private sms: SmsService,
) {}
/** 平台管理员 / 代理统一登录(按 userType 签发对应 JWT */
@@ -42,9 +45,19 @@ export class AuthService {
return this.login(username, password, portal);
}
async login(username: string, password: string, portal: 'player' | 'admin' | 'agent') {
async login(
username: string,
password: string,
portal: 'player' | 'admin' | 'agent',
countryCode?: string,
) {
const lookupUsername =
portal === 'player'
? resolvePlayerLoginUsername(username, countryCode)
: username.trim();
const user = await this.prisma.user.findUnique({
where: { username },
where: { username: lookupUsername },
include: { auth: true, adminRole: { include: { role: true } } },
});
@@ -143,23 +156,35 @@ export class AuthService {
}
async registerPlayer(data: {
username: string;
phone: string;
countryCode: string;
password: string;
smsCode: string;
sessionId: string;
inviteCode?: string;
locale?: string;
}) {
const username = data.username.trim();
if (!username) {
throw appBadRequest('USERNAME_REQUIRED');
}
const phone = normalizePhone(data.countryCode, data.phone);
const username = phone;
try {
assertPlayerUsername(username);
} catch {
throw appBadRequest('USERNAME_FORMAT_INVALID');
throw appBadRequest('PHONE_INVALID');
}
if (!data.password || data.password.length < 8) {
throw appBadRequest('PASSWORD_MIN_LENGTH');
}
if (!data.smsCode?.trim() || !data.sessionId?.trim()) {
throw appBadRequest('SMS_CODE_REQUIRED');
}
await this.sms.verifyCode({
phone: data.phone,
countryCode: data.countryCode,
code: data.smsCode.trim(),
sessionId: data.sessionId.trim(),
});
const { parentId, sponsorId, inviteId } = await this.resolveInviteSponsor(data.inviteCode);
const inviteSponsorId = parentId == null && sponsorId != null ? sponsorId : null;
@@ -169,7 +194,7 @@ export class AuthService {
select: { id: true },
});
if (existing) {
throw appBadRequest('USERNAME_TAKEN');
throw appBadRequest('PHONE_TAKEN');
}
const hash = await this.hashPassword(data.password);
@@ -195,7 +220,7 @@ export class AuthService {
});
await tx.userPreference.create({
data: { userId: created.id, locale },
data: { userId: created.id, locale, phone },
});
if (inviteId) {