geo distance meter

This commit is contained in:
2025-11-06 15:34:51 +09:00
parent 0bbeb0767b
commit 240864617f
8 changed files with 290 additions and 11 deletions

View File

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

View 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
View 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

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

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

View File

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

View File

@@ -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
};
}
// Отправляем пользователю информацию с кнопками подтверждения

View File

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