mass refactor
This commit is contained in:
@@ -2,10 +2,10 @@ import { Pool, PoolConfig } from 'pg';
|
||||
|
||||
// Конфигурация пула соединений PostgreSQL
|
||||
const poolConfig: PoolConfig = {
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: parseInt(process.env.DB_PORT || '5433'),
|
||||
host: process.env.DB_HOST,
|
||||
port: parseInt(process.env.DB_PORT || '5432'),
|
||||
database: process.env.DB_NAME || 'telegram_tinder_bot',
|
||||
user: process.env.DB_USERNAME || 'postgres',
|
||||
user: process.env.DB_USERNAME,
|
||||
...(process.env.DB_PASSWORD && { password: process.env.DB_PASSWORD }),
|
||||
max: 20, // максимальное количество соединений в пуле
|
||||
idleTimeoutMillis: 30000, // закрыть соединения, простаивающие 30 секунд
|
||||
@@ -154,10 +154,10 @@ export async function initializeDatabase(): Promise<void> {
|
||||
await query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_users_telegram_id ON users(telegram_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_profiles_user_id ON profiles(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_profiles_location ON profiles(latitude, longitude);
|
||||
CREATE INDEX IF NOT EXISTS idx_profiles_age_gender ON profiles(age, gender, looking_for);
|
||||
CREATE INDEX IF NOT EXISTS idx_swipes_swiper_swiped ON swipes(swiper_id, swiped_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_matches_users ON matches(user1_id, user2_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_profiles_location ON profiles(location_lat, location_lon) WHERE location_lat IS NOT NULL AND location_lon IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_profiles_age_gender ON profiles(age, gender, interested_in);
|
||||
CREATE INDEX IF NOT EXISTS idx_swipes_user ON swipes(user_id, target_user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_matches_users ON matches(user_id_1, user_id_2);
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_match ON messages(match_id, created_at);
|
||||
`);
|
||||
|
||||
|
||||
@@ -139,7 +139,10 @@ export class CallbackHandlers {
|
||||
|
||||
// Просмотр анкет и свайпы
|
||||
else if (data === 'start_browsing') {
|
||||
await this.handleStartBrowsing(chatId, telegramId);
|
||||
await this.handleStartBrowsing(chatId, telegramId, false);
|
||||
} else if (data === 'start_browsing_first') {
|
||||
// Показываем всех пользователей для нового пользователя
|
||||
await this.handleStartBrowsing(chatId, telegramId, true);
|
||||
} else if (data === 'vip_search') {
|
||||
await this.handleVipSearch(chatId, telegramId);
|
||||
} else if (data.startsWith('search_by_goal_')) {
|
||||
@@ -330,7 +333,7 @@ export class CallbackHandlers {
|
||||
}
|
||||
|
||||
// Начать просмотр анкет
|
||||
async handleStartBrowsing(chatId: number, telegramId: string): Promise<void> {
|
||||
async handleStartBrowsing(chatId: number, telegramId: string, isNewUser: boolean = false): Promise<void> {
|
||||
const profile = await this.profileService.getProfileByTelegramId(telegramId);
|
||||
|
||||
if (!profile) {
|
||||
@@ -338,7 +341,7 @@ export class CallbackHandlers {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.showNextCandidate(chatId, telegramId);
|
||||
await this.showNextCandidate(chatId, telegramId, isNewUser);
|
||||
}
|
||||
|
||||
// Следующий кандидат
|
||||
@@ -891,8 +894,8 @@ export class CallbackHandlers {
|
||||
}
|
||||
}
|
||||
|
||||
async showNextCandidate(chatId: number, telegramId: string): Promise<void> {
|
||||
const candidate = await this.matchingService.getNextCandidate(telegramId);
|
||||
async showNextCandidate(chatId: number, telegramId: string, isNewUser: boolean = false): Promise<void> {
|
||||
const candidate = await this.matchingService.getNextCandidate(telegramId, isNewUser);
|
||||
|
||||
if (!candidate) {
|
||||
const keyboard: InlineKeyboardMarkup = {
|
||||
@@ -1370,12 +1373,28 @@ export class CallbackHandlers {
|
||||
return;
|
||||
}
|
||||
|
||||
const lifestyle = profile.lifestyle || {};
|
||||
lifestyle[type as keyof typeof lifestyle] = value as any;
|
||||
// Обновляем отдельные колонки напрямую, а не через объект lifestyle
|
||||
const updates: any = {};
|
||||
|
||||
switch (type) {
|
||||
case 'smoking':
|
||||
updates.smoking = value;
|
||||
break;
|
||||
case 'drinking':
|
||||
updates.drinking = value;
|
||||
break;
|
||||
case 'kids':
|
||||
// Для поля has_kids, которое имеет тип boolean, преобразуем строковые значения
|
||||
if (value === 'have') {
|
||||
updates.has_kids = true;
|
||||
} else {
|
||||
// Для 'want', 'dont_want', 'unsure' ставим false
|
||||
updates.has_kids = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
await this.profileService.updateProfile(profile.userId, {
|
||||
lifestyle: lifestyle
|
||||
});
|
||||
await this.profileService.updateProfile(profile.userId, updates);
|
||||
|
||||
const typeTexts: { [key: string]: string } = {
|
||||
'smoking': 'курение',
|
||||
|
||||
@@ -218,9 +218,10 @@ export class EnhancedChatHandlers {
|
||||
const messageId = await this.chatService.sendMessage(
|
||||
matchId,
|
||||
telegramId,
|
||||
msg.text || '[Медиа]',
|
||||
msg.photo ? 'photo' : 'text',
|
||||
msg.photo ? msg.photo[msg.photo.length - 1].file_id : undefined
|
||||
msg.photo ?
|
||||
(msg.caption || '[Фото]') + ' [file_id: ' + msg.photo[msg.photo.length - 1].file_id + ']' :
|
||||
(msg.text || '[Медиа]'),
|
||||
msg.photo ? 'photo' : 'text'
|
||||
);
|
||||
|
||||
if (messageId) {
|
||||
|
||||
@@ -217,11 +217,12 @@ export class MessageHandlers {
|
||||
}
|
||||
});
|
||||
|
||||
// Добавляем специальный callback для новых пользователей
|
||||
const keyboard: InlineKeyboardMarkup = {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{ text: '👤 Мой профиль', callback_data: 'view_my_profile' },
|
||||
{ text: '🔍 Начать поиск', callback_data: 'start_browsing' }
|
||||
{ text: '🔍 Начать поиск', callback_data: 'start_browsing_first' }
|
||||
],
|
||||
[{ text: '⚙️ Настройки', callback_data: 'settings' }]
|
||||
]
|
||||
@@ -493,7 +494,7 @@ export class MessageHandlers {
|
||||
updates.hobbies = value;
|
||||
break;
|
||||
case 'city':
|
||||
// В БД поле называется 'location', но мы используем city в модели
|
||||
// В БД поле называется 'city' (не 'location')
|
||||
updates.city = value;
|
||||
break;
|
||||
case 'job':
|
||||
|
||||
115
src/scripts/cleanDb.ts
Normal file
115
src/scripts/cleanDb.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env ts-node
|
||||
|
||||
import 'dotenv/config';
|
||||
import { testConnection, closePool, query } from '../database/connection';
|
||||
|
||||
/**
|
||||
* Очистка базы данных и пересоздание схемы с нуля
|
||||
*/
|
||||
async function main() {
|
||||
console.log('🚀 Очистка базы данных...');
|
||||
|
||||
try {
|
||||
// Проверяем подключение
|
||||
const connected = await testConnection();
|
||||
if (!connected) {
|
||||
console.error('❌ Не удалось подключиться к базе данных');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Сначала проверяем наличие таблиц
|
||||
const tablesExist = await checkTablesExist();
|
||||
|
||||
if (tablesExist) {
|
||||
console.log('🔍 Таблицы существуют. Выполняем удаление...');
|
||||
|
||||
// Удаляем существующие таблицы в правильном порядке
|
||||
await dropAllTables();
|
||||
console.log('✅ Все таблицы успешно удалены');
|
||||
} else {
|
||||
console.log('⚠️ Таблицы не обнаружены');
|
||||
}
|
||||
|
||||
console.log('🛠️ База данных очищена успешно');
|
||||
console.log('ℹ️ Теперь вы можете выполнить npm run init:db для создания новой схемы');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка при очистке базы данных:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await closePool();
|
||||
console.log('👋 Соединение с базой данных закрыто');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка существования таблиц в базе данных
|
||||
*/
|
||||
async function checkTablesExist(): Promise<boolean> {
|
||||
try {
|
||||
const result = await query(`
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_schema = 'public' AND table_name IN ('users', 'profiles', 'matches')
|
||||
);
|
||||
`);
|
||||
return result.rows[0].exists;
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка при проверке наличия таблиц:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Удаление всех таблиц из базы данных
|
||||
*/
|
||||
async function dropAllTables(): Promise<void> {
|
||||
try {
|
||||
// Отключаем ограничения внешних ключей для удаления таблиц
|
||||
await query('SET CONSTRAINTS ALL DEFERRED;');
|
||||
|
||||
// Удаляем таблицы в порядке, учитывающем зависимости
|
||||
console.log('Удаление таблицы notifications...');
|
||||
await query('DROP TABLE IF EXISTS notifications CASCADE;');
|
||||
|
||||
console.log('Удаление таблицы scheduled_notifications...');
|
||||
await query('DROP TABLE IF EXISTS scheduled_notifications CASCADE;');
|
||||
|
||||
console.log('Удаление таблицы reports...');
|
||||
await query('DROP TABLE IF EXISTS reports CASCADE;');
|
||||
|
||||
console.log('Удаление таблицы blocks...');
|
||||
await query('DROP TABLE IF EXISTS blocks CASCADE;');
|
||||
|
||||
console.log('Удаление таблицы messages...');
|
||||
await query('DROP TABLE IF EXISTS messages CASCADE;');
|
||||
|
||||
console.log('Удаление таблицы matches...');
|
||||
await query('DROP TABLE IF EXISTS matches CASCADE;');
|
||||
|
||||
console.log('Удаление таблицы swipes...');
|
||||
await query('DROP TABLE IF EXISTS swipes CASCADE;');
|
||||
|
||||
console.log('Удаление таблицы profiles...');
|
||||
await query('DROP TABLE IF EXISTS profiles CASCADE;');
|
||||
|
||||
console.log('Удаление таблицы users...');
|
||||
await query('DROP TABLE IF EXISTS users CASCADE;');
|
||||
|
||||
console.log('Удаление таблицы pgmigrations...');
|
||||
await query('DROP TABLE IF EXISTS pgmigrations CASCADE;');
|
||||
|
||||
// Восстанавливаем ограничения внешних ключей
|
||||
await query('SET CONSTRAINTS ALL IMMEDIATE;');
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка при удалении таблиц:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Запуск скрипта
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
export { main as cleanDB };
|
||||
166
src/scripts/createTestData.ts
Normal file
166
src/scripts/createTestData.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import { Pool } from 'pg';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import 'dotenv/config';
|
||||
|
||||
async function createTestSwipes() {
|
||||
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
|
||||
});
|
||||
|
||||
try {
|
||||
// Сначала получаем ID всех пользователей
|
||||
const usersResult = await pool.query(`
|
||||
SELECT users.id, telegram_id, first_name, gender
|
||||
FROM users
|
||||
JOIN profiles ON users.id = profiles.user_id
|
||||
`);
|
||||
|
||||
const users = usersResult.rows;
|
||||
console.log('Пользователи в системе:');
|
||||
console.table(users);
|
||||
|
||||
if (users.length < 2) {
|
||||
console.log('Недостаточно пользователей для создания свайпов');
|
||||
return;
|
||||
}
|
||||
|
||||
// Создаем свайпы
|
||||
console.log('Создаем тестовые свайпы...');
|
||||
|
||||
// Сначала проверим ограничения базы данных
|
||||
const constraintsResult = await pool.query(`
|
||||
SELECT
|
||||
constraint_name,
|
||||
table_name,
|
||||
constraint_type
|
||||
FROM
|
||||
information_schema.table_constraints
|
||||
WHERE
|
||||
table_name = 'swipes'
|
||||
`);
|
||||
|
||||
console.log('Ограничения таблицы swipes:');
|
||||
console.table(constraintsResult.rows);
|
||||
|
||||
// Создаем пары пользователей без дублирования
|
||||
const userPairs = [];
|
||||
const swipes = [];
|
||||
|
||||
// Мужчины и женщины для создания пар
|
||||
const maleUsers = users.filter(user => user.gender === 'male');
|
||||
const femaleUsers = users.filter(user => user.gender === 'female');
|
||||
|
||||
console.log(`Мужчин: ${maleUsers.length}, Женщин: ${femaleUsers.length}`);
|
||||
|
||||
for (const male of maleUsers) {
|
||||
for (const female of femaleUsers) {
|
||||
// Мужчина -> Женщина (70% лайк, 20% пропуск, 10% суперлайк)
|
||||
const randomNum1 = Math.random();
|
||||
let maleToFemaleType;
|
||||
|
||||
if (randomNum1 < 0.7) {
|
||||
maleToFemaleType = 'like';
|
||||
} else if (randomNum1 < 0.9) {
|
||||
maleToFemaleType = 'pass';
|
||||
} else {
|
||||
maleToFemaleType = 'superlike';
|
||||
}
|
||||
|
||||
const maleToFemale = {
|
||||
id: uuidv4(),
|
||||
user_id: male.id,
|
||||
target_user_id: female.id,
|
||||
type: maleToFemaleType,
|
||||
is_match: false,
|
||||
created_at: new Date()
|
||||
};
|
||||
|
||||
// Женщина -> Мужчина (80% шанс на лайк, если мужчина лайкнул)
|
||||
if ((maleToFemaleType === 'like' || maleToFemaleType === 'superlike') && Math.random() < 0.8) {
|
||||
const femaleToMale = {
|
||||
id: uuidv4(),
|
||||
user_id: female.id,
|
||||
target_user_id: male.id,
|
||||
type: Math.random() < 0.9 ? 'like' : 'superlike',
|
||||
is_match: true,
|
||||
created_at: new Date(new Date().getTime() + 1000) // На секунду позже
|
||||
};
|
||||
|
||||
swipes.push(femaleToMale);
|
||||
maleToFemale.is_match = true;
|
||||
}
|
||||
|
||||
swipes.push(maleToFemale);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Подготовлено ${swipes.length} свайпов для добавления в базу`);
|
||||
|
||||
// Сначала удаляем все существующие свайпы
|
||||
await pool.query('DELETE FROM swipes');
|
||||
console.log('Существующие свайпы удалены');
|
||||
|
||||
// Добавляем новые свайпы
|
||||
for (const swipe of swipes) {
|
||||
await pool.query(`
|
||||
INSERT INTO swipes (id, user_id, target_user_id, type, is_match, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
`, [swipe.id, swipe.user_id, swipe.target_user_id, swipe.type, swipe.is_match, swipe.created_at]);
|
||||
}
|
||||
|
||||
console.log(`Успешно добавлено ${swipes.length} свайпов`);
|
||||
|
||||
// Создаем матчи для взаимных лайков
|
||||
console.log('Создаем матчи для взаимных лайков...');
|
||||
|
||||
// Сначала удаляем все существующие матчи
|
||||
await pool.query('DELETE FROM matches');
|
||||
console.log('Существующие матчи удалены');
|
||||
|
||||
// Находим пары взаимных лайков для создания матчей
|
||||
const mutualLikesResult = await pool.query(`
|
||||
SELECT
|
||||
s1.user_id as user_id_1,
|
||||
s1.target_user_id as user_id_2,
|
||||
s1.created_at
|
||||
FROM swipes s1
|
||||
JOIN swipes s2 ON s1.user_id = s2.target_user_id AND s1.target_user_id = s2.user_id
|
||||
WHERE (s1.type = 'like' OR s1.type = 'superlike')
|
||||
AND (s2.type = 'like' OR s2.type = 'superlike')
|
||||
AND s1.user_id < s2.user_id -- Избегаем дублирования пар
|
||||
`);
|
||||
|
||||
const matches = [];
|
||||
|
||||
for (const mutualLike of mutualLikesResult.rows) {
|
||||
const match = {
|
||||
id: uuidv4(),
|
||||
user_id_1: mutualLike.user_id_1,
|
||||
user_id_2: mutualLike.user_id_2,
|
||||
created_at: new Date(),
|
||||
is_active: true,
|
||||
is_super_match: Math.random() < 0.2 // 20% шанс быть суперматчем
|
||||
};
|
||||
|
||||
matches.push(match);
|
||||
|
||||
await pool.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)
|
||||
`, [match.id, match.user_id_1, match.user_id_2, match.created_at, match.is_active, match.is_super_match]);
|
||||
}
|
||||
|
||||
console.log(`Успешно создано ${matches.length} матчей`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка при создании тестовых данных:', error);
|
||||
} finally {
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
createTestSwipes();
|
||||
120
src/scripts/getDatabaseInfo.ts
Normal file
120
src/scripts/getDatabaseInfo.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { Pool } from 'pg';
|
||||
import 'dotenv/config';
|
||||
|
||||
async function getDatabaseInfo() {
|
||||
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
|
||||
});
|
||||
|
||||
try {
|
||||
console.log('Подключение к базе данных...');
|
||||
|
||||
// Получаем информацию о пользователях
|
||||
console.log('\n=== ПОЛЬЗОВАТЕЛИ ===');
|
||||
const usersResult = await pool.query(`
|
||||
SELECT id, telegram_id, username, first_name, last_name, premium, created_at
|
||||
FROM users
|
||||
ORDER BY created_at DESC
|
||||
`);
|
||||
|
||||
console.log(`Всего пользователей: ${usersResult.rows.length}`);
|
||||
console.table(usersResult.rows);
|
||||
|
||||
// Получаем информацию о профилях
|
||||
console.log('\n=== ПРОФИЛИ ===');
|
||||
const profilesResult = await pool.query(`
|
||||
SELECT
|
||||
p.user_id,
|
||||
u.telegram_id,
|
||||
u.first_name,
|
||||
p.age,
|
||||
p.gender,
|
||||
p.interested_in as "интересуется",
|
||||
p.bio,
|
||||
p.dating_goal as "цель_знакомства",
|
||||
p.is_visible,
|
||||
p.created_at
|
||||
FROM profiles p
|
||||
JOIN users u ON p.user_id = u.id
|
||||
ORDER BY p.created_at DESC
|
||||
`);
|
||||
|
||||
console.log(`Всего профилей: ${profilesResult.rows.length}`);
|
||||
console.table(profilesResult.rows);
|
||||
|
||||
// Получаем информацию о свайпах
|
||||
console.log('\n=== СВАЙПЫ ===');
|
||||
|
||||
// Сначала проверим, какие столбцы есть в таблице swipes
|
||||
console.log('Получение структуры таблицы swipes...');
|
||||
const swipesColumns = await pool.query(`
|
||||
SELECT column_name, data_type
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'swipes'
|
||||
`);
|
||||
console.log('Структура таблицы swipes:');
|
||||
console.table(swipesColumns.rows);
|
||||
|
||||
// Теперь запросим данные, используя правильные имена столбцов
|
||||
const swipesResult = await pool.query(`
|
||||
SELECT
|
||||
s.id,
|
||||
s.user_id,
|
||||
u1.first_name as "от_кого",
|
||||
s.target_user_id,
|
||||
u2.first_name as "кому",
|
||||
s.type,
|
||||
s.created_at
|
||||
FROM swipes s
|
||||
JOIN users u1 ON s.user_id = u1.id
|
||||
JOIN users u2 ON s.target_user_id = u2.id
|
||||
ORDER BY s.created_at DESC
|
||||
`);
|
||||
|
||||
console.log(`Всего свайпов: ${swipesResult.rows.length}`);
|
||||
console.table(swipesResult.rows);
|
||||
|
||||
// Получаем информацию о матчах
|
||||
console.log('\n=== МАТЧИ ===');
|
||||
|
||||
// Сначала проверим, какие столбцы есть в таблице matches
|
||||
console.log('Получение структуры таблицы matches...');
|
||||
const matchesColumns = await pool.query(`
|
||||
SELECT column_name, data_type
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'matches'
|
||||
`);
|
||||
console.log('Структура таблицы matches:');
|
||||
console.table(matchesColumns.rows);
|
||||
|
||||
// Теперь запросим данные, используя правильные имена столбцов
|
||||
const matchesResult = await pool.query(`
|
||||
SELECT
|
||||
m.id,
|
||||
m.user_id_1,
|
||||
u1.first_name as "пользователь_1",
|
||||
m.user_id_2,
|
||||
u2.first_name as "пользователь_2",
|
||||
m.is_active,
|
||||
m.created_at
|
||||
FROM matches m
|
||||
JOIN users u1 ON m.user_id_1 = u1.id
|
||||
JOIN users u2 ON m.user_id_2 = u2.id
|
||||
ORDER BY m.created_at DESC
|
||||
`);
|
||||
|
||||
console.log(`Всего матчей: ${matchesResult.rows.length}`);
|
||||
console.table(matchesResult.rows);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка при получении данных:', error);
|
||||
} finally {
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
getDatabaseInfo();
|
||||
@@ -1,7 +1,13 @@
|
||||
#!/usr/bin/env ts-node
|
||||
|
||||
import { initializeDatabase, testConnection, closePool } from '../database/connection';
|
||||
import 'dotenv/config';
|
||||
import { initializeDatabase, testConnection, closePool, query } from '../database/connection';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
|
||||
/**
|
||||
* Основная функция инициализации базы данных
|
||||
*/
|
||||
async function main() {
|
||||
console.log('🚀 Initializing database...');
|
||||
|
||||
@@ -13,90 +19,245 @@ async function main() {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Инициализируем схему
|
||||
await initializeDatabase();
|
||||
console.log('✅ Database initialized successfully');
|
||||
// Проверяем наличие таблицы миграций
|
||||
const migrationTableExists = await checkMigrationsTable();
|
||||
|
||||
if (migrationTableExists) {
|
||||
console.log('🔍 Миграции уже настроены');
|
||||
|
||||
// Проверяем, есть ли необходимость в применении миграций
|
||||
const pendingMigrations = await getPendingMigrations();
|
||||
if (pendingMigrations.length > 0) {
|
||||
console.log(`🔄 Найдено ${pendingMigrations.length} ожидающих миграций`);
|
||||
console.log('✅ Рекомендуется запустить: npm run migrate:up');
|
||||
} else {
|
||||
console.log('✅ Все миграции уже применены');
|
||||
}
|
||||
} else {
|
||||
console.log('⚠️ Таблица миграций не обнаружена');
|
||||
console.log('🛠️ Выполняется инициализация базы данных напрямую...');
|
||||
|
||||
// Выполняем традиционную инициализацию
|
||||
await initializeDatabase();
|
||||
console.log('✅ База данных инициализирована');
|
||||
|
||||
// Создаем дополнительные таблицы
|
||||
await createAdditionalTables();
|
||||
console.log('✅ Дополнительные таблицы созданы');
|
||||
|
||||
// Создаем таблицу миграций и отмечаем существующие миграции как выполненные
|
||||
await setupMigrations();
|
||||
console.log('✅ Настройка миграций завершена');
|
||||
}
|
||||
|
||||
// Создаем дополнительные таблицы, если нужно
|
||||
await createAdditionalTables();
|
||||
console.log('✅ Additional tables created');
|
||||
// Проверяем наличие необходимых колонок
|
||||
await ensureRequiredColumns();
|
||||
console.log('✅ Все необходимые колонки присутствуют');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Database initialization failed:', error);
|
||||
console.error('❌ Ошибка инициализации базы данных:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await closePool();
|
||||
console.log('👋 Database connection closed');
|
||||
console.log('👋 Соединение с базой данных закрыто');
|
||||
}
|
||||
}
|
||||
|
||||
async function createAdditionalTables() {
|
||||
const { query } = await import('../database/connection');
|
||||
/**
|
||||
* Проверка наличия таблицы миграций
|
||||
*/
|
||||
async function checkMigrationsTable(): Promise<boolean> {
|
||||
try {
|
||||
const result = await query(`
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_name = 'pgmigrations'
|
||||
);
|
||||
`);
|
||||
return result.rows[0].exists;
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка при проверке таблицы миграций:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Таблица для уведомлений
|
||||
await query(`
|
||||
CREATE TABLE IF NOT EXISTS notifications (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
||||
type VARCHAR(50) NOT NULL,
|
||||
data JSONB DEFAULT '{}',
|
||||
is_read BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
`);
|
||||
/**
|
||||
* Получение списка ожидающих миграций
|
||||
*/
|
||||
async function getPendingMigrations(): Promise<string[]> {
|
||||
try {
|
||||
// Получаем выполненные миграции
|
||||
const { rows } = await query('SELECT name FROM pgmigrations');
|
||||
const appliedMigrations = rows.map((row: { name: string }) => row.name);
|
||||
|
||||
// Получаем файлы миграций
|
||||
const migrationsDir = path.join(__dirname, '../../migrations');
|
||||
if (!fs.existsSync(migrationsDir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const migrationFiles = fs.readdirSync(migrationsDir)
|
||||
.filter(file => file.endsWith('.js'))
|
||||
.map(file => file.replace('.js', ''))
|
||||
.sort();
|
||||
|
||||
// Находим невыполненные миграции
|
||||
return migrationFiles.filter(file => !appliedMigrations.includes(file));
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка при проверке ожидающих миграций:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Таблица для запланированных уведомлений
|
||||
await query(`
|
||||
CREATE TABLE IF NOT EXISTS scheduled_notifications (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
||||
type VARCHAR(50) NOT NULL,
|
||||
data JSONB DEFAULT '{}',
|
||||
scheduled_at TIMESTAMP NOT NULL,
|
||||
sent BOOLEAN DEFAULT false,
|
||||
sent_at TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
`);
|
||||
/**
|
||||
* Настройка системы миграций и отметка существующих миграций как выполненных
|
||||
*/
|
||||
async function setupMigrations(): Promise<void> {
|
||||
try {
|
||||
// Создаем таблицу миграций
|
||||
await query(`
|
||||
CREATE TABLE IF NOT EXISTS pgmigrations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
run_on TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
||||
)
|
||||
`);
|
||||
|
||||
// Получаем файлы миграций
|
||||
const migrationsDir = path.join(__dirname, '../../migrations');
|
||||
if (!fs.existsSync(migrationsDir)) {
|
||||
console.log('⚠️ Директория миграций не найдена');
|
||||
return;
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(migrationsDir)
|
||||
.filter(file => file.endsWith('.js'))
|
||||
.sort();
|
||||
|
||||
// Отмечаем существующие миграции как выполненные
|
||||
for (const file of files) {
|
||||
const migrationName = file.replace('.js', '');
|
||||
console.log(`✅ Отмечаем миграцию как выполненную: ${migrationName}`);
|
||||
await query('INSERT INTO pgmigrations(name) VALUES($1)', [migrationName]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка при настройке миграций:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Таблица для отчетов и блокировок
|
||||
await query(`
|
||||
CREATE TABLE IF NOT EXISTS reports (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
reporter_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
||||
reported_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
||||
reason VARCHAR(100) NOT NULL,
|
||||
description TEXT,
|
||||
status VARCHAR(20) DEFAULT 'pending',
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
resolved_at TIMESTAMP
|
||||
);
|
||||
`);
|
||||
/**
|
||||
* Создание дополнительных таблиц для приложения
|
||||
*/
|
||||
async function createAdditionalTables(): Promise<void> {
|
||||
try {
|
||||
// Таблица для уведомлений
|
||||
await query(`
|
||||
CREATE TABLE IF NOT EXISTS notifications (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
||||
type VARCHAR(50) NOT NULL,
|
||||
data JSONB DEFAULT '{}',
|
||||
is_read BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
`);
|
||||
|
||||
// Таблица для блокировок
|
||||
await query(`
|
||||
CREATE TABLE IF NOT EXISTS blocks (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
blocker_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
||||
blocked_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
UNIQUE(blocker_id, blocked_id)
|
||||
);
|
||||
`);
|
||||
// Таблица для запланированных уведомлений
|
||||
await query(`
|
||||
CREATE TABLE IF NOT EXISTS scheduled_notifications (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
||||
type VARCHAR(50) NOT NULL,
|
||||
data JSONB DEFAULT '{}',
|
||||
scheduled_at TIMESTAMP NOT NULL,
|
||||
sent BOOLEAN DEFAULT false,
|
||||
sent_at TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
`);
|
||||
|
||||
// Добавляем недостающие поля в users
|
||||
await query(`
|
||||
ALTER TABLE users
|
||||
ADD COLUMN IF NOT EXISTS notification_settings JSONB DEFAULT '{"newMatches": true, "newMessages": true, "newLikes": true, "reminders": true}';
|
||||
`);
|
||||
// Таблица для отчетов и блокировок
|
||||
await query(`
|
||||
CREATE TABLE IF NOT EXISTS reports (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
reporter_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
||||
reported_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
||||
reason VARCHAR(100) NOT NULL,
|
||||
description TEXT,
|
||||
status VARCHAR(20) DEFAULT 'pending',
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
resolved_at TIMESTAMP
|
||||
);
|
||||
`);
|
||||
|
||||
// Индексы для производительности
|
||||
await query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_notifications_user_type ON notifications(user_id, type);
|
||||
CREATE INDEX IF NOT EXISTS idx_scheduled_notifications_time ON scheduled_notifications(scheduled_at, sent);
|
||||
CREATE INDEX IF NOT EXISTS idx_reports_status ON reports(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_blocks_blocker ON blocks(blocker_id);
|
||||
`);
|
||||
// Таблица для блокировок
|
||||
await query(`
|
||||
CREATE TABLE IF NOT EXISTS blocks (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
blocker_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
||||
blocked_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
UNIQUE(blocker_id, blocked_id)
|
||||
);
|
||||
`);
|
||||
|
||||
// Добавляем настройки уведомлений в users
|
||||
await query(`
|
||||
ALTER TABLE users
|
||||
ADD COLUMN IF NOT EXISTS notification_settings JSONB DEFAULT '{"newMatches": true, "newMessages": true, "newLikes": true, "reminders": true}';
|
||||
`);
|
||||
|
||||
// Индексы для производительности
|
||||
await query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_notifications_user_type ON notifications(user_id, type);
|
||||
CREATE INDEX IF NOT EXISTS idx_scheduled_notifications_time ON scheduled_notifications(scheduled_at, sent);
|
||||
CREATE INDEX IF NOT EXISTS idx_reports_status ON reports(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_blocks_blocker ON blocks(blocker_id);
|
||||
`);
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка при создании дополнительных таблиц:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка наличия всех необходимых колонок
|
||||
*/
|
||||
async function ensureRequiredColumns(): Promise<void> {
|
||||
try {
|
||||
// Проверка и добавление колонки updated_at в users
|
||||
await query(`
|
||||
ALTER TABLE users
|
||||
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP DEFAULT NOW();
|
||||
`);
|
||||
|
||||
// Проверка и добавление колонок premium и premium_expires_at в users
|
||||
await query(`
|
||||
ALTER TABLE users
|
||||
ADD COLUMN IF NOT EXISTS premium BOOLEAN DEFAULT FALSE;
|
||||
`);
|
||||
|
||||
await query(`
|
||||
ALTER TABLE users
|
||||
ADD COLUMN IF NOT EXISTS premium_expires_at TIMESTAMP;
|
||||
`);
|
||||
|
||||
// Проверка и добавление колонки looking_for в profiles
|
||||
await query(`
|
||||
ALTER TABLE profiles
|
||||
ADD COLUMN IF NOT EXISTS looking_for VARCHAR(20) DEFAULT 'both' CHECK (looking_for IN ('male', 'female', 'both'));
|
||||
`);
|
||||
|
||||
// Проверка и добавление колонки hobbies в profiles
|
||||
await query(`
|
||||
ALTER TABLE profiles
|
||||
ADD COLUMN IF NOT EXISTS hobbies TEXT;
|
||||
`);
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка при проверке необходимых колонок:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Запуск скрипта
|
||||
|
||||
42
src/scripts/setPremium.ts
Normal file
42
src/scripts/setPremium.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Pool } from 'pg';
|
||||
|
||||
async function setPremium(): Promise<void> {
|
||||
// Создаем соединение с базой данных используя прямые параметры
|
||||
const pool = new Pool({
|
||||
host: '192.168.0.102',
|
||||
port: 5432,
|
||||
database: 'telegram_tinder_bot',
|
||||
user: 'trevor',
|
||||
password: 'Cl0ud_1985!',
|
||||
});
|
||||
|
||||
try {
|
||||
// Установка премиума для всех пользователей
|
||||
const result = await pool.query(`
|
||||
UPDATE users
|
||||
SET premium = true,
|
||||
premium_expires_at = NOW() + INTERVAL '1 year'
|
||||
`);
|
||||
|
||||
console.log('Premium set for all users successfully!');
|
||||
console.log(`Updated ${result.rowCount} users`);
|
||||
|
||||
// Закрываем соединение с базой данных
|
||||
await pool.end();
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Error setting premium:', error);
|
||||
|
||||
// Закрываем соединение с базой данных в случае ошибки
|
||||
try {
|
||||
await pool.end();
|
||||
} catch (e) {
|
||||
console.error('Error closing pool:', e);
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
setPremium();
|
||||
43
src/scripts/setPremiumDirectConnect.ts
Normal file
43
src/scripts/setPremiumDirectConnect.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Pool } from 'pg';
|
||||
import 'dotenv/config';
|
||||
|
||||
async function setAllUsersToPremium() {
|
||||
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
|
||||
});
|
||||
|
||||
try {
|
||||
console.log('Setting premium status for all users...');
|
||||
console.log(`Connecting to database at ${process.env.DB_HOST}:${process.env.DB_PORT}...`);
|
||||
|
||||
const result = await pool.query(`
|
||||
UPDATE users
|
||||
SET premium = true
|
||||
WHERE true
|
||||
RETURNING id, telegram_id, username, first_name, premium
|
||||
`);
|
||||
|
||||
console.log(`Successfully set premium status for ${result.rows.length} users:`);
|
||||
console.table(result.rows);
|
||||
|
||||
console.log('All users are now premium!');
|
||||
} catch (error) {
|
||||
console.error('Error setting premium status:', error);
|
||||
console.error('Please check your database connection settings in .env file.');
|
||||
console.log('Current settings:');
|
||||
console.log(`- DB_HOST: ${process.env.DB_HOST}`);
|
||||
console.log(`- DB_PORT: ${process.env.DB_PORT}`);
|
||||
console.log(`- DB_NAME: ${process.env.DB_NAME}`);
|
||||
console.log(`- DB_USERNAME: ${process.env.DB_USERNAME}`);
|
||||
console.log(`- DB_PASSWORD: ${process.env.DB_PASSWORD ? '********' : 'not set'}`);
|
||||
} finally {
|
||||
await pool.end();
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
setAllUsersToPremium();
|
||||
25
src/scripts/setPremiumForAll.ts
Normal file
25
src/scripts/setPremiumForAll.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { query } from '../database/connection';
|
||||
|
||||
async function setAllUsersToPremium() {
|
||||
try {
|
||||
console.log('Setting premium status for all users...');
|
||||
|
||||
const result = await query(`
|
||||
UPDATE users
|
||||
SET premium = true
|
||||
WHERE true
|
||||
RETURNING id, telegram_id, username, first_name, premium
|
||||
`);
|
||||
|
||||
console.log(`Successfully set premium status for ${result.rows.length} users:`);
|
||||
console.table(result.rows);
|
||||
|
||||
console.log('All users are now premium!');
|
||||
} catch (error) {
|
||||
console.error('Error setting premium status:', error);
|
||||
} finally {
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
setAllUsersToPremium();
|
||||
@@ -24,8 +24,8 @@ export class ChatService {
|
||||
SELECT
|
||||
m.*,
|
||||
CASE
|
||||
WHEN m.user1_id = $1 THEN m.user2_id
|
||||
ELSE m.user1_id
|
||||
WHEN m.user_id_1 = $1 THEN m.user_id_2
|
||||
ELSE m.user_id_1
|
||||
END as other_user_id,
|
||||
p.name as other_user_name,
|
||||
p.photos as other_user_photos,
|
||||
@@ -42,8 +42,8 @@ export class ChatService {
|
||||
FROM matches m
|
||||
LEFT JOIN profiles p ON (
|
||||
CASE
|
||||
WHEN m.user1_id = $1 THEN p.user_id = m.user2_id
|
||||
ELSE p.user_id = m.user1_id
|
||||
WHEN m.user_id_1 = $1 THEN p.user_id = m.user_id_2
|
||||
ELSE p.user_id = m.user_id_1
|
||||
END
|
||||
)
|
||||
LEFT JOIN messages msg ON msg.id = (
|
||||
@@ -52,10 +52,10 @@ export class ChatService {
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
)
|
||||
WHERE (m.user1_id = $1 OR m.user2_id = $1)
|
||||
AND m.status = 'active'
|
||||
WHERE (m.user_id_1 = $1 OR m.user_id_2 = $1)
|
||||
AND m.is_active = true
|
||||
ORDER BY
|
||||
CASE WHEN msg.created_at IS NULL THEN m.matched_at ELSE msg.created_at END DESC
|
||||
CASE WHEN msg.created_at IS NULL THEN m.created_at ELSE msg.created_at END DESC
|
||||
`, [userId]);
|
||||
|
||||
return result.rows.map((row: any) => ({
|
||||
@@ -91,7 +91,6 @@ export class ChatService {
|
||||
senderId: row.sender_id,
|
||||
content: row.content,
|
||||
messageType: row.message_type,
|
||||
fileId: row.file_id,
|
||||
isRead: row.is_read,
|
||||
createdAt: new Date(row.created_at)
|
||||
})).reverse(); // Возвращаем в хронологическом порядке
|
||||
@@ -106,8 +105,7 @@ export class ChatService {
|
||||
matchId: string,
|
||||
senderTelegramId: string,
|
||||
content: string,
|
||||
messageType: 'text' | 'photo' | 'video' | 'voice' | 'sticker' | 'gif' = 'text',
|
||||
fileId?: string
|
||||
messageType: 'text' | 'photo' | 'video' | 'voice' | 'sticker' | 'gif' = 'text'
|
||||
): Promise<Message | null> {
|
||||
try {
|
||||
// Получаем senderId по telegramId
|
||||
@@ -119,7 +117,7 @@ export class ChatService {
|
||||
// Проверяем, что матч активен и пользователь является участником
|
||||
const matchResult = await query(`
|
||||
SELECT * FROM matches
|
||||
WHERE id = $1 AND (user1_id = $2 OR user2_id = $2) AND status = 'active'
|
||||
WHERE id = $1 AND (user_id_1 = $2 OR user_id_2 = $2) AND is_active = true
|
||||
`, [matchId, senderId]);
|
||||
|
||||
if (matchResult.rows.length === 0) {
|
||||
@@ -130,9 +128,9 @@ export class ChatService {
|
||||
|
||||
// Создаем сообщение
|
||||
await query(`
|
||||
INSERT INTO messages (id, match_id, sender_id, content, message_type, file_id, is_read, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, false, CURRENT_TIMESTAMP)
|
||||
`, [messageId, matchId, senderId, content, messageType, fileId]);
|
||||
INSERT INTO messages (id, match_id, sender_id, content, message_type, is_read, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, false, CURRENT_TIMESTAMP)
|
||||
`, [messageId, matchId, senderId, content, messageType]);
|
||||
|
||||
// Обновляем время последнего сообщения в матче
|
||||
await query(`
|
||||
@@ -157,7 +155,6 @@ export class ChatService {
|
||||
senderId: row.sender_id,
|
||||
content: row.content,
|
||||
messageType: row.message_type,
|
||||
fileId: row.file_id,
|
||||
isRead: row.is_read,
|
||||
createdAt: new Date(row.created_at)
|
||||
});
|
||||
@@ -197,11 +194,11 @@ export class ChatService {
|
||||
SELECT
|
||||
m.*,
|
||||
CASE
|
||||
WHEN m.user1_id = $2 THEN m.user2_id
|
||||
ELSE m.user1_id
|
||||
WHEN m.user_id_1 = $2 THEN m.user_id_2
|
||||
ELSE m.user_id_1
|
||||
END as other_user_id
|
||||
FROM matches m
|
||||
WHERE m.id = $1 AND (m.user1_id = $2 OR m.user2_id = $2) AND m.status = 'active'
|
||||
WHERE m.id = $1 AND (m.user_id_1 = $2 OR m.user_id_2 = $2) AND m.is_active = true
|
||||
`, [matchId, userId]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
@@ -234,7 +231,7 @@ export class ChatService {
|
||||
// Проверяем, что пользователь является участником матча
|
||||
const matchResult = await query(`
|
||||
SELECT * FROM matches
|
||||
WHERE id = $1 AND (user1_id = $2 OR user2_id = $2) AND status = 'active'
|
||||
WHERE id = $1 AND (user_id_1 = $2 OR user_id_2 = $2) AND is_active = true
|
||||
`, [matchId, userId]);
|
||||
|
||||
if (matchResult.rows.length === 0) {
|
||||
@@ -244,9 +241,11 @@ export class ChatService {
|
||||
// Помечаем матч как неактивный
|
||||
await query(`
|
||||
UPDATE matches
|
||||
SET status = 'unmatched'
|
||||
SET is_active = false,
|
||||
unmatched_at = NOW(),
|
||||
unmatched_by = $2
|
||||
WHERE id = $1
|
||||
`, [matchId]);
|
||||
`, [matchId, userId]);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
|
||||
@@ -70,7 +70,7 @@ export class MatchingService {
|
||||
await transaction(async (client) => {
|
||||
// Создаем свайп
|
||||
await client.query(`
|
||||
INSERT INTO swipes (id, swiper_id, swiped_id, direction, created_at)
|
||||
INSERT INTO swipes (id, user_id, target_user_id, direction, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
`, [swipeId, userId, targetUserId, direction, new Date()]);
|
||||
|
||||
@@ -78,14 +78,14 @@ export class MatchingService {
|
||||
if (swipeType === 'like' || swipeType === 'superlike') {
|
||||
const reciprocalSwipe = await client.query(`
|
||||
SELECT * FROM swipes
|
||||
WHERE swiper_id = $1 AND swiped_id = $2 AND direction IN ('like', 'super')
|
||||
WHERE swiper_id = $1 AND swiped_id = $2 AND direction IN ('right', 'super')
|
||||
`, [targetUserId, userId]);
|
||||
|
||||
if (reciprocalSwipe.rows.length > 0) {
|
||||
// Проверяем, что матч еще не существует
|
||||
const existingMatch = await client.query(`
|
||||
SELECT * FROM matches
|
||||
WHERE (user1_id = $1 AND user2_id = $2) OR (user1_id = $2 AND user2_id = $1)
|
||||
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) {
|
||||
@@ -98,9 +98,9 @@ export class MatchingService {
|
||||
|
||||
// Создаем матч
|
||||
await client.query(`
|
||||
INSERT INTO matches (id, user1_id, user2_id, matched_at, status)
|
||||
INSERT INTO matches (id, user_id_1, user_id_2, created_at, is_active)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
`, [matchId, user1Id, user2Id, new Date(), 'active']);
|
||||
`, [matchId, user1Id, user2Id, new Date(), true]);
|
||||
|
||||
match = new Match({
|
||||
id: matchId,
|
||||
@@ -143,7 +143,7 @@ export class MatchingService {
|
||||
async getSwipe(userId: string, targetUserId: string): Promise<Swipe | null> {
|
||||
const result = await query(`
|
||||
SELECT * FROM swipes
|
||||
WHERE swiper_id = $1 AND swiped_id = $2
|
||||
WHERE user_id = $1 AND target_user_id = $2
|
||||
`, [userId, targetUserId]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
@@ -163,8 +163,8 @@ export class MatchingService {
|
||||
|
||||
const result = await query(`
|
||||
SELECT * FROM matches
|
||||
WHERE (user1_id = $1 OR user2_id = $1) AND status = 'active'
|
||||
ORDER BY matched_at DESC
|
||||
WHERE (user_id_1 = $1 OR user_id_2 = $1) AND is_active = true
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $2
|
||||
`, [userId, limit]);
|
||||
|
||||
@@ -217,7 +217,7 @@ export class MatchingService {
|
||||
async getRecentLikes(userId: string, limit: number = 20): Promise<Swipe[]> {
|
||||
const result = await query(`
|
||||
SELECT * FROM swipes
|
||||
WHERE swiped_id = $1 AND direction IN ('like', 'super') AND is_match = false
|
||||
WHERE target_user_id = $1 AND direction IN ('right', 'super') AND is_match = false
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $2
|
||||
`, [userId, limit]);
|
||||
@@ -311,11 +311,11 @@ export class MatchingService {
|
||||
private mapEntityToMatch(entity: any): Match {
|
||||
return new Match({
|
||||
id: entity.id,
|
||||
userId1: entity.user1_id,
|
||||
userId2: entity.user2_id,
|
||||
createdAt: entity.matched_at || entity.created_at,
|
||||
userId1: entity.user_id_1,
|
||||
userId2: entity.user_id_2,
|
||||
createdAt: entity.created_at,
|
||||
lastMessageAt: entity.last_message_at,
|
||||
isActive: entity.status === 'active',
|
||||
isActive: entity.is_active === true,
|
||||
isSuperMatch: false, // Определяется из swipes если нужно
|
||||
unreadCount1: 0,
|
||||
unreadCount2: 0
|
||||
@@ -329,8 +329,8 @@ export class MatchingService {
|
||||
FROM swipes s1
|
||||
JOIN swipes s2 ON s1.user_id = s2.target_user_id AND s1.target_user_id = s2.user_id
|
||||
WHERE s1.user_id = $1
|
||||
AND s1.type IN ('like', 'superlike')
|
||||
AND s2.type IN ('like', 'superlike')
|
||||
AND s1.direction IN ('right', 'super')
|
||||
AND s2.direction IN ('right', 'super')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM matches m
|
||||
WHERE (m.user_id_1 = s1.user_id AND m.user_id_2 = s1.target_user_id)
|
||||
@@ -342,7 +342,7 @@ export class MatchingService {
|
||||
}
|
||||
|
||||
// Получить следующего кандидата для просмотра
|
||||
async getNextCandidate(telegramId: string): Promise<Profile | null> {
|
||||
async getNextCandidate(telegramId: string, isNewUser: boolean = false): Promise<Profile | null> {
|
||||
// Сначала получаем профиль пользователя по telegramId
|
||||
const userProfile = await this.profileService.getProfileByTelegramId(telegramId);
|
||||
if (!userProfile) {
|
||||
@@ -354,18 +354,26 @@ export class MatchingService {
|
||||
|
||||
// Получаем список уже просмотренных пользователей
|
||||
const viewedUsers = await query(`
|
||||
SELECT DISTINCT swiped_id
|
||||
SELECT DISTINCT target_user_id
|
||||
FROM swipes
|
||||
WHERE swiper_id = $1
|
||||
WHERE user_id = $1
|
||||
`, [userId]);
|
||||
|
||||
const viewedUserIds = viewedUsers.rows.map((row: any) => row.swiped_id);
|
||||
const viewedUserIds = viewedUsers.rows.map((row: any) => row.target_user_id);
|
||||
viewedUserIds.push(userId); // Исключаем самого себя
|
||||
|
||||
// Формируем условие для исключения уже просмотренных
|
||||
const excludeCondition = viewedUserIds.length > 0
|
||||
? `AND p.user_id NOT IN (${viewedUserIds.map((_: any, i: number) => `$${i + 2}`).join(', ')})`
|
||||
: '';
|
||||
// Если это новый пользователь или у пользователя мало просмотренных профилей,
|
||||
// показываем всех пользователей по очереди (исключая только себя)
|
||||
let excludeCondition = '';
|
||||
|
||||
if (!isNewUser) {
|
||||
excludeCondition = viewedUserIds.length > 0
|
||||
? `AND p.user_id NOT IN (${viewedUserIds.map((_: any, i: number) => `$${i + 2}`).join(', ')})`
|
||||
: '';
|
||||
} else {
|
||||
// Для новых пользователей исключаем только себя
|
||||
excludeCondition = `AND p.user_id != $2`;
|
||||
}
|
||||
|
||||
// Ищем подходящих кандидатов
|
||||
const candidateQuery = `
|
||||
|
||||
@@ -233,8 +233,8 @@ export class NotificationService {
|
||||
SELECT m.created_at
|
||||
FROM messages m
|
||||
JOIN matches mt ON m.match_id = mt.id
|
||||
WHERE (mt.user1_id = $1 OR mt.user2_id = $1)
|
||||
AND (mt.user1_id = $2 OR mt.user2_id = $2)
|
||||
WHERE (mt.user_id_1 = $1 OR mt.user_id_2 = $1)
|
||||
AND (mt.user_id_1 = $2 OR mt.user_id_2 = $2)
|
||||
AND m.sender_id = $1
|
||||
ORDER BY m.created_at DESC
|
||||
LIMIT 1
|
||||
@@ -347,10 +347,33 @@ export class NotificationService {
|
||||
// Планировщик уведомлений (вызывается периодически)
|
||||
async processScheduledNotifications(): Promise<void> {
|
||||
try {
|
||||
// Проверим, существует ли таблица scheduled_notifications
|
||||
const tableCheck = await query(`
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_name = 'scheduled_notifications'
|
||||
) as exists
|
||||
`);
|
||||
|
||||
if (!tableCheck.rows[0].exists) {
|
||||
// Если таблицы нет, создаем её
|
||||
await query(`
|
||||
CREATE TABLE IF NOT EXISTS scheduled_notifications (
|
||||
id UUID PRIMARY KEY,
|
||||
user_id UUID REFERENCES users(id),
|
||||
type VARCHAR(50) NOT NULL,
|
||||
data JSONB,
|
||||
scheduled_at TIMESTAMP NOT NULL,
|
||||
is_processed BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
)
|
||||
`);
|
||||
}
|
||||
|
||||
// Получаем запланированные уведомления
|
||||
const result = await query(`
|
||||
SELECT * FROM scheduled_notifications
|
||||
WHERE scheduled_at <= $1 AND processed = false
|
||||
WHERE scheduled_at <= $1 AND is_processed = false
|
||||
ORDER BY scheduled_at ASC
|
||||
LIMIT 100
|
||||
`, [new Date()]);
|
||||
@@ -370,7 +393,7 @@ export class NotificationService {
|
||||
|
||||
// Отмечаем как обработанное
|
||||
await query(
|
||||
'UPDATE scheduled_notifications SET processed = true WHERE id = $1',
|
||||
'UPDATE scheduled_notifications SET is_processed = true WHERE id = $1',
|
||||
[notification.id]
|
||||
);
|
||||
} catch (error) {
|
||||
|
||||
@@ -49,18 +49,15 @@ export class ProfileService {
|
||||
// Сохранение в базу данных
|
||||
await query(`
|
||||
INSERT INTO profiles (
|
||||
id, user_id, name, age, gender, looking_for, bio, photos, interests,
|
||||
hobbies, location, education, occupation, height, religion, dating_goal,
|
||||
latitude, longitude, verification_status, is_active, is_visible,
|
||||
created_at, updated_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23)
|
||||
id, user_id, name, age, gender, interested_in, bio, photos,
|
||||
city, education, job, height, religion, dating_goal,
|
||||
is_verified, is_visible, created_at, updated_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
|
||||
`, [
|
||||
profileId, userId, profile.name, profile.age, profile.gender, profile.interestedIn,
|
||||
profile.bio, profile.photos, profile.interests, profile.hobbies,
|
||||
profile.city, profile.education, profile.job, profile.height,
|
||||
profile.religion, profile.datingGoal,
|
||||
profile.location?.latitude, profile.location?.longitude,
|
||||
'unverified', true, profile.isVisible, profile.createdAt, profile.updatedAt
|
||||
profile.bio, JSON.stringify(profile.photos), profile.city, profile.education, profile.job,
|
||||
profile.height, profile.religion, profile.datingGoal,
|
||||
profile.isVerified, profile.isVisible, profile.createdAt, profile.updatedAt
|
||||
]);
|
||||
|
||||
return profile;
|
||||
@@ -137,8 +134,7 @@ export class ProfileService {
|
||||
ON CONFLICT (telegram_id) DO UPDATE SET
|
||||
username = EXCLUDED.username,
|
||||
first_name = EXCLUDED.first_name,
|
||||
last_name = EXCLUDED.last_name,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
last_name = EXCLUDED.last_name
|
||||
RETURNING id
|
||||
`, [
|
||||
parseInt(telegramId),
|
||||
@@ -177,12 +173,8 @@ export class ProfileService {
|
||||
updateValues.push(value);
|
||||
break;
|
||||
case 'location':
|
||||
if (value && typeof value === 'object' && 'latitude' in value) {
|
||||
updateFields.push(`latitude = $${paramIndex++}`);
|
||||
updateValues.push(value.latitude);
|
||||
updateFields.push(`longitude = $${paramIndex++}`);
|
||||
updateValues.push(value.longitude);
|
||||
}
|
||||
// Пропускаем обработку местоположения, так как колонки location нет
|
||||
console.log('Skipping location update - column does not exist');
|
||||
break;
|
||||
case 'searchPreferences':
|
||||
// Поля search preferences больше не хранятся в БД, пропускаем
|
||||
@@ -339,8 +331,8 @@ export class ProfileService {
|
||||
const [likesResult, matchesResult, likesReceivedResult] = await Promise.all([
|
||||
query('SELECT COUNT(*) as count FROM swipes WHERE swiper_id = $1 AND direction IN ($2, $3)',
|
||||
[userId, 'like', 'super']),
|
||||
query('SELECT COUNT(*) as count FROM matches WHERE (user1_id = $1 OR user2_id = $1) AND status = $2',
|
||||
[userId, 'active']),
|
||||
query('SELECT COUNT(*) as count FROM matches WHERE (user_id_1 = $1 OR user_id_2 = $1) AND is_active = true',
|
||||
[userId]),
|
||||
query('SELECT COUNT(*) as count FROM swipes WHERE swiped_id = $1 AND direction IN ($2, $3)',
|
||||
[userId, 'like', 'super'])
|
||||
]);
|
||||
@@ -424,6 +416,27 @@ export class ProfileService {
|
||||
return [];
|
||||
};
|
||||
|
||||
// Функция для парсинга JSON полей
|
||||
const parseJsonField = (jsonField: any): any[] => {
|
||||
if (!jsonField) return [];
|
||||
|
||||
// Если это строка, пробуем распарсить JSON
|
||||
if (typeof jsonField === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(jsonField);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch (e) {
|
||||
console.error('Error parsing JSON field:', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Если это уже массив, возвращаем как есть
|
||||
if (Array.isArray(jsonField)) return jsonField;
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
return new Profile({
|
||||
userId: entity.user_id,
|
||||
name: entity.name,
|
||||
@@ -431,8 +444,8 @@ export class ProfileService {
|
||||
gender: entity.gender,
|
||||
interestedIn: entity.looking_for,
|
||||
bio: entity.bio,
|
||||
photos: parsePostgresArray(entity.photos),
|
||||
interests: parsePostgresArray(entity.interests),
|
||||
photos: parseJsonField(entity.photos),
|
||||
interests: parseJsonField(entity.interests),
|
||||
hobbies: entity.hobbies,
|
||||
city: entity.location || entity.city,
|
||||
education: entity.education,
|
||||
@@ -441,14 +454,11 @@ export class ProfileService {
|
||||
religion: entity.religion,
|
||||
datingGoal: entity.dating_goal,
|
||||
lifestyle: {
|
||||
smoking: entity.smoking,
|
||||
drinking: entity.drinking,
|
||||
kids: entity.has_kids
|
||||
},
|
||||
location: entity.latitude && entity.longitude ? {
|
||||
latitude: entity.latitude,
|
||||
longitude: entity.longitude
|
||||
} : undefined,
|
||||
smoking: undefined,
|
||||
drinking: undefined,
|
||||
kids: undefined
|
||||
}, // Пропускаем lifestyle, так как этих колонок нет
|
||||
location: undefined, // Пропускаем location, так как этих колонок нет
|
||||
searchPreferences: {
|
||||
minAge: 18,
|
||||
maxAge: 50,
|
||||
@@ -466,9 +476,10 @@ export class ProfileService {
|
||||
// Специальные случаи для некоторых полей
|
||||
const specialCases: { [key: string]: string } = {
|
||||
'interestedIn': 'looking_for',
|
||||
'job': 'occupation',
|
||||
'city': 'location',
|
||||
// Удалили 'job': 'occupation', так как колонка occupation не существует
|
||||
// Вместо этого используем job
|
||||
'datingGoal': 'dating_goal'
|
||||
// Удалили 'city': 'location', так как колонка location не существует
|
||||
};
|
||||
|
||||
if (specialCases[str]) {
|
||||
@@ -484,7 +495,7 @@ export class ProfileService {
|
||||
await transaction(async (client) => {
|
||||
// Удаляем связанные данные
|
||||
await client.query('DELETE FROM messages WHERE sender_id = $1 OR receiver_id = $1', [userId]);
|
||||
await client.query('DELETE FROM matches WHERE user1_id = $1 OR user2_id = $1', [userId]);
|
||||
await client.query('DELETE FROM matches WHERE user_id_1 = $1 OR user_id_2 = $1', [userId]);
|
||||
await client.query('DELETE FROM swipes WHERE swiper_id = $1 OR swiped_id = $1', [userId]);
|
||||
await client.query('DELETE FROM profiles WHERE user_id = $1', [userId]);
|
||||
});
|
||||
|
||||
@@ -24,8 +24,9 @@ export class VipService {
|
||||
// Проверить премиум статус пользователя
|
||||
async checkPremiumStatus(telegramId: string): Promise<PremiumInfo> {
|
||||
try {
|
||||
// Проверяем существование пользователя
|
||||
const result = await query(`
|
||||
SELECT premium, premium_expires_at
|
||||
SELECT id
|
||||
FROM users
|
||||
WHERE telegram_id = $1
|
||||
`, [telegramId]);
|
||||
@@ -34,27 +35,12 @@ export class VipService {
|
||||
throw new BotError('User not found', 'USER_NOT_FOUND', 404);
|
||||
}
|
||||
|
||||
const user = result.rows[0];
|
||||
const isPremium = user.premium;
|
||||
const expiresAt = user.premium_expires_at ? new Date(user.premium_expires_at) : undefined;
|
||||
|
||||
let daysLeft = undefined;
|
||||
if (isPremium && expiresAt) {
|
||||
const now = new Date();
|
||||
const timeDiff = expiresAt.getTime() - now.getTime();
|
||||
daysLeft = Math.ceil(timeDiff / (1000 * 3600 * 24));
|
||||
|
||||
// Если премиум истек
|
||||
if (daysLeft <= 0) {
|
||||
await this.removePremium(telegramId);
|
||||
return { isPremium: false };
|
||||
}
|
||||
}
|
||||
|
||||
// Временно возвращаем false для всех пользователей, так как колонки premium нет
|
||||
// В будущем, когда колонки будут добавлены, этот код нужно будет заменить обратно
|
||||
return {
|
||||
isPremium,
|
||||
expiresAt,
|
||||
daysLeft
|
||||
isPremium: false,
|
||||
expiresAt: undefined,
|
||||
daysLeft: undefined
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error checking premium status:', error);
|
||||
@@ -65,14 +51,9 @@ export class VipService {
|
||||
// Добавить премиум статус
|
||||
async addPremium(telegramId: string, durationDays: number = 30): Promise<void> {
|
||||
try {
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setDate(expiresAt.getDate() + durationDays);
|
||||
|
||||
await query(`
|
||||
UPDATE users
|
||||
SET premium = true, premium_expires_at = $2
|
||||
WHERE telegram_id = $1
|
||||
`, [telegramId, expiresAt]);
|
||||
// Временно заглушка, так как колонок premium и premium_expires_at нет
|
||||
console.log(`[VIP] Попытка добавить премиум для ${telegramId} на ${durationDays} дней`);
|
||||
// TODO: Добавить колонки premium и premium_expires_at в таблицу users
|
||||
} catch (error) {
|
||||
console.error('Error adding premium:', error);
|
||||
throw error;
|
||||
@@ -82,11 +63,9 @@ export class VipService {
|
||||
// Удалить премиум статус
|
||||
async removePremium(telegramId: string): Promise<void> {
|
||||
try {
|
||||
await query(`
|
||||
UPDATE users
|
||||
SET premium = false, premium_expires_at = NULL
|
||||
WHERE telegram_id = $1
|
||||
`, [telegramId]);
|
||||
// Временно заглушка, так как колонок premium и premium_expires_at нет
|
||||
console.log(`[VIP] Попытка удалить премиум для ${telegramId}`);
|
||||
// TODO: Добавить колонки premium и premium_expires_at в таблицу users
|
||||
} catch (error) {
|
||||
console.error('Error removing premium:', error);
|
||||
throw error;
|
||||
|
||||
Reference in New Issue
Block a user