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 { MessageHandlers } from './messageHandlers';
|
||||||
import { ProfileEditController } from '../controllers/profileEditController';
|
import { ProfileEditController } from '../controllers/profileEditController';
|
||||||
import { EnhancedChatHandlers } from './enhancedChatHandlers';
|
import { EnhancedChatHandlers } from './enhancedChatHandlers';
|
||||||
|
import { VipController } from '../controllers/vipController';
|
||||||
|
import { VipService } from '../services/vipService';
|
||||||
|
|
||||||
export class CallbackHandlers {
|
export class CallbackHandlers {
|
||||||
private bot: TelegramBot;
|
private bot: TelegramBot;
|
||||||
@@ -15,6 +17,8 @@ export class CallbackHandlers {
|
|||||||
private messageHandlers: MessageHandlers;
|
private messageHandlers: MessageHandlers;
|
||||||
private profileEditController: ProfileEditController;
|
private profileEditController: ProfileEditController;
|
||||||
private enhancedChatHandlers: EnhancedChatHandlers;
|
private enhancedChatHandlers: EnhancedChatHandlers;
|
||||||
|
private vipController: VipController;
|
||||||
|
private vipService: VipService;
|
||||||
|
|
||||||
constructor(bot: TelegramBot, messageHandlers: MessageHandlers) {
|
constructor(bot: TelegramBot, messageHandlers: MessageHandlers) {
|
||||||
this.bot = bot;
|
this.bot = bot;
|
||||||
@@ -24,6 +28,8 @@ export class CallbackHandlers {
|
|||||||
this.messageHandlers = messageHandlers;
|
this.messageHandlers = messageHandlers;
|
||||||
this.profileEditController = new ProfileEditController(this.profileService);
|
this.profileEditController = new ProfileEditController(this.profileService);
|
||||||
this.enhancedChatHandlers = new EnhancedChatHandlers(bot);
|
this.enhancedChatHandlers = new EnhancedChatHandlers(bot);
|
||||||
|
this.vipController = new VipController(bot);
|
||||||
|
this.vipService = new VipService();
|
||||||
}
|
}
|
||||||
|
|
||||||
register(): void {
|
register(): void {
|
||||||
@@ -211,7 +217,30 @@ export class CallbackHandlers {
|
|||||||
} else if (data === 'back_to_browsing') {
|
} else if (data === 'back_to_browsing') {
|
||||||
await this.handleStartBrowsing(chatId, telegramId);
|
await this.handleStartBrowsing(chatId, telegramId);
|
||||||
} else if (data === 'get_vip') {
|
} 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 {
|
else {
|
||||||
@@ -1724,20 +1753,30 @@ export class CallbackHandlers {
|
|||||||
const profile = await this.profileService.getProfileByTelegramId(telegramId);
|
const profile = await this.profileService.getProfileByTelegramId(telegramId);
|
||||||
|
|
||||||
if (profile) {
|
if (profile) {
|
||||||
const keyboard: InlineKeyboardMarkup = {
|
// Проверяем премиум статус
|
||||||
inline_keyboard: [
|
const premiumInfo = await this.vipService.checkPremiumStatus(telegramId);
|
||||||
[
|
|
||||||
{ text: '👤 Мой профиль', callback_data: 'view_my_profile' },
|
let keyboardRows = [
|
||||||
{ text: '🔍 Просмотр анкет', callback_data: 'start_browsing' }
|
[
|
||||||
],
|
{ 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: 'view_matches' }
|
||||||
[
|
|
||||||
{ text: '⚙️ Настройки', callback_data: 'settings' }
|
|
||||||
]
|
|
||||||
]
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
// Добавляем кнопку 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(
|
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