feat: internationalize API error responses by locale
Add shared error codes with zh/en/ms messages, coded app exceptions, and locale-aware global filter. Frontends send X-Locale so error text matches the active UI language. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Injectable, BadRequestException } from '@nestjs/common';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import { SUPPORTED_LOCALES, isValidAvatarKey, assertPlayerUsername } from '@thebet365/shared';
|
||||
import { PrismaService } from '../../shared/prisma/prisma.service';
|
||||
import { SystemConfigService } from '../../shared/config/system-config.service';
|
||||
import { AgentsService } from '../agent/agents.service';
|
||||
import { appBadRequest, appForbidden, appNotFound } from '../../shared/common/app-error';
|
||||
|
||||
export type PlayerListFilters = {
|
||||
keyword?: string;
|
||||
@@ -118,23 +119,23 @@ export class UsersService {
|
||||
where: { id: userId },
|
||||
include: { preferences: true },
|
||||
});
|
||||
if (!user) throw new NotFoundException('User not found');
|
||||
if (!user) throw appNotFound('USER_NOT_FOUND');
|
||||
|
||||
if (data.username !== undefined) {
|
||||
const nextUsername = data.username.trim();
|
||||
if (!nextUsername) throw new BadRequestException('账号名称不能为空');
|
||||
if (!nextUsername) throw appBadRequest('USERNAME_REQUIRED');
|
||||
try {
|
||||
assertPlayerUsername(nextUsername);
|
||||
} catch (e) {
|
||||
throw new BadRequestException(e instanceof Error ? e.message : '玩家用户名格式无效');
|
||||
} catch {
|
||||
throw appBadRequest('USERNAME_FORMAT_INVALID');
|
||||
}
|
||||
const settings = await this.systemConfig.getPlayerAccountSettings();
|
||||
if (!settings.allowUsernameChange) {
|
||||
throw new ForbiddenException('当前平台未开放玩家自行修改账号名称');
|
||||
throw appForbidden('USERNAME_CHANGE_DISABLED');
|
||||
}
|
||||
if (nextUsername !== user.username) {
|
||||
const taken = await this.prisma.user.findUnique({ where: { username: nextUsername } });
|
||||
if (taken) throw new BadRequestException('账号名称已被占用');
|
||||
if (taken) throw appBadRequest('USERNAME_TAKEN');
|
||||
await this.prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: { username: nextUsername },
|
||||
@@ -148,7 +149,7 @@ export class UsersService {
|
||||
if (data.avatarKey !== undefined) {
|
||||
avatarKey = data.avatarKey?.trim() || null;
|
||||
if (avatarKey && !isValidAvatarKey(avatarKey)) {
|
||||
throw new BadRequestException('无效头像');
|
||||
throw appBadRequest('INVALID_AVATAR');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,7 +177,7 @@ export class UsersService {
|
||||
|
||||
async updateLocale(userId: bigint, locale: string) {
|
||||
if (!(SUPPORTED_LOCALES as readonly string[]).includes(locale)) {
|
||||
throw new BadRequestException('Unsupported locale');
|
||||
throw appBadRequest('UNSUPPORTED_LOCALE');
|
||||
}
|
||||
await this.prisma.user.update({
|
||||
where: { id: userId },
|
||||
@@ -266,7 +267,7 @@ export class UsersService {
|
||||
auth: { select: { lastLoginAt: true, loginFailCount: true, lockedUntil: true } },
|
||||
},
|
||||
});
|
||||
if (!user) throw new NotFoundException('玩家不存在');
|
||||
if (!user) throw appNotFound('PLAYER_NOT_FOUND');
|
||||
|
||||
const [betCount, betStake] = await Promise.all([
|
||||
this.prisma.bet.count({ where: { userId: playerId } }),
|
||||
@@ -302,23 +303,23 @@ export class UsersService {
|
||||
where: { id: playerId, userType: 'PLAYER', deletedAt: null },
|
||||
include: { auth: true },
|
||||
});
|
||||
if (!user) throw new NotFoundException('玩家不存在');
|
||||
if (!user) throw appNotFound('PLAYER_NOT_FOUND');
|
||||
|
||||
if (data.status && !['ACTIVE', 'SUSPENDED'].includes(data.status)) {
|
||||
throw new BadRequestException('无效状态');
|
||||
throw appBadRequest('INVALID_STATUS');
|
||||
}
|
||||
|
||||
if (data.username !== undefined) {
|
||||
const nextUsername = data.username.trim();
|
||||
if (!nextUsername) throw new BadRequestException('账号名称不能为空');
|
||||
if (!nextUsername) throw appBadRequest('USERNAME_REQUIRED');
|
||||
try {
|
||||
assertPlayerUsername(nextUsername);
|
||||
} catch (e) {
|
||||
throw new BadRequestException(e instanceof Error ? e.message : '玩家用户名格式无效');
|
||||
} catch {
|
||||
throw appBadRequest('USERNAME_FORMAT_INVALID');
|
||||
}
|
||||
if (nextUsername !== user.username) {
|
||||
const taken = await this.prisma.user.findUnique({ where: { username: nextUsername } });
|
||||
if (taken) throw new BadRequestException('账号名称已被占用');
|
||||
if (taken) throw appBadRequest('USERNAME_TAKEN');
|
||||
await this.prisma.user.update({
|
||||
where: { id: playerId },
|
||||
data: { username: nextUsername },
|
||||
@@ -328,8 +329,8 @@ export class UsersService {
|
||||
|
||||
if (data.password !== undefined) {
|
||||
const nextPassword = data.password;
|
||||
if (nextPassword.length < 8) throw new BadRequestException('密码至少 8 位');
|
||||
if (!user.auth) throw new BadRequestException('账号认证信息缺失');
|
||||
if (nextPassword.length < 8) throw appBadRequest('PASSWORD_MIN_LENGTH');
|
||||
if (!user.auth) throw appBadRequest('AUTH_INFO_MISSING');
|
||||
const hash = await bcrypt.hash(nextPassword, 10);
|
||||
await this.prisma.userAuth.update({
|
||||
where: { userId: playerId },
|
||||
|
||||
Reference in New Issue
Block a user