dev #3

Merged
trevor merged 4 commits from dev into main 2025-09-18 05:21:26 +00:00
26 changed files with 2132 additions and 154 deletions
Showing only changes of commit 85027a7747 - Show all commits

90
fixes.md Normal file
View File

@@ -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]);
```
## Примечание
После внесения исправлений рекомендуется проверить все остальные места в коде, где могут использоваться эти имена столбцов, и убедиться в их согласованности.

View File

@@ -0,0 +1,44 @@
import { MigrationBuilder, ColumnDefinitions } from 'node-pg-migrate';
export const shorthands: ColumnDefinitions | undefined = undefined;
export async function up(pgm: MigrationBuilder): Promise<void> {
// Создание таблицы 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<void> {
pgm.dropTable('profile_views', { cascade: true });
}

153
profile_views_patch.ts Normal file
View File

@@ -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<boolean> {
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<string[]> {
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<boolean> {
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;
}
}

66
scripts/checkDatabase.js Normal file
View File

@@ -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();

View File

@@ -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();

55
scripts/cleanDatabase.js Normal file
View File

@@ -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();

66
scripts/clearDatabase.js Normal file
View File

@@ -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();

81
scripts/clearDatabase.mjs Normal file
View File

@@ -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();

View File

@@ -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';

View File

@@ -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();

View File

@@ -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());

102
scripts/testMatching.js Normal file
View File

@@ -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();

View File

@@ -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();

View File

@@ -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 = `

View File

@@ -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 [];
}
}
}