prisma
This commit is contained in:
parent
4dee8677df
commit
0fb6e53d7a
4 changed files with 10041 additions and 57 deletions
|
|
@ -4,4 +4,5 @@ alwaysApply: true
|
|||
App runs on a dedicated server with coolify and docker.
|
||||
Migrations are run via docker on the server.
|
||||
Keep code clean, never add todos and stubs outside tests.
|
||||
Use strict static types, never add 'any' type
|
||||
Keep the final review short.
|
||||
9886
backend/package-lock.json
generated
Normal file
9886
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -34,12 +34,12 @@
|
|||
"@prisma/adapter-pg": "^7.2.0",
|
||||
"@prisma/client": "^7.2.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
"pg": "^8.13.1",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.3",
|
||||
"nanoid": "^5.1.6",
|
||||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"pg": "^8.13.1",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"socket.io": "^4.8.3"
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { RoomsService } from '../rooms/rooms.service';
|
|||
import { RoomEventsService } from './room-events.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { RoomPackService } from '../room-pack/room-pack.service';
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
interface PlayerAction {
|
||||
action: 'revealAnswer' | 'nextQuestion' | 'prevQuestion';
|
||||
|
|
@ -23,6 +24,30 @@ interface PlayerAction {
|
|||
answerId?: string; // UUID ответа
|
||||
}
|
||||
|
||||
interface Question {
|
||||
id: string;
|
||||
text?: string;
|
||||
question?: string;
|
||||
answers: Array<{
|
||||
id: string;
|
||||
text: string;
|
||||
points: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
type RoomWithPack = Prisma.RoomGetPayload<{
|
||||
include: {
|
||||
roomPack: true;
|
||||
participants: true;
|
||||
}
|
||||
}> & {
|
||||
currentQuestionId?: string | null;
|
||||
};
|
||||
|
||||
interface RevealedAnswers {
|
||||
[questionId: string]: string[];
|
||||
}
|
||||
|
||||
@WebSocketGateway({
|
||||
cors: {
|
||||
origin: process.env.CORS_ORIGIN || 'http://localhost:5173',
|
||||
|
|
@ -85,24 +110,30 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
|
|||
await this.roomsService.updateRoomStatus(payload.roomId, 'PLAYING');
|
||||
|
||||
// Инициализировать первый вопрос и игрока
|
||||
const room = await this.prisma.room.findUnique({
|
||||
const room = (await this.prisma.room.findUnique({
|
||||
where: { id: payload.roomId },
|
||||
include: {
|
||||
roomPack: true,
|
||||
participants: { where: { isActive: true }, orderBy: { joinedAt: 'asc' } }
|
||||
}
|
||||
});
|
||||
} as Prisma.RoomInclude,
|
||||
})) as unknown as RoomWithPack | null;
|
||||
|
||||
if (room) {
|
||||
const questions = room.roomPack?.questions as any[] || [];
|
||||
const questions = ((room.roomPack as unknown as { questions?: Question[] } | null)?.questions || []) as Question[];
|
||||
const firstQuestion = questions[0];
|
||||
const firstParticipant = room.participants[0];
|
||||
|
||||
if (firstQuestion && firstParticipant) {
|
||||
// Убеждаемся что firstQuestion.id - строка (UUID)
|
||||
const firstQuestionId = firstQuestion?.id && typeof firstQuestion.id === 'string'
|
||||
? firstQuestion.id
|
||||
: null;
|
||||
|
||||
if (firstQuestionId && firstParticipant) {
|
||||
await this.prisma.room.update({
|
||||
where: { id: payload.roomId },
|
||||
data: {
|
||||
currentQuestionId: firstQuestion.id,
|
||||
currentQuestionId: firstQuestionId,
|
||||
currentQuestionIndex: 0,
|
||||
currentPlayerId: firstParticipant.id,
|
||||
}
|
||||
});
|
||||
|
|
@ -115,13 +146,13 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
|
|||
@SubscribeMessage('playerAction')
|
||||
async handlePlayerAction(client: Socket, payload: PlayerAction) {
|
||||
// Получаем комнату с данными
|
||||
const room = await this.prisma.room.findUnique({
|
||||
const room = (await this.prisma.room.findUnique({
|
||||
where: { id: payload.roomId },
|
||||
include: {
|
||||
roomPack: true,
|
||||
participants: { where: { isActive: true }, orderBy: { joinedAt: 'asc' } }
|
||||
},
|
||||
});
|
||||
} as unknown as Prisma.RoomInclude,
|
||||
})) as unknown as RoomWithPack | null;
|
||||
|
||||
if (!room) {
|
||||
client.emit('error', { message: 'Room not found' });
|
||||
|
|
@ -165,7 +196,7 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
|
|||
return;
|
||||
}
|
||||
|
||||
const questions = room.roomPack?.questions as any[] || [];
|
||||
const questions = ((room.roomPack as { questions?: Question[] } | null)?.questions || []) as Question[];
|
||||
const question = questions.find(q => q.id === payload.questionId);
|
||||
|
||||
if (!question) {
|
||||
|
|
@ -180,8 +211,8 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
|
|||
}
|
||||
|
||||
// Обновляем revealedAnswers
|
||||
const revealed = (room.revealedAnswers as any) || {};
|
||||
const currentRevealed: string[] = revealed[payload.questionId] || [];
|
||||
const revealed = (room.revealedAnswers as RevealedAnswers) || {};
|
||||
const currentRevealed: string[] = revealed[payload.questionId || ''] || [];
|
||||
|
||||
if (!currentRevealed.includes(payload.answerId)) {
|
||||
currentRevealed.push(payload.answerId);
|
||||
|
|
@ -196,12 +227,12 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
|
|||
// Сохраняем revealedAnswers
|
||||
await this.prisma.room.update({
|
||||
where: { id: payload.roomId },
|
||||
data: { revealedAnswers: revealed }
|
||||
data: { revealedAnswers: revealed as Prisma.InputJsonValue }
|
||||
});
|
||||
|
||||
// Определяем следующего игрока
|
||||
const participants = room.participants;
|
||||
const currentIdx = participants.findIndex((p: any) => p.id === payload.participantId);
|
||||
const currentIdx = participants.findIndex((p) => p.id === payload.participantId);
|
||||
const nextIdx = (currentIdx + 1) % participants.length;
|
||||
const nextPlayerId = participants[nextIdx]?.id;
|
||||
|
||||
|
|
@ -240,9 +271,9 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
|
|||
}
|
||||
}
|
||||
|
||||
private async handleNextQuestionAction(payload: PlayerAction, room: any) {
|
||||
const questions = room.roomPack?.questions as any[] || [];
|
||||
const currentIdx = questions.findIndex((q: any) => q.id === room.currentQuestionId);
|
||||
private async handleNextQuestionAction(payload: PlayerAction, room: RoomWithPack) {
|
||||
const questions = ((room.roomPack as { questions?: Question[] } | null)?.questions || []) as Question[];
|
||||
const currentIdx = questions.findIndex((q) => q.id === (room.currentQuestionId as string | null));
|
||||
|
||||
if (currentIdx < questions.length - 1) {
|
||||
const nextQuestion = questions[currentIdx + 1];
|
||||
|
|
@ -250,15 +281,15 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
|
|||
where: { id: payload.roomId },
|
||||
data: {
|
||||
currentQuestionId: nextQuestion.id,
|
||||
currentQuestionIndex: currentIdx + 1 // Для совместимости
|
||||
currentQuestionIndex: currentIdx + 1
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async handlePrevQuestionAction(payload: PlayerAction, room: any) {
|
||||
const questions = room.roomPack?.questions as any[] || [];
|
||||
const currentIdx = questions.findIndex((q: any) => q.id === room.currentQuestionId);
|
||||
private async handlePrevQuestionAction(payload: PlayerAction, room: RoomWithPack) {
|
||||
const questions = ((room.roomPack as { questions?: Question[] } | null)?.questions || []) as Question[];
|
||||
const currentIdx = questions.findIndex((q) => q.id === (room.currentQuestionId as string | null));
|
||||
|
||||
if (currentIdx > 0) {
|
||||
const prevQuestion = questions[currentIdx - 1];
|
||||
|
|
@ -266,7 +297,7 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
|
|||
where: { id: payload.roomId },
|
||||
data: {
|
||||
currentQuestionId: prevQuestion.id,
|
||||
currentQuestionIndex: currentIdx - 1 // Для совместимости
|
||||
currentQuestionIndex: currentIdx - 1
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -274,7 +305,7 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
|
|||
|
||||
// КЛЮЧЕВОЙ МЕТОД: Отправка полного состояния
|
||||
private async broadcastFullState(roomCode: string) {
|
||||
const room = await this.prisma.room.findUnique({
|
||||
const room = (await this.prisma.room.findUnique({
|
||||
where: { code: roomCode },
|
||||
include: {
|
||||
participants: {
|
||||
|
|
@ -283,22 +314,39 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
|
|||
},
|
||||
roomPack: true,
|
||||
host: { select: { id: true, name: true } }
|
||||
}
|
||||
});
|
||||
} as Prisma.RoomInclude,
|
||||
})) as unknown as RoomWithPack | null;
|
||||
|
||||
if (!room) return;
|
||||
|
||||
const questions = room.roomPack?.questions as any[] || [];
|
||||
const questions = ((room.roomPack as unknown as { questions?: Question[] } | null)?.questions || []) as Question[];
|
||||
|
||||
// Инициализация currentQuestionId если не установлен
|
||||
let currentQuestionId = room.currentQuestionId;
|
||||
// Инициализация currentQuestionId если не установлен или невалиден
|
||||
let currentQuestionId = (room.currentQuestionId as string | null) || null;
|
||||
|
||||
// Проверяем, что currentQuestionId валиден (существует в вопросах)
|
||||
if (currentQuestionId) {
|
||||
const questionExists = questions.some((q: any) => q.id === currentQuestionId);
|
||||
if (!questionExists) {
|
||||
currentQuestionId = null; // Сбрасываем если вопрос удален
|
||||
}
|
||||
}
|
||||
|
||||
// Устанавливаем первый вопрос если нет текущего
|
||||
if (!currentQuestionId && questions.length > 0) {
|
||||
currentQuestionId = questions[0].id;
|
||||
const firstQuestion = questions[0];
|
||||
// Убеждаемся что id - строка (UUID)
|
||||
if (firstQuestion.id && typeof firstQuestion.id === 'string') {
|
||||
currentQuestionId = firstQuestion.id;
|
||||
await this.prisma.room.update({
|
||||
where: { id: room.id },
|
||||
data: { currentQuestionId }
|
||||
data: {
|
||||
currentQuestionId: currentQuestionId,
|
||||
currentQuestionIndex: 0
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const fullState = {
|
||||
roomId: room.id,
|
||||
|
|
@ -306,20 +354,20 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
|
|||
status: room.status,
|
||||
currentQuestionId: currentQuestionId,
|
||||
currentPlayerId: room.currentPlayerId,
|
||||
revealedAnswers: room.revealedAnswers,
|
||||
revealedAnswers: room.revealedAnswers as RevealedAnswers,
|
||||
isGameOver: room.isGameOver,
|
||||
hostId: room.hostId,
|
||||
participants: room.participants.map(p => ({
|
||||
participants: room.participants.map((p) => ({
|
||||
id: p.id,
|
||||
userId: p.userId,
|
||||
name: p.name,
|
||||
role: p.role,
|
||||
score: p.score
|
||||
})),
|
||||
questions: questions.map((q: any) => ({
|
||||
questions: questions.map((q) => ({
|
||||
id: q.id,
|
||||
text: q.text || q.question,
|
||||
answers: (q.answers || []).map((a: any) => ({
|
||||
text: q.text || q.question || '',
|
||||
answers: (q.answers || []).map((a) => ({
|
||||
id: a.id,
|
||||
text: a.text,
|
||||
points: a.points
|
||||
|
|
@ -355,30 +403,37 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
|
|||
return;
|
||||
}
|
||||
|
||||
const room = await this.prisma.room.findUnique({
|
||||
const room = (await this.prisma.room.findUnique({
|
||||
where: { id: payload.roomId },
|
||||
include: {
|
||||
roomPack: true,
|
||||
participants: { where: { isActive: true }, orderBy: { joinedAt: 'asc' } }
|
||||
}
|
||||
});
|
||||
} as Prisma.RoomInclude,
|
||||
})) as unknown as RoomWithPack | null;
|
||||
|
||||
const questions = room?.roomPack?.questions as any[] || [];
|
||||
if (room) {
|
||||
const questions = ((room.roomPack as unknown as { questions?: Question[] } | null)?.questions || []) as Question[];
|
||||
const firstQuestion = questions[0];
|
||||
const firstParticipant = room?.participants[0];
|
||||
const firstParticipant = room.participants[0];
|
||||
|
||||
// Убеждаемся что firstQuestion.id - строка (UUID)
|
||||
const firstQuestionId = firstQuestion?.id && typeof firstQuestion.id === 'string'
|
||||
? firstQuestion.id
|
||||
: null;
|
||||
|
||||
await this.prisma.room.update({
|
||||
where: { id: payload.roomId },
|
||||
data: {
|
||||
status: 'WAITING',
|
||||
currentQuestionId: firstQuestionId,
|
||||
currentQuestionIndex: 0,
|
||||
currentQuestionId: firstQuestion?.id || null,
|
||||
revealedAnswers: {},
|
||||
revealedAnswers: {} as Prisma.InputJsonValue,
|
||||
currentPlayerId: firstParticipant?.id || null,
|
||||
isGameOver: false,
|
||||
answeredQuestions: 0,
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await this.prisma.participant.updateMany({
|
||||
where: { roomId: payload.roomId },
|
||||
|
|
@ -396,7 +451,49 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect, On
|
|||
return;
|
||||
}
|
||||
|
||||
// Обновляем вопросы через service (который добавит UUID если нужно)
|
||||
await this.roomsService.updateRoomPack(payload.roomId, payload.questions);
|
||||
|
||||
// После обновления вопросов проверяем и обновляем currentQuestionId
|
||||
const room = (await this.prisma.room.findUnique({
|
||||
where: { id: payload.roomId },
|
||||
include: { roomPack: true } as unknown as Prisma.RoomInclude,
|
||||
})) as unknown as RoomWithPack | null;
|
||||
|
||||
if (room) {
|
||||
const questions = ((room.roomPack as unknown as { questions?: Question[] } | null)?.questions || []) as Question[];
|
||||
const currentQuestionId = (room.currentQuestionId as string | null) || null;
|
||||
|
||||
// Проверяем, что currentQuestionId все еще валиден
|
||||
let validQuestionId = currentQuestionId;
|
||||
if (currentQuestionId) {
|
||||
const questionExists = questions.some((q: any) => q.id === currentQuestionId);
|
||||
if (!questionExists) {
|
||||
// Текущий вопрос был удален, устанавливаем первый
|
||||
validQuestionId = questions[0]?.id && typeof questions[0].id === 'string'
|
||||
? questions[0].id
|
||||
: null;
|
||||
}
|
||||
} else if (questions.length > 0) {
|
||||
// Если нет currentQuestionId, устанавливаем первый
|
||||
validQuestionId = questions[0].id && typeof questions[0].id === 'string'
|
||||
? questions[0].id
|
||||
: null;
|
||||
}
|
||||
|
||||
// Обновляем currentQuestionId если изменился
|
||||
if (validQuestionId !== currentQuestionId) {
|
||||
const questionIndex = questions.findIndex((q: any) => q.id === validQuestionId);
|
||||
await this.prisma.room.update({
|
||||
where: { id: payload.roomId },
|
||||
data: {
|
||||
currentQuestionId: validQuestionId,
|
||||
currentQuestionIndex: questionIndex >= 0 ? questionIndex : 0
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await this.broadcastFullState(payload.roomCode);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue