geo distance meter
This commit is contained in:
7
Makefile
7
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
|
||||
|
||||
85
bin/CLEAR_INTERACTIONS_README.md
Normal file
85
bin/CLEAR_INTERACTIONS_README.md
Normal file
@@ -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) для безопасности
|
||||
- Все операции выполняются атомарно
|
||||
- В случае ошибки изменения откатываются
|
||||
|
||||
## Примечания
|
||||
|
||||
- ⚠️ **Необратимая операция!** Удаленные данные нельзя восстановить
|
||||
- 💡 Рекомендуется делать резервную копию БД перед запуском
|
||||
- 🔒 Убедитесь, что у вас есть права на удаление данных в БД
|
||||
71
bin/clear_interactions.sh
Executable file
71
bin/clear_interactions.sh
Executable file
@@ -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
|
||||
17
sql/add_location_coordinates.sql
Normal file
17
sql/add_location_coordinates.sql
Normal file
@@ -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)';
|
||||
42
sql/clear_interactions.sql
Normal file
42
sql/clear_interactions.sql
Normal file
@@ -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;
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
// Отправляем пользователю информацию с кнопками подтверждения
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user