From 85027a774705bd7815bbe170d57d130d58c902a2 Mon Sep 17 00:00:00 2001 From: "Choi A.K." Date: Thu, 18 Sep 2025 11:42:18 +0900 Subject: [PATCH] mainly functional matching --- fixes.md | 90 +++++++++ .../1631980000000_add_profile_views_table.ts | 44 +++++ profile_views_patch.ts | 153 ++++++++++++++ scripts/checkDatabase.js | 66 +++++++ scripts/checkProfileViews.js | 64 ++++++ scripts/cleanDatabase.js | 55 ++++++ scripts/clearDatabase.js | 66 +++++++ scripts/clearDatabase.mjs | 81 ++++++++ scripts/clear_database.sql | 26 +++ scripts/createProfileViewsTable.js | 86 ++++++++ scripts/createProfileViewsTable.ts | 70 +++++++ scripts/testMatching.js | 102 ++++++++++ scripts/testProfileViews.js | 98 +++++++++ src/services/matchingService.ts | 186 ++++++++++++------ src/services/profileService.ts | 69 +++++-- 15 files changed, 1179 insertions(+), 77 deletions(-) create mode 100644 fixes.md create mode 100644 migrations/1631980000000_add_profile_views_table.ts create mode 100644 profile_views_patch.ts create mode 100644 scripts/checkDatabase.js create mode 100644 scripts/checkProfileViews.js create mode 100644 scripts/cleanDatabase.js create mode 100644 scripts/clearDatabase.js create mode 100644 scripts/clearDatabase.mjs create mode 100644 scripts/clear_database.sql create mode 100644 scripts/createProfileViewsTable.js create mode 100644 scripts/createProfileViewsTable.ts create mode 100644 scripts/testMatching.js create mode 100644 scripts/testProfileViews.js diff --git a/fixes.md b/fixes.md new file mode 100644 index 0000000..ded6c38 --- /dev/null +++ b/fixes.md @@ -0,0 +1,90 @@ +# Исправления ошибок в коде + +## Проблема: Несоответствие имен столбцов в таблице swipes + +В коде обнаружены несоответствия в названиях столбцов при работе с таблицей `swipes`. Используются два разных варианта именования: + +1. `user_id` и `target_user_id` +2. `swiper_id` и `swiped_id` + +Судя по ошибкам в консоли и анализу кода, корректными именами столбцов являются `user_id` и `target_user_id`. + +## Необходимые исправления + +### 1. В файле `profileService.ts` - метод `deleteProfile`: + +```typescript +// Неверно: +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]); +``` + +### 2. В файле `matchingService.ts` - метод `performSwipe`: + +```typescript +// Неверно: +const reciprocalSwipe = await client.query(` + SELECT * FROM swipes + WHERE swiper_id = $1 AND swiped_id = $2 AND direction IN ('right', 'super') +`, [targetUserId, userId]); + +// Исправить на: +const reciprocalSwipe = await client.query(` + SELECT * FROM swipes + WHERE user_id = $1 AND target_user_id = $2 AND direction IN ('right', 'super') +`, [targetUserId, userId]); +``` + +### 3. В файле `matchingService.ts` - метод `getRecentLikes`: + +```typescript +// Неверно (если используется метод mapEntityToSwipe): +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 + }); +} + +// Исправить на: +private mapEntityToSwipe(entity: any): Swipe { + return new Swipe({ + id: entity.id, + userId: entity.user_id, + targetUserId: entity.target_user_id, + type: this.convertDirectionToSwipeType(entity.direction), + timestamp: entity.created_at, + isMatch: entity.is_match + }); +} +``` + +### 4. В файле `matchingService.ts` - метод `getDailySwipeStats`: + +```typescript +// Неверно: +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 result = await query(` + SELECT direction, COUNT(*) as count + FROM swipes + WHERE user_id = $1 AND created_at >= $2 + GROUP BY direction +`, [userId, today]); +``` + +## Примечание + +После внесения исправлений рекомендуется проверить все остальные места в коде, где могут использоваться эти имена столбцов, и убедиться в их согласованности. diff --git a/migrations/1631980000000_add_profile_views_table.ts b/migrations/1631980000000_add_profile_views_table.ts new file mode 100644 index 0000000..73b79c6 --- /dev/null +++ b/migrations/1631980000000_add_profile_views_table.ts @@ -0,0 +1,44 @@ +import { MigrationBuilder, ColumnDefinitions } from 'node-pg-migrate'; + +export const shorthands: ColumnDefinitions | undefined = undefined; + +export async function up(pgm: MigrationBuilder): Promise { + // Создание таблицы profile_views для хранения информации о просмотренных профилях + pgm.createTable('profile_views', { + id: { type: 'uuid', primaryKey: true, default: pgm.func('uuid_generate_v4()') }, + viewer_id: { + type: 'uuid', + notNull: true, + references: 'users', + onDelete: 'CASCADE' + }, + viewed_profile_id: { + type: 'uuid', + notNull: true, + references: 'profiles(user_id)', + onDelete: 'CASCADE' + }, + view_date: { type: 'timestamp', notNull: true, default: pgm.func('now()') }, + view_type: { type: 'varchar(20)', notNull: true, default: 'browse' }, // browse, match, like, etc. + }); + + // Создание индекса для быстрого поиска по паре (просмотревший - просмотренный) + pgm.createIndex('profile_views', ['viewer_id', 'viewed_profile_id'], { + unique: true, + name: 'profile_views_viewer_viewed_idx' + }); + + // Индекс для быстрого поиска по viewer_id + pgm.createIndex('profile_views', ['viewer_id'], { + name: 'profile_views_viewer_idx' + }); + + // Индекс для быстрого поиска по viewed_profile_id + pgm.createIndex('profile_views', ['viewed_profile_id'], { + name: 'profile_views_viewed_idx' + }); +} + +export async function down(pgm: MigrationBuilder): Promise { + pgm.dropTable('profile_views', { cascade: true }); +} diff --git a/profile_views_patch.ts b/profile_views_patch.ts new file mode 100644 index 0000000..f9abfb6 --- /dev/null +++ b/profile_views_patch.ts @@ -0,0 +1,153 @@ +// Патч для учета просмотренных профилей в функциональности бота + +// 1. Добавляем функцию recordProfileView в ProfileController +import { Profile, ProfileData } from '../models/Profile'; +import { ProfileService } from '../services/profileService'; + +export class ProfileController { + constructor(private profileService: ProfileService) {} + + // Существующие методы... + + // Новый метод для записи просмотра профиля + async recordProfileView(viewerTelegramId: string, viewedTelegramId: string, viewType: string = 'browse'): Promise { + try { + // Получаем внутренние ID пользователей + const viewerId = await this.profileService.getUserIdByTelegramId(viewerTelegramId); + const viewedId = await this.profileService.getUserIdByTelegramId(viewedTelegramId); + + if (!viewerId || !viewedId) { + console.error('Не удалось найти пользователей для записи просмотра профиля'); + return false; + } + + // Проверяем существование таблицы profile_views + const checkTableResult = await this.profileService.checkTableExists('profile_views'); + + if (checkTableResult) { + // Записываем просмотр + await this.profileService.recordProfileView(viewerId, viewedId, viewType); + console.log(`Просмотр профиля записан: ${viewerTelegramId} просмотрел ${viewedTelegramId}`); + return true; + } else { + console.log('Таблица profile_views не существует, просмотр не записан'); + return false; + } + } catch (error) { + console.error('Ошибка при записи просмотра профиля:', error); + return false; + } + } + + // Новый метод для получения списка просмотренных профилей + async getViewedProfiles(telegramId: string, limit: number = 50): Promise { + try { + // Получаем внутренний ID пользователя + const userId = await this.profileService.getUserIdByTelegramId(telegramId); + + if (!userId) { + console.error('Не удалось найти пользователя для получения списка просмотренных профилей'); + return []; + } + + // Проверяем существование таблицы profile_views + const checkTableResult = await this.profileService.checkTableExists('profile_views'); + + if (checkTableResult) { + // Получаем список просмотренных профилей + return await this.profileService.getViewedProfiles(userId, limit); + } else { + console.log('Таблица profile_views не существует, возвращаем пустой список'); + return []; + } + } catch (error) { + console.error('Ошибка при получении списка просмотренных профилей:', error); + return []; + } + } +} + +// 2. Добавляем функцию для проверки существования таблицы в ProfileService +async checkTableExists(tableName: string): Promise { + try { + const result = await query(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = $1 + ); + `, [tableName]); + + return result.rows.length > 0 && result.rows[0].exists; + } catch (error) { + console.error(`Ошибка проверки существования таблицы ${tableName}:`, error); + return false; + } +} + +// 3. Обновляем обработчик показа профиля, чтобы записывать просмотры +async function handleShowProfile(ctx: any) { + // Существующий код... + + // После успешного отображения профиля записываем просмотр + const viewerTelegramId = ctx.from.id.toString(); + const viewedTelegramId = candidateProfile.telegram_id.toString(); + + try { + const profileController = new ProfileController(new ProfileService()); + await profileController.recordProfileView(viewerTelegramId, viewedTelegramId, 'browse'); + } catch (error) { + console.error('Ошибка при записи просмотра профиля:', error); + } + + // Остальной код... +} + +// 4. Обновляем функцию getNextCandidate, чтобы учитывать просмотренные профили +async function getNextCandidate(ctx: any) { + const telegramId = ctx.from.id.toString(); + const isNewUser = false; // Определяем, является ли пользователь новым + + try { + // Сначала пытаемся получить профили, которые пользователь еще не просматривал + const matchingService = new MatchingService(); + const profileService = new ProfileService(); + const profileController = new ProfileController(profileService); + + // Получаем UUID пользователя + const userId = await profileService.getUserIdByTelegramId(telegramId); + + if (!userId) { + console.error('Не удалось найти пользователя для получения следующего кандидата'); + return null; + } + + // Получаем список просмотренных профилей + const viewedProfiles = await profileController.getViewedProfiles(telegramId); + + // Получаем профиль пользователя + const userProfile = await profileService.getProfileByTelegramId(telegramId); + + if (!userProfile) { + console.error('Не удалось найти профиль пользователя для получения следующего кандидата'); + return null; + } + + // Ищем подходящий профиль с учетом просмотренных + const nextCandidate = await matchingService.getNextCandidate(telegramId, isNewUser); + + // Если найден кандидат, записываем просмотр + if (nextCandidate) { + const viewedTelegramId = await profileService.getTelegramIdByUserId(nextCandidate.userId); + + if (viewedTelegramId) { + await profileController.recordProfileView(telegramId, viewedTelegramId, 'browse'); + } + } + + return nextCandidate; + } catch (error) { + console.error('Ошибка при получении следующего кандидата:', error); + return null; + } +} diff --git a/scripts/checkDatabase.js b/scripts/checkDatabase.js new file mode 100644 index 0000000..c0b65c0 --- /dev/null +++ b/scripts/checkDatabase.js @@ -0,0 +1,66 @@ +const { Pool } = require('pg'); +require('dotenv').config(); + +const pool = new Pool({ + host: process.env.DB_HOST, + port: parseInt(process.env.DB_PORT || '5432'), + database: process.env.DB_NAME, + user: process.env.DB_USERNAME, + password: process.env.DB_PASSWORD +}); + +async function checkDatabase() { + const client = await pool.connect(); + try { + console.log('\n===== ПРОВЕРКА СОСТОЯНИЯ БАЗЫ ДАННЫХ ====='); + + // Проверка таблицы users + const usersResult = await client.query('SELECT COUNT(*) as count FROM users'); + console.log(`Пользователей в БД: ${usersResult.rows[0].count}`); + if (parseInt(usersResult.rows[0].count) > 0) { + const users = await client.query('SELECT id, telegram_id, username, first_name FROM users LIMIT 10'); + console.log('Последние пользователи:'); + users.rows.forEach(user => { + console.log(` - ID: ${user.id.substring(0, 8)}... | Telegram: ${user.telegram_id} | Имя: ${user.first_name || user.username}`); + }); + } + + // Проверка таблицы profiles + const profilesResult = await client.query('SELECT COUNT(*) as count FROM profiles'); + console.log(`\nПрофилей в БД: ${profilesResult.rows[0].count}`); + if (parseInt(profilesResult.rows[0].count) > 0) { + const profiles = await client.query(` + SELECT p.id, p.user_id, p.name, p.age, p.gender, p.interested_in, p.is_visible + FROM profiles p + ORDER BY p.created_at DESC + LIMIT 10 + `); + console.log('Последние профили:'); + profiles.rows.forEach(profile => { + console.log(` - ID: ${profile.id.substring(0, 8)}... | UserID: ${profile.user_id.substring(0, 8)}... | Имя: ${profile.name} | Возраст: ${profile.age} | Пол: ${profile.gender} | Интересы: ${profile.interested_in} | Виден: ${profile.is_visible}`); + }); + } + + // Проверка таблицы swipes + const swipesResult = await client.query('SELECT COUNT(*) as count FROM swipes'); + console.log(`\nСвайпов в БД: ${swipesResult.rows[0].count}`); + + // Проверка таблицы profile_views + const viewsResult = await client.query('SELECT COUNT(*) as count FROM profile_views'); + console.log(`Просмотров профилей в БД: ${viewsResult.rows[0].count}`); + + // Проверка таблицы matches + const matchesResult = await client.query('SELECT COUNT(*) as count FROM matches'); + console.log(`Матчей в БД: ${matchesResult.rows[0].count}`); + + console.log('\n===== ПРОВЕРКА ЗАВЕРШЕНА =====\n'); + } catch (e) { + console.error('Ошибка при проверке базы данных:', e); + } finally { + client.release(); + await pool.end(); + } +} + +// Запускаем проверку +checkDatabase(); diff --git a/scripts/checkProfileViews.js b/scripts/checkProfileViews.js new file mode 100644 index 0000000..22ebad7 --- /dev/null +++ b/scripts/checkProfileViews.js @@ -0,0 +1,64 @@ +// Скрипт для проверки таблицы profile_views +const { Pool } = require('pg'); +require('dotenv').config(); + +const pool = new Pool({ + host: process.env.DB_HOST, + port: parseInt(process.env.DB_PORT || '5432'), + database: process.env.DB_NAME, + user: process.env.DB_USERNAME, + password: process.env.DB_PASSWORD +}); + +async function checkProfileViewsTable() { + const client = await pool.connect(); + try { + console.log('Проверка таблицы profile_views...'); + + // Проверяем наличие таблицы + const tableCheck = await client.query(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = 'profile_views' + ); + `); + + const tableExists = tableCheck.rows[0].exists; + console.log(`Таблица profile_views ${tableExists ? 'существует' : 'не существует'}`); + + if (tableExists) { + // Проверяем количество записей в таблице + const countResult = await client.query('SELECT COUNT(*) FROM profile_views'); + console.log(`Количество записей в таблице: ${countResult.rows[0].count}`); + + // Получаем данные из таблицы + const dataResult = await client.query(` + SELECT pv.*, + v.telegram_id as viewer_telegram_id, + vp.telegram_id as viewed_telegram_id + FROM profile_views pv + LEFT JOIN users v ON pv.viewer_id = v.id + LEFT JOIN users vp ON pv.viewed_profile_id = vp.id + LIMIT 10 + `); + + if (dataResult.rows.length > 0) { + console.log('Данные из таблицы profile_views:'); + dataResult.rows.forEach((row, index) => { + console.log(`${index + 1}. Просмотр: ${row.viewer_telegram_id || 'Неизвестно'} → ${row.viewed_telegram_id || 'Неизвестно'}, дата: ${row.view_date}`); + }); + } else { + console.log('Таблица profile_views пуста'); + } + } + } catch (error) { + console.error('Ошибка при проверке таблицы profile_views:', error); + } finally { + client.release(); + await pool.end(); + } +} + +// Запускаем проверку +checkProfileViewsTable(); diff --git a/scripts/cleanDatabase.js b/scripts/cleanDatabase.js new file mode 100644 index 0000000..8ad6c9c --- /dev/null +++ b/scripts/cleanDatabase.js @@ -0,0 +1,55 @@ +const { Pool } = require('pg'); +require('dotenv').config(); + +const pool = new Pool({ + host: process.env.DB_HOST, + port: parseInt(process.env.DB_PORT || '5432'), + database: process.env.DB_NAME, + user: process.env.DB_USERNAME, + password: process.env.DB_PASSWORD +}); + +async function cleanDatabase() { + const client = await pool.connect(); + try { + console.log('Очистка базы данных...'); + await client.query('BEGIN'); + + // Отключаем временно foreign key constraints + await client.query('SET CONSTRAINTS ALL DEFERRED'); + + // Очищаем таблицы в правильном порядке + console.log('Очистка таблицы messages...'); + await client.query('DELETE FROM messages'); + + console.log('Очистка таблицы profile_views...'); + await client.query('DELETE FROM profile_views'); + + console.log('Очистка таблицы matches...'); + await client.query('DELETE FROM matches'); + + console.log('Очистка таблицы swipes...'); + await client.query('DELETE FROM swipes'); + + console.log('Очистка таблицы profiles...'); + await client.query('DELETE FROM profiles'); + + console.log('Очистка таблицы users...'); + await client.query('DELETE FROM users'); + + // Возвращаем foreign key constraints + await client.query('SET CONSTRAINTS ALL IMMEDIATE'); + + await client.query('COMMIT'); + console.log('✅ База данных успешно очищена'); + } catch (e) { + await client.query('ROLLBACK'); + console.error('❌ Ошибка при очистке базы данных:', e); + } finally { + client.release(); + await pool.end(); + } +} + +// Запускаем функцию очистки +cleanDatabase(); diff --git a/scripts/clearDatabase.js b/scripts/clearDatabase.js new file mode 100644 index 0000000..28b17f3 --- /dev/null +++ b/scripts/clearDatabase.js @@ -0,0 +1,66 @@ +// Скрипт для очистки всех таблиц в базе данных +import { Pool } from 'pg'; +import dotenv from 'dotenv'; + +// Загружаем переменные окружения из .env файла +dotenv.config(); + +const pool = new Pool({ + host: process.env.DB_HOST, + port: parseInt(process.env.DB_PORT || '5432'), + database: process.env.DB_NAME, + user: process.env.DB_USERNAME, + password: process.env.DB_PASSWORD +}); + +async function clearDatabase() { + const client = await pool.connect(); + try { + console.log('Начинаем очистку базы данных...'); + + // Начинаем транзакцию + await client.query('BEGIN'); + + // Отключаем внешние ключи на время выполнения (если они используются) + // await client.query('SET session_replication_role = \'replica\''); + + // Очистка таблиц в порядке, учитывающем зависимости + console.log('Очистка таблицы сообщений...'); + await client.query('TRUNCATE TABLE messages CASCADE'); + + console.log('Очистка таблицы просмотров профилей...'); + await client.query('TRUNCATE TABLE profile_views CASCADE'); + + console.log('Очистка таблицы свайпов...'); + await client.query('TRUNCATE TABLE swipes CASCADE'); + + console.log('Очистка таблицы матчей...'); + await client.query('TRUNCATE TABLE matches CASCADE'); + + console.log('Очистка таблицы профилей...'); + await client.query('TRUNCATE TABLE profiles CASCADE'); + + console.log('Очистка таблицы пользователей...'); + await client.query('TRUNCATE TABLE users CASCADE'); + + // Возвращаем внешние ключи (если они использовались) + // await client.query('SET session_replication_role = \'origin\''); + + // Фиксируем транзакцию + await client.query('COMMIT'); + + console.log('Все таблицы успешно очищены!'); + } catch (error) { + // В случае ошибки откатываем транзакцию + await client.query('ROLLBACK'); + console.error('Произошла ошибка при очистке базы данных:', error); + } finally { + // Освобождаем клиента + client.release(); + // Закрываем пул соединений + await pool.end(); + } +} + +// Запускаем функцию очистки +clearDatabase(); diff --git a/scripts/clearDatabase.mjs b/scripts/clearDatabase.mjs new file mode 100644 index 0000000..a0835f7 --- /dev/null +++ b/scripts/clearDatabase.mjs @@ -0,0 +1,81 @@ +// Скрипт для очистки всех таблиц в базе данных +import { Pool } from 'pg'; +import dotenv from 'dotenv'; + +// Загружаем переменные окружения из .env файла +dotenv.config(); + +const pool = new Pool({ + host: process.env.DB_HOST, + port: parseInt(process.env.DB_PORT || '5432'), + database: process.env.DB_NAME, + user: process.env.DB_USERNAME, + password: process.env.DB_PASSWORD +}); + +async function clearDatabase() { + const client = await pool.connect(); + try { + console.log('Начинаем очистку базы данных...'); + + // Начинаем транзакцию + await client.query('BEGIN'); + + // Получаем список существующих таблиц + const tablesResult = await client.query(` + SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'public' + AND table_type = 'BASE TABLE' + `); + + const tables = tablesResult.rows.map(row => row.table_name); + console.log('Найдены таблицы:', tables.join(', ')); + + // Очистка таблиц в порядке, учитывающем зависимости + if (tables.includes('messages')) { + console.log('Очистка таблицы messages...'); + await client.query('TRUNCATE TABLE messages CASCADE'); + } + + if (tables.includes('swipes')) { + console.log('Очистка таблицы swipes...'); + await client.query('TRUNCATE TABLE swipes CASCADE'); + } + + if (tables.includes('matches')) { + console.log('Очистка таблицы matches...'); + await client.query('TRUNCATE TABLE matches CASCADE'); + } + + if (tables.includes('profiles')) { + console.log('Очистка таблицы profiles...'); + await client.query('TRUNCATE TABLE profiles CASCADE'); + } + + if (tables.includes('users')) { + console.log('Очистка таблицы users...'); + await client.query('TRUNCATE TABLE users CASCADE'); + } + + // Возвращаем внешние ключи (если они использовались) + // await client.query('SET session_replication_role = \'origin\''); + + // Фиксируем транзакцию + await client.query('COMMIT'); + + console.log('Все таблицы успешно очищены!'); + } catch (error) { + // В случае ошибки откатываем транзакцию + await client.query('ROLLBACK'); + console.error('Произошла ошибка при очистке базы данных:', error); + } finally { + // Освобождаем клиента + client.release(); + // Закрываем пул соединений + await pool.end(); + } +} + +// Запускаем функцию очистки +clearDatabase(); diff --git a/scripts/clear_database.sql b/scripts/clear_database.sql new file mode 100644 index 0000000..f1071a9 --- /dev/null +++ b/scripts/clear_database.sql @@ -0,0 +1,26 @@ +-- Скрипт для очистки всех таблиц в базе данных +-- Важно: таблицы очищаются в порядке, учитывающем зависимости между ними + +-- Отключаем внешние ключи на время выполнения (если они используются) +-- SET session_replication_role = 'replica'; + +-- Очистка таблицы сообщений +TRUNCATE TABLE messages CASCADE; + +-- Очистка таблицы просмотров профилей +TRUNCATE TABLE profile_views CASCADE; + +-- Очистка таблицы свайпов +TRUNCATE TABLE swipes CASCADE; + +-- Очистка таблицы матчей +TRUNCATE TABLE matches CASCADE; + +-- Очистка таблицы профилей +TRUNCATE TABLE profiles CASCADE; + +-- Очистка таблицы пользователей +TRUNCATE TABLE users CASCADE; + +-- Возвращаем внешние ключи (если они использовались) +-- SET session_replication_role = 'origin'; diff --git a/scripts/createProfileViewsTable.js b/scripts/createProfileViewsTable.js new file mode 100644 index 0000000..dc9899d --- /dev/null +++ b/scripts/createProfileViewsTable.js @@ -0,0 +1,86 @@ +const { Pool } = require('pg'); +require('dotenv').config(); + +const pool = new Pool({ + host: process.env.DB_HOST, + port: parseInt(process.env.DB_PORT || '5432'), + database: process.env.DB_NAME, + user: process.env.DB_USERNAME, + password: process.env.DB_PASSWORD +}); + +async function createProfileViewsTable() { + const client = await pool.connect(); + try { + console.log('Creating profile_views table...'); + await client.query('BEGIN'); + + // Включаем расширение uuid-ossp, если оно еще не включено + await client.query(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`); + + // Создаем таблицу profile_views, если она не существует + await client.query(` + CREATE TABLE IF NOT EXISTS profile_views ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + viewer_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + viewed_profile_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + view_date TIMESTAMP NOT NULL DEFAULT NOW(), + view_type VARCHAR(20) NOT NULL DEFAULT 'browse' + ) + `); + + // Создаем уникальный индекс для пары (просмотревший - просмотренный) + await client.query(` + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_indexes + WHERE indexname = 'profile_views_viewer_viewed_idx' + ) THEN + CREATE UNIQUE INDEX profile_views_viewer_viewed_idx + ON profile_views (viewer_id, viewed_profile_id); + END IF; + END $$; + `); + + // Создаем индекс для быстрого поиска по viewer_id + await client.query(` + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_indexes + WHERE indexname = 'profile_views_viewer_idx' + ) THEN + CREATE INDEX profile_views_viewer_idx + ON profile_views (viewer_id); + END IF; + END $$; + `); + + // Создаем индекс для быстрого поиска по viewed_profile_id + await client.query(` + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_indexes + WHERE indexname = 'profile_views_viewed_idx' + ) THEN + CREATE INDEX profile_views_viewed_idx + ON profile_views (viewed_profile_id); + END IF; + END $$; + `); + + await client.query('COMMIT'); + console.log('Table profile_views created successfully'); + } catch (e) { + await client.query('ROLLBACK'); + console.error('Error creating table:', e); + } finally { + client.release(); + await pool.end(); + } +} + +// Запускаем функцию создания таблицы +createProfileViewsTable(); diff --git a/scripts/createProfileViewsTable.ts b/scripts/createProfileViewsTable.ts new file mode 100644 index 0000000..101e274 --- /dev/null +++ b/scripts/createProfileViewsTable.ts @@ -0,0 +1,70 @@ +// Скрипт для создания таблицы profile_views + +// Функция для ручного запуска создания таблицы profile_views +async function createProfileViewsTable() { + const client = await require('../database/connection').pool.connect(); + try { + console.log('Создание таблицы profile_views...'); + + // Проверяем, существует ли уже таблица profile_views + const tableCheck = await client.query(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = 'profile_views' + ); + `); + + if (tableCheck.rows[0].exists) { + console.log('Таблица profile_views уже существует, пропускаем создание'); + return; + } + + // Начинаем транзакцию + await client.query('BEGIN'); + + // Создаем таблицу profile_views + await client.query(` + CREATE TABLE profile_views ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + viewer_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + viewed_profile_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + view_date TIMESTAMP NOT NULL DEFAULT NOW(), + view_type VARCHAR(20) NOT NULL DEFAULT 'browse' + ); + `); + + // Создаем индекс для быстрого поиска по паре (просмотревший - просмотренный) + await client.query(` + CREATE UNIQUE INDEX profile_views_viewer_viewed_idx ON profile_views (viewer_id, viewed_profile_id); + `); + + // Индекс для быстрого поиска по viewer_id + await client.query(` + CREATE INDEX profile_views_viewer_idx ON profile_views (viewer_id); + `); + + // Индекс для быстрого поиска по viewed_profile_id + await client.query(` + CREATE INDEX profile_views_viewed_idx ON profile_views (viewed_profile_id); + `); + + // Фиксируем транзакцию + await client.query('COMMIT'); + + console.log('Таблица profile_views успешно создана!'); + } catch (error) { + // В случае ошибки откатываем транзакцию + await client.query('ROLLBACK'); + console.error('Произошла ошибка при создании таблицы profile_views:', error); + } finally { + // Освобождаем клиента + client.release(); + } +} + +// Запускаем функцию создания таблицы +createProfileViewsTable() + .then(() => console.log('Скрипт выполнен')) + .catch(err => console.error('Ошибка выполнения скрипта:', err)) + .finally(() => process.exit()); diff --git a/scripts/testMatching.js b/scripts/testMatching.js new file mode 100644 index 0000000..3eaa217 --- /dev/null +++ b/scripts/testMatching.js @@ -0,0 +1,102 @@ +require('dotenv').config(); +const { MatchingService } = require('../dist/services/matchingService'); +const { ProfileService } = require('../dist/services/profileService'); + +// Функция для создания тестовых пользователей +async function createTestUsers() { + const profileService = new ProfileService(); + + console.log('Создание тестовых пользователей...'); + + // Создаем мужской профиль + const maleUserId = await profileService.ensureUser('123456', { + username: 'test_male', + first_name: 'Иван', + last_name: 'Тестов' + }); + + await profileService.createProfile(maleUserId, { + name: 'Иван', + age: 30, + gender: 'male', + interestedIn: 'female', + bio: 'Тестовый мужской профиль', + photos: ['photo1.jpg'], + city: 'Москва', + searchPreferences: { + minAge: 18, + maxAge: 45, + maxDistance: 50 + } + }); + console.log(`Создан мужской профиль: userId=${maleUserId}, telegramId=123456`); + + // Создаем женский профиль + const femaleUserId = await profileService.ensureUser('654321', { + username: 'test_female', + first_name: 'Анна', + last_name: 'Тестова' + }); + + await profileService.createProfile(femaleUserId, { + name: 'Анна', + age: 28, + gender: 'female', + interestedIn: 'male', + bio: 'Тестовый женский профиль', + photos: ['photo2.jpg'], + city: 'Москва', + searchPreferences: { + minAge: 25, + maxAge: 40, + maxDistance: 30 + } + }); + console.log(`Создан женский профиль: userId=${femaleUserId}, telegramId=654321`); + + console.log('Тестовые пользователи созданы успешно'); +} + +// Функция для тестирования подбора анкет +async function testMatching() { + console.log('\n===== ТЕСТИРОВАНИЕ ПОДБОРА АНКЕТ ====='); + + const matchingService = new MatchingService(); + + console.log('\nТест 1: Получение анкеты для мужского профиля (должна вернуться женская анкета)'); + const femaleProfile = await matchingService.getNextCandidate('123456', true); + if (femaleProfile) { + console.log(`✓ Получена анкета: ${femaleProfile.name}, возраст: ${femaleProfile.age}, пол: ${femaleProfile.gender}`); + } else { + console.log('✗ Анкета не найдена'); + } + + console.log('\nТест 2: Получение анкеты для женского профиля (должна вернуться мужская анкета)'); + const maleProfile = await matchingService.getNextCandidate('654321', true); + if (maleProfile) { + console.log(`✓ Получена анкета: ${maleProfile.name}, возраст: ${maleProfile.age}, пол: ${maleProfile.gender}`); + } else { + console.log('✗ Анкета не найдена'); + } + + console.log('\n===== ТЕСТИРОВАНИЕ ЗАВЕРШЕНО =====\n'); + + // Завершение работы скрипта + process.exit(0); +} + +// Главная функция +async function main() { + try { + // Создаем тестовых пользователей + await createTestUsers(); + + // Тестируем подбор анкет + await testMatching(); + } catch (error) { + console.error('Ошибка при выполнении тестов:', error); + process.exit(1); + } +} + +main(); diff --git a/scripts/testProfileViews.js b/scripts/testProfileViews.js new file mode 100644 index 0000000..55195b9 --- /dev/null +++ b/scripts/testProfileViews.js @@ -0,0 +1,98 @@ +// Тестирование работы с таблицей profile_views +const { Pool } = require('pg'); +require('dotenv').config(); + +const pool = new Pool({ + host: process.env.DB_HOST, + port: parseInt(process.env.DB_PORT || '5432'), + database: process.env.DB_NAME, + user: process.env.DB_USERNAME, + password: process.env.DB_PASSWORD +}); + +// Функция для тестирования записи просмотра профиля +async function testRecordProfileView(viewerId, viewedProfileId) { + const client = await pool.connect(); + try { + console.log(`Запись просмотра профиля: ${viewerId} просмотрел ${viewedProfileId}`); + + // Получаем UUID пользователей + const viewerResult = await client.query('SELECT id FROM users WHERE telegram_id = $1', [viewerId]); + if (viewerResult.rows.length === 0) { + console.log(`Пользователь с telegram_id ${viewerId} не найден, создаём нового пользователя`); + const newUserResult = await client.query(` + INSERT INTO users (telegram_id, username, first_name, last_name) + VALUES ($1, $2, $3, $4) RETURNING id + `, [viewerId, `user_${viewerId}`, `Имя ${viewerId}`, `Фамилия ${viewerId}`]); + + var viewerUuid = newUserResult.rows[0].id; + } else { + var viewerUuid = viewerResult.rows[0].id; + } + + const viewedResult = await client.query('SELECT id FROM users WHERE telegram_id = $1', [viewedProfileId]); + if (viewedResult.rows.length === 0) { + console.log(`Пользователь с telegram_id ${viewedProfileId} не найден, создаём нового пользователя`); + const newUserResult = await client.query(` + INSERT INTO users (telegram_id, username, first_name, last_name) + VALUES ($1, $2, $3, $4) RETURNING id + `, [viewedProfileId, `user_${viewedProfileId}`, `Имя ${viewedProfileId}`, `Фамилия ${viewedProfileId}`]); + + var viewedUuid = newUserResult.rows[0].id; + } else { + var viewedUuid = viewedResult.rows[0].id; + } + + console.log(`UUID просматривающего: ${viewerUuid}`); + console.log(`UUID просматриваемого: ${viewedUuid}`); + + // Записываем просмотр + await client.query(` + 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 view_date = NOW(), view_type = $3 + `, [viewerUuid, viewedUuid, 'browse']); + + console.log('Просмотр профиля успешно записан'); + + // Получаем список просмотренных профилей + const viewedProfiles = await client.query(` + SELECT v.viewed_profile_id, v.view_date, u.telegram_id + FROM profile_views v + JOIN users u ON u.id = v.viewed_profile_id + WHERE v.viewer_id = $1 + ORDER BY v.view_date DESC + `, [viewerUuid]); + + console.log('Список просмотренных профилей:'); + viewedProfiles.rows.forEach((row, index) => { + console.log(`${index + 1}. ID: ${row.telegram_id}, просмотрен: ${row.view_date}`); + }); + + return true; + } catch (error) { + console.error('Ошибка записи просмотра профиля:', error); + return false; + } finally { + client.release(); + } +} + +// Запускаем тест +async function runTest() { + try { + // Тестируем запись просмотра профиля + await testRecordProfileView(123456, 789012); + await testRecordProfileView(123456, 345678); + await testRecordProfileView(789012, 123456); + + console.log('Тесты завершены успешно'); + } catch (error) { + console.error('Ошибка при выполнении тестов:', error); + } finally { + await pool.end(); + } +} + +runTest(); diff --git a/src/services/matchingService.ts b/src/services/matchingService.ts index e064a44..6d54a21 100644 --- a/src/services/matchingService.ts +++ b/src/services/matchingService.ts @@ -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 { 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 { + 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 { 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 = ` diff --git a/src/services/profileService.ts b/src/services/profileService.ts index 6b4fcdb..fe35dd6 100644 --- a/src/services/profileService.ts +++ b/src/services/profileService.ts @@ -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 { 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 { 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 { + 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 []; + } + } } \ No newline at end of file