mainly functional matching
This commit is contained in:
@@ -17,24 +17,6 @@ export class MatchingService {
|
||||
}
|
||||
|
||||
// Выполнить свайп
|
||||
// Конвертация типов свайпов между 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;
|
||||
@@ -63,22 +45,21 @@ export class MatchingService {
|
||||
}
|
||||
|
||||
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, user_id, target_user_id, direction, created_at)
|
||||
INSERT INTO swipes (id, user_id, target_user_id, type, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
`, [swipeId, userId, targetUserId, direction, new Date()]);
|
||||
`, [swipeId, userId, targetUserId, swipeType, 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 ('right', 'super')
|
||||
WHERE user_id = $1 AND target_user_id = $2 AND type IN ('like', 'superlike')
|
||||
`, [targetUserId, userId]);
|
||||
|
||||
if (reciprocalSwipe.rows.length > 0) {
|
||||
@@ -91,7 +72,7 @@ export class MatchingService {
|
||||
if (existingMatch.rows.length === 0) {
|
||||
isMatch = true;
|
||||
const matchId = uuidv4();
|
||||
const isSuperMatch = swipeType === 'superlike' || reciprocalSwipe.rows[0].direction === 'super';
|
||||
const isSuperMatch = swipeType === 'superlike' || reciprocalSwipe.rows[0].type === 'superlike';
|
||||
|
||||
// Упорядочиваем пользователей для консистентности
|
||||
const [user1Id, user2Id] = userId < targetUserId ? [userId, targetUserId] : [targetUserId, userId];
|
||||
@@ -300,7 +281,7 @@ export class MatchingService {
|
||||
async getRecentLikes(userId: string, limit: number = 20): Promise<Swipe[]> {
|
||||
const result = await query(`
|
||||
SELECT * FROM swipes
|
||||
WHERE target_user_id = $1 AND direction IN ('right', 'super') AND is_match = false
|
||||
WHERE target_user_id = $1 AND type IN ('like', 'superlike') AND is_match = false
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $2
|
||||
`, [userId, limit]);
|
||||
@@ -319,10 +300,10 @@ export class MatchingService {
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
const result = await query(`
|
||||
SELECT direction, COUNT(*) as count
|
||||
SELECT type, COUNT(*) as count
|
||||
FROM swipes
|
||||
WHERE swiper_id = $1 AND created_at >= $2
|
||||
GROUP BY direction
|
||||
WHERE user_id = $1 AND created_at >= $2
|
||||
GROUP BY type
|
||||
`, [userId, today]);
|
||||
|
||||
const stats = {
|
||||
@@ -336,11 +317,11 @@ export class MatchingService {
|
||||
const count = parseInt(row.count);
|
||||
stats.total += count;
|
||||
|
||||
switch (row.direction) {
|
||||
switch (row.type) {
|
||||
case 'like':
|
||||
stats.likes = count;
|
||||
break;
|
||||
case 'super':
|
||||
case 'superlike':
|
||||
stats.superlikes = count;
|
||||
break;
|
||||
case 'pass':
|
||||
@@ -382,9 +363,9 @@ export class MatchingService {
|
||||
private mapEntityToSwipe(entity: any): Swipe {
|
||||
return new Swipe({
|
||||
id: entity.id,
|
||||
userId: entity.swiper_id,
|
||||
targetUserId: entity.swiped_id,
|
||||
type: this.convertDirectionToSwipeType(entity.direction),
|
||||
userId: entity.user_id || entity.swiper_id,
|
||||
targetUserId: entity.target_user_id || entity.swiped_id,
|
||||
type: entity.type || 'pass',
|
||||
timestamp: entity.created_at,
|
||||
isMatch: entity.is_match
|
||||
});
|
||||
@@ -412,8 +393,8 @@ export class MatchingService {
|
||||
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.direction IN ('right', 'super')
|
||||
AND s2.direction IN ('right', 'super')
|
||||
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)
|
||||
@@ -426,73 +407,156 @@ export class MatchingService {
|
||||
|
||||
// Получить следующего кандидата для просмотра
|
||||
async getNextCandidate(telegramId: string, isNewUser: boolean = false): Promise<Profile | null> {
|
||||
console.log(`[DEBUG] getNextCandidate вызван для telegramId=${telegramId}, isNewUser=${isNewUser}`);
|
||||
|
||||
// Сначала получаем профиль пользователя по telegramId
|
||||
const userProfile = await this.profileService.getProfileByTelegramId(telegramId);
|
||||
if (!userProfile) {
|
||||
console.log(`[ERROR] Профиль пользователя с telegramId=${telegramId} не найден`);
|
||||
throw new BotError('User profile not found', 'PROFILE_NOT_FOUND');
|
||||
}
|
||||
console.log(`[DEBUG] Найден профиль пользователя:`, JSON.stringify({
|
||||
userId: userProfile.userId,
|
||||
gender: userProfile.gender,
|
||||
interestedIn: userProfile.interestedIn,
|
||||
minAge: userProfile.searchPreferences?.minAge,
|
||||
maxAge: userProfile.searchPreferences?.maxAge
|
||||
}));
|
||||
|
||||
// Получаем UUID пользователя
|
||||
const userId = userProfile.userId;
|
||||
|
||||
// Получаем список уже просмотренных пользователей
|
||||
const viewedUsers = await query(`
|
||||
SELECT DISTINCT target_user_id
|
||||
FROM swipes
|
||||
WHERE user_id = $1
|
||||
`, [userId]);
|
||||
|
||||
const viewedUserIds = viewedUsers.rows.map((row: any) => row.target_user_id);
|
||||
viewedUserIds.push(userId); // Исключаем самого себя
|
||||
|
||||
// Если это новый пользователь или у пользователя мало просмотренных профилей,
|
||||
// показываем всех пользователей по очереди (исключая только себя)
|
||||
let excludeCondition = '';
|
||||
|
||||
if (!isNewUser) {
|
||||
excludeCondition = viewedUserIds.length > 0
|
||||
? `AND p.user_id NOT IN (${viewedUserIds.map((_: any, i: number) => `$${i + 2}`).join(', ')})`
|
||||
: '';
|
||||
// Определяем, каким должен быть пол показываемых профилей
|
||||
let targetGender: string;
|
||||
if (userProfile.interestedIn === 'male' || userProfile.interestedIn === 'female') {
|
||||
targetGender = userProfile.interestedIn;
|
||||
} else {
|
||||
// Для новых пользователей исключаем только себя
|
||||
excludeCondition = `AND p.user_id != $2`;
|
||||
// Если "both" или другое значение, показываем противоположный пол
|
||||
targetGender = userProfile.gender === 'male' ? 'female' : 'male';
|
||||
}
|
||||
|
||||
console.log(`[DEBUG] Определен целевой пол для поиска: ${targetGender}`);
|
||||
|
||||
// Получаем список просмотренных профилей из новой таблицы profile_views
|
||||
// и добавляем также профили из свайпов для полной совместимости
|
||||
console.log(`[DEBUG] Запрашиваем просмотренные и свайпнутые профили для userId=${userId}`);
|
||||
const [viewedProfilesResult, swipedProfilesResult] = await Promise.all([
|
||||
query(`
|
||||
SELECT DISTINCT viewed_profile_id
|
||||
FROM profile_views
|
||||
WHERE viewer_id = $1
|
||||
`, [userId]),
|
||||
query(`
|
||||
SELECT DISTINCT target_user_id
|
||||
FROM swipes
|
||||
WHERE user_id = $1
|
||||
`, [userId])
|
||||
]);
|
||||
console.log(`[DEBUG] Найдено ${viewedProfilesResult.rows.length} просмотренных и ${swipedProfilesResult.rows.length} свайпнутых профилей`);
|
||||
|
||||
// Объединяем просмотренные и свайпнутые профили в один список
|
||||
const viewedUserIds = [
|
||||
...viewedProfilesResult.rows.map((row: any) => row.viewed_profile_id),
|
||||
...swipedProfilesResult.rows.map((row: any) => row.target_user_id)
|
||||
];
|
||||
|
||||
// Всегда добавляем самого пользователя в список исключений
|
||||
viewedUserIds.push(userId);
|
||||
|
||||
// Удаляем дубликаты
|
||||
const uniqueViewedIds = [...new Set(viewedUserIds)];
|
||||
console.log(`[DEBUG] Всего ${uniqueViewedIds.length} уникальных исключаемых профилей`);
|
||||
|
||||
// Формируем параметры запроса
|
||||
let params: any[] = [];
|
||||
let excludeCondition: string = '';
|
||||
|
||||
// Для новых пользователей исключаем только себя
|
||||
if (isNewUser || uniqueViewedIds.length <= 1) {
|
||||
params = [userId];
|
||||
excludeCondition = 'AND p.user_id != $1';
|
||||
console.log(`[DEBUG] Режим нового пользователя: исключаем только самого себя`);
|
||||
} else {
|
||||
// Для остальных исключаем все просмотренные профили
|
||||
params = [...uniqueViewedIds];
|
||||
const placeholders = uniqueViewedIds.map((_: any, i: number) => `$${i + 1}`).join(', ');
|
||||
excludeCondition = `AND p.user_id NOT IN (${placeholders})`;
|
||||
console.log(`[DEBUG] Стандартный режим: исключаем ${uniqueViewedIds.length} профилей`);
|
||||
}
|
||||
|
||||
// Ищем подходящих кандидатов
|
||||
// Выполним предварительный запрос для проверки наличия доступных анкет
|
||||
const countQuery = `
|
||||
SELECT COUNT(*) as count
|
||||
FROM profiles p
|
||||
JOIN users u ON p.user_id = u.id
|
||||
WHERE p.is_visible = true
|
||||
AND p.gender = '${targetGender}'
|
||||
${excludeCondition}
|
||||
`;
|
||||
|
||||
console.log(`[DEBUG] Проверка наличия подходящих анкет...`);
|
||||
console.log(`[DEBUG] SQL запрос count: ${countQuery}`);
|
||||
console.log(`[DEBUG] Параметры count: ${JSON.stringify(params)}`);
|
||||
const countResult = await query(countQuery, params);
|
||||
const availableProfilesCount = parseInt(countResult.rows[0]?.count || '0');
|
||||
console.log(`[DEBUG] Найдено ${availableProfilesCount} доступных профилей`);
|
||||
|
||||
// Используем определенный ранее targetGender для поиска
|
||||
console.log(`[DEBUG] Поиск кандидата для gender=${targetGender}, возраст: ${userProfile.searchPreferences.minAge}-${userProfile.searchPreferences.maxAge}`);
|
||||
|
||||
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.gender = '${targetGender}'
|
||||
AND p.age BETWEEN ${userProfile.searchPreferences.minAge} AND ${userProfile.searchPreferences.maxAge}
|
||||
${excludeCondition}
|
||||
ORDER BY RANDOM()
|
||||
LIMIT 1
|
||||
`;
|
||||
console.log(`[DEBUG] SQL запрос: ${candidateQuery}`);
|
||||
console.log(`[DEBUG] Параметры: ${JSON.stringify(params)}`);
|
||||
|
||||
const params = [userProfile.interestedIn, ...viewedUserIds];
|
||||
const result = await query(candidateQuery, params);
|
||||
console.log(`[DEBUG] Результаты запроса: найдено ${result.rows.length} профилей`);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
console.log(`[DEBUG] Подходящие кандидаты не найдены`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const candidateData = result.rows[0];
|
||||
console.log(`[DEBUG] Найден подходящий кандидат: ${candidateData.name}, возраст: ${candidateData.age}`);
|
||||
|
||||
// Записываем просмотр профиля в новую таблицу profile_views
|
||||
try {
|
||||
const viewerTelegramId = telegramId;
|
||||
const viewedTelegramId = candidateData.telegram_id.toString();
|
||||
|
||||
console.log(`[DEBUG] Записываем просмотр профиля: viewer=${viewerTelegramId}, viewed=${viewedTelegramId}`);
|
||||
// Асинхронно записываем просмотр, но не ждем завершения
|
||||
this.profileService.recordProfileView(viewerTelegramId, viewedTelegramId, 'browse')
|
||||
.catch(err => console.error(`[ERROR] Ошибка записи просмотра профиля:`, err));
|
||||
} catch (err) {
|
||||
console.error(`[ERROR] Ошибка записи просмотра профиля:`, err);
|
||||
}
|
||||
|
||||
// Используем ProfileService для правильного маппинга данных
|
||||
return this.profileService.mapEntityToProfile(candidateData);
|
||||
const profile = this.profileService.mapEntityToProfile(candidateData);
|
||||
console.log(`[DEBUG] Профиль преобразован и возвращается клиенту`);
|
||||
return profile;
|
||||
}
|
||||
|
||||
// VIP функция: поиск кандидатов по цели знакомства
|
||||
async getCandidatesWithGoal(userProfile: Profile, targetGoal: string): Promise<Profile[]> {
|
||||
const swipedUsersResult = await query(`
|
||||
SELECT swiped_id
|
||||
SELECT target_user_id
|
||||
FROM swipes
|
||||
WHERE swiper_id = $1
|
||||
WHERE user_id = $1
|
||||
`, [userProfile.userId]);
|
||||
|
||||
const swipedUserIds = swipedUsersResult.rows.map((row: any) => row.swiped_id);
|
||||
const swipedUserIds = swipedUsersResult.rows.map((row: any) => row.target_user_id);
|
||||
swipedUserIds.push(userProfile.userId); // Исключаем себя
|
||||
|
||||
let candidateQuery = `
|
||||
|
||||
@@ -496,7 +496,7 @@ export class ProfileService {
|
||||
// Удаляем связанные данные
|
||||
await client.query('DELETE FROM messages WHERE sender_id = $1 OR receiver_id = $1', [userId]);
|
||||
await client.query('DELETE FROM matches WHERE user_id_1 = $1 OR user_id_2 = $1', [userId]);
|
||||
await client.query('DELETE FROM swipes WHERE swiper_id = $1 OR swiped_id = $1', [userId]);
|
||||
await client.query('DELETE FROM swipes WHERE user_id = $1 OR target_user_id = $1', [userId]);
|
||||
await client.query('DELETE FROM profiles WHERE user_id = $1', [userId]);
|
||||
});
|
||||
return true;
|
||||
@@ -526,16 +526,38 @@ export class ProfileService {
|
||||
// Записать просмотр профиля
|
||||
async recordProfileView(viewerId: string, viewedProfileId: string, viewType: string = 'browse'): Promise<void> {
|
||||
try {
|
||||
// Преобразуем строковые ID в числа для запросов
|
||||
const viewerTelegramId = typeof viewerId === 'string' ? parseInt(viewerId) : viewerId;
|
||||
const viewedTelegramId = typeof viewedProfileId === 'string' ? parseInt(viewedProfileId) : viewedProfileId;
|
||||
|
||||
// Получаем внутренние ID пользователей
|
||||
const viewerIdResult = await query('SELECT id FROM users WHERE telegram_id = $1', [viewerTelegramId]);
|
||||
if (viewerIdResult.rows.length === 0) {
|
||||
throw new Error(`User with telegram_id ${viewerId} not found`);
|
||||
}
|
||||
|
||||
const viewedUserResult = await query('SELECT id FROM users WHERE telegram_id = $1', [viewedTelegramId]);
|
||||
if (viewedUserResult.rows.length === 0) {
|
||||
throw new Error(`User with telegram_id ${viewedProfileId} not found`);
|
||||
}
|
||||
|
||||
const viewerUuid = viewerIdResult.rows[0].id;
|
||||
const viewedUuid = viewedUserResult.rows[0].id;
|
||||
|
||||
// Не записываем просмотры своего профиля
|
||||
if (viewerUuid === viewedUuid) {
|
||||
console.log('Skipping self-view record');
|
||||
return;
|
||||
}
|
||||
|
||||
await query(`
|
||||
INSERT INTO profile_views (viewer_id, viewed_profile_id, view_type)
|
||||
VALUES (
|
||||
(SELECT id FROM users WHERE telegram_id = $1),
|
||||
(SELECT id FROM profiles WHERE user_id = (SELECT id FROM users WHERE telegram_id = $2)),
|
||||
$3
|
||||
)
|
||||
INSERT INTO profile_views (viewer_id, viewed_profile_id, view_type, view_date)
|
||||
VALUES ($1, $2, $3, NOW())
|
||||
ON CONFLICT (viewer_id, viewed_profile_id) DO UPDATE
|
||||
SET viewed_at = CURRENT_TIMESTAMP, view_type = EXCLUDED.view_type
|
||||
`, [viewerId, viewedProfileId, viewType]);
|
||||
SET view_date = NOW(), view_type = $3
|
||||
`, [viewerUuid, viewedUuid, viewType]);
|
||||
|
||||
console.log(`Recorded profile view: ${viewerId} viewed ${viewedProfileId}`);
|
||||
} catch (error) {
|
||||
console.error('Error recording profile view:', error);
|
||||
}
|
||||
@@ -547,8 +569,7 @@ export class ProfileService {
|
||||
const result = await query(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM profile_views pv
|
||||
JOIN profiles p ON pv.viewed_profile_id = p.id
|
||||
WHERE p.user_id = $1
|
||||
WHERE viewed_profile_id = $1
|
||||
`, [userId]);
|
||||
|
||||
return parseInt(result.rows[0].count) || 0;
|
||||
@@ -562,14 +583,12 @@ export class ProfileService {
|
||||
async getProfileViewers(userId: string, limit: number = 10): Promise<Profile[]> {
|
||||
try {
|
||||
const result = await query(`
|
||||
SELECT DISTINCT p.*, u.telegram_id, u.username, u.first_name, u.last_name
|
||||
SELECT DISTINCT p.*
|
||||
FROM profile_views pv
|
||||
JOIN profiles target_p ON pv.viewed_profile_id = target_p.id
|
||||
JOIN users viewer_u ON pv.viewer_id = viewer_u.id
|
||||
JOIN profiles p ON viewer_u.id = p.user_id
|
||||
JOIN users u ON p.user_id = u.id
|
||||
WHERE target_p.user_id = $1
|
||||
ORDER BY pv.viewed_at DESC
|
||||
WHERE pv.viewed_profile_id = $1
|
||||
ORDER BY pv.view_date DESC
|
||||
LIMIT $2
|
||||
`, [userId, limit]);
|
||||
|
||||
@@ -579,4 +598,22 @@ export class ProfileService {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Получить список просмотренных профилей
|
||||
async getViewedProfiles(userId: string, limit: number = 50): Promise<string[]> {
|
||||
try {
|
||||
const result = await query(`
|
||||
SELECT viewed_profile_id
|
||||
FROM profile_views
|
||||
WHERE viewer_id = $1
|
||||
ORDER BY view_date DESC
|
||||
LIMIT $2
|
||||
`, [userId, limit]);
|
||||
|
||||
return result.rows.map((row: any) => row.viewed_profile_id);
|
||||
} catch (error) {
|
||||
console.error('Error getting viewed profiles:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user