feat(database-schema): добавляет ссылку на активный чат в схему профиля пользователя

 feat(likes.service): обновляет логику создания лайков с учетом активного чата

 feat(likes.module): импортирует модуль Gateways для работы с чатами

 feat(feed.service): добавляет условие для фильтрации профилей без активного чата

 feat(storage.service): устанавливает политику доступа к бакету S3 для получения объектов

 feat(likes-response.dto): добавляет поле чата в ответ на лайк

 feat(media.controller): добавляет описание для загрузки медиафайлов

 feat(chat.service): добавляет возможность закрытия чата с отчетом или сообщением

 feat(chat.controller): обновляет метод закрытия чата для обработки отчетов

 feat(app.module): добавляет модуль Dev для разработки в не продакшн среде
This commit is contained in:
Oscar
2026-06-09 15:39:38 +03:00
parent 97f8891861
commit cd98f04987
14 changed files with 251 additions and 36 deletions

View File

@@ -2,9 +2,10 @@ import { BadRequestException, ForbiddenException, Injectable, NotFoundException
import { and, eq, or } from 'drizzle-orm';
import { ConfigService } from '@nestjs/config';
import { DrizzleService } from '../../database/drizzle.service';
import { like, match, profile, user } from '../../database/schema';
import { chat, like, match, profile, user } from '../../database/schema';
import { NotificationsService } from '../../notifications/notifications.service';
import { RedisService } from '../../redis/redis.service';
import { ChatGateway } from '../../gateways/chat.gateway';
import { CreateLikeDto } from './dto/create-like.dto';
@Injectable()
@@ -14,6 +15,7 @@ export class LikesService {
private readonly notificationsService: NotificationsService,
private readonly redisService: RedisService,
private readonly configService: ConfigService,
private readonly chatGateway: ChatGateway,
) {}
async createLike(userId: string, dto: CreateLikeDto) {
@@ -23,11 +25,15 @@ export class LikesService {
throw new BadRequestException('Cannot like yourself');
}
const maxMatches = this.configService.get<number>('app.maxMatchesBeforePause');
const activeMatchesCount = await this.getMatchesCount(dto.sourceProfileId);
if (activeMatchesCount >= maxMatches) {
const [sourceProfile] = await this.drizzleService.db
.select({ activeChatId: profile.activeChatId })
.from(profile)
.where(eq(profile.id, dto.sourceProfileId))
.limit(1);
if (sourceProfile?.activeChatId) {
throw new BadRequestException(
`Profile has ${activeMatchesCount} matches. Resolve them before searching for new ones.`,
'У вас уже есть активный матч. Завершите текущий диалог перед поиском.',
);
}
@@ -57,7 +63,7 @@ export class LikesService {
return this.checkAndCreateMatch(dto.sourceProfileId, dto.targetProfileId, newLike);
}
return { like: newLike, match: null };
return { like: newLike, match: null, chat: null };
}
async getMyMatches(userId: string, profileId: string) {
@@ -82,7 +88,17 @@ export class LikesService {
)
.limit(1);
if (reverseLike.length === 0) return { like: newLike, match: null };
if (reverseLike.length === 0) return { like: newLike, match: null, chat: null };
const [targetProfile] = await this.drizzleService.db
.select({ activeChatId: profile.activeChatId })
.from(profile)
.where(eq(profile.id, profileId2))
.limit(1);
if (targetProfile?.activeChatId) {
return { like: newLike, match: null, chat: null };
}
const existingMatch = await this.drizzleService.db
.select()
@@ -95,27 +111,56 @@ export class LikesService {
)
.limit(1);
if (existingMatch.length > 0) return { like: newLike, match: existingMatch[0] };
if (existingMatch.length > 0) {
const existingChat = await this.drizzleService.db
.select()
.from(chat)
.where(
or(
and(eq(chat.profile1Id, profileId1), eq(chat.profile2Id, profileId2)),
and(eq(chat.profile1Id, profileId2), eq(chat.profile2Id, profileId1)),
),
)
.limit(1);
return { like: newLike, match: existingMatch[0], chat: existingChat[0] ?? null };
}
const [newMatch] = await this.drizzleService.db
.insert(match)
.values({ profile1Id: profileId1, profile2Id: profileId2 })
.returning();
await this.notifyMatch(profileId1, profileId2, newMatch.id);
const [newChat] = await this.drizzleService.db
.insert(chat)
.values({
profile1Id: profileId1,
profile2Id: profileId2,
status: 'active',
} as any)
.returning();
return { like: newLike, match: newMatch };
await this.drizzleService.db
.update(profile)
.set({ activeChatId: newChat.id } as any)
.where(or(eq(profile.id, profileId1), eq(profile.id, profileId2)));
await this.notifyMatch(profileId1, profileId2, newMatch.id, newChat.id);
return { like: newLike, match: newMatch, chat: newChat };
}
private async getMatchesCount(profileId: string): Promise<number> {
const matches = await this.drizzleService.db
.select({ id: match.id })
.from(match)
.where(or(eq(match.profile1Id, profileId), eq(match.profile2Id, profileId)));
return matches.length;
}
private async notifyMatch(profileId1: string, profileId2: string, matchId: string, chatId: string) {
this.chatGateway.emitToProfile(profileId1, 'match_created', {
matchId,
chatId,
partnerProfileId: profileId2,
});
this.chatGateway.emitToProfile(profileId2, 'match_created', {
matchId,
chatId,
partnerProfileId: profileId1,
});
private async notifyMatch(profileId1: string, profileId2: string, matchId: string) {
const profiles = await this.drizzleService.db
.select({ userId: profile.userId })
.from(profile)
@@ -131,16 +176,16 @@ export class LikesService {
if (u?.fcmToken) {
await this.notificationsService.sendPushNotification(
u.fcmToken,
'New Match!',
'You have a new match! Start chatting now.',
{ matchId, type: 'match' },
'Новый матч!',
'У вас новый матч. Начните общение прямо сейчас.',
{ matchId, chatId, type: 'match_created' },
);
}
}
await this.redisService.publish(
'match:created',
JSON.stringify({ matchId, profileId1, profileId2 }),
JSON.stringify({ matchId, chatId, profileId1, profileId2 }),
);
}