import { v4 as uuidv4 } from 'uuid'; import { query, transaction } from '../database/connection'; import { Swipe, SwipeData, SwipeType } from '../models/Swipe'; import { Match, MatchData } from '../models/Match'; import { Profile } from '../models/Profile'; import { ProfileService } from './profileService'; import { NotificationService } from './notificationService'; import { BotError } from '../types'; export class MatchingService { private profileService: ProfileService; private notificationService: NotificationService; constructor() { this.profileService = new ProfileService(); this.notificationService = new NotificationService(); } // Выполнить свайп // Конвертация типов свайпов между API и БД private convertSwipeTypeToDirection(swipeType: SwipeType): string { switch (swipeType) { case 'like': return 'right'; case 'pass': return 'left'; case 'superlike': return 'super'; default: return 'left'; } } private convertDirectionToSwipeType(direction: string): SwipeType { switch (direction) { case 'right': return 'like'; case 'left': return 'pass'; case 'super': return 'superlike'; default: return 'pass'; } } async performSwipe(telegramId: string, targetTelegramId: string, swipeType: SwipeType): Promise<{ swipe: Swipe; isMatch: boolean; match?: Match; }> { // Получить профили пользователей const userProfile = await this.profileService.getProfileByTelegramId(telegramId); const targetProfile = await this.profileService.getProfileByUserId(targetTelegramId); if (!userProfile || !targetProfile) { throw new BotError('Profile not found', 'PROFILE_NOT_FOUND', 400); } const userId = userProfile.userId; const targetUserId = targetProfile.userId; // Проверяем, что пользователь не свайпает сам себя if (userId === targetUserId) { throw new BotError('Cannot swipe yourself', 'INVALID_SWIPE'); } // Проверяем, что свайп еще не был сделан const existingSwipe = await this.getSwipe(userId, targetUserId); if (existingSwipe) { throw new BotError('Already swiped this profile', 'ALREADY_SWIPED'); } const swipeId = uuidv4(); const direction = this.convertSwipeTypeToDirection(swipeType); let isMatch = false; let match: Match | undefined; await transaction(async (client) => { // Создаем свайп await client.query(` INSERT INTO swipes (id, swiper_id, swiped_id, direction, created_at) VALUES ($1, $2, $3, $4, $5) `, [swipeId, userId, targetUserId, direction, new Date()]); // Если это лайк или суперлайк, проверяем взаимность if (swipeType === 'like' || swipeType === 'superlike') { const reciprocalSwipe = await client.query(` SELECT * FROM swipes WHERE swiper_id = $1 AND swiped_id = $2 AND direction IN ('like', 'super') `, [targetUserId, userId]); if (reciprocalSwipe.rows.length > 0) { isMatch = true; const matchId = uuidv4(); const isSuperMatch = swipeType === 'superlike' || reciprocalSwipe.rows[0].direction === 'super'; // Создаем матч await client.query(` INSERT INTO matches (id, user1_id, user2_id, matched_at, status) VALUES ($1, $2, $3, $4, $5) `, [matchId, userId, targetUserId, new Date(), 'active']); match = new Match({ id: matchId, userId1: userId, userId2: targetUserId, createdAt: new Date(), isActive: true, isSuperMatch: false, unreadCount1: 0, unreadCount2: 0 }); } } }); const swipe = new Swipe({ id: swipeId, userId, targetUserId, type: swipeType, timestamp: new Date(), isMatch }); // Отправляем уведомления if (swipeType === 'like' || swipeType === 'superlike') { this.notificationService.sendLikeNotification(targetTelegramId, telegramId, swipeType === 'superlike'); } if (isMatch && match) { this.notificationService.sendMatchNotification(userId, targetUserId); this.notificationService.sendMatchNotification(targetUserId, userId); } return { swipe, isMatch, match }; } // Получить свайп между двумя пользователями async getSwipe(userId: string, targetUserId: string): Promise { const result = await query(` SELECT * FROM swipes WHERE swiper_id = $1 AND swiped_id = $2 `, [userId, targetUserId]); if (result.rows.length === 0) { return null; } return this.mapEntityToSwipe(result.rows[0]); } // Получить все матчи пользователя по telegram ID async getUserMatches(telegramId: string, limit: number = 50): Promise { // Сначала получаем userId по telegramId const userId = await this.profileService.getUserIdByTelegramId(telegramId); if (!userId) { return []; } const result = await query(` SELECT * FROM matches WHERE (user1_id = $1 OR user2_id = $1) AND status = 'active' ORDER BY matched_at DESC LIMIT $2 `, [userId, limit]); return result.rows.map((row: any) => this.mapEntityToMatch(row)); } // Получить матч по ID async getMatchById(matchId: string): Promise { const result = await query(` SELECT * FROM matches WHERE id = $1 `, [matchId]); if (result.rows.length === 0) { return null; } return this.mapEntityToMatch(result.rows[0]); } // Получить матч между двумя пользователями async getMatchBetweenUsers(userId1: string, userId2: string): Promise { const result = await query(` SELECT * FROM matches WHERE ((user_id_1 = $1 AND user_id_2 = $2) OR (user_id_1 = $2 AND user_id_2 = $1)) AND is_active = true `, [userId1, userId2]); if (result.rows.length === 0) { return null; } return this.mapEntityToMatch(result.rows[0]); } // Размэтчить (деактивировать матч) async unmatch(userId: string, matchId: string): Promise { const match = await this.getMatchById(matchId); if (!match || !match.includesUser(userId)) { throw new BotError('Match not found or access denied', 'MATCH_NOT_FOUND'); } await query(` UPDATE matches SET is_active = false WHERE id = $1 `, [matchId]); return true; } // Получить недавние лайки async getRecentLikes(userId: string, limit: number = 20): Promise { const result = await query(` SELECT * FROM swipes WHERE swiped_id = $1 AND direction IN ('like', 'super') AND is_match = false ORDER BY created_at DESC LIMIT $2 `, [userId, limit]); return result.rows.map((row: any) => this.mapEntityToSwipe(row)); } // Получить статистику свайпов пользователя за день async getDailySwipeStats(userId: string): Promise<{ likes: number; superlikes: number; passes: number; total: number; }> { const today = new Date(); today.setHours(0, 0, 0, 0); const result = await query(` SELECT direction, COUNT(*) as count FROM swipes WHERE swiper_id = $1 AND created_at >= $2 GROUP BY direction `, [userId, today]); const stats = { likes: 0, superlikes: 0, passes: 0, total: 0 }; result.rows.forEach((row: any) => { const count = parseInt(row.count); stats.total += count; switch (row.direction) { case 'like': stats.likes = count; break; case 'super': stats.superlikes = count; break; case 'pass': stats.passes = count; break; } }); return stats; } // Проверить лимиты свайпов async checkSwipeLimits(userId: string): Promise<{ canLike: boolean; canSuperLike: boolean; likesLeft: number; superLikesLeft: number; }> { const stats = await this.getDailySwipeStats(userId); const likesPerDay = 100; // Из конфига const superLikesPerDay = 1; // Из конфига return { canLike: stats.likes < likesPerDay, canSuperLike: stats.superlikes < superLikesPerDay, likesLeft: Math.max(0, likesPerDay - stats.likes), superLikesLeft: Math.max(0, superLikesPerDay - stats.superlikes) }; } // Получить рекомендации для пользователя async getRecommendations(userId: string, limit: number = 10): Promise { return this.profileService.findCompatibleProfiles(userId, limit) .then(profiles => profiles.map(p => p.userId)); } // Преобразование entity в модель Swipe private mapEntityToSwipe(entity: any): Swipe { return new Swipe({ id: entity.id, userId: entity.swiper_id, targetUserId: entity.swiped_id, type: this.convertDirectionToSwipeType(entity.direction), timestamp: entity.created_at, isMatch: entity.is_match }); } // Преобразование entity в модель Match private mapEntityToMatch(entity: any): Match { return new Match({ id: entity.id, userId1: entity.user1_id, userId2: entity.user2_id, createdAt: entity.matched_at || entity.created_at, lastMessageAt: entity.last_message_at, isActive: entity.status === 'active', isSuperMatch: false, // Определяется из swipes если нужно unreadCount1: 0, unreadCount2: 0 }); } // Получить взаимные лайки (потенциальные матчи) async getMutualLikes(userId: string): Promise { const result = await query(` SELECT DISTINCT s1.target_user_id FROM swipes s1 JOIN swipes s2 ON s1.user_id = s2.target_user_id AND s1.target_user_id = s2.user_id WHERE s1.user_id = $1 AND s1.type IN ('like', 'superlike') AND s2.type IN ('like', 'superlike') AND NOT EXISTS ( SELECT 1 FROM matches m WHERE (m.user_id_1 = s1.user_id AND m.user_id_2 = s1.target_user_id) OR (m.user_id_1 = s1.target_user_id AND m.user_id_2 = s1.user_id) ) `, [userId]); return result.rows.map((row: any) => row.target_user_id); } // Получить следующего кандидата для просмотра async getNextCandidate(telegramId: string): Promise { // Сначала получаем профиль пользователя по telegramId const userProfile = await this.profileService.getProfileByTelegramId(telegramId); if (!userProfile) { throw new BotError('User profile not found', 'PROFILE_NOT_FOUND'); } // Получаем UUID пользователя const userId = userProfile.userId; // Получаем список уже просмотренных пользователей const viewedUsers = await query(` SELECT DISTINCT swiped_id FROM swipes WHERE swiper_id = $1 `, [userId]); const viewedUserIds = viewedUsers.rows.map((row: any) => row.swiped_id); viewedUserIds.push(userId); // Исключаем самого себя // Формируем условие для исключения уже просмотренных const excludeCondition = viewedUserIds.length > 0 ? `AND p.user_id NOT IN (${viewedUserIds.map((_: any, i: number) => `$${i + 2}`).join(', ')})` : ''; // Ищем подходящих кандидатов const candidateQuery = ` SELECT p.*, u.telegram_id, u.username, u.first_name, u.last_name FROM profiles p JOIN users u ON p.user_id = u.id WHERE p.is_visible = true AND p.gender = $1 AND p.age BETWEEN ${userProfile.searchPreferences.minAge} AND ${userProfile.searchPreferences.maxAge} ${excludeCondition} ORDER BY RANDOM() LIMIT 1 `; const params = [userProfile.interestedIn, ...viewedUserIds]; const result = await query(candidateQuery, params); if (result.rows.length === 0) { return null; } const candidateData = result.rows[0]; // Используем ProfileService для правильного маппинга данных return this.profileService.mapEntityToProfile(candidateData); } // VIP функция: поиск кандидатов по цели знакомства async getCandidatesWithGoal(userProfile: Profile, targetGoal: string): Promise { const swipedUsersResult = await query(` SELECT swiped_id FROM swipes WHERE swiper_id = $1 `, [userProfile.userId]); const swipedUserIds = swipedUsersResult.rows.map((row: any) => row.swiped_id); swipedUserIds.push(userProfile.userId); // Исключаем себя let candidateQuery = ` SELECT DISTINCT p.*, u.telegram_id, u.username, u.first_name, u.last_name FROM profiles p JOIN users u ON p.user_id = u.id WHERE p.is_visible = true AND p.is_active = true AND p.gender = $1 AND p.dating_goal = $2 AND p.user_id NOT IN (${swipedUserIds.map((_: any, i: number) => `$${i + 3}`).join(', ')}) ORDER BY p.created_at DESC LIMIT 50 `; const params = [userProfile.interestedIn, targetGoal, ...swipedUserIds]; const result = await query(candidateQuery, params); return result.rows.map((row: any) => this.profileService.mapEntityToProfile(row)); } }