feat(player): 注册账号、登录双模式与移动端性能优化

注册必填 7-32 位账号,手机号区号/本地号分存;登录默认账号模式并支持切换手机号登录;Player i18n 拆包与赛事接口优化。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-12 10:56:51 +08:00
parent 83f0f380c5
commit 312c3c5816
35 changed files with 1944 additions and 1394 deletions

View File

@@ -9,7 +9,7 @@ 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';
import { normalizePhone, resolvePlayerLoginCandidates, stripLocalPhoneDigits } from './sms/phone.util';
const MAX_LOGIN_FAILS = 5;
const LOCK_DURATION_MS = 15 * 60 * 1000;
@@ -51,15 +51,51 @@ export class AuthService {
portal: 'player' | 'admin' | 'agent',
countryCode?: string,
) {
const lookupUsername =
portal === 'player'
? resolvePlayerLoginUsername(username, countryCode)
: username.trim();
let user;
if (portal === 'player') {
const trimmed = username.trim();
if (/[a-zA-Z]/.test(trimmed)) {
user = await this.prisma.user.findUnique({
where: { username: trimmed },
include: { auth: true, adminRole: { include: { role: true } } },
});
} else {
const candidates = resolvePlayerLoginCandidates(username, countryCode);
let matches = await this.prisma.user.findMany({
where: {
username: { in: candidates },
userType: 'PLAYER',
deletedAt: null,
},
include: { auth: true, adminRole: { include: { role: true } } },
});
const user = await this.prisma.user.findUnique({
where: { username: lookupUsername },
include: { auth: true, adminRole: { include: { role: true } } },
});
if (matches.length === 0 && countryCode?.trim()) {
const dial = countryCode.replace(/\D/g, '');
const local = stripLocalPhoneDigits(username);
matches = await this.prisma.user.findMany({
where: {
userType: 'PLAYER',
deletedAt: null,
preferences: {
phoneCountryDial: dial,
phoneLocal: local,
},
},
include: { auth: true, adminRole: { include: { role: true } } },
});
}
if (matches.length === 1) {
user = matches[0];
}
}
} else {
user = await this.prisma.user.findUnique({
where: { username: username.trim() },
include: { auth: true, adminRole: { include: { role: true } } },
});
}
if (!user || !user.auth) {
throw appUnauthorized('INVALID_CREDENTIALS');
@@ -156,6 +192,7 @@ export class AuthService {
}
async registerPlayer(data: {
username: string;
phone: string;
countryCode: string;
password: string;
@@ -164,14 +201,20 @@ export class AuthService {
inviteCode?: string;
locale?: string;
}) {
const phone = normalizePhone(data.countryCode, data.phone);
const username = phone;
const username = data.username.trim();
if (!username) {
throw appBadRequest('USERNAME_REQUIRED');
}
try {
assertPlayerUsername(username);
} catch {
throw appBadRequest('PHONE_INVALID');
throw appBadRequest('USERNAME_FORMAT_INVALID');
}
const dial = data.countryCode.replace(/\D/g, '');
const phoneLocal = stripLocalPhoneDigits(data.phone);
const phone = normalizePhone(dial, data.phone);
if (!data.password || data.password.length < 8) {
throw appBadRequest('PASSWORD_MIN_LENGTH');
}
@@ -189,11 +232,23 @@ export class AuthService {
const { parentId, sponsorId, inviteId } = await this.resolveInviteSponsor(data.inviteCode);
const inviteSponsorId = parentId == null && sponsorId != null ? sponsorId : null;
const existing = await this.prisma.user.findUnique({
const existingUser = await this.prisma.user.findUnique({
where: { username },
select: { id: true },
});
if (existing) {
if (existingUser) {
throw appBadRequest('USERNAME_TAKEN');
}
const existingPhone = await this.prisma.userPreference.findFirst({
where: {
phoneCountryDial: dial,
phoneLocal,
user: { deletedAt: null, userType: 'PLAYER' },
},
select: { userId: true },
});
if (existingPhone) {
throw appBadRequest('PHONE_TAKEN');
}
@@ -220,7 +275,13 @@ export class AuthService {
});
await tx.userPreference.create({
data: { userId: created.id, locale, phone },
data: {
userId: created.id,
locale,
phone,
phoneCountryDial: dial,
phoneLocal,
},
});
if (inviteId) {