mass refactor

This commit is contained in:
2025-09-18 08:31:14 +09:00
parent 856bf3ca2a
commit bdd7d0424f
58 changed files with 3009 additions and 291 deletions

View File

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

View File

@@ -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': 'курение',

View File

@@ -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) {

View File

@@ -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
View 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 };

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

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

View File

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

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

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

View File

@@ -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) {

View File

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

View File

@@ -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) {

View File

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

View File

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