feat: VIP search now shows only opposite gender - Modified VIP search filtering to always show opposite gender regardless of user's interested_in preference - Male users see only female profiles - Female users see only male profiles - Improved gender filtering logic in vipService.ts
This commit is contained in:
105
VIP_FUNCTIONS.md
Normal file
105
VIP_FUNCTIONS.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# VIP Функции - Документация
|
||||
|
||||
## Обзор
|
||||
Реализованы VIP функции с проверкой премиум статуса пользователя в базе данных.
|
||||
|
||||
## База данных
|
||||
|
||||
### Новые поля в таблице users:
|
||||
- `premium` (BOOLEAN) - флаг премиум статуса
|
||||
- `premium_expires_at` (TIMESTAMP) - дата окончания премиум
|
||||
|
||||
## Логика работы
|
||||
|
||||
### 1. Кнопка "VIP Поиск"
|
||||
- **Если premium = false**: показывает информацию о премиум и предложение купить
|
||||
- **Если premium = true**: открывает VIP поиск с фильтрами
|
||||
|
||||
### 2. VIP Поиск включает:
|
||||
|
||||
#### Быстрый VIP поиск
|
||||
- Только пользователи с фото
|
||||
- Только онлайн пользователи
|
||||
|
||||
#### Расширенный поиск
|
||||
- Фильтр по возрасту
|
||||
- Фильтр по городу
|
||||
- Фильтр по целям знакомства
|
||||
- Фильтр по хобби
|
||||
- Фильтр по образу жизни
|
||||
|
||||
#### Поиск по целям знакомства
|
||||
- Серьезные отношения
|
||||
- Общение и дружба
|
||||
- Развлечения
|
||||
- Деловые знакомства
|
||||
|
||||
#### Поиск по хобби
|
||||
- Фильтрация по массиву хобби в профиле
|
||||
|
||||
## Файлы
|
||||
|
||||
### Новые файлы:
|
||||
- `src/services/vipService.ts` - сервис для работы с VIP функциями
|
||||
- `src/controllers/vipController.ts` - контроллер VIP поиска
|
||||
- `src/database/migrations/add_premium_field.sql` - миграция для premium полей
|
||||
|
||||
### Изменённые файлы:
|
||||
- `src/handlers/callbackHandlers.ts` - добавлены VIP обработчики
|
||||
|
||||
## Методы VipService
|
||||
|
||||
### checkPremiumStatus(telegramId: string)
|
||||
Проверяет премиум статус пользователя, автоматически убирает истёкший премиум.
|
||||
|
||||
### addPremium(telegramId: string, durationDays: number)
|
||||
Добавляет премиум статус на указанное количество дней.
|
||||
|
||||
### vipSearch(telegramId: string, filters: VipSearchFilters)
|
||||
Выполняет VIP поиск с фильтрами (только для премиум пользователей).
|
||||
|
||||
### getPremiumFeatures()
|
||||
Возвращает описание премиум возможностей.
|
||||
|
||||
## Методы VipController
|
||||
|
||||
### showVipSearch(chatId, telegramId)
|
||||
Основной метод - показывает VIP поиск или информацию о премиум.
|
||||
|
||||
### performQuickVipSearch(chatId, telegramId)
|
||||
Быстрый VIP поиск (фото + онлайн).
|
||||
|
||||
### showDatingGoalSearch(chatId, telegramId)
|
||||
Показывает поиск по целям знакомства.
|
||||
|
||||
## Тестирование
|
||||
|
||||
### Добавить премиум пользователю:
|
||||
```sql
|
||||
UPDATE users SET premium = true, premium_expires_at = NOW() + INTERVAL '30 days'
|
||||
WHERE telegram_id = 'YOUR_TELEGRAM_ID';
|
||||
```
|
||||
|
||||
### Убрать премиум:
|
||||
```sql
|
||||
UPDATE users SET premium = false, premium_expires_at = NULL
|
||||
WHERE telegram_id = 'YOUR_TELEGRAM_ID';
|
||||
```
|
||||
|
||||
## Callback данные
|
||||
|
||||
- `get_vip` / `vip_search` - показать VIP поиск
|
||||
- `vip_quick_search` - быстрый VIP поиск
|
||||
- `vip_advanced_search` - расширенный поиск
|
||||
- `vip_dating_goal_search` - поиск по целям
|
||||
- `vip_goal_{goal}` - поиск по конкретной цели
|
||||
- `vip_like_{telegramId}` - VIP лайк
|
||||
- `vip_superlike_{telegramId}` - VIP супер-лайк
|
||||
- `vip_dislike_{telegramId}` - VIP дизлайк
|
||||
|
||||
## Безопасность
|
||||
|
||||
- Все VIP функции проверяют премиум статус
|
||||
- Автоматическое удаление истёкшего премиум
|
||||
- Валидация всех входных данных
|
||||
- Проверка существования пользователей перед операциями
|
||||
291
src/controllers/vipController.ts
Normal file
291
src/controllers/vipController.ts
Normal file
@@ -0,0 +1,291 @@
|
||||
import TelegramBot, { InlineKeyboardMarkup } from 'node-telegram-bot-api';
|
||||
import { VipService, VipSearchFilters } from '../services/vipService';
|
||||
import { ProfileService } from '../services/profileService';
|
||||
|
||||
interface VipSearchState {
|
||||
filters: VipSearchFilters;
|
||||
currentStep: string;
|
||||
}
|
||||
|
||||
export class VipController {
|
||||
private bot: TelegramBot;
|
||||
private vipService: VipService;
|
||||
private profileService: ProfileService;
|
||||
private vipSearchStates: Map<string, VipSearchState> = new Map();
|
||||
|
||||
constructor(bot: TelegramBot) {
|
||||
this.bot = bot;
|
||||
this.vipService = new VipService();
|
||||
this.profileService = new ProfileService();
|
||||
}
|
||||
|
||||
// Показать VIP поиск или информацию о премиум
|
||||
async showVipSearch(chatId: number, telegramId: string): Promise<void> {
|
||||
try {
|
||||
const premiumInfo = await this.vipService.checkPremiumStatus(telegramId);
|
||||
|
||||
if (!premiumInfo.isPremium) {
|
||||
// Показываем информацию о премиум
|
||||
await this.showPremiumInfo(chatId);
|
||||
} else {
|
||||
// Показываем VIP поиск
|
||||
await this.showVipSearchMenu(chatId, telegramId, premiumInfo);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error showing VIP search:', error);
|
||||
await this.bot.sendMessage(chatId, '❌ Ошибка при загрузке VIP поиска');
|
||||
}
|
||||
}
|
||||
|
||||
// Показать информацию о премиум подписке
|
||||
private async showPremiumInfo(chatId: number): Promise<void> {
|
||||
const premiumText = this.vipService.getPremiumFeatures();
|
||||
|
||||
const keyboard: InlineKeyboardMarkup = {
|
||||
inline_keyboard: [
|
||||
[{ text: '💎 Купить VIP', url: 'https://t.me/admin_bot' }],
|
||||
[{ text: '🔙 Назад в меню', callback_data: 'main_menu' }]
|
||||
]
|
||||
};
|
||||
|
||||
await this.bot.sendMessage(chatId, premiumText, {
|
||||
reply_markup: keyboard
|
||||
});
|
||||
}
|
||||
|
||||
// Показать меню VIP поиска
|
||||
private async showVipSearchMenu(chatId: number, telegramId: string, premiumInfo: any): Promise<void> {
|
||||
const daysText = premiumInfo.daysLeft ? ` (остался ${premiumInfo.daysLeft} дн.)` : '';
|
||||
|
||||
const text = `💎 VIP ПОИСК 💎\n\n` +
|
||||
`✅ Премиум статус активен${daysText}\n\n` +
|
||||
`🎯 Выберите тип поиска:`;
|
||||
|
||||
const keyboard: InlineKeyboardMarkup = {
|
||||
inline_keyboard: [
|
||||
[{ text: '🔍 Быстрый VIP поиск', callback_data: 'vip_quick_search' }],
|
||||
[{ text: '⚙️ Расширенный поиск с фильтрами', callback_data: 'vip_advanced_search' }],
|
||||
[{ text: '🎯 Поиск по целям знакомства', callback_data: 'vip_dating_goal_search' }],
|
||||
[{ text: '🎨 Поиск по хобби', callback_data: 'vip_hobbies_search' }],
|
||||
[{ text: '🔙 Назад в меню', callback_data: 'main_menu' }]
|
||||
]
|
||||
};
|
||||
|
||||
await this.bot.sendMessage(chatId, text, {
|
||||
reply_markup: keyboard
|
||||
});
|
||||
}
|
||||
|
||||
// Быстрый VIP поиск
|
||||
async performQuickVipSearch(chatId: number, telegramId: string): Promise<void> {
|
||||
try {
|
||||
const filters: VipSearchFilters = {
|
||||
hasPhotos: true,
|
||||
isOnline: true
|
||||
};
|
||||
|
||||
const results = await this.vipService.vipSearch(telegramId, filters);
|
||||
await this.showSearchResults(chatId, telegramId, results, 'Быстрый VIP поиск');
|
||||
} catch (error) {
|
||||
console.error('Error in quick VIP search:', error);
|
||||
await this.bot.sendMessage(chatId, '❌ Ошибка при выполнении поиска');
|
||||
}
|
||||
}
|
||||
|
||||
// Начать настройку фильтров для расширенного поиска
|
||||
async startAdvancedSearch(chatId: number, telegramId: string): Promise<void> {
|
||||
const state: VipSearchState = {
|
||||
filters: {},
|
||||
currentStep: 'age_min'
|
||||
};
|
||||
|
||||
this.vipSearchStates.set(telegramId, state);
|
||||
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
'⚙️ Расширенный VIP поиск\n\n' +
|
||||
'🔢 Укажите минимальный возраст (18-65) или отправьте "-" чтобы пропустить:',
|
||||
{ }
|
||||
);
|
||||
}
|
||||
|
||||
// Поиск по целям знакомства
|
||||
async showDatingGoalSearch(chatId: number, telegramId: string): Promise<void> {
|
||||
const keyboard: InlineKeyboardMarkup = {
|
||||
inline_keyboard: [
|
||||
[{ text: '💕 Серьёзные отношения', callback_data: 'vip_goal_serious' }],
|
||||
[{ text: '🎉 Лёгкие отношения', callback_data: 'vip_goal_casual' }],
|
||||
[{ text: '👥 Дружба', callback_data: 'vip_goal_friends' }],
|
||||
[{ text: '🔥 Одна ночь', callback_data: 'vip_goal_one_night' }],
|
||||
[{ text: '😏 FWB', callback_data: 'vip_goal_fwb' }],
|
||||
[{ text: '💎 Спонсорство', callback_data: 'vip_goal_sugar' }],
|
||||
[{ text: '💍 Брак с переездом', callback_data: 'vip_goal_marriage_abroad' }],
|
||||
[{ text: '💫 Полиамория', callback_data: 'vip_goal_polyamory' }],
|
||||
[{ text: '🤷♀️ Пока не определился', callback_data: 'vip_goal_unsure' }],
|
||||
[{ text: '🔙 Назад', callback_data: 'vip_search' }]
|
||||
]
|
||||
};
|
||||
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
'🎯 Поиск по целям знакомства\n\nВыберите цель:',
|
||||
{
|
||||
reply_markup: keyboard
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Выполнить поиск по цели знакомства
|
||||
async performDatingGoalSearch(chatId: number, telegramId: string, goal: string): Promise<void> {
|
||||
try {
|
||||
// Используем значения из базы данных как есть
|
||||
const filters: VipSearchFilters = {
|
||||
datingGoal: goal,
|
||||
hasPhotos: true
|
||||
};
|
||||
|
||||
const results = await this.vipService.vipSearch(telegramId, filters);
|
||||
|
||||
const goalNames: { [key: string]: string } = {
|
||||
'serious': 'Серьёзные отношения',
|
||||
'casual': 'Лёгкие отношения',
|
||||
'friends': 'Дружба',
|
||||
'one_night': 'Одна ночь',
|
||||
'fwb': 'FWB',
|
||||
'sugar': 'Спонсорство',
|
||||
'marriage_abroad': 'Брак с переездом',
|
||||
'polyamory': 'Полиамория',
|
||||
'unsure': 'Пока не определился'
|
||||
};
|
||||
|
||||
const goalName = goalNames[goal] || goal;
|
||||
await this.showSearchResults(chatId, telegramId, results, `Поиск: ${goalName}`);
|
||||
} catch (error) {
|
||||
console.error('Error in dating goal search:', error);
|
||||
await this.bot.sendMessage(chatId, '❌ Ошибка при выполнении поиска');
|
||||
}
|
||||
}
|
||||
|
||||
// Показать результаты поиска
|
||||
private async showSearchResults(chatId: number, telegramId: string, results: any[], searchType: string): Promise<void> {
|
||||
if (results.length === 0) {
|
||||
const keyboard: InlineKeyboardMarkup = {
|
||||
inline_keyboard: [
|
||||
[{ text: '🔍 Новый поиск', callback_data: 'vip_search' }],
|
||||
[{ text: '🔙 Главное меню', callback_data: 'main_menu' }]
|
||||
]
|
||||
};
|
||||
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
`😔 ${searchType}\n\n` +
|
||||
'К сожалению, никого не найдено по вашим критериям.\n\n' +
|
||||
'💡 Попробуйте изменить фильтры или выполнить обычный поиск.',
|
||||
{
|
||||
reply_markup: keyboard,
|
||||
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
`🎉 ${searchType}\n\n` +
|
||||
`Найдено: ${results.length} ${this.getCountText(results.length)}\n\n` +
|
||||
'Начинаем просмотр профилей...',
|
||||
{ }
|
||||
);
|
||||
|
||||
// Показываем первый профиль из результатов
|
||||
const firstProfile = results[0];
|
||||
await this.showVipSearchProfile(chatId, telegramId, firstProfile, results, 0);
|
||||
}
|
||||
|
||||
// Показать профиль из VIP поиска
|
||||
private async showVipSearchProfile(chatId: number, telegramId: string, profile: any, allResults: any[], currentIndex: number): Promise<void> {
|
||||
try {
|
||||
let profileText = `💎 VIP Поиск (${currentIndex + 1}/${allResults.length})\n\n`;
|
||||
profileText += `👤 ${profile.name}, ${profile.age}\n`;
|
||||
profileText += `📍 ${profile.city || 'Не указан'}\n`;
|
||||
|
||||
if (profile.dating_goal) {
|
||||
const goalText = this.getDatingGoalText(profile.dating_goal);
|
||||
profileText += `🎯 ${goalText}\n`;
|
||||
}
|
||||
|
||||
if (profile.bio) {
|
||||
profileText += `\n📝 ${profile.bio}\n`;
|
||||
}
|
||||
|
||||
if (profile.is_online) {
|
||||
profileText += `\n🟢 Онлайн\n`;
|
||||
}
|
||||
|
||||
const keyboard: InlineKeyboardMarkup = {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{ text: '❤️', callback_data: `vip_like_${profile.telegram_id}` },
|
||||
{ text: '⭐', callback_data: `vip_superlike_${profile.telegram_id}` },
|
||||
{ text: '👎', callback_data: `vip_dislike_${profile.telegram_id}` }
|
||||
],
|
||||
[{ text: '👤 Полный профиль', callback_data: `view_profile_${profile.user_id}` }],
|
||||
[
|
||||
{ text: '⬅️ Предыдущий', callback_data: `vip_prev_${currentIndex}` },
|
||||
{ text: '➡️ Следующий', callback_data: `vip_next_${currentIndex}` }
|
||||
],
|
||||
[{ text: '🔍 Новый поиск', callback_data: 'vip_search' }],
|
||||
[{ text: '🔙 Главное меню', callback_data: 'main_menu' }]
|
||||
]
|
||||
};
|
||||
|
||||
// Проверяем есть ли фотографии
|
||||
if (profile.photos && Array.isArray(profile.photos) && profile.photos.length > 0) {
|
||||
await this.bot.sendPhoto(chatId, profile.photos[0], {
|
||||
caption: profileText,
|
||||
reply_markup: keyboard,
|
||||
|
||||
});
|
||||
} else {
|
||||
await this.bot.sendMessage(chatId, profileText, {
|
||||
reply_markup: keyboard,
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
// Сохраняем результаты поиска для навигации
|
||||
// Можно сохранить в Redis или временной переменной
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error showing VIP search profile:', error);
|
||||
await this.bot.sendMessage(chatId, '❌ Ошибка при показе профиля');
|
||||
}
|
||||
}
|
||||
|
||||
private getCountText(count: number): string {
|
||||
const lastDigit = count % 10;
|
||||
const lastTwoDigits = count % 100;
|
||||
|
||||
if (lastTwoDigits >= 11 && lastTwoDigits <= 14) {
|
||||
return 'пользователей';
|
||||
}
|
||||
|
||||
switch (lastDigit) {
|
||||
case 1: return 'пользователь';
|
||||
case 2:
|
||||
case 3:
|
||||
case 4: return 'пользователя';
|
||||
default: return 'пользователей';
|
||||
}
|
||||
}
|
||||
|
||||
private getDatingGoalText(goal: string): string {
|
||||
const goals: { [key: string]: string } = {
|
||||
'serious_relationship': 'Серьезные отношения',
|
||||
'friendship': 'Общение и дружба',
|
||||
'fun': 'Развлечения',
|
||||
'networking': 'Деловые знакомства'
|
||||
};
|
||||
return goals[goal] || 'Не указано';
|
||||
}
|
||||
}
|
||||
10
src/database/migrations/add_premium_field.sql
Normal file
10
src/database/migrations/add_premium_field.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
-- Добавление поля premium для VIP функций
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS premium BOOLEAN DEFAULT FALSE;
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS premium_expires_at TIMESTAMP WITH TIME ZONE DEFAULT NULL;
|
||||
|
||||
-- Индекс для быстрого поиска premium пользователей
|
||||
CREATE INDEX IF NOT EXISTS idx_users_premium ON users(premium, premium_expires_at);
|
||||
|
||||
-- Комментарии
|
||||
COMMENT ON COLUMN users.premium IS 'VIP статус пользователя';
|
||||
COMMENT ON COLUMN users.premium_expires_at IS 'Дата окончания VIP статуса';
|
||||
@@ -6,6 +6,8 @@ import { Profile } from '../models/Profile';
|
||||
import { MessageHandlers } from './messageHandlers';
|
||||
import { ProfileEditController } from '../controllers/profileEditController';
|
||||
import { EnhancedChatHandlers } from './enhancedChatHandlers';
|
||||
import { VipController } from '../controllers/vipController';
|
||||
import { VipService } from '../services/vipService';
|
||||
|
||||
export class CallbackHandlers {
|
||||
private bot: TelegramBot;
|
||||
@@ -15,6 +17,8 @@ export class CallbackHandlers {
|
||||
private messageHandlers: MessageHandlers;
|
||||
private profileEditController: ProfileEditController;
|
||||
private enhancedChatHandlers: EnhancedChatHandlers;
|
||||
private vipController: VipController;
|
||||
private vipService: VipService;
|
||||
|
||||
constructor(bot: TelegramBot, messageHandlers: MessageHandlers) {
|
||||
this.bot = bot;
|
||||
@@ -24,6 +28,8 @@ export class CallbackHandlers {
|
||||
this.messageHandlers = messageHandlers;
|
||||
this.profileEditController = new ProfileEditController(this.profileService);
|
||||
this.enhancedChatHandlers = new EnhancedChatHandlers(bot);
|
||||
this.vipController = new VipController(bot);
|
||||
this.vipService = new VipService();
|
||||
}
|
||||
|
||||
register(): void {
|
||||
@@ -211,7 +217,30 @@ export class CallbackHandlers {
|
||||
} else if (data === 'back_to_browsing') {
|
||||
await this.handleStartBrowsing(chatId, telegramId);
|
||||
} else if (data === 'get_vip') {
|
||||
await this.handleGetVip(chatId, telegramId);
|
||||
await this.vipController.showVipSearch(chatId, telegramId);
|
||||
}
|
||||
|
||||
// VIP функции
|
||||
else if (data === 'vip_search') {
|
||||
await this.vipController.showVipSearch(chatId, telegramId);
|
||||
} else if (data === 'vip_quick_search') {
|
||||
await this.vipController.performQuickVipSearch(chatId, telegramId);
|
||||
} else if (data === 'vip_advanced_search') {
|
||||
await this.vipController.startAdvancedSearch(chatId, telegramId);
|
||||
} else if (data === 'vip_dating_goal_search') {
|
||||
await this.vipController.showDatingGoalSearch(chatId, telegramId);
|
||||
} else if (data.startsWith('vip_goal_')) {
|
||||
const goal = data.replace('vip_goal_', '');
|
||||
await this.vipController.performDatingGoalSearch(chatId, telegramId, goal);
|
||||
} else if (data.startsWith('vip_like_')) {
|
||||
const targetTelegramId = data.replace('vip_like_', '');
|
||||
await this.handleVipLike(chatId, telegramId, targetTelegramId);
|
||||
} else if (data.startsWith('vip_superlike_')) {
|
||||
const targetTelegramId = data.replace('vip_superlike_', '');
|
||||
await this.handleVipSuperlike(chatId, telegramId, targetTelegramId);
|
||||
} else if (data.startsWith('vip_dislike_')) {
|
||||
const targetTelegramId = data.replace('vip_dislike_', '');
|
||||
await this.handleVipDislike(chatId, telegramId, targetTelegramId);
|
||||
}
|
||||
|
||||
else {
|
||||
@@ -1724,20 +1753,30 @@ export class CallbackHandlers {
|
||||
const profile = await this.profileService.getProfileByTelegramId(telegramId);
|
||||
|
||||
if (profile) {
|
||||
const keyboard: InlineKeyboardMarkup = {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{ text: '👤 Мой профиль', callback_data: 'view_my_profile' },
|
||||
{ text: '🔍 Просмотр анкет', callback_data: 'start_browsing' }
|
||||
],
|
||||
[
|
||||
{ text: '💕 Мои матчи', callback_data: 'view_matches' },
|
||||
{ text: '⭐ VIP поиск', callback_data: 'vip_search' }
|
||||
],
|
||||
[
|
||||
{ text: '⚙️ Настройки', callback_data: 'settings' }
|
||||
]
|
||||
// Проверяем премиум статус
|
||||
const premiumInfo = await this.vipService.checkPremiumStatus(telegramId);
|
||||
|
||||
let keyboardRows = [
|
||||
[
|
||||
{ text: '👤 Мой профиль', callback_data: 'view_my_profile' },
|
||||
{ text: '🔍 Просмотр анкет', callback_data: 'start_browsing' }
|
||||
],
|
||||
[
|
||||
{ text: '💕 Мои матчи', callback_data: 'view_matches' }
|
||||
]
|
||||
];
|
||||
|
||||
// Добавляем кнопку VIP поиска если есть премиум, или кнопку "Получить VIP" если нет
|
||||
if (premiumInfo && premiumInfo.isPremium) {
|
||||
keyboardRows[1].push({ text: '⭐ VIP поиск', callback_data: 'vip_search' });
|
||||
} else {
|
||||
keyboardRows[1].push({ text: '💎 Получить VIP', callback_data: 'get_vip' });
|
||||
}
|
||||
|
||||
keyboardRows.push([{ text: '⚙️ Настройки', callback_data: 'settings' }]);
|
||||
|
||||
const keyboard: InlineKeyboardMarkup = {
|
||||
inline_keyboard: keyboardRows
|
||||
};
|
||||
|
||||
await this.bot.sendMessage(
|
||||
@@ -1886,4 +1925,95 @@ export class CallbackHandlers {
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// VIP лайк
|
||||
async handleVipLike(chatId: number, telegramId: string, targetTelegramId: string): Promise<void> {
|
||||
try {
|
||||
// Получаем user_id по telegram_id для совместимости с существующей логикой
|
||||
const targetUserId = await this.profileService.getUserIdByTelegramId(targetTelegramId);
|
||||
if (!targetUserId) {
|
||||
throw new Error('Target user not found');
|
||||
}
|
||||
|
||||
const result = await this.matchingService.performSwipe(telegramId, targetTelegramId, 'like');
|
||||
|
||||
if (result.isMatch) {
|
||||
// Это матч!
|
||||
const targetProfile = await this.profileService.getProfileByUserId(targetUserId);
|
||||
|
||||
const keyboard: InlineKeyboardMarkup = {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{ text: '💬 Написать сообщение', callback_data: 'chat_' + targetUserId },
|
||||
{ text: '📱 Нативный чат', callback_data: 'open_native_chat_' + result.match?.id }
|
||||
],
|
||||
[{ text: '🔍 Продолжить VIP поиск', callback_data: 'vip_search' }]
|
||||
]
|
||||
};
|
||||
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
'🎉 ЭТО МАТЧ! 💕\n\n' +
|
||||
'Вы понравились друг другу с ' + (targetProfile?.name || 'этим пользователем') + '!\n\n' +
|
||||
'Теперь вы можете начать общение!',
|
||||
{ reply_markup: keyboard }
|
||||
);
|
||||
} else {
|
||||
await this.bot.sendMessage(chatId, '👍 Лайк отправлен! Продолжайте VIP поиск.');
|
||||
}
|
||||
} catch (error) {
|
||||
await this.bot.sendMessage(chatId, '❌ Ошибка при отправке лайка');
|
||||
console.error('VIP Like error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// VIP супер-лайк
|
||||
async handleVipSuperlike(chatId: number, telegramId: string, targetTelegramId: string): Promise<void> {
|
||||
try {
|
||||
const targetUserId = await this.profileService.getUserIdByTelegramId(targetTelegramId);
|
||||
if (!targetUserId) {
|
||||
throw new Error('Target user not found');
|
||||
}
|
||||
|
||||
const result = await this.matchingService.performSwipe(telegramId, targetTelegramId, 'superlike');
|
||||
|
||||
if (result.isMatch) {
|
||||
const targetProfile = await this.profileService.getProfileByUserId(targetUserId);
|
||||
|
||||
const keyboard: InlineKeyboardMarkup = {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{ text: '💬 Написать сообщение', callback_data: 'chat_' + targetUserId },
|
||||
{ text: '📱 Нативный чат', callback_data: 'open_native_chat_' + result.match?.id }
|
||||
],
|
||||
[{ text: '🔍 Продолжить VIP поиск', callback_data: 'vip_search' }]
|
||||
]
|
||||
};
|
||||
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
'⭐ СУПЕР МАТЧ! ⭐\n\n' +
|
||||
'Ваш супер-лайк привел к матчу с ' + (targetProfile?.name || 'этим пользователем') + '!\n\n' +
|
||||
'Начните общение прямо сейчас!',
|
||||
{ reply_markup: keyboard }
|
||||
);
|
||||
} else {
|
||||
await this.bot.sendMessage(chatId, '⭐ Супер-лайк отправлен! Это повышает ваши шансы.');
|
||||
}
|
||||
} catch (error) {
|
||||
await this.bot.sendMessage(chatId, '❌ Ошибка при отправке супер-лайка');
|
||||
console.error('VIP Superlike error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// VIP дизлайк
|
||||
async handleVipDislike(chatId: number, telegramId: string, targetTelegramId: string): Promise<void> {
|
||||
try {
|
||||
await this.matchingService.performSwipe(telegramId, targetTelegramId, 'pass');
|
||||
await this.bot.sendMessage(chatId, '👎 Профиль пропущен. Продолжайте VIP поиск.');
|
||||
} catch (error) {
|
||||
await this.bot.sendMessage(chatId, '❌ Ошибка при выполнении действия');
|
||||
console.error('VIP Dislike error:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
257
src/services/vipService.ts
Normal file
257
src/services/vipService.ts
Normal file
@@ -0,0 +1,257 @@
|
||||
import { query } from '../database/connection';
|
||||
import { BotError } from '../types/index';
|
||||
|
||||
export interface VipSearchFilters {
|
||||
ageMin?: number;
|
||||
ageMax?: number;
|
||||
city?: string;
|
||||
datingGoal?: string;
|
||||
hobbies?: string[];
|
||||
lifestyle?: string[];
|
||||
distance?: number;
|
||||
hasPhotos?: boolean;
|
||||
isOnline?: boolean;
|
||||
}
|
||||
|
||||
export interface PremiumInfo {
|
||||
isPremium: boolean;
|
||||
expiresAt?: Date;
|
||||
daysLeft?: number;
|
||||
}
|
||||
|
||||
export class VipService {
|
||||
|
||||
// Проверить премиум статус пользователя
|
||||
async checkPremiumStatus(telegramId: string): Promise<PremiumInfo> {
|
||||
try {
|
||||
const result = await query(`
|
||||
SELECT premium, premium_expires_at
|
||||
FROM users
|
||||
WHERE telegram_id = $1
|
||||
`, [telegramId]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isPremium,
|
||||
expiresAt,
|
||||
daysLeft
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error checking premium status:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Добавить премиум статус
|
||||
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]);
|
||||
} catch (error) {
|
||||
console.error('Error adding premium:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Удалить премиум статус
|
||||
async removePremium(telegramId: string): Promise<void> {
|
||||
try {
|
||||
await query(`
|
||||
UPDATE users
|
||||
SET premium = false, premium_expires_at = NULL
|
||||
WHERE telegram_id = $1
|
||||
`, [telegramId]);
|
||||
} catch (error) {
|
||||
console.error('Error removing premium:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// VIP поиск с фильтрами
|
||||
async vipSearch(telegramId: string, filters: VipSearchFilters): Promise<any[]> {
|
||||
try {
|
||||
// Проверяем премиум статус
|
||||
const premiumInfo = await this.checkPremiumStatus(telegramId);
|
||||
if (!premiumInfo.isPremium) {
|
||||
throw new BotError('Premium subscription required', 'PREMIUM_REQUIRED', 403);
|
||||
}
|
||||
|
||||
// Получаем профиль пользователя
|
||||
const userProfile = await query(`
|
||||
SELECT p.*, u.telegram_id
|
||||
FROM profiles p
|
||||
JOIN users u ON p.user_id = u.id
|
||||
WHERE u.telegram_id = $1
|
||||
`, [telegramId]);
|
||||
|
||||
if (userProfile.rows.length === 0) {
|
||||
throw new BotError('Profile not found', 'PROFILE_NOT_FOUND', 404);
|
||||
}
|
||||
|
||||
const currentUser = userProfile.rows[0];
|
||||
|
||||
// Строим запрос с фильтрами
|
||||
let query_text = `
|
||||
SELECT p.*, u.telegram_id,
|
||||
CASE WHEN u.updated_at > NOW() - INTERVAL '15 minutes' THEN true ELSE false END as is_online
|
||||
FROM profiles p
|
||||
JOIN users u ON p.user_id = u.id
|
||||
LEFT JOIN swipes s ON (
|
||||
s.swiper_id = $1 AND s.swiped_id = u.id
|
||||
)
|
||||
WHERE u.telegram_id != $2
|
||||
AND s.id IS NULL
|
||||
AND p.is_active = true
|
||||
`;
|
||||
|
||||
let params = [currentUser.user_id, telegramId];
|
||||
let paramIndex = 3;
|
||||
|
||||
// Фильтр по противоположному полу
|
||||
if (currentUser.gender === 'male') {
|
||||
query_text += ` AND p.gender = 'female'`;
|
||||
} else if (currentUser.gender === 'female') {
|
||||
query_text += ` AND p.gender = 'male'`;
|
||||
} else {
|
||||
// Если пол не определен или 'other', показываем всех кроме того же пола
|
||||
query_text += ` AND p.gender != $${paramIndex}`;
|
||||
params.push(currentUser.gender);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// Фильтр по возрасту
|
||||
if (filters.ageMin) {
|
||||
query_text += ` AND p.age >= $${paramIndex}`;
|
||||
params.push(filters.ageMin);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (filters.ageMax) {
|
||||
query_text += ` AND p.age <= $${paramIndex}`;
|
||||
params.push(filters.ageMax);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// Фильтр по городу
|
||||
if (filters.city) {
|
||||
query_text += ` AND LOWER(p.city) LIKE LOWER($${paramIndex})`;
|
||||
params.push(`%${filters.city}%`);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// Фильтр по цели знакомства
|
||||
if (filters.datingGoal) {
|
||||
query_text += ` AND p.dating_goal = $${paramIndex}`;
|
||||
params.push(filters.datingGoal);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// Фильтр по хобби
|
||||
if (filters.hobbies && filters.hobbies.length > 0) {
|
||||
const hobbyConditions = filters.hobbies.map((_, index) => {
|
||||
return `LOWER(p.hobbies) LIKE LOWER($${paramIndex + index})`;
|
||||
});
|
||||
query_text += ` AND (${hobbyConditions.join(' OR ')})`;
|
||||
filters.hobbies.forEach(hobby => {
|
||||
params.push(`%${hobby}%`);
|
||||
});
|
||||
paramIndex += filters.hobbies.length;
|
||||
}
|
||||
|
||||
// Фильтр по образу жизни
|
||||
if (filters.lifestyle && filters.lifestyle.length > 0) {
|
||||
const lifestyleConditions = filters.lifestyle.map((field) => {
|
||||
const condition = `p.lifestyle ? $${paramIndex}`;
|
||||
params.push(field);
|
||||
paramIndex++;
|
||||
return condition;
|
||||
});
|
||||
query_text += ` AND (${lifestyleConditions.join(' OR ')})`;
|
||||
}
|
||||
|
||||
// Фильтр по наличию фото
|
||||
if (filters.hasPhotos) {
|
||||
query_text += ` AND p.photos IS NOT NULL AND array_length(p.photos, 1) > 0`;
|
||||
}
|
||||
|
||||
// Фильтр по онлайн статусу
|
||||
if (filters.isOnline) {
|
||||
query_text += ` AND u.updated_at > NOW() - INTERVAL '15 minutes'`;
|
||||
}
|
||||
|
||||
query_text += ` ORDER BY
|
||||
CASE WHEN u.updated_at > NOW() - INTERVAL '15 minutes' THEN 0 ELSE 1 END,
|
||||
u.updated_at DESC,
|
||||
p.created_at DESC
|
||||
LIMIT 50`;
|
||||
|
||||
const result = await query(query_text, params);
|
||||
return result.rows;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in VIP search:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Получить информацию о премиум возможностях
|
||||
getPremiumFeatures(): string {
|
||||
return `💎 ПРЕМИУМ ПОДПИСКА 💎
|
||||
|
||||
🔥 Что дает VIP статус:
|
||||
|
||||
🎯 VIP Поиск с фильтрами:
|
||||
• Поиск по возрасту
|
||||
• Поиск по городу
|
||||
• Фильтр по целям знакомства
|
||||
• Поиск по хобби и интересам
|
||||
• Фильтр по образу жизни
|
||||
• Только пользователи с фото
|
||||
• Только онлайн пользователи
|
||||
|
||||
⚡ Дополнительные возможности:
|
||||
• Неограниченные супер-лайки
|
||||
• Просмотр кто лайкнул вас
|
||||
• Возможность отменить свайп
|
||||
• Приоритет в показе другим
|
||||
• Расширенная статистика
|
||||
• Скрытый режим просмотра
|
||||
|
||||
💰 Тарифы:
|
||||
• 1 месяц - 299₽
|
||||
• 3 месяца - 699₽ (экономия 25%)
|
||||
• 6 месяцев - 1199₽ (экономия 33%)
|
||||
• 1 год - 1999₽ (экономия 44%)
|
||||
|
||||
📞 Для покупки обратитесь к администратору:
|
||||
@admin_bot
|
||||
|
||||
✨ Попробуйте VIP уже сегодня!`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user