diff --git a/Makefile b/Makefile index e8106c1..0117f97 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # Makefile для Telegram Tinder Bot -.PHONY: help install update run migrate fix-docker clean +.PHONY: help install update run migrate fix-docker clean clear-interactions # Значения по умолчанию DB_HOST ?= db @@ -17,6 +17,7 @@ help: @echo "make run - Запуск бота в контейнере" @echo "make migrate - Применение миграций базы данных" @echo "make fix-docker - Исправление проблем с Docker" + @echo "make clear-interactions - Очистка матчей, свайпов и сообщений" @echo "make clean - Очистка и остановка контейнеров" install: @@ -95,6 +96,10 @@ fix-docker: @docker rm -f postgres-tinder adminer-tinder telegram-tinder-bot 2>/dev/null || true @docker system prune -f --volumes >/dev/null 2>&1 || true +clear-interactions: + @echo "Очистка взаимодействий пользователей..." + @bash bin/clear_interactions.sh + clean: @echo "Очистка..." @docker-compose down || true diff --git a/bin/CLEAR_INTERACTIONS_README.md b/bin/CLEAR_INTERACTIONS_README.md new file mode 100644 index 0000000..23dddbf --- /dev/null +++ b/bin/CLEAR_INTERACTIONS_README.md @@ -0,0 +1,85 @@ +# Скрипт очистки взаимодействий пользователей + +## Описание + +Этот скрипт удаляет все взаимодействия между пользователями, оставляя только сами профили. Полезно для тестирования или сброса состояния приложения. + +## Что удаляется + +- ✅ **Messages** - все сообщения в чатах +- ✅ **Matches** - все матчи между пользователями +- ✅ **Profile Views** - все просмотры профилей +- ✅ **Swipes** - все свайпы (лайки, дизлайки, суперлайки) +- ✅ **Notifications** - все уведомления + +## Что НЕ удаляется + +- ❌ **Users** - пользователи остаются +- ❌ **Profiles** - профили пользователей остаются + +## Использование + +### Способ 1: Через Makefile (рекомендуется) + +```bash +make clear-interactions +``` + +### Способ 2: Прямой запуск скрипта + +```bash +./bin/clear_interactions.sh +``` + +### Способ 3: Прямое выполнение SQL + +```bash +PGPASSWORD='your_password' psql -h host -U username -d database -f sql/clear_interactions.sql +``` + +## Подтверждение + +Скрипт запросит подтверждение перед выполнением: + +``` +Вы уверены, что хотите продолжить? (yes/no): +``` + +Введите `yes` для продолжения или `no` для отмены. + +## Требования + +- Файл `.env` должен существовать и содержать переменные: + - `DB_HOST` + - `DB_PORT` + - `DB_NAME` + - `DB_USERNAME` + - `DB_PASSWORD` + +## Вывод + +После успешного выполнения скрипт покажет статистику: + +``` + table_name | remaining_records +-------------------+------------------- + messages | 0 + matches | 0 + profile_views | 0 + swipes | 0 + notifications | 0 + users | 2 + profiles | 2 +``` + +## Безопасность + +- Скрипт использует транзакцию (BEGIN/COMMIT) для безопасности +- Все операции выполняются атомарно +- В случае ошибки изменения откатываются + +## Примечания + +- ⚠️ **Необратимая операция!** Удаленные данные нельзя восстановить +- 💡 Рекомендуется делать резервную копию БД перед запуском +- 🔒 Убедитесь, что у вас есть права на удаление данных в БД diff --git a/bin/clear_interactions.sh b/bin/clear_interactions.sh new file mode 100755 index 0000000..a854152 --- /dev/null +++ b/bin/clear_interactions.sh @@ -0,0 +1,71 @@ +#!/bin/bash + +# Скрипт для очистки всех взаимодействий между пользователями +# Использование: ./clear_interactions.sh + +set -e + +# Цвета для вывода +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo -e "${YELLOW}================================================${NC}" +echo -e "${YELLOW} Скрипт очистки взаимодействий пользователей${NC}" +echo -e "${YELLOW}================================================${NC}" +echo "" +echo -e "${RED}ВНИМАНИЕ!${NC} Будут удалены:" +echo " - Все сообщения (messages)" +echo " - Все матчи (matches)" +echo " - Все просмотры профилей (profile_views)" +echo " - Все свайпы (swipes)" +echo " - Все уведомления (notifications)" +echo "" +echo -e "Профили пользователей ${GREEN}НЕ${NC} будут удалены." +echo "" + +# Запрос подтверждения +read -p "Вы уверены, что хотите продолжить? (yes/no): " confirmation + +if [ "$confirmation" != "yes" ]; then + echo -e "${YELLOW}Операция отменена.${NC}" + exit 0 +fi + +echo "" +echo -e "${YELLOW}Загрузка переменных окружения...${NC}" + +# Загрузка переменных из .env файла +if [ -f .env ]; then + export $(cat .env | grep -v '^#' | xargs) +else + echo -e "${RED}Ошибка: файл .env не найден!${NC}" + exit 1 +fi + +# Проверка наличия необходимых переменных +if [ -z "$DB_HOST" ] || [ -z "$DB_PORT" ] || [ -z "$DB_NAME" ] || [ -z "$DB_USERNAME" ] || [ -z "$DB_PASSWORD" ]; then + echo -e "${RED}Ошибка: не все переменные БД определены в .env${NC}" + exit 1 +fi + +echo -e "${GREEN}Переменные загружены успешно.${NC}" +echo "" +echo -e "${YELLOW}Выполнение SQL скрипта...${NC}" + +# Выполнение SQL скрипта +PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USERNAME" -d "$DB_NAME" -f sql/clear_interactions.sql + +if [ $? -eq 0 ]; then + echo "" + echo -e "${GREEN}================================================${NC}" + echo -e "${GREEN} ✅ Очистка выполнена успешно!${NC}" + echo -e "${GREEN}================================================${NC}" +else + echo "" + echo -e "${RED}================================================${NC}" + echo -e "${RED} ❌ Ошибка при выполнении очистки!${NC}" + echo -e "${RED}================================================${NC}" + exit 1 +fi diff --git a/sql/add_location_coordinates.sql b/sql/add_location_coordinates.sql new file mode 100644 index 0000000..06c9d3c --- /dev/null +++ b/sql/add_location_coordinates.sql @@ -0,0 +1,17 @@ +-- Миграция: Добавление колонок для хранения координат местоположения +-- Дата: 2025-01-20 +-- Описание: Добавляет location_lat и location_lon для хранения GPS-координат, +-- полученных через Kakao Maps API, для расчета расстояния между пользователями + +-- Добавляем колонки для широты и долготы +ALTER TABLE profiles ADD COLUMN IF NOT EXISTS location_lat DECIMAL(10, 8); +ALTER TABLE profiles ADD COLUMN IF NOT EXISTS location_lon DECIMAL(11, 8); + +-- Создаем индекс для быстрого поиска по координатам +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; + +-- Комментарии для документации +COMMENT ON COLUMN profiles.location_lat IS 'Широта местоположения пользователя (из Kakao Maps)'; +COMMENT ON COLUMN profiles.location_lon IS 'Долгота местоположения пользователя (из Kakao Maps)'; diff --git a/sql/clear_interactions.sql b/sql/clear_interactions.sql new file mode 100644 index 0000000..f6b1c01 --- /dev/null +++ b/sql/clear_interactions.sql @@ -0,0 +1,42 @@ +-- Скрипт для очистки всех взаимодействий между пользователями +-- Удаляет матчи, сообщения, свайпы и показы анкет +-- Оставляет только пользователей и их профили + +-- Начало транзакции +BEGIN; + +-- Удаление всех сообщений в чатах +DELETE FROM messages; + +-- Удаление всех матчей +DELETE FROM matches; + +-- Удаление всех просмотров профилей +DELETE FROM profile_views; + +-- Удаление всех свайпов (лайки, дизлайки, суперлайки) +DELETE FROM swipes; + +-- Удаление всех уведомлений +DELETE FROM notifications; + +-- Фиксация транзакции +COMMIT; + +-- Вывод статистики после очистки +SELECT + 'messages' as table_name, + COUNT(*) as remaining_records +FROM messages +UNION ALL +SELECT 'matches', COUNT(*) FROM matches +UNION ALL +SELECT 'profile_views', COUNT(*) FROM profile_views +UNION ALL +SELECT 'swipes', COUNT(*) FROM swipes +UNION ALL +SELECT 'notifications', COUNT(*) FROM notifications +UNION ALL +SELECT 'users', COUNT(*) FROM users +UNION ALL +SELECT 'profiles', COUNT(*) FROM profiles; diff --git a/src/handlers/callbackHandlers.ts b/src/handlers/callbackHandlers.ts index 19fa09c..92fe5b3 100644 --- a/src/handlers/callbackHandlers.ts +++ b/src/handlers/callbackHandlers.ts @@ -137,6 +137,11 @@ export class CallbackHandlers { console.log(`User ${telegramId} confirmed city edit: ${editState.tempCity}`); // Обновляем город в профиле await this.messageHandlers.updateProfileField(telegramId, 'city', editState.tempCity); + // Обновляем координаты, если они есть + if (editState.tempLocation) { + console.log(`User ${telegramId} updating location: lat=${editState.tempLocation.latitude}, lon=${editState.tempLocation.longitude}`); + await this.messageHandlers.updateProfileField(telegramId, 'location', editState.tempLocation); + } // Очищаем состояние this.messageHandlers.clearProfileEditState(telegramId); // Убираем inline-кнопки @@ -1061,7 +1066,20 @@ export class CallbackHandlers { const mainPhotoFileId = profile.photos[0]; // Первое фото - главное let profileText = '👤 ' + profile.name + ', ' + profile.age + '\n'; - profileText += '📍 ' + (profile.city || 'Не указан') + '\n'; + profileText += '📍 ' + (profile.city || 'Не указан'); + + // Добавляем расстояние, если это не владелец профиля и есть viewerId + if (!isOwner && viewerId) { + const viewerProfile = await this.profileService.getProfileByTelegramId(viewerId); + if (viewerProfile && viewerProfile.location && profile.location) { + const distance = viewerProfile.getDistanceTo(profile); + if (distance !== null) { + profileText += ` (${Math.round(distance)} км)`; + } + } + } + profileText += '\n'; + if (profile.job) profileText += '💼 ' + profile.job + '\n'; if (profile.education) profileText += '🎓 ' + profile.education + '\n'; if (profile.height) profileText += '📏 ' + profile.height + ' см\n'; @@ -1204,8 +1222,21 @@ export class CallbackHandlers { const candidatePhotoFileId = candidate.photos[0]; // Первое фото - главное + // Получаем профиль текущего пользователя для вычисления расстояния + const userProfile = await this.profileService.getProfileByTelegramId(telegramId); + let candidateText = candidate.name + ', ' + candidate.age + '\n'; - candidateText += '📍 ' + (candidate.city || 'Не указан') + '\n'; + candidateText += '📍 ' + (candidate.city || 'Не указан'); + + // Добавляем расстояние, если есть координаты у обоих пользователей + if (userProfile && userProfile.location && candidate.location) { + const distance = userProfile.getDistanceTo(candidate); + if (distance !== null) { + candidateText += ` (${Math.round(distance)} км)`; + } + } + candidateText += '\n'; + if (candidate.job) candidateText += '💼 ' + candidate.job + '\n'; if (candidate.education) candidateText += '🎓 ' + candidate.education + '\n'; if (candidate.height) candidateText += '📏 ' + candidate.height + ' см\n'; diff --git a/src/handlers/messageHandlers.ts b/src/handlers/messageHandlers.ts index cb642b1..a76a00b 100644 --- a/src/handlers/messageHandlers.ts +++ b/src/handlers/messageHandlers.ts @@ -21,6 +21,7 @@ interface ProfileEditState { waitingForInput: boolean; field: string; tempCity?: string; // Временное хранение города для подтверждения + tempLocation?: { latitude: number; longitude: number }; // Временное хранение координат } export class MessageHandlers { @@ -268,6 +269,7 @@ export class MessageHandlers { interestedIn: interestedIn, bio: profileData.bio, city: profileData.city, + location: profileData.location, // Добавляем координаты photos: profileData.photos, interests: [], searchPreferences: { @@ -598,6 +600,10 @@ export class MessageHandlers { // В БД поле называется 'city' (не 'location') updates.city = value; break; + case 'location': + // Обновляем координаты + updates.location = value; + break; case 'job': // В БД поле называется 'occupation', но мы используем job в модели updates.job = value; @@ -705,8 +711,12 @@ export class MessageHandlers { // Логируем результат console.log(`KakaoMaps resolved for user ${userId}: city=${cityName}, address=${displayAddress}`); - // Временно сохраняем город (пока не подтвержден пользователем) + // Временно сохраняем город И координаты (пока не подтверждены пользователем) userState.data.city = cityName; + userState.data.location = { + latitude: msg.location.latitude, + longitude: msg.location.longitude + }; userState.step = 'confirm_city'; // Отправляем пользователю информацию с кнопками подтверждения @@ -794,10 +804,14 @@ export class MessageHandlers { // Логируем результат console.log(`KakaoMaps resolved for user ${userId} during edit: city=${cityName}, address=${displayAddress}`); - // Временно сохраняем город в состояние редактирования + // Временно сохраняем город И координаты в состояние редактирования const editState = this.profileEditStates.get(userId); if (editState) { editState.tempCity = cityName; + editState.tempLocation = { + latitude: msg.location.latitude, + longitude: msg.location.longitude + }; } // Отправляем пользователю информацию с кнопками подтверждения diff --git a/src/services/profileService.ts b/src/services/profileService.ts index fe7a4bd..17572f8 100644 --- a/src/services/profileService.ts +++ b/src/services/profileService.ts @@ -50,9 +50,9 @@ export class ProfileService { await query(` INSERT INTO profiles ( id, user_id, name, age, gender, interested_in, bio, photos, - city, education, job, height, religion, dating_goal, + city, education, job, height, location_lat, location_lon, 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) + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20) ON CONFLICT (user_id) DO UPDATE SET name = EXCLUDED.name, age = EXCLUDED.age, @@ -64,6 +64,8 @@ export class ProfileService { education = EXCLUDED.education, job = EXCLUDED.job, height = EXCLUDED.height, + location_lat = EXCLUDED.location_lat, + location_lon = EXCLUDED.location_lon, religion = EXCLUDED.religion, dating_goal = EXCLUDED.dating_goal, is_verified = EXCLUDED.is_verified, @@ -72,7 +74,10 @@ export class ProfileService { `, [ profileId, userId, profile.name, profile.age, profile.gender, profile.interestedIn, profile.bio, profile.photos, profile.city, profile.education, profile.job, - profile.height, profile.religion, profile.datingGoal, + profile.height, + profile.location?.latitude || null, + profile.location?.longitude || null, + profile.religion, profile.datingGoal, profile.isVerified, profile.isVisible, profile.createdAt, profile.updatedAt ]); @@ -190,8 +195,14 @@ export class ProfileService { updateValues.push(Array.isArray(value) ? value : [value]); break; case 'location': - // Пропускаем обработку местоположения, так как колонки location нет - console.log('Skipping location update - column does not exist'); + // Сохраняем координаты в location_lat и location_lon + if (value && typeof value === 'object' && 'latitude' in value && 'longitude' in value) { + updateFields.push(`location_lat = $${paramIndex++}`); + updateValues.push(value.latitude); + updateFields.push(`location_lon = $${paramIndex++}`); + updateValues.push(value.longitude); + console.log(`Updating location: lat=${value.latitude}, lon=${value.longitude}`); + } break; case 'searchPreferences': // Поля search preferences больше не хранятся в БД, пропускаем @@ -475,7 +486,10 @@ export class ProfileService { drinking: undefined, kids: undefined }, // Пропускаем lifestyle, так как этих колонок нет - location: undefined, // Пропускаем location, так как этих колонок нет + location: (entity.location_lat && entity.location_lon) ? { + latitude: parseFloat(entity.location_lat), + longitude: parseFloat(entity.location_lon) + } : undefined, searchPreferences: { minAge: 18, maxAge: 50,