Compare commits
2 Commits
bdd7d0424f
...
85027a7747
| Author | SHA1 | Date | |
|---|---|---|---|
| 85027a7747 | |||
| e275a9856b |
@@ -9,12 +9,12 @@ services:
|
|||||||
- db
|
- db
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
- DB_HOST=db
|
- DB_HOST={DB_HOST}
|
||||||
- DB_PORT=5432
|
- DB_PORT={DB_PORT}
|
||||||
- DB_NAME=telegram_tinder_bot
|
- DB_NAME={DB_NAME}
|
||||||
- DB_USERNAME=postgres
|
- DB_USERNAME={DB_USERNAME}
|
||||||
- DB_PASSWORD=${DB_PASSWORD}
|
- DB_PASSWORD={DB_PASSWORD}
|
||||||
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
|
- TELEGRAM_BOT_TOKEN={TELEGRAM_BOT_TOKEN}
|
||||||
volumes:
|
volumes:
|
||||||
- ./uploads:/app/uploads
|
- ./uploads:/app/uploads
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
90
fixes.md
Normal file
90
fixes.md
Normal 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]);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Примечание
|
||||||
|
|
||||||
|
После внесения исправлений рекомендуется проверить все остальные места в коде, где могут использоваться эти имена столбцов, и убедиться в их согласованности.
|
||||||
174
init-notifications-db.js
Normal file
174
init-notifications-db.js
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
require('dotenv').config();
|
||||||
|
const { Pool } = require('pg');
|
||||||
|
const { v4: uuidv4 } = require('uuid');
|
||||||
|
|
||||||
|
// Функция для запуска скрипта
|
||||||
|
async function initializeDatabase() {
|
||||||
|
console.log('Starting database initialization script...');
|
||||||
|
|
||||||
|
const pool = new Pool({
|
||||||
|
host: 'localhost', // Используем localhost
|
||||||
|
port: 5432, // Используем стандартный порт 5432
|
||||||
|
database: 'telegram_tinder_bot',
|
||||||
|
user: 'postgres',
|
||||||
|
password: '',
|
||||||
|
max: 5,
|
||||||
|
connectionTimeoutMillis: 5000
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('DB Connection Details:');
|
||||||
|
console.log('- Host: localhost');
|
||||||
|
console.log('- Port: 5432');
|
||||||
|
console.log('- Database: telegram_tinder_bot');
|
||||||
|
console.log('- User: postgres');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Проверяем подключение
|
||||||
|
console.log('Testing connection...');
|
||||||
|
const client = await pool.connect();
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('✅ Connected to database successfully!');
|
||||||
|
|
||||||
|
// 1. Создаем расширение для генерации UUID если его нет
|
||||||
|
console.log('Creating UUID extension...');
|
||||||
|
await client.query(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
|
||||||
|
|
||||||
|
// 2. Создаем таблицу для шаблонов уведомлений
|
||||||
|
console.log('Creating notification_templates table...');
|
||||||
|
await client.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS notification_templates (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
type VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
message_template TEXT NOT NULL,
|
||||||
|
button_template JSONB NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// 3. Вставляем базовые шаблоны
|
||||||
|
console.log('Inserting notification templates...');
|
||||||
|
|
||||||
|
const templates = [
|
||||||
|
{
|
||||||
|
id: uuidv4(),
|
||||||
|
type: 'new_like',
|
||||||
|
title: 'Новый лайк!',
|
||||||
|
message_template: '❤️ *{{name}}* поставил(а) вам лайк!\n\nВозраст: {{age}}\n{{city}}\n\nОтветьте взаимностью или посмотрите профиль.',
|
||||||
|
button_template: JSON.stringify({
|
||||||
|
inline_keyboard: [
|
||||||
|
[{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }],
|
||||||
|
[
|
||||||
|
{ text: '❤️ Лайк в ответ', callback_data: 'like_back:{{userId}}' },
|
||||||
|
{ text: '⛔️ Пропустить', callback_data: 'dislike_profile:{{userId}}' }
|
||||||
|
],
|
||||||
|
[{ text: '💕 Открыть все лайки', callback_data: 'view_likes' }]
|
||||||
|
]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: uuidv4(),
|
||||||
|
type: 'super_like',
|
||||||
|
title: 'Супер-лайк!',
|
||||||
|
message_template: '⭐️ *{{name}}* отправил(а) вам супер-лайк!\n\nВозраст: {{age}}\n{{city}}\n\nВы произвели особое впечатление! Ответьте взаимностью или посмотрите профиль.',
|
||||||
|
button_template: JSON.stringify({
|
||||||
|
inline_keyboard: [
|
||||||
|
[{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }],
|
||||||
|
[
|
||||||
|
{ text: '❤️ Лайк в ответ', callback_data: 'like_back:{{userId}}' },
|
||||||
|
{ text: '⛔️ Пропустить', callback_data: 'dislike_profile:{{userId}}' }
|
||||||
|
],
|
||||||
|
[{ text: '💕 Открыть все лайки', callback_data: 'view_likes' }]
|
||||||
|
]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: uuidv4(),
|
||||||
|
type: 'new_match',
|
||||||
|
title: 'Новый матч!',
|
||||||
|
message_template: '🎊 *Ура! Это взаимно!* 🎊\n\nВы и *{{name}}* понравились друг другу!\nВозраст: {{age}}\n{{city}}\n\nСделайте первый шаг - напишите сообщение!',
|
||||||
|
button_template: JSON.stringify({
|
||||||
|
inline_keyboard: [
|
||||||
|
[{ text: '💬 Начать общение', callback_data: 'open_native_chat_{{matchId}}' }],
|
||||||
|
[
|
||||||
|
{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' },
|
||||||
|
{ text: '📋 Все матчи', callback_data: 'native_chats' }
|
||||||
|
]
|
||||||
|
]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: uuidv4(),
|
||||||
|
type: 'new_message',
|
||||||
|
title: 'Новое сообщение!',
|
||||||
|
message_template: '💌 *Новое сообщение!*\n\nОт: *{{name}}*\n\n"{{message}}"\n\nОтветьте на сообщение прямо сейчас!',
|
||||||
|
button_template: JSON.stringify({
|
||||||
|
inline_keyboard: [
|
||||||
|
[{ text: '📩 Ответить', callback_data: 'open_native_chat_{{matchId}}' }],
|
||||||
|
[
|
||||||
|
{ text: '👤 Профиль', callback_data: 'view_profile:{{userId}}' },
|
||||||
|
{ text: '📋 Все чаты', callback_data: 'native_chats' }
|
||||||
|
]
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Вставляем шаблоны с проверкой на конфликты
|
||||||
|
for (const template of templates) {
|
||||||
|
await client.query(`
|
||||||
|
INSERT INTO notification_templates
|
||||||
|
(id, type, title, message_template, button_template, created_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, NOW())
|
||||||
|
ON CONFLICT (type) DO UPDATE
|
||||||
|
SET title = $3,
|
||||||
|
message_template = $4,
|
||||||
|
button_template = $5
|
||||||
|
`, [template.id, template.type, template.title, template.message_template, template.button_template]);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Notification templates created/updated successfully');
|
||||||
|
|
||||||
|
// 4. Создаем таблицу для хранения логов уведомлений если её нет
|
||||||
|
console.log('Creating notifications table...');
|
||||||
|
await client.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS notifications (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
user_id UUID NOT NULL,
|
||||||
|
type VARCHAR(50) NOT NULL,
|
||||||
|
data JSONB NOT NULL,
|
||||||
|
is_read BOOLEAN DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log('✅ Notifications table created successfully');
|
||||||
|
|
||||||
|
// 5. Проверяем, что таблицы созданы
|
||||||
|
const tablesResult = await client.query(`
|
||||||
|
SELECT table_name
|
||||||
|
FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name IN ('notification_templates', 'notifications')
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log('Created tables:');
|
||||||
|
tablesResult.rows.forEach(row => {
|
||||||
|
console.log(`- ${row.table_name}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
await pool.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Database initialization completed successfully');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Database initialization error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запускаем скрипт
|
||||||
|
initializeDatabase();
|
||||||
44
migrations/1631980000000_add_profile_views_table.ts
Normal file
44
migrations/1631980000000_add_profile_views_table.ts
Normal 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 });
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/* eslint-disable camelcase */
|
||||||
|
|
||||||
|
exports.shorthands = undefined;
|
||||||
|
|
||||||
|
exports.up = pgm => {
|
||||||
|
// Проверяем существование таблицы scheduled_notifications
|
||||||
|
pgm.sql(`
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.tables
|
||||||
|
WHERE table_name = 'scheduled_notifications'
|
||||||
|
) THEN
|
||||||
|
-- Проверяем, нет ли уже столбца processed
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name = 'scheduled_notifications' AND column_name = 'processed'
|
||||||
|
) THEN
|
||||||
|
-- Добавляем столбец processed
|
||||||
|
ALTER TABLE scheduled_notifications ADD COLUMN processed BOOLEAN DEFAULT FALSE;
|
||||||
|
END IF;
|
||||||
|
ELSE
|
||||||
|
-- Создаем таблицу, если она не существует
|
||||||
|
CREATE TABLE scheduled_notifications (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
user_id UUID REFERENCES users(id),
|
||||||
|
type VARCHAR(50) NOT NULL,
|
||||||
|
data JSONB,
|
||||||
|
scheduled_at TIMESTAMP NOT NULL,
|
||||||
|
processed BOOLEAN DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
`);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = pgm => {
|
||||||
|
pgm.sql(`
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name = 'scheduled_notifications' AND column_name = 'processed'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE scheduled_notifications DROP COLUMN processed;
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
`);
|
||||||
|
};
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
"premium:direct": "ts-node src/scripts/setPremiumDirectConnect.ts",
|
"premium:direct": "ts-node src/scripts/setPremiumDirectConnect.ts",
|
||||||
"db:info": "ts-node src/scripts/getDatabaseInfo.ts",
|
"db:info": "ts-node src/scripts/getDatabaseInfo.ts",
|
||||||
"db:test-data": "ts-node src/scripts/createTestData.ts",
|
"db:test-data": "ts-node src/scripts/createTestData.ts",
|
||||||
|
"enhance-notifications": "ts-node src/scripts/enhanceNotifications.ts",
|
||||||
"update": "bash ./bin/update.sh",
|
"update": "bash ./bin/update.sh",
|
||||||
"update:win": ".\\bin\\update.bat",
|
"update:win": ".\\bin\\update.bat",
|
||||||
"start:sh": "bash ./bin/start_bot.sh"
|
"start:sh": "bash ./bin/start_bot.sh"
|
||||||
|
|||||||
153
profile_views_patch.ts
Normal file
153
profile_views_patch.ts
Normal 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
66
scripts/checkDatabase.js
Normal 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();
|
||||||
64
scripts/checkProfileViews.js
Normal file
64
scripts/checkProfileViews.js
Normal 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
55
scripts/cleanDatabase.js
Normal 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
66
scripts/clearDatabase.js
Normal 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
81
scripts/clearDatabase.mjs
Normal 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();
|
||||||
26
scripts/clear_database.sql
Normal file
26
scripts/clear_database.sql
Normal 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';
|
||||||
86
scripts/createProfileViewsTable.js
Normal file
86
scripts/createProfileViewsTable.js
Normal 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();
|
||||||
70
scripts/createProfileViewsTable.ts
Normal file
70
scripts/createProfileViewsTable.ts
Normal 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
102
scripts/testMatching.js
Normal 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();
|
||||||
98
scripts/testProfileViews.js
Normal file
98
scripts/testProfileViews.js
Normal 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();
|
||||||
@@ -10,6 +10,7 @@ import { VipController } from '../controllers/vipController';
|
|||||||
import { VipService } from '../services/vipService';
|
import { VipService } from '../services/vipService';
|
||||||
import { TranslationController } from '../controllers/translationController';
|
import { TranslationController } from '../controllers/translationController';
|
||||||
import { t } from '../services/localizationService';
|
import { t } from '../services/localizationService';
|
||||||
|
import { LikeBackHandler } from './likeBackHandler';
|
||||||
|
|
||||||
export class CallbackHandlers {
|
export class CallbackHandlers {
|
||||||
private bot: TelegramBot;
|
private bot: TelegramBot;
|
||||||
@@ -22,6 +23,7 @@ export class CallbackHandlers {
|
|||||||
private vipController: VipController;
|
private vipController: VipController;
|
||||||
private vipService: VipService;
|
private vipService: VipService;
|
||||||
private translationController: TranslationController;
|
private translationController: TranslationController;
|
||||||
|
private likeBackHandler: LikeBackHandler;
|
||||||
|
|
||||||
constructor(bot: TelegramBot, messageHandlers: MessageHandlers) {
|
constructor(bot: TelegramBot, messageHandlers: MessageHandlers) {
|
||||||
this.bot = bot;
|
this.bot = bot;
|
||||||
@@ -34,6 +36,7 @@ export class CallbackHandlers {
|
|||||||
this.vipController = new VipController(bot);
|
this.vipController = new VipController(bot);
|
||||||
this.vipService = new VipService();
|
this.vipService = new VipService();
|
||||||
this.translationController = new TranslationController();
|
this.translationController = new TranslationController();
|
||||||
|
this.likeBackHandler = new LikeBackHandler(bot);
|
||||||
}
|
}
|
||||||
|
|
||||||
register(): void {
|
register(): void {
|
||||||
@@ -167,6 +170,12 @@ export class CallbackHandlers {
|
|||||||
await this.handleMorePhotos(chatId, telegramId, targetUserId);
|
await this.handleMorePhotos(chatId, telegramId, targetUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Обработка лайков и ответных лайков из уведомлений
|
||||||
|
else if (data.startsWith('like_back:')) {
|
||||||
|
const targetUserId = data.replace('like_back:', '');
|
||||||
|
await this.likeBackHandler.handleLikeBack(chatId, telegramId, targetUserId);
|
||||||
|
}
|
||||||
|
|
||||||
// Матчи и чаты
|
// Матчи и чаты
|
||||||
else if (data === 'view_matches') {
|
else if (data === 'view_matches') {
|
||||||
await this.handleViewMatches(chatId, telegramId);
|
await this.handleViewMatches(chatId, telegramId);
|
||||||
@@ -385,9 +394,15 @@ export class CallbackHandlers {
|
|||||||
await this.bot.sendMessage(chatId, '👍 Лайк отправлен!');
|
await this.bot.sendMessage(chatId, '👍 Лайк отправлен!');
|
||||||
await this.showNextCandidate(chatId, telegramId);
|
await this.showNextCandidate(chatId, telegramId);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
await this.bot.sendMessage(chatId, '❌ Ошибка при отправке лайка');
|
// Проверяем, связана ли ошибка с уже существующим свайпом
|
||||||
console.error('Like error:', error);
|
if (error.message === 'Already swiped this profile' || error.code === 'ALREADY_SWIPED') {
|
||||||
|
await this.bot.sendMessage(chatId, '❓ Вы уже оценивали этот профиль ранее');
|
||||||
|
await this.showNextCandidate(chatId, telegramId);
|
||||||
|
} else {
|
||||||
|
await this.bot.sendMessage(chatId, '❌ Ошибка при отправке лайка');
|
||||||
|
console.error('Like error:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,9 +417,15 @@ export class CallbackHandlers {
|
|||||||
|
|
||||||
await this.matchingService.performSwipe(telegramId, targetTelegramId, 'pass');
|
await this.matchingService.performSwipe(telegramId, targetTelegramId, 'pass');
|
||||||
await this.showNextCandidate(chatId, telegramId);
|
await this.showNextCandidate(chatId, telegramId);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
await this.bot.sendMessage(chatId, '❌ Ошибка при отправке дизлайка');
|
// Проверяем, связана ли ошибка с уже существующим свайпом
|
||||||
console.error('Dislike error:', error);
|
if (error.message === 'Already swiped this profile' || error.code === 'ALREADY_SWIPED') {
|
||||||
|
await this.bot.sendMessage(chatId, '❓ Вы уже оценивали этот профиль ранее');
|
||||||
|
await this.showNextCandidate(chatId, telegramId);
|
||||||
|
} else {
|
||||||
|
await this.bot.sendMessage(chatId, '❌ Ошибка при отправке дизлайка');
|
||||||
|
console.error('Dislike error:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,9 +464,73 @@ export class CallbackHandlers {
|
|||||||
await this.bot.sendMessage(chatId, '💖 Супер лайк отправлен!');
|
await this.bot.sendMessage(chatId, '💖 Супер лайк отправлен!');
|
||||||
await this.showNextCandidate(chatId, telegramId);
|
await this.showNextCandidate(chatId, telegramId);
|
||||||
}
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
// Проверяем, связана ли ошибка с уже существующим свайпом
|
||||||
|
if (error.message === 'Already swiped this profile' || error.code === 'ALREADY_SWIPED') {
|
||||||
|
await this.bot.sendMessage(chatId, '❓ Вы уже оценивали этот профиль ранее');
|
||||||
|
await this.showNextCandidate(chatId, telegramId);
|
||||||
|
} else {
|
||||||
|
await this.bot.sendMessage(chatId, '❌ Ошибка при отправке супер лайка');
|
||||||
|
console.error('Superlike error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработка обратного лайка из уведомления
|
||||||
|
async handleLikeBack(chatId: number, telegramId: string, targetUserId: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Получаем информацию о пользователях
|
||||||
|
const targetProfile = await this.profileService.getProfileByUserId(targetUserId);
|
||||||
|
|
||||||
|
if (!targetProfile) {
|
||||||
|
await this.bot.sendMessage(chatId, '❌ Не удалось найти профиль');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем telegram ID целевого пользователя для свайпа
|
||||||
|
const targetTelegramId = await this.profileService.getTelegramIdByUserId(targetUserId);
|
||||||
|
if (!targetTelegramId) {
|
||||||
|
await this.bot.sendMessage(chatId, '❌ Не удалось найти пользователя');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Выполняем свайп
|
||||||
|
const result = await this.matchingService.performSwipe(telegramId, targetTelegramId, 'like');
|
||||||
|
|
||||||
|
if (result.isMatch) {
|
||||||
|
// Это матч!
|
||||||
|
await this.bot.sendMessage(
|
||||||
|
chatId,
|
||||||
|
'🎉 *Поздравляем! Это взаимно!*\n\n' +
|
||||||
|
`Вы и *${targetProfile.name}* понравились друг другу!\n` +
|
||||||
|
'Теперь вы можете начать общение.',
|
||||||
|
{
|
||||||
|
parse_mode: 'Markdown',
|
||||||
|
reply_markup: {
|
||||||
|
inline_keyboard: [
|
||||||
|
[{ text: '💬 Начать общение', callback_data: `start_chat:${targetUserId}` }],
|
||||||
|
[{ text: '👀 Посмотреть профиль', callback_data: `view_profile:${targetUserId}` }]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await this.bot.sendMessage(
|
||||||
|
chatId,
|
||||||
|
'❤️ Вам понравился профиль ' + targetProfile.name + '!\n\n' +
|
||||||
|
'Если вы также понравитесь этому пользователю, будет создан матч.',
|
||||||
|
{
|
||||||
|
reply_markup: {
|
||||||
|
inline_keyboard: [
|
||||||
|
[{ text: '🔍 Продолжить поиск', callback_data: 'start_browsing' }]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await this.bot.sendMessage(chatId, '❌ Ошибка при отправке супер лайка');
|
console.error('Error in handleLikeBack:', error);
|
||||||
console.error('Superlike error:', error);
|
await this.bot.sendMessage(chatId, '❌ Произошла ошибка при обработке лайка');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,9 +561,28 @@ export class CallbackHandlers {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 1; i < targetProfile.photos.length; i++) {
|
// Отправляем фотографии в виде медиа-группы (коллажа)
|
||||||
const photoFileId = targetProfile.photos[i];
|
// Создаем массив объектов медиа для группового отправления
|
||||||
await this.bot.sendPhoto(chatId, photoFileId);
|
const mediaGroup = targetProfile.photos.slice(1).map((photoFileId, index) => ({
|
||||||
|
type: 'photo' as const,
|
||||||
|
media: photoFileId,
|
||||||
|
caption: index === 0 ? `📸 Дополнительные фото ${targetProfile.name}` : undefined
|
||||||
|
}));
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Отправляем все фото одним сообщением (медиа-группой)
|
||||||
|
await this.bot.sendMediaGroup(chatId, mediaGroup);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending media group:', error);
|
||||||
|
|
||||||
|
// Если не получилось отправить медиа-группой, отправляем по одной
|
||||||
|
for (let i = 1; i < targetProfile.photos.length; i++) {
|
||||||
|
try {
|
||||||
|
await this.bot.sendPhoto(chatId, targetProfile.photos[i]);
|
||||||
|
} catch (photoError) {
|
||||||
|
console.error(`Error sending photo ${i}:`, photoError);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyboard: InlineKeyboardMarkup = {
|
const keyboard: InlineKeyboardMarkup = {
|
||||||
@@ -807,6 +911,7 @@ export class CallbackHandlers {
|
|||||||
|
|
||||||
// Вспомогательные методы
|
// Вспомогательные методы
|
||||||
async showProfile(chatId: number, profile: Profile, isOwner: boolean = false, viewerId?: string): Promise<void> {
|
async showProfile(chatId: number, profile: Profile, isOwner: boolean = false, viewerId?: string): Promise<void> {
|
||||||
|
const hasMultiplePhotos = profile.photos.length > 1;
|
||||||
const mainPhotoFileId = profile.photos[0]; // Первое фото - главное
|
const mainPhotoFileId = profile.photos[0]; // Первое фото - главное
|
||||||
|
|
||||||
let profileText = '👤 ' + profile.name + ', ' + profile.age + '\n';
|
let profileText = '👤 ' + profile.name + ', ' + profile.age + '\n';
|
||||||
@@ -876,20 +981,43 @@ export class CallbackHandlers {
|
|||||||
|
|
||||||
if (hasValidPhoto) {
|
if (hasValidPhoto) {
|
||||||
try {
|
try {
|
||||||
await this.bot.sendPhoto(chatId, mainPhotoFileId, {
|
if (hasMultiplePhotos) {
|
||||||
caption: profileText,
|
// Если есть несколько фото, отправляем их как медиа-группу (коллаж)
|
||||||
reply_markup: keyboard
|
const mediaGroup = profile.photos.map((photoFileId, index) => ({
|
||||||
});
|
type: 'photo' as const,
|
||||||
|
media: photoFileId,
|
||||||
|
caption: index === 0 ? profileText : undefined,
|
||||||
|
parse_mode: index === 0 ? 'Markdown' as const : undefined
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Сначала отправляем медиа-группу
|
||||||
|
await this.bot.sendMediaGroup(chatId, mediaGroup);
|
||||||
|
|
||||||
|
// Затем отправляем отдельное сообщение с кнопками
|
||||||
|
await this.bot.sendMessage(chatId, '📸 Выберите действие:', {
|
||||||
|
reply_markup: keyboard
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Если только одно фото, отправляем его с текстом
|
||||||
|
await this.bot.sendPhoto(chatId, mainPhotoFileId, {
|
||||||
|
caption: profileText,
|
||||||
|
reply_markup: keyboard,
|
||||||
|
parse_mode: 'Markdown'
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('Error sending profile photos:', error);
|
||||||
// Если не удалось отправить фото, отправляем текст
|
// Если не удалось отправить фото, отправляем текст
|
||||||
await this.bot.sendMessage(chatId, '🖼 Фото недоступно\n\n' + profileText, {
|
await this.bot.sendMessage(chatId, '🖼 Фото недоступно\n\n' + profileText, {
|
||||||
reply_markup: keyboard
|
reply_markup: keyboard,
|
||||||
|
parse_mode: 'Markdown'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Отправляем как текстовое сообщение
|
// Отправляем как текстовое сообщение
|
||||||
await this.bot.sendMessage(chatId, profileText, {
|
await this.bot.sendMessage(chatId, profileText, {
|
||||||
reply_markup: keyboard
|
reply_markup: keyboard,
|
||||||
|
parse_mode: 'Markdown'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,25 +168,24 @@ export class EnhancedChatHandlers {
|
|||||||
|
|
||||||
// ===== СИСТЕМА УВЕДОМЛЕНИЙ =====
|
// ===== СИСТЕМА УВЕДОМЛЕНИЙ =====
|
||||||
|
|
||||||
// Отправить уведомление о новом сообщении
|
// Отправить уведомление о новом сообщении - теперь используем NotificationService
|
||||||
async sendMessageNotification(receiverTelegramId: string, senderName: string, messagePreview: string, matchId: string): Promise<void> {
|
async sendMessageNotification(receiverTelegramId: string, senderName: string, messagePreview: string, matchId: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const receiverChatId = parseInt(receiverTelegramId);
|
// Получаем идентификаторы пользователей для использования в NotificationService
|
||||||
|
const receiverUserId = await this.profileService.getUserIdByTelegramId(receiverTelegramId);
|
||||||
|
const sender = await this.chatService.getMatchInfo(matchId, receiverTelegramId);
|
||||||
|
|
||||||
await this.bot.sendMessage(
|
if (!receiverUserId || !sender?.otherUserId) {
|
||||||
receiverChatId,
|
console.error('Failed to get user IDs for notification');
|
||||||
`💌 *Новое сообщение от ${senderName}*\n\n` +
|
return;
|
||||||
`"${this.escapeMarkdown(messagePreview)}"\n\n` +
|
}
|
||||||
'👆 Нажмите "Открыть чат" для ответа',
|
|
||||||
{
|
// Используем сервис уведомлений для отправки более красивого уведомления
|
||||||
parse_mode: 'Markdown',
|
await this.notificationService.sendMessageNotification(
|
||||||
reply_markup: {
|
receiverUserId,
|
||||||
inline_keyboard: [
|
sender.otherUserId,
|
||||||
[{ text: '💬 Открыть чат', callback_data: `open_native_chat_${matchId}` }],
|
messagePreview,
|
||||||
[{ text: '📱 Все чаты', callback_data: 'native_chats' }]
|
matchId
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error sending message notification:', error);
|
console.error('Error sending message notification:', error);
|
||||||
|
|||||||
76
src/handlers/likeBackHandler.ts
Normal file
76
src/handlers/likeBackHandler.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import TelegramBot from 'node-telegram-bot-api';
|
||||||
|
import { ProfileService } from '../services/profileService';
|
||||||
|
import { MatchingService } from '../services/matchingService';
|
||||||
|
|
||||||
|
export class LikeBackHandler {
|
||||||
|
private bot: TelegramBot;
|
||||||
|
private profileService: ProfileService;
|
||||||
|
private matchingService: MatchingService;
|
||||||
|
|
||||||
|
constructor(bot: TelegramBot) {
|
||||||
|
this.bot = bot;
|
||||||
|
this.profileService = new ProfileService();
|
||||||
|
this.matchingService = new MatchingService();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для обработки обратного лайка из уведомления
|
||||||
|
async handleLikeBack(chatId: number, telegramId: string, targetUserId: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Получаем информацию о пользователях
|
||||||
|
const [userId, targetProfile] = await Promise.all([
|
||||||
|
this.profileService.getUserIdByTelegramId(telegramId),
|
||||||
|
this.profileService.getProfileByUserId(targetUserId)
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!userId || !targetProfile) {
|
||||||
|
await this.bot.sendMessage(chatId, '❌ Не удалось найти профиль');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, есть ли уже свайп
|
||||||
|
const existingSwipe = await this.matchingService.getSwipeBetweenUsers(userId, targetUserId);
|
||||||
|
if (existingSwipe) {
|
||||||
|
await this.bot.sendMessage(chatId, '❓ Вы уже оценили этот профиль ранее.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем свайп (лайк)
|
||||||
|
const result = await this.matchingService.createSwipe(userId, targetUserId, 'like');
|
||||||
|
|
||||||
|
if (result.isMatch) {
|
||||||
|
// Это матч!
|
||||||
|
await this.bot.sendMessage(
|
||||||
|
chatId,
|
||||||
|
'🎉 *Поздравляем! Это взаимно!*\n\n' +
|
||||||
|
`Вы и *${targetProfile.name}* понравились друг другу!\n` +
|
||||||
|
'Теперь вы можете начать общение.',
|
||||||
|
{
|
||||||
|
parse_mode: 'Markdown',
|
||||||
|
reply_markup: {
|
||||||
|
inline_keyboard: [
|
||||||
|
[{ text: '💬 Начать общение', callback_data: `start_chat:${targetUserId}` }],
|
||||||
|
[{ text: '👀 Посмотреть профиль', callback_data: `view_profile:${targetUserId}` }]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await this.bot.sendMessage(
|
||||||
|
chatId,
|
||||||
|
'❤️ Вам понравился профиль ' + targetProfile.name + '!\n\n' +
|
||||||
|
'Если вы также понравитесь этому пользователю, будет создан матч.',
|
||||||
|
{
|
||||||
|
reply_markup: {
|
||||||
|
inline_keyboard: [
|
||||||
|
[{ text: '🔍 Продолжить поиск', callback_data: 'start_browsing' }]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in handleLikeBack:', error);
|
||||||
|
await this.bot.sendMessage(chatId, '❌ Произошла ошибка при обработке лайка');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
133
src/scripts/enhanceNotifications.ts
Normal file
133
src/scripts/enhanceNotifications.ts
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import { query } from '../database/connection';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
|
// Загружаем переменные окружения
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Скрипт для обновления механизма уведомлений
|
||||||
|
*/
|
||||||
|
export async function enhanceNotifications() {
|
||||||
|
try {
|
||||||
|
console.log('Enhancing notifications system...');
|
||||||
|
console.log('DB Connection Details:');
|
||||||
|
console.log(`- Host: ${process.env.DB_HOST}`);
|
||||||
|
console.log(`- Port: ${process.env.DB_PORT}`);
|
||||||
|
console.log(`- Database: ${process.env.DB_NAME}`);
|
||||||
|
|
||||||
|
// 1. Создаем расширение для генерации UUID, если его нет
|
||||||
|
await query(`
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp"
|
||||||
|
`);
|
||||||
|
|
||||||
|
// 2. Создаем таблицу для хранения типов уведомлений и шаблонов сообщений, если её нет
|
||||||
|
await query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS notification_templates (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
type VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
message_template TEXT NOT NULL,
|
||||||
|
button_template JSONB NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// 2. Вставляем базовые шаблоны для различных типов уведомлений
|
||||||
|
const templates = [
|
||||||
|
{
|
||||||
|
id: uuidv4(),
|
||||||
|
type: 'new_like',
|
||||||
|
title: 'Новый лайк!',
|
||||||
|
message_template: '❤️ *{{name}}* поставил(а) вам лайк!\n\nВозраст: {{age}}\n{{city}}\n\nОтветьте взаимностью или посмотрите профиль.',
|
||||||
|
button_template: JSON.stringify({
|
||||||
|
inline_keyboard: [
|
||||||
|
[{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }],
|
||||||
|
[
|
||||||
|
{ text: '❤️ Лайк в ответ', callback_data: 'like_back:{{userId}}' },
|
||||||
|
{ text: '⛔️ Пропустить', callback_data: 'dislike_profile:{{userId}}' }
|
||||||
|
],
|
||||||
|
[{ text: '💕 Открыть все лайки', callback_data: 'view_likes' }]
|
||||||
|
]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: uuidv4(),
|
||||||
|
type: 'super_like',
|
||||||
|
title: 'Супер-лайк!',
|
||||||
|
message_template: '⭐️ *{{name}}* отправил(а) вам супер-лайк!\n\nВозраст: {{age}}\n{{city}}\n\nВы произвели особое впечатление! Ответьте взаимностью или посмотрите профиль.',
|
||||||
|
button_template: JSON.stringify({
|
||||||
|
inline_keyboard: [
|
||||||
|
[{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }],
|
||||||
|
[
|
||||||
|
{ text: '❤️ Лайк в ответ', callback_data: 'like_back:{{userId}}' },
|
||||||
|
{ text: '⛔️ Пропустить', callback_data: 'dislike_profile:{{userId}}' }
|
||||||
|
],
|
||||||
|
[{ text: '💕 Открыть все лайки', callback_data: 'view_likes' }]
|
||||||
|
]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: uuidv4(),
|
||||||
|
type: 'new_match',
|
||||||
|
title: 'Новый матч!',
|
||||||
|
message_template: '🎊 *Ура! Это взаимно!* 🎊\n\nВы и *{{name}}* понравились друг другу!\nВозраст: {{age}}\n{{city}}\n\nСделайте первый шаг - напишите сообщение!',
|
||||||
|
button_template: JSON.stringify({
|
||||||
|
inline_keyboard: [
|
||||||
|
[{ text: '💬 Начать общение', callback_data: 'open_native_chat_{{matchId}}' }],
|
||||||
|
[
|
||||||
|
{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' },
|
||||||
|
{ text: '📋 Все матчи', callback_data: 'native_chats' }
|
||||||
|
]
|
||||||
|
]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: uuidv4(),
|
||||||
|
type: 'new_message',
|
||||||
|
title: 'Новое сообщение!',
|
||||||
|
message_template: '💌 *Новое сообщение!*\n\nОт: *{{name}}*\n\n"{{message}}"\n\nОтветьте на сообщение прямо сейчас!',
|
||||||
|
button_template: JSON.stringify({
|
||||||
|
inline_keyboard: [
|
||||||
|
[{ text: '📩 Ответить', callback_data: 'open_native_chat_{{matchId}}' }],
|
||||||
|
[
|
||||||
|
{ text: '👤 Профиль', callback_data: 'view_profile:{{userId}}' },
|
||||||
|
{ text: '📋 Все чаты', callback_data: 'native_chats' }
|
||||||
|
]
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Вставляем шаблоны с проверкой на конфликты
|
||||||
|
for (const template of templates) {
|
||||||
|
await query(`
|
||||||
|
INSERT INTO notification_templates
|
||||||
|
(id, type, title, message_template, button_template, created_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, NOW())
|
||||||
|
ON CONFLICT (type) DO UPDATE
|
||||||
|
SET title = $3,
|
||||||
|
message_template = $4,
|
||||||
|
button_template = $5
|
||||||
|
`, [template.id, template.type, template.title, template.message_template, template.button_template]);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Notification templates updated successfully');
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error enhancing notifications:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если запускаем файл напрямую
|
||||||
|
if (require.main === module) {
|
||||||
|
enhanceNotifications().then(() => {
|
||||||
|
console.log('Notification system enhancement completed');
|
||||||
|
process.exit(0);
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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<{
|
async performSwipe(telegramId: string, targetTelegramId: string, swipeType: SwipeType): Promise<{
|
||||||
swipe: Swipe;
|
swipe: Swipe;
|
||||||
@@ -63,22 +45,21 @@ export class MatchingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const swipeId = uuidv4();
|
const swipeId = uuidv4();
|
||||||
const direction = this.convertSwipeTypeToDirection(swipeType);
|
|
||||||
let isMatch = false;
|
let isMatch = false;
|
||||||
let match: Match | undefined;
|
let match: Match | undefined;
|
||||||
|
|
||||||
await transaction(async (client) => {
|
await transaction(async (client) => {
|
||||||
// Создаем свайп
|
// Создаем свайп
|
||||||
await client.query(`
|
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)
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
`, [swipeId, userId, targetUserId, direction, new Date()]);
|
`, [swipeId, userId, targetUserId, swipeType, new Date()]);
|
||||||
|
|
||||||
// Если это лайк или суперлайк, проверяем взаимность
|
// Если это лайк или суперлайк, проверяем взаимность
|
||||||
if (swipeType === 'like' || swipeType === 'superlike') {
|
if (swipeType === 'like' || swipeType === 'superlike') {
|
||||||
const reciprocalSwipe = await client.query(`
|
const reciprocalSwipe = await client.query(`
|
||||||
SELECT * FROM swipes
|
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]);
|
`, [targetUserId, userId]);
|
||||||
|
|
||||||
if (reciprocalSwipe.rows.length > 0) {
|
if (reciprocalSwipe.rows.length > 0) {
|
||||||
@@ -91,7 +72,7 @@ export class MatchingService {
|
|||||||
if (existingMatch.rows.length === 0) {
|
if (existingMatch.rows.length === 0) {
|
||||||
isMatch = true;
|
isMatch = true;
|
||||||
const matchId = uuidv4();
|
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];
|
const [user1Id, user2Id] = userId < targetUserId ? [userId, targetUserId] : [targetUserId, userId];
|
||||||
@@ -152,6 +133,89 @@ export class MatchingService {
|
|||||||
|
|
||||||
return this.mapEntityToSwipe(result.rows[0]);
|
return this.mapEntityToSwipe(result.rows[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Получить свайп между двумя пользователями (псевдоним для getSwipe)
|
||||||
|
async getSwipeBetweenUsers(userId: string, targetUserId: string): Promise<Swipe | null> {
|
||||||
|
return this.getSwipe(userId, targetUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создать свайп (лайк, дислайк или суперлайк)
|
||||||
|
async createSwipe(userId: string, targetUserId: string, swipeType: SwipeType): Promise<{
|
||||||
|
swipe: Swipe;
|
||||||
|
isMatch: boolean;
|
||||||
|
match?: Match;
|
||||||
|
}> {
|
||||||
|
const swipeId = uuidv4();
|
||||||
|
let isMatch = false;
|
||||||
|
let match: Match | undefined;
|
||||||
|
|
||||||
|
await transaction(async (client) => {
|
||||||
|
// Создаем свайп
|
||||||
|
await client.query(`
|
||||||
|
INSERT INTO swipes (id, user_id, target_user_id, type, created_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
|
`, [swipeId, userId, targetUserId, swipeType, new Date()]);
|
||||||
|
|
||||||
|
// Если это лайк или суперлайк, проверяем взаимность
|
||||||
|
if (swipeType === 'like' || swipeType === 'superlike') {
|
||||||
|
const reciprocalSwipe = await client.query(`
|
||||||
|
SELECT * FROM swipes
|
||||||
|
WHERE user_id = $1 AND target_user_id = $2 AND type IN ('like', 'superlike')
|
||||||
|
`, [targetUserId, userId]);
|
||||||
|
|
||||||
|
if (reciprocalSwipe.rows.length > 0) {
|
||||||
|
// Проверяем, что матч еще не существует
|
||||||
|
const existingMatch = await client.query(`
|
||||||
|
SELECT * FROM matches
|
||||||
|
WHERE (user_id_1 = $1 AND user_id_2 = $2) OR (user_id_1 = $2 AND user_id_2 = $1)
|
||||||
|
`, [userId, targetUserId]);
|
||||||
|
|
||||||
|
if (existingMatch.rows.length === 0) {
|
||||||
|
isMatch = true;
|
||||||
|
const matchId = uuidv4();
|
||||||
|
const isSuperMatch = swipeType === 'superlike' || reciprocalSwipe.rows[0].type === 'superlike';
|
||||||
|
|
||||||
|
// Упорядочиваем пользователей для консистентности
|
||||||
|
const [user1Id, user2Id] = userId < targetUserId ? [userId, targetUserId] : [targetUserId, userId];
|
||||||
|
|
||||||
|
// Создаем матч
|
||||||
|
await client.query(`
|
||||||
|
INSERT INTO matches (id, user_id_1, user_id_2, created_at, is_active, is_super_match)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6)
|
||||||
|
`, [matchId, user1Id, user2Id, new Date(), true, isSuperMatch]);
|
||||||
|
|
||||||
|
match = new Match({
|
||||||
|
id: matchId,
|
||||||
|
userId1: user1Id,
|
||||||
|
userId2: user2Id,
|
||||||
|
createdAt: new Date(),
|
||||||
|
isActive: true,
|
||||||
|
isSuperMatch: isSuperMatch,
|
||||||
|
unreadCount1: 0,
|
||||||
|
unreadCount2: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обновляем свайпы, отмечая что они образуют матч
|
||||||
|
await client.query(`
|
||||||
|
UPDATE swipes SET is_match = true
|
||||||
|
WHERE (user_id = $1 AND target_user_id = $2) OR (user_id = $2 AND target_user_id = $1)
|
||||||
|
`, [userId, targetUserId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const swipe = new Swipe({
|
||||||
|
id: swipeId,
|
||||||
|
userId,
|
||||||
|
targetUserId,
|
||||||
|
type: swipeType,
|
||||||
|
timestamp: new Date(),
|
||||||
|
isMatch
|
||||||
|
});
|
||||||
|
|
||||||
|
return { swipe, isMatch, match };
|
||||||
|
}
|
||||||
|
|
||||||
// Получить все матчи пользователя по telegram ID
|
// Получить все матчи пользователя по telegram ID
|
||||||
async getUserMatches(telegramId: string, limit: number = 50): Promise<Match[]> {
|
async getUserMatches(telegramId: string, limit: number = 50): Promise<Match[]> {
|
||||||
@@ -217,7 +281,7 @@ export class MatchingService {
|
|||||||
async getRecentLikes(userId: string, limit: number = 20): Promise<Swipe[]> {
|
async getRecentLikes(userId: string, limit: number = 20): Promise<Swipe[]> {
|
||||||
const result = await query(`
|
const result = await query(`
|
||||||
SELECT * FROM swipes
|
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
|
ORDER BY created_at DESC
|
||||||
LIMIT $2
|
LIMIT $2
|
||||||
`, [userId, limit]);
|
`, [userId, limit]);
|
||||||
@@ -236,10 +300,10 @@ export class MatchingService {
|
|||||||
today.setHours(0, 0, 0, 0);
|
today.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
const result = await query(`
|
const result = await query(`
|
||||||
SELECT direction, COUNT(*) as count
|
SELECT type, COUNT(*) as count
|
||||||
FROM swipes
|
FROM swipes
|
||||||
WHERE swiper_id = $1 AND created_at >= $2
|
WHERE user_id = $1 AND created_at >= $2
|
||||||
GROUP BY direction
|
GROUP BY type
|
||||||
`, [userId, today]);
|
`, [userId, today]);
|
||||||
|
|
||||||
const stats = {
|
const stats = {
|
||||||
@@ -253,11 +317,11 @@ export class MatchingService {
|
|||||||
const count = parseInt(row.count);
|
const count = parseInt(row.count);
|
||||||
stats.total += count;
|
stats.total += count;
|
||||||
|
|
||||||
switch (row.direction) {
|
switch (row.type) {
|
||||||
case 'like':
|
case 'like':
|
||||||
stats.likes = count;
|
stats.likes = count;
|
||||||
break;
|
break;
|
||||||
case 'super':
|
case 'superlike':
|
||||||
stats.superlikes = count;
|
stats.superlikes = count;
|
||||||
break;
|
break;
|
||||||
case 'pass':
|
case 'pass':
|
||||||
@@ -299,9 +363,9 @@ export class MatchingService {
|
|||||||
private mapEntityToSwipe(entity: any): Swipe {
|
private mapEntityToSwipe(entity: any): Swipe {
|
||||||
return new Swipe({
|
return new Swipe({
|
||||||
id: entity.id,
|
id: entity.id,
|
||||||
userId: entity.swiper_id,
|
userId: entity.user_id || entity.swiper_id,
|
||||||
targetUserId: entity.swiped_id,
|
targetUserId: entity.target_user_id || entity.swiped_id,
|
||||||
type: this.convertDirectionToSwipeType(entity.direction),
|
type: entity.type || 'pass',
|
||||||
timestamp: entity.created_at,
|
timestamp: entity.created_at,
|
||||||
isMatch: entity.is_match
|
isMatch: entity.is_match
|
||||||
});
|
});
|
||||||
@@ -329,8 +393,8 @@ export class MatchingService {
|
|||||||
FROM swipes s1
|
FROM swipes s1
|
||||||
JOIN swipes s2 ON s1.user_id = s2.target_user_id AND s1.target_user_id = s2.user_id
|
JOIN swipes s2 ON s1.user_id = s2.target_user_id AND s1.target_user_id = s2.user_id
|
||||||
WHERE s1.user_id = $1
|
WHERE s1.user_id = $1
|
||||||
AND s1.direction IN ('right', 'super')
|
AND s1.type IN ('like', 'superlike')
|
||||||
AND s2.direction IN ('right', 'super')
|
AND s2.type IN ('like', 'superlike')
|
||||||
AND NOT EXISTS (
|
AND NOT EXISTS (
|
||||||
SELECT 1 FROM matches m
|
SELECT 1 FROM matches m
|
||||||
WHERE (m.user_id_1 = s1.user_id AND m.user_id_2 = s1.target_user_id)
|
WHERE (m.user_id_1 = s1.user_id AND m.user_id_2 = s1.target_user_id)
|
||||||
@@ -343,73 +407,156 @@ export class MatchingService {
|
|||||||
|
|
||||||
// Получить следующего кандидата для просмотра
|
// Получить следующего кандидата для просмотра
|
||||||
async getNextCandidate(telegramId: string, isNewUser: boolean = false): Promise<Profile | null> {
|
async getNextCandidate(telegramId: string, isNewUser: boolean = false): Promise<Profile | null> {
|
||||||
|
console.log(`[DEBUG] getNextCandidate вызван для telegramId=${telegramId}, isNewUser=${isNewUser}`);
|
||||||
|
|
||||||
// Сначала получаем профиль пользователя по telegramId
|
// Сначала получаем профиль пользователя по telegramId
|
||||||
const userProfile = await this.profileService.getProfileByTelegramId(telegramId);
|
const userProfile = await this.profileService.getProfileByTelegramId(telegramId);
|
||||||
if (!userProfile) {
|
if (!userProfile) {
|
||||||
|
console.log(`[ERROR] Профиль пользователя с telegramId=${telegramId} не найден`);
|
||||||
throw new BotError('User profile not found', 'PROFILE_NOT_FOUND');
|
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 пользователя
|
// Получаем UUID пользователя
|
||||||
const userId = userProfile.userId;
|
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
|
let targetGender: string;
|
||||||
? `AND p.user_id NOT IN (${viewedUserIds.map((_: any, i: number) => `$${i + 2}`).join(', ')})`
|
if (userProfile.interestedIn === 'male' || userProfile.interestedIn === 'female') {
|
||||||
: '';
|
targetGender = userProfile.interestedIn;
|
||||||
} else {
|
} else {
|
||||||
// Для новых пользователей исключаем только себя
|
// Если "both" или другое значение, показываем противоположный пол
|
||||||
excludeCondition = `AND p.user_id != $2`;
|
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 = `
|
const candidateQuery = `
|
||||||
SELECT p.*, u.telegram_id, u.username, u.first_name, u.last_name
|
SELECT p.*, u.telegram_id, u.username, u.first_name, u.last_name
|
||||||
FROM profiles p
|
FROM profiles p
|
||||||
JOIN users u ON p.user_id = u.id
|
JOIN users u ON p.user_id = u.id
|
||||||
WHERE p.is_visible = true
|
WHERE p.is_visible = true
|
||||||
AND p.gender = $1
|
AND p.gender = '${targetGender}'
|
||||||
AND p.age BETWEEN ${userProfile.searchPreferences.minAge} AND ${userProfile.searchPreferences.maxAge}
|
AND p.age BETWEEN ${userProfile.searchPreferences.minAge} AND ${userProfile.searchPreferences.maxAge}
|
||||||
${excludeCondition}
|
${excludeCondition}
|
||||||
ORDER BY RANDOM()
|
ORDER BY RANDOM()
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
`;
|
`;
|
||||||
|
console.log(`[DEBUG] SQL запрос: ${candidateQuery}`);
|
||||||
|
console.log(`[DEBUG] Параметры: ${JSON.stringify(params)}`);
|
||||||
|
|
||||||
const params = [userProfile.interestedIn, ...viewedUserIds];
|
|
||||||
const result = await query(candidateQuery, params);
|
const result = await query(candidateQuery, params);
|
||||||
|
console.log(`[DEBUG] Результаты запроса: найдено ${result.rows.length} профилей`);
|
||||||
|
|
||||||
if (result.rows.length === 0) {
|
if (result.rows.length === 0) {
|
||||||
|
console.log(`[DEBUG] Подходящие кандидаты не найдены`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const candidateData = result.rows[0];
|
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 для правильного маппинга данных
|
// Используем ProfileService для правильного маппинга данных
|
||||||
return this.profileService.mapEntityToProfile(candidateData);
|
const profile = this.profileService.mapEntityToProfile(candidateData);
|
||||||
|
console.log(`[DEBUG] Профиль преобразован и возвращается клиенту`);
|
||||||
|
return profile;
|
||||||
}
|
}
|
||||||
|
|
||||||
// VIP функция: поиск кандидатов по цели знакомства
|
// VIP функция: поиск кандидатов по цели знакомства
|
||||||
async getCandidatesWithGoal(userProfile: Profile, targetGoal: string): Promise<Profile[]> {
|
async getCandidatesWithGoal(userProfile: Profile, targetGoal: string): Promise<Profile[]> {
|
||||||
const swipedUsersResult = await query(`
|
const swipedUsersResult = await query(`
|
||||||
SELECT swiped_id
|
SELECT target_user_id
|
||||||
FROM swipes
|
FROM swipes
|
||||||
WHERE swiper_id = $1
|
WHERE user_id = $1
|
||||||
`, [userProfile.userId]);
|
`, [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); // Исключаем себя
|
swipedUserIds.push(userProfile.userId); // Исключаем себя
|
||||||
|
|
||||||
let candidateQuery = `
|
let candidateQuery = `
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import TelegramBot from 'node-telegram-bot-api';
|
import TelegramBot from 'node-telegram-bot-api';
|
||||||
import { query } from '../database/connection';
|
import { query } from '../database/connection';
|
||||||
import { ProfileService } from './profileService';
|
import { ProfileService } from './profileService';
|
||||||
import config from '../../config/default.json';
|
|
||||||
|
|
||||||
export interface NotificationData {
|
export interface NotificationData {
|
||||||
userId: string;
|
userId: string;
|
||||||
@@ -19,6 +18,126 @@ export class NotificationService {
|
|||||||
this.profileService = new ProfileService();
|
this.profileService = new ProfileService();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Получить шаблон уведомления из базы данных или использовать встроенный
|
||||||
|
private async getNotificationTemplate(type: string): Promise<{
|
||||||
|
title: string;
|
||||||
|
messageTemplate: string;
|
||||||
|
buttonTemplate: any;
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
// Попытка получить шаблон из базы данных
|
||||||
|
const result = await query(`
|
||||||
|
SELECT title, message_template, button_template
|
||||||
|
FROM notification_templates
|
||||||
|
WHERE type = $1
|
||||||
|
`, [type]);
|
||||||
|
|
||||||
|
if (result.rows.length > 0) {
|
||||||
|
return {
|
||||||
|
title: result.rows[0].title,
|
||||||
|
messageTemplate: result.rows[0].message_template,
|
||||||
|
buttonTemplate: result.rows[0].button_template
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log('Using default template as database is not available:', error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если не удалось получить из базы или произошла ошибка, используем встроенные шаблоны
|
||||||
|
const defaultTemplates: Record<string, any> = {
|
||||||
|
'new_like': {
|
||||||
|
title: 'Новый лайк!',
|
||||||
|
messageTemplate: '❤️ *{{name}}* поставил(а) вам лайк!\n\nВозраст: {{age}}\n{{city}}\n\nОтветьте взаимностью или посмотрите профиль.',
|
||||||
|
buttonTemplate: {
|
||||||
|
inline_keyboard: [
|
||||||
|
[{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }],
|
||||||
|
[
|
||||||
|
{ text: '❤️ Лайк в ответ', callback_data: 'like_back:{{userId}}' },
|
||||||
|
{ text: '⛔️ Пропустить', callback_data: 'dislike_profile:{{userId}}' }
|
||||||
|
],
|
||||||
|
[{ text: '💕 Открыть все лайки', callback_data: 'view_likes' }]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'super_like': {
|
||||||
|
title: 'Супер-лайк!',
|
||||||
|
messageTemplate: '⭐️ *{{name}}* отправил(а) вам супер-лайк!\n\nВозраст: {{age}}\n{{city}}\n\nВы произвели особое впечатление! Ответьте взаимностью или посмотрите профиль.',
|
||||||
|
buttonTemplate: {
|
||||||
|
inline_keyboard: [
|
||||||
|
[{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }],
|
||||||
|
[
|
||||||
|
{ text: '❤️ Лайк в ответ', callback_data: 'like_back:{{userId}}' },
|
||||||
|
{ text: '⛔️ Пропустить', callback_data: 'dislike_profile:{{userId}}' }
|
||||||
|
],
|
||||||
|
[{ text: '💕 Открыть все лайки', callback_data: 'view_likes' }]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'new_match': {
|
||||||
|
title: 'Новый матч!',
|
||||||
|
messageTemplate: '🎊 *Ура! Это взаимно!* 🎊\n\nВы и *{{name}}* понравились друг другу!\nВозраст: {{age}}\n{{city}}\n\nСделайте первый шаг - напишите сообщение!',
|
||||||
|
buttonTemplate: {
|
||||||
|
inline_keyboard: [
|
||||||
|
[{ text: '💬 Начать общение', callback_data: 'open_native_chat_{{matchId}}' }],
|
||||||
|
[
|
||||||
|
{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' },
|
||||||
|
{ text: '📋 Все матчи', callback_data: 'native_chats' }
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'new_message': {
|
||||||
|
title: 'Новое сообщение!',
|
||||||
|
messageTemplate: '💌 *Новое сообщение!*\n\nОт: *{{name}}*\n\n"{{message}}"\n\nОтветьте на сообщение прямо сейчас!',
|
||||||
|
buttonTemplate: {
|
||||||
|
inline_keyboard: [
|
||||||
|
[{ text: '📩 Ответить', callback_data: 'open_native_chat_{{matchId}}' }],
|
||||||
|
[
|
||||||
|
{ text: '👤 Профиль', callback_data: 'view_profile:{{userId}}' },
|
||||||
|
{ text: '📋 Все чаты', callback_data: 'native_chats' }
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return defaultTemplates[type] || {
|
||||||
|
title: 'Уведомление',
|
||||||
|
messageTemplate: 'Новое уведомление',
|
||||||
|
buttonTemplate: { inline_keyboard: [] }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Применить данные к шаблону
|
||||||
|
private applyTemplateData(template: string, data: Record<string, any>): string {
|
||||||
|
return template.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
|
||||||
|
return data[key] !== undefined ? data[key] : '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Применить данные к шаблону кнопок
|
||||||
|
private applyTemplateDataToButtons(buttonTemplate: any, data: Record<string, any>): any {
|
||||||
|
const result = JSON.parse(JSON.stringify(buttonTemplate)); // глубокая копия
|
||||||
|
|
||||||
|
// Рекурсивная функция для замены в любой вложенности
|
||||||
|
const replaceInObject = (obj: any): any => {
|
||||||
|
if (typeof obj === 'string') {
|
||||||
|
return this.applyTemplateData(obj, data);
|
||||||
|
} else if (Array.isArray(obj)) {
|
||||||
|
return obj.map(item => replaceInObject(item));
|
||||||
|
} else if (obj !== null && typeof obj === 'object') {
|
||||||
|
const newObj: Record<string, any> = {};
|
||||||
|
for (const key in obj) {
|
||||||
|
newObj[key] = replaceInObject(obj[key]);
|
||||||
|
}
|
||||||
|
return newObj;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
|
|
||||||
|
return replaceInObject(result);
|
||||||
|
}
|
||||||
|
|
||||||
// Отправить уведомление о новом лайке
|
// Отправить уведомление о новом лайке
|
||||||
async sendLikeNotification(targetTelegramId: string, likerTelegramId: string, isSuperLike: boolean = false): Promise<void> {
|
async sendLikeNotification(targetTelegramId: string, likerTelegramId: string, isSuperLike: boolean = false): Promise<void> {
|
||||||
try {
|
try {
|
||||||
@@ -30,25 +149,41 @@ export class NotificationService {
|
|||||||
if (!targetUser || !likerProfile || !this.bot) {
|
if (!targetUser || !likerProfile || !this.bot) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = isSuperLike
|
// Получаем шаблон уведомления
|
||||||
? `⭐ ${likerProfile.name} отправил вам суперлайк!`
|
const templateType = isSuperLike ? 'super_like' : 'new_like';
|
||||||
: `💖 ${likerProfile.name} поставил вам лайк!`;
|
const template = await this.getNotificationTemplate(templateType);
|
||||||
|
|
||||||
|
// Подготовка данных для шаблона
|
||||||
|
const templateData = {
|
||||||
|
name: likerProfile.name,
|
||||||
|
age: likerProfile.age.toString(),
|
||||||
|
city: likerProfile.city || '',
|
||||||
|
userId: likerProfile.userId
|
||||||
|
};
|
||||||
|
|
||||||
|
// Применяем данные к шаблону сообщения
|
||||||
|
const message = this.applyTemplateData(template.messageTemplate, templateData);
|
||||||
|
|
||||||
|
// Применяем данные к шаблону кнопок
|
||||||
|
const keyboard = this.applyTemplateDataToButtons(template.buttonTemplate, templateData);
|
||||||
|
|
||||||
|
// Отправляем уведомление
|
||||||
await this.bot.sendMessage(targetUser.telegram_id, message, {
|
await this.bot.sendMessage(targetUser.telegram_id, message, {
|
||||||
reply_markup: {
|
parse_mode: 'Markdown',
|
||||||
inline_keyboard: [[
|
reply_markup: keyboard
|
||||||
{ text: '👀 Посмотреть профиль', callback_data: `view_profile:${likerProfile.userId}` },
|
|
||||||
{ text: '💕 Начать знакомиться', callback_data: 'start_browsing' }
|
|
||||||
]]
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Логируем уведомление
|
// Логируем уведомление
|
||||||
await this.logNotification({
|
await this.logNotification({
|
||||||
userId: targetUser.id,
|
userId: targetUser.id,
|
||||||
type: isSuperLike ? 'super_like' : 'new_like',
|
type: templateType,
|
||||||
data: { likerUserId: likerProfile.userId, likerName: likerProfile.name }
|
data: {
|
||||||
|
likerUserId: likerProfile.userId,
|
||||||
|
likerName: likerProfile.name,
|
||||||
|
age: likerProfile.age,
|
||||||
|
city: likerProfile.city
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error sending like notification:', error);
|
console.error('Error sending like notification:', error);
|
||||||
@@ -67,22 +202,50 @@ export class NotificationService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = `🎉 У вас новый матч с ${matchedProfile.name}!\n\nТеперь вы можете начать общение.`;
|
// Получаем матч-ID для перехода в чат
|
||||||
|
const matchResult = await query(`
|
||||||
|
SELECT id FROM matches
|
||||||
|
WHERE (user_id_1 = $1 AND user_id_2 = $2) OR (user_id_1 = $2 AND user_id_2 = $1)
|
||||||
|
AND is_active = true
|
||||||
|
`, [userId, matchedUserId]);
|
||||||
|
|
||||||
|
const matchId = matchResult.rows[0]?.id;
|
||||||
|
|
||||||
|
// Получаем шаблон уведомления
|
||||||
|
const template = await this.getNotificationTemplate('new_match');
|
||||||
|
|
||||||
|
// Подготовка данных для шаблона
|
||||||
|
const templateData = {
|
||||||
|
name: matchedProfile.name,
|
||||||
|
age: matchedProfile.age.toString(),
|
||||||
|
city: matchedProfile.city || '',
|
||||||
|
userId: matchedProfile.userId,
|
||||||
|
matchId: matchId || ''
|
||||||
|
};
|
||||||
|
|
||||||
|
// Применяем данные к шаблону сообщения
|
||||||
|
const message = this.applyTemplateData(template.messageTemplate, templateData);
|
||||||
|
|
||||||
|
// Применяем данные к шаблону кнопок
|
||||||
|
const keyboard = this.applyTemplateDataToButtons(template.buttonTemplate, templateData);
|
||||||
|
|
||||||
|
// Отправляем уведомление
|
||||||
await this.bot.sendMessage(user.telegram_id, message, {
|
await this.bot.sendMessage(user.telegram_id, message, {
|
||||||
reply_markup: {
|
parse_mode: 'Markdown',
|
||||||
inline_keyboard: [[
|
reply_markup: keyboard
|
||||||
{ text: '💬 Написать сообщение', callback_data: `start_chat:${matchedUserId}` },
|
|
||||||
{ text: '👀 Посмотреть профиль', callback_data: `view_profile:${matchedUserId}` }
|
|
||||||
]]
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Логируем уведомление
|
// Логируем уведомление
|
||||||
await this.logNotification({
|
await this.logNotification({
|
||||||
userId,
|
userId,
|
||||||
type: 'new_match',
|
type: 'new_match',
|
||||||
data: { matchedUserId, matchedName: matchedProfile.name }
|
data: {
|
||||||
|
matchedUserId,
|
||||||
|
matchedName: matchedProfile.name,
|
||||||
|
age: matchedProfile.age,
|
||||||
|
city: matchedProfile.city,
|
||||||
|
matchId
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error sending match notification:', error);
|
console.error('Error sending match notification:', error);
|
||||||
@@ -90,7 +253,7 @@ export class NotificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Отправить уведомление о новом сообщении
|
// Отправить уведомление о новом сообщении
|
||||||
async sendMessageNotification(receiverId: string, senderId: string, messageContent: string): Promise<void> {
|
async sendMessageNotification(receiverId: string, senderId: string, messageContent: string, matchId?: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const [receiver, senderProfile] = await Promise.all([
|
const [receiver, senderProfile] = await Promise.all([
|
||||||
this.getUserByUserId(receiverId),
|
this.getUserByUserId(receiverId),
|
||||||
@@ -107,25 +270,55 @@ export class NotificationService {
|
|||||||
return; // Не отправляем уведомление, если пользователь активен в чате
|
return; // Не отправляем уведомление, если пользователь активен в чате
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Если matchId не передан, пытаемся его получить
|
||||||
|
let actualMatchId = matchId;
|
||||||
|
if (!actualMatchId) {
|
||||||
|
const matchResult = await query(`
|
||||||
|
SELECT id FROM matches
|
||||||
|
WHERE (user_id_1 = $1 AND user_id_2 = $2) OR (user_id_1 = $2 AND user_id_2 = $1)
|
||||||
|
AND is_active = true
|
||||||
|
`, [receiverId, senderId]);
|
||||||
|
|
||||||
|
actualMatchId = matchResult.rows[0]?.id;
|
||||||
|
}
|
||||||
|
|
||||||
const truncatedMessage = messageContent.length > 50
|
const truncatedMessage = messageContent.length > 50
|
||||||
? messageContent.substring(0, 50) + '...'
|
? messageContent.substring(0, 50) + '...'
|
||||||
: messageContent;
|
: messageContent;
|
||||||
|
|
||||||
const message = `💬 Новое сообщение от ${senderProfile.name}:\n\n${truncatedMessage}`;
|
// Получаем шаблон уведомления
|
||||||
|
const template = await this.getNotificationTemplate('new_message');
|
||||||
|
|
||||||
|
// Подготовка данных для шаблона
|
||||||
|
const templateData = {
|
||||||
|
name: senderProfile.name,
|
||||||
|
message: truncatedMessage,
|
||||||
|
userId: senderProfile.userId,
|
||||||
|
matchId: actualMatchId || ''
|
||||||
|
};
|
||||||
|
|
||||||
|
// Применяем данные к шаблону сообщения
|
||||||
|
const message = this.applyTemplateData(template.messageTemplate, templateData);
|
||||||
|
|
||||||
|
// Применяем данные к шаблону кнопок
|
||||||
|
const keyboard = this.applyTemplateDataToButtons(template.buttonTemplate, templateData);
|
||||||
|
|
||||||
|
// Отправляем уведомление
|
||||||
await this.bot.sendMessage(receiver.telegram_id, message, {
|
await this.bot.sendMessage(receiver.telegram_id, message, {
|
||||||
reply_markup: {
|
parse_mode: 'Markdown',
|
||||||
inline_keyboard: [[
|
reply_markup: keyboard
|
||||||
{ text: '💬 Ответить', callback_data: `open_chat:${senderId}` }
|
|
||||||
]]
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Логируем уведомление
|
// Логируем уведомление
|
||||||
await this.logNotification({
|
await this.logNotification({
|
||||||
userId: receiverId,
|
userId: receiverId,
|
||||||
type: 'new_message',
|
type: 'new_message',
|
||||||
data: { senderId, senderName: senderProfile.name, messageContent: truncatedMessage }
|
data: {
|
||||||
|
senderId,
|
||||||
|
senderName: senderProfile.name,
|
||||||
|
messageContent: truncatedMessage,
|
||||||
|
matchId: actualMatchId
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error sending message notification:', error);
|
console.error('Error sending message notification:', error);
|
||||||
@@ -364,7 +557,7 @@ export class NotificationService {
|
|||||||
type VARCHAR(50) NOT NULL,
|
type VARCHAR(50) NOT NULL,
|
||||||
data JSONB,
|
data JSONB,
|
||||||
scheduled_at TIMESTAMP NOT NULL,
|
scheduled_at TIMESTAMP NOT NULL,
|
||||||
is_processed BOOLEAN DEFAULT FALSE,
|
processed BOOLEAN DEFAULT FALSE,
|
||||||
created_at TIMESTAMP DEFAULT NOW()
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
@@ -373,7 +566,7 @@ export class NotificationService {
|
|||||||
// Получаем запланированные уведомления
|
// Получаем запланированные уведомления
|
||||||
const result = await query(`
|
const result = await query(`
|
||||||
SELECT * FROM scheduled_notifications
|
SELECT * FROM scheduled_notifications
|
||||||
WHERE scheduled_at <= $1 AND is_processed = false
|
WHERE scheduled_at <= $1 AND processed = false
|
||||||
ORDER BY scheduled_at ASC
|
ORDER BY scheduled_at ASC
|
||||||
LIMIT 100
|
LIMIT 100
|
||||||
`, [new Date()]);
|
`, [new Date()]);
|
||||||
@@ -393,7 +586,7 @@ export class NotificationService {
|
|||||||
|
|
||||||
// Отмечаем как обработанное
|
// Отмечаем как обработанное
|
||||||
await query(
|
await query(
|
||||||
'UPDATE scheduled_notifications SET is_processed = true WHERE id = $1',
|
'UPDATE scheduled_notifications SET processed = true WHERE id = $1',
|
||||||
[notification.id]
|
[notification.id]
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -404,4 +597,4 @@ export class NotificationService {
|
|||||||
console.error('Error processing scheduled notifications:', error);
|
console.error('Error processing scheduled notifications:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,8 +169,8 @@ export class ProfileService {
|
|||||||
case 'photos':
|
case 'photos':
|
||||||
case 'interests':
|
case 'interests':
|
||||||
updateFields.push(`${this.camelToSnake(key)} = $${paramIndex++}`);
|
updateFields.push(`${this.camelToSnake(key)} = $${paramIndex++}`);
|
||||||
// Для PostgreSQL массивы передаем как есть, не как JSON строки
|
// Для PostgreSQL массивы должны быть преобразованы в JSON-строку
|
||||||
updateValues.push(value);
|
updateValues.push(JSON.stringify(value));
|
||||||
break;
|
break;
|
||||||
case 'location':
|
case 'location':
|
||||||
// Пропускаем обработку местоположения, так как колонки location нет
|
// Пропускаем обработку местоположения, так как колонки location нет
|
||||||
@@ -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 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 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]);
|
await client.query('DELETE FROM profiles WHERE user_id = $1', [userId]);
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
@@ -526,16 +526,38 @@ export class ProfileService {
|
|||||||
// Записать просмотр профиля
|
// Записать просмотр профиля
|
||||||
async recordProfileView(viewerId: string, viewedProfileId: string, viewType: string = 'browse'): Promise<void> {
|
async recordProfileView(viewerId: string, viewedProfileId: string, viewType: string = 'browse'): Promise<void> {
|
||||||
try {
|
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(`
|
await query(`
|
||||||
INSERT INTO profile_views (viewer_id, viewed_profile_id, view_type)
|
INSERT INTO profile_views (viewer_id, viewed_profile_id, view_type, view_date)
|
||||||
VALUES (
|
VALUES ($1, $2, $3, NOW())
|
||||||
(SELECT id FROM users WHERE telegram_id = $1),
|
|
||||||
(SELECT id FROM profiles WHERE user_id = (SELECT id FROM users WHERE telegram_id = $2)),
|
|
||||||
$3
|
|
||||||
)
|
|
||||||
ON CONFLICT (viewer_id, viewed_profile_id) DO UPDATE
|
ON CONFLICT (viewer_id, viewed_profile_id) DO UPDATE
|
||||||
SET viewed_at = CURRENT_TIMESTAMP, view_type = EXCLUDED.view_type
|
SET view_date = NOW(), view_type = $3
|
||||||
`, [viewerId, viewedProfileId, viewType]);
|
`, [viewerUuid, viewedUuid, viewType]);
|
||||||
|
|
||||||
|
console.log(`Recorded profile view: ${viewerId} viewed ${viewedProfileId}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error recording profile view:', error);
|
console.error('Error recording profile view:', error);
|
||||||
}
|
}
|
||||||
@@ -547,8 +569,7 @@ export class ProfileService {
|
|||||||
const result = await query(`
|
const result = await query(`
|
||||||
SELECT COUNT(*) as count
|
SELECT COUNT(*) as count
|
||||||
FROM profile_views pv
|
FROM profile_views pv
|
||||||
JOIN profiles p ON pv.viewed_profile_id = p.id
|
WHERE viewed_profile_id = $1
|
||||||
WHERE p.user_id = $1
|
|
||||||
`, [userId]);
|
`, [userId]);
|
||||||
|
|
||||||
return parseInt(result.rows[0].count) || 0;
|
return parseInt(result.rows[0].count) || 0;
|
||||||
@@ -562,14 +583,12 @@ export class ProfileService {
|
|||||||
async getProfileViewers(userId: string, limit: number = 10): Promise<Profile[]> {
|
async getProfileViewers(userId: string, limit: number = 10): Promise<Profile[]> {
|
||||||
try {
|
try {
|
||||||
const result = await query(`
|
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
|
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 users viewer_u ON pv.viewer_id = viewer_u.id
|
||||||
JOIN profiles p ON viewer_u.id = p.user_id
|
JOIN profiles p ON viewer_u.id = p.user_id
|
||||||
JOIN users u ON p.user_id = u.id
|
WHERE pv.viewed_profile_id = $1
|
||||||
WHERE target_p.user_id = $1
|
ORDER BY pv.view_date DESC
|
||||||
ORDER BY pv.viewed_at DESC
|
|
||||||
LIMIT $2
|
LIMIT $2
|
||||||
`, [userId, limit]);
|
`, [userId, limit]);
|
||||||
|
|
||||||
@@ -579,4 +598,22 @@ export class ProfileService {
|
|||||||
return [];
|
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 [];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
37
test-connection.js
Normal file
37
test-connection.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
require('dotenv').config();
|
||||||
|
const { Pool } = require('pg');
|
||||||
|
|
||||||
|
// Используем параметры напрямую из .env
|
||||||
|
const pool = new Pool({
|
||||||
|
host: process.env.DB_HOST,
|
||||||
|
port: process.env.DB_PORT,
|
||||||
|
database: process.env.DB_NAME,
|
||||||
|
user: process.env.DB_USERNAME,
|
||||||
|
password: process.env.DB_PASSWORD,
|
||||||
|
max: 5,
|
||||||
|
connectionTimeoutMillis: 5000
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('DB Connection Details:');
|
||||||
|
console.log(`- Host: ${process.env.DB_HOST}`);
|
||||||
|
console.log(`- Port: ${process.env.DB_PORT}`);
|
||||||
|
console.log(`- Database: ${process.env.DB_NAME}`);
|
||||||
|
console.log(`- User: ${process.env.DB_USERNAME}`);
|
||||||
|
|
||||||
|
async function testConnection() {
|
||||||
|
try {
|
||||||
|
const client = await pool.connect();
|
||||||
|
try {
|
||||||
|
const result = await client.query('SELECT NOW() as current_time');
|
||||||
|
console.log('✅ Connected to database successfully!');
|
||||||
|
console.log(`Current database time: ${result.rows[0].current_time}`);
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
await pool.end();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to connect to database:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testConnection();
|
||||||
0
test-db.js
Normal file
0
test-db.js
Normal file
Reference in New Issue
Block a user