Compare commits
10 Commits
dev
...
240864617f
| Author | SHA1 | Date | |
|---|---|---|---|
| 240864617f | |||
| 0bbeb0767b | |||
| 88d9ccd75d | |||
| 9281388959 | |||
| 0566901fa4 | |||
| e907dffe8c | |||
| fdd0580554 | |||
| 29d6255f22 | |||
| d02a742278 | |||
| 155e4d3b7b |
@@ -46,6 +46,13 @@ JWT_SECRET=your_jwt_secret_here
|
||||
# Encryption key for sensitive data
|
||||
ENCRYPTION_KEY=your_encryption_key_here
|
||||
|
||||
# === KAKAO MAPS API ===
|
||||
|
||||
# Kakao REST API Key for location services (Get from https://developers.kakao.com/)
|
||||
# You can use either KAKAO_REST_API_KEY or KAKAO_MAP_REST_KEY
|
||||
KAKAO_REST_API_KEY=your_kakao_rest_api_key_here
|
||||
# KAKAO_MAP_REST_KEY=your_kakao_rest_api_key_here
|
||||
|
||||
# === ADVANCED SETTINGS ===
|
||||
|
||||
# Notification check interval in milliseconds (default: 60000 - 1 minute)
|
||||
|
||||
107
Makefile
Normal file
107
Makefile
Normal file
@@ -0,0 +1,107 @@
|
||||
# Makefile для Telegram Tinder Bot
|
||||
|
||||
.PHONY: help install update run migrate fix-docker clean clear-interactions
|
||||
|
||||
# Значения по умолчанию
|
||||
DB_HOST ?= db
|
||||
DB_PORT ?= 5432
|
||||
DB_NAME ?= telegram_tinder_bot
|
||||
DB_USERNAME ?= postgres
|
||||
DB_PASSWORD ?= postgres
|
||||
|
||||
# Основные команды
|
||||
help:
|
||||
@echo "========== Telegram Tinder Bot Makefile =========="
|
||||
@echo "make install - Установка зависимостей"
|
||||
@echo "make update - Обновление кода из репозитория"
|
||||
@echo "make run - Запуск бота в контейнере"
|
||||
@echo "make migrate - Применение миграций базы данных"
|
||||
@echo "make fix-docker - Исправление проблем с Docker"
|
||||
@echo "make clear-interactions - Очистка матчей, свайпов и сообщений"
|
||||
@echo "make clean - Очистка и остановка контейнеров"
|
||||
|
||||
install:
|
||||
@echo "Установка зависимостей..."
|
||||
@if ! command -v docker &> /dev/null; then \
|
||||
echo "Установка Docker..."; \
|
||||
echo "Удаление конфликтующих пакетов..."; \
|
||||
sudo apt remove -y docker.io containerd runc 2>/dev/null || true; \
|
||||
sudo apt update; \
|
||||
sudo apt install -y apt-transport-https ca-certificates curl software-properties-common; \
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg; \
|
||||
echo "deb [arch=$$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null; \
|
||||
sudo apt update; \
|
||||
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin; \
|
||||
sudo systemctl start docker; \
|
||||
sudo systemctl enable docker; \
|
||||
sudo usermod -aG docker $$USER; \
|
||||
echo "Docker установлен. Перелогиньтесь для применения группы docker."; \
|
||||
fi
|
||||
@if [ ! -f .env ]; then \
|
||||
echo "Создание .env файла..."; \
|
||||
cp .env.example .env 2>/dev/null || cp .env.production .env 2>/dev/null || echo "NODE_ENV=production" > .env; \
|
||||
echo "Пожалуйста, отредактируйте файл .env!"; \
|
||||
fi
|
||||
@mkdir -p logs uploads && chmod -R 777 logs uploads
|
||||
|
||||
update:
|
||||
@echo "Обновление кода..."
|
||||
@git fetch --all
|
||||
@git pull origin main || git pull origin master || echo "Не удалось обновить код"
|
||||
@if [ -f package.json ]; then npm ci || npm install; fi
|
||||
@echo "Пересборка контейнеров..."
|
||||
@docker compose down || docker-compose down || true
|
||||
@docker compose build || docker-compose build
|
||||
@docker compose up -d || docker-compose up -d
|
||||
@echo "Применение миграций к базе данных..."
|
||||
@sleep 5
|
||||
@make migrate
|
||||
@echo "✅ Обновление завершено! Бот перезапущен с новой версией."
|
||||
|
||||
run:
|
||||
@echo "Запуск бота..."
|
||||
@docker-compose down || true
|
||||
@make fix-docker
|
||||
@docker-compose build
|
||||
@docker-compose up -d
|
||||
@echo "Бот запущен! Для просмотра логов: docker-compose logs -f"
|
||||
|
||||
migrate:
|
||||
@echo "Применение миграций к базе данных..."
|
||||
@if [ ! -f .env ]; then \
|
||||
echo "❌ Файл .env не найден! Создайте его перед применением миграций."; \
|
||||
exit 1; \
|
||||
fi
|
||||
@. ./.env && export $$(cat .env | grep -v '^#' | xargs) && \
|
||||
echo "Подключение к БД: $$DB_HOST:$$DB_PORT/$$DB_NAME ($$DB_USERNAME)" && \
|
||||
echo "Создание расширения uuid-ossp..." && \
|
||||
PGPASSWORD="$$DB_PASSWORD" psql -h $$DB_HOST -p $$DB_PORT -U $$DB_USERNAME -d $$DB_NAME \
|
||||
-c "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";" 2>/dev/null || true && \
|
||||
echo "Применение consolidated.sql..." && \
|
||||
PGPASSWORD="$$DB_PASSWORD" psql -h $$DB_HOST -p $$DB_PORT -U $$DB_USERNAME -d $$DB_NAME \
|
||||
-f sql/consolidated.sql 2>&1 | grep -E "(ERROR|CREATE|ALTER)" || true && \
|
||||
echo "Применение дополнительных миграций..." && \
|
||||
for sql_file in sql/add_*.sql; do \
|
||||
[ -f "$$sql_file" ] && echo " - Применение $$(basename $$sql_file)..." && \
|
||||
PGPASSWORD="$$DB_PASSWORD" psql -h $$DB_HOST -p $$DB_PORT -U $$DB_USERNAME -d $$DB_NAME \
|
||||
-f "$$sql_file" 2>&1 | grep -v "NOTICE" || true; \
|
||||
done && \
|
||||
echo "✅ Миграции применены успешно!"
|
||||
|
||||
fix-docker:
|
||||
@echo "Исправление Docker конфигурации..."
|
||||
@if [ -f Dockerfile ] && grep -q "RUN npm run build" Dockerfile; then \
|
||||
sed -i 's/RUN npm run build/RUN npm run build:linux/g' Dockerfile; \
|
||||
fi
|
||||
@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
|
||||
@rm -rf temp_migrations node_modules/.cache
|
||||
@echo "Очистка завершена"
|
||||
49
additional_translations.json
Normal file
49
additional_translations.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"noProfile": {
|
||||
"message": "❌ У вас пока нет профиля.\\nСоздайте его для начала использования бота!",
|
||||
"createButton": "🚀 Создать профиль"
|
||||
},
|
||||
"noMatches": {
|
||||
"message": "💔 У вас пока нет матчей.\\n\\n🔍 Попробуйте просмотреть больше анкет!\\nИспользуйте /browse для поиска.",
|
||||
"browsing": "🔍 Найти еще"
|
||||
},
|
||||
"matches": {
|
||||
"title": "💕 Ваши матчи:",
|
||||
"openChats": "💬 Открыть чаты",
|
||||
"nativeChats": "📱 Нативные чаты",
|
||||
"findMore": "🔍 Найти еще",
|
||||
"cityNotSpecified": "Не указан"
|
||||
},
|
||||
"profileCreation": {
|
||||
"start": "👋 Давайте создадим ваш профиль!\\n\\n📝 Сначала напишите ваше имя:",
|
||||
"enterName": "❌ Пожалуйста, отправьте текстовое сообщение с вашим именем",
|
||||
"enterAge": "📅 Отлично! Теперь укажите ваш возраст:",
|
||||
"ageNotNumber": "❌ Пожалуйста, отправьте число",
|
||||
"ageInvalid": "❌ Возраст должен быть числом от 18 до 100",
|
||||
"enterCity": "📍 Прекрасно! В каком городе вы живете?",
|
||||
"cityText": "❌ Пожалуйста, отправьте название города",
|
||||
"enterBio": "📝 Теперь расскажите немного о себе (био):\\n\\n💡 Например: хобби, интересы, что ищете в отношениях и т.д.",
|
||||
"bioText": "❌ Пожалуйста, отправьте текстовое описание",
|
||||
"enterPhoto": "📸 Отлично! Теперь отправьте ваше фото:\\n\\n💡 Лучше использовать качественное фото лица",
|
||||
"photoRequired": "❌ Пожалуйста, отправьте фотографию",
|
||||
"error": "❌ Произошла ошибка. Попробуйте еще раз.",
|
||||
"createError": "❌ Ошибка при создании профиля. Попробуйте еще раз позже."
|
||||
},
|
||||
"profileView": {
|
||||
"cityNotSpecified": "Не указан",
|
||||
"bioNotSpecified": "Описание не указано",
|
||||
"editProfile": "✏️ Редактировать",
|
||||
"managePhotos": "📸 Фото",
|
||||
"startBrowsing": "🔍 Начать поиск",
|
||||
"backToBrowsing": "👈 Назад"
|
||||
},
|
||||
"browsing": {
|
||||
"noMoreProfiles": "🎉 Вы просмотрели всех доступных кандидатов!\\n\\n⏰ Попробуйте позже - возможно появятся новые анкеты!",
|
||||
"needProfile": "❌ Сначала создайте профиль!\\nИспользуйте команду /start",
|
||||
"cityNotSpecified": "Не указан"
|
||||
},
|
||||
"general": {
|
||||
"greeting": "Привет! 👋\\n\\nИспользуйте команды для навигации:\\n/start - Главное меню\\n/help - Справка\\n/profile - Мой профиль\\n/browse - Поиск анкет",
|
||||
"photoManagement": "📸 Для управления фотографиями используйте:"
|
||||
}
|
||||
}
|
||||
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) для безопасности
|
||||
- Все операции выполняются атомарно
|
||||
- В случае ошибки изменения откатываются
|
||||
|
||||
## Примечания
|
||||
|
||||
- ⚠️ **Необратимая операция!** Удаленные данные нельзя восстановить
|
||||
- 💡 Рекомендуется делать резервную копию БД перед запуском
|
||||
- 🔒 Убедитесь, что у вас есть права на удаление данных в БД
|
||||
54
bin/QUICK_FIX.md
Normal file
54
bin/QUICK_FIX.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Быстрое исправление проблем с миграциями
|
||||
|
||||
## Проблема
|
||||
При запуске миграций возникают ошибки с TypeScript-файлами и проблемы с модульными разрешениями.
|
||||
|
||||
## Быстрое решение
|
||||
|
||||
1. **Примените прямые SQL-миграции (рекомендуемый способ)**:
|
||||
```bash
|
||||
chmod +x bin/apply_direct_sql.sh
|
||||
./bin/apply_direct_sql.sh
|
||||
```
|
||||
Этот скрипт создаст и применит консолидированную SQL-миграцию, которая создаст все необходимые таблицы.
|
||||
|
||||
2. **Создайте консолидированную JS-миграцию**:
|
||||
```bash
|
||||
chmod +x bin/create_consolidated_migration.sh
|
||||
./bin/create_consolidated_migration.sh
|
||||
```
|
||||
Затем примените её:
|
||||
```bash
|
||||
DATABASE_URL="postgres://$DB_USERNAME:$DB_PASSWORD@$DB_HOST:$DB_PORT/$DB_NAME" npx node-pg-migrate up
|
||||
```
|
||||
|
||||
## Проверка результата
|
||||
|
||||
После выполнения миграций проверьте наличие таблиц в базе данных:
|
||||
|
||||
```bash
|
||||
export PGPASSWORD=$DB_PASSWORD
|
||||
psql -h $DB_HOST -p $DB_PORT -U $DB_USERNAME -d $DB_NAME -c "\dt"
|
||||
```
|
||||
|
||||
## Если проблемы сохраняются
|
||||
|
||||
1. **Проверьте доступность базы данных**:
|
||||
```bash
|
||||
export PGPASSWORD=$DB_PASSWORD
|
||||
psql -h $DB_HOST -p $DB_PORT -U $DB_USERNAME -d $DB_NAME -c "SELECT 1"
|
||||
```
|
||||
|
||||
2. **Проверьте правильность переменных окружения**:
|
||||
```bash
|
||||
echo "DB_HOST: $DB_HOST"
|
||||
echo "DB_PORT: $DB_PORT"
|
||||
echo "DB_NAME: $DB_NAME"
|
||||
echo "DB_USERNAME: $DB_USERNAME"
|
||||
```
|
||||
|
||||
3. **Установите PostgreSQL-клиент**, если он отсутствует:
|
||||
```bash
|
||||
apt-get update
|
||||
apt-get install -y postgresql-client
|
||||
```
|
||||
@@ -2,6 +2,24 @@
|
||||
|
||||
Этот документ описывает процесс автоматического обновления бота с помощью созданных скриптов.
|
||||
|
||||
## Доступные скрипты
|
||||
|
||||
### apply_all_patches.sh
|
||||
Применяет все SQL патчи к базе данных в правильном порядке:
|
||||
- Основная схема (consolidated.sql)
|
||||
- Исправление триггера looking_for
|
||||
- Добавление колонок job и state
|
||||
|
||||
```bash
|
||||
./bin/apply_all_patches.sh
|
||||
```
|
||||
|
||||
### apply_migrations.sh
|
||||
Применяет Node.js миграции через node-pg-migrate.
|
||||
|
||||
### apply_direct_sql.sh
|
||||
Применяет SQL файлы напрямую через psql.
|
||||
|
||||
## Скрипт обновления
|
||||
|
||||
Скрипт обновления выполняет следующие действия:
|
||||
|
||||
76
bin/apply_all_patches.sh
Executable file
76
bin/apply_all_patches.sh
Executable file
@@ -0,0 +1,76 @@
|
||||
#!/bin/bash
|
||||
# Применение всех патчей базы данных
|
||||
# Использование: ./bin/apply_all_patches.sh
|
||||
|
||||
set -e # Остановка при ошибке
|
||||
|
||||
# Загрузка переменных окружения
|
||||
if [ -f .env ]; then
|
||||
source .env
|
||||
else
|
||||
echo "❌ Файл .env не найден!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Проверка обязательных переменных
|
||||
if [ -z "$DB_HOST" ] || [ -z "$DB_PORT" ] || [ -z "$DB_NAME" ] || [ -z "$DB_USERNAME" ] || [ -z "$DB_PASSWORD" ]; then
|
||||
echo "❌ Не все переменные DB_* заданы в .env"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🔧 Применение патчей к базе данных..."
|
||||
echo "📍 Сервер: $DB_HOST:$DB_PORT"
|
||||
echo "📂 База данных: $DB_NAME"
|
||||
echo ""
|
||||
|
||||
# Функция применения патча
|
||||
apply_patch() {
|
||||
local patch_file=$1
|
||||
local description=$2
|
||||
|
||||
if [ ! -f "$patch_file" ]; then
|
||||
echo "⚠️ Патч $patch_file не найден, пропуск..."
|
||||
return
|
||||
fi
|
||||
|
||||
echo "📝 Применение: $description"
|
||||
if PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USERNAME" -d "$DB_NAME" -f "$patch_file" > /dev/null 2>&1; then
|
||||
echo "✅ Патч применен: $patch_file"
|
||||
else
|
||||
echo "⚠️ Ошибка при применении: $patch_file (возможно уже применен)"
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Применение патчей в правильном порядке
|
||||
apply_patch "sql/consolidated.sql" "Основная схема БД (16 таблиц)"
|
||||
apply_patch "sql/fix_looking_for_column.sql" "Исправление триггера и колонки looking_for"
|
||||
apply_patch "sql/add_job_and_state_columns.sql" "Добавление колонок job и state"
|
||||
|
||||
echo "🎉 Все патчи обработаны!"
|
||||
echo ""
|
||||
echo "🔍 Проверка применения патчей..."
|
||||
|
||||
# Проверка критичных колонок
|
||||
PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USERNAME" -d "$DB_NAME" << 'EOF'
|
||||
SELECT
|
||||
CASE
|
||||
WHEN EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'profiles' AND column_name = 'job')
|
||||
THEN '✅ profiles.job существует'
|
||||
ELSE '❌ profiles.job НЕ НАЙДЕНА'
|
||||
END as status_job,
|
||||
CASE
|
||||
WHEN EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'state')
|
||||
THEN '✅ users.state существует'
|
||||
ELSE '❌ users.state НЕ НАЙДЕНА'
|
||||
END as status_state,
|
||||
CASE
|
||||
WHEN EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'profiles' AND column_name = 'interested_in')
|
||||
THEN '✅ profiles.interested_in существует'
|
||||
ELSE '❌ profiles.interested_in НЕ НАЙДЕНА'
|
||||
END as status_interested_in;
|
||||
EOF
|
||||
|
||||
echo ""
|
||||
echo "✅ Готово! Теперь можно перезапустить бота:"
|
||||
echo " docker compose restart bot"
|
||||
208
bin/apply_direct_sql.sh
Normal file
208
bin/apply_direct_sql.sh
Normal file
@@ -0,0 +1,208 @@
|
||||
#!/bin/bash
|
||||
# apply_direct_sql.sh - Прямое выполнение SQL-миграций с помощью psql
|
||||
|
||||
echo "🚀 Прямое выполнение SQL-миграций..."
|
||||
|
||||
# Загрузка переменных окружения из .env
|
||||
if [ -f .env ]; then
|
||||
echo "📝 Загрузка переменных окружения из .env..."
|
||||
set -o allexport
|
||||
source .env
|
||||
set +o allexport
|
||||
else
|
||||
echo "⚠️ Файл .env не найден, используем значения по умолчанию"
|
||||
export DB_HOST="localhost"
|
||||
export DB_PORT="5432"
|
||||
export DB_NAME="telegram_tinder_bot"
|
||||
export DB_USERNAME="postgres"
|
||||
export DB_PASSWORD="postgres"
|
||||
fi
|
||||
|
||||
# Создаем консолидированный SQL-файл
|
||||
echo "📝 Создание консолидированного SQL-файла..."
|
||||
consolidated_sql="consolidated_migration.sql"
|
||||
|
||||
cat > "$consolidated_sql" << EOL
|
||||
-- Консолидированная миграция для Telegram Tinder Bot
|
||||
-- Создана автоматически: $(date)
|
||||
|
||||
-- Создаем таблицу migrations, если её еще нет
|
||||
CREATE TABLE IF NOT EXISTS migrations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Основная структура базы данных
|
||||
|
||||
-- Таблица пользователей
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id BIGINT PRIMARY KEY,
|
||||
username VARCHAR(255),
|
||||
first_name VARCHAR(255),
|
||||
last_name VARCHAR(255),
|
||||
language_code VARCHAR(10),
|
||||
is_bot BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
state VARCHAR(50) DEFAULT 'START',
|
||||
state_data JSONB,
|
||||
gender VARCHAR(10),
|
||||
looking_for VARCHAR(10),
|
||||
bio TEXT,
|
||||
age INTEGER,
|
||||
location VARCHAR(255),
|
||||
photos JSONB DEFAULT '[]'::jsonb,
|
||||
interests TEXT[],
|
||||
premium BOOLEAN DEFAULT FALSE,
|
||||
premium_expires_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- Таблица профилей
|
||||
CREATE TABLE IF NOT EXISTS profiles (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255),
|
||||
age INTEGER,
|
||||
gender VARCHAR(10),
|
||||
bio TEXT,
|
||||
photos JSONB DEFAULT '[]'::jsonb,
|
||||
interests TEXT[],
|
||||
location VARCHAR(255),
|
||||
religion VARCHAR(50),
|
||||
education VARCHAR(255),
|
||||
job VARCHAR(255),
|
||||
height INTEGER,
|
||||
smoking VARCHAR(50),
|
||||
drinking VARCHAR(50),
|
||||
looking_for VARCHAR(10),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Таблица лайков
|
||||
CREATE TABLE IF NOT EXISTS likes (
|
||||
id SERIAL PRIMARY KEY,
|
||||
from_user_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
|
||||
to_user_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
|
||||
is_like BOOLEAN NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(from_user_id, to_user_id)
|
||||
);
|
||||
|
||||
-- Таблица матчей
|
||||
CREATE TABLE IF NOT EXISTS matches (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user1_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
|
||||
user2_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
active BOOLEAN DEFAULT TRUE,
|
||||
UNIQUE(user1_id, user2_id)
|
||||
);
|
||||
|
||||
-- Таблица сообщений
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id SERIAL PRIMARY KEY,
|
||||
match_id INTEGER REFERENCES matches(id) ON DELETE CASCADE,
|
||||
sender_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
|
||||
message_text TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Таблица просмотров профилей
|
||||
CREATE TABLE IF NOT EXISTS profile_views (
|
||||
id SERIAL PRIMARY KEY,
|
||||
viewer_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
|
||||
viewed_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
|
||||
viewed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(viewer_id, viewed_id)
|
||||
);
|
||||
|
||||
-- Таблица уведомлений
|
||||
CREATE TABLE IF NOT EXISTS notifications (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
|
||||
type VARCHAR(50) NOT NULL,
|
||||
data JSONB,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
scheduled_for TIMESTAMP,
|
||||
processed BOOLEAN DEFAULT FALSE
|
||||
);
|
||||
|
||||
-- Индексы для оптимизации запросов
|
||||
|
||||
-- Индексы для таблицы пользователей
|
||||
CREATE INDEX IF NOT EXISTS users_username_idx ON users(username);
|
||||
CREATE INDEX IF NOT EXISTS users_gender_idx ON users(gender);
|
||||
CREATE INDEX IF NOT EXISTS users_looking_for_idx ON users(looking_for);
|
||||
CREATE INDEX IF NOT EXISTS users_premium_idx ON users(premium);
|
||||
|
||||
-- Индексы для таблицы лайков
|
||||
CREATE INDEX IF NOT EXISTS likes_from_user_id_idx ON likes(from_user_id);
|
||||
CREATE INDEX IF NOT EXISTS likes_to_user_id_idx ON likes(to_user_id);
|
||||
CREATE INDEX IF NOT EXISTS likes_is_like_idx ON likes(is_like);
|
||||
|
||||
-- Индексы для таблицы матчей
|
||||
CREATE INDEX IF NOT EXISTS matches_user1_id_idx ON matches(user1_id);
|
||||
CREATE INDEX IF NOT EXISTS matches_user2_id_idx ON matches(user2_id);
|
||||
CREATE INDEX IF NOT EXISTS matches_active_idx ON matches(active);
|
||||
|
||||
-- Индексы для таблицы сообщений
|
||||
CREATE INDEX IF NOT EXISTS messages_match_id_idx ON messages(match_id);
|
||||
CREATE INDEX IF NOT EXISTS messages_sender_id_idx ON messages(sender_id);
|
||||
|
||||
-- Индексы для таблицы профилей
|
||||
CREATE INDEX IF NOT EXISTS profiles_user_id_idx ON profiles(user_id);
|
||||
CREATE INDEX IF NOT EXISTS profiles_gender_idx ON profiles(gender);
|
||||
CREATE INDEX IF NOT EXISTS profiles_looking_for_idx ON profiles(looking_for);
|
||||
|
||||
-- Индексы для таблицы просмотров профилей
|
||||
CREATE INDEX IF NOT EXISTS profile_views_viewer_id_idx ON profile_views(viewer_id);
|
||||
CREATE INDEX IF NOT EXISTS profile_views_viewed_id_idx ON profile_views(viewed_id);
|
||||
|
||||
-- Индексы для таблицы уведомлений
|
||||
CREATE INDEX IF NOT EXISTS notifications_user_id_idx ON notifications(user_id);
|
||||
CREATE INDEX IF NOT EXISTS notifications_scheduled_for_idx ON notifications(scheduled_for);
|
||||
CREATE INDEX IF NOT EXISTS notifications_processed_idx ON notifications(processed);
|
||||
|
||||
-- Запись о выполнении миграции
|
||||
INSERT INTO migrations (name) VALUES ('consolidated_migration.sql')
|
||||
ON CONFLICT DO NOTHING;
|
||||
EOL
|
||||
|
||||
echo "✅ Консолидированный SQL-файл создан: $consolidated_sql"
|
||||
|
||||
# Вывод информации о подключении
|
||||
echo "🔍 Используемые параметры подключения:"
|
||||
echo "DB_HOST: $DB_HOST"
|
||||
echo "DB_PORT: $DB_PORT"
|
||||
echo "DB_NAME: $DB_NAME"
|
||||
echo "DB_USERNAME: $DB_USERNAME"
|
||||
echo "DB_PASSWORD: ********"
|
||||
|
||||
# Проверка наличия psql
|
||||
if command -v psql >/dev/null; then
|
||||
echo "✅ Найдена команда psql, продолжаем..."
|
||||
else
|
||||
echo "⚠️ Команда psql не найдена, установите PostgreSQL клиент:"
|
||||
echo "apt-get update && apt-get install -y postgresql-client"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Применение миграции
|
||||
echo "🔄 Применение консолидированной миграции..."
|
||||
export PGPASSWORD=$DB_PASSWORD
|
||||
psql -h $DB_HOST -p $DB_PORT -U $DB_USERNAME -d $DB_NAME -f "$consolidated_sql"
|
||||
|
||||
# Проверка результата
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✅ Миграция успешно применена!"
|
||||
else
|
||||
echo "❌ Ошибка при применении миграции!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Удаление временного файла
|
||||
rm -f "$consolidated_sql"
|
||||
|
||||
echo "🚀 Миграция базы данных завершена!"
|
||||
75
bin/apply_migrations.sh
Normal file
75
bin/apply_migrations.sh
Normal file
@@ -0,0 +1,75 @@
|
||||
#!/bin/bash
|
||||
# apply_migrations.sh - Скрипт для ручного применения миграций
|
||||
|
||||
echo "🔄 Ручное применение миграций базы данных..."
|
||||
|
||||
# Загрузка переменных окружения из .env
|
||||
if [ -f .env ]; then
|
||||
echo "📝 Загрузка переменных окружения из .env..."
|
||||
export $(grep -v '^#' .env | xargs)
|
||||
else
|
||||
echo "⚠️ Файл .env не найден, используем значения по умолчанию"
|
||||
export DB_HOST="localhost"
|
||||
export DB_PORT="5432"
|
||||
export DB_NAME="telegram_tinder_bot"
|
||||
export DB_USERNAME="postgres"
|
||||
export DB_PASSWORD="postgres"
|
||||
fi
|
||||
|
||||
# Проверка на существование директории миграций
|
||||
if [ ! -d "migrations" ] && [ ! -d "src/database/migrations" ]; then
|
||||
echo "❌ Не найдены директории с миграциями!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Вывод информации о подключении
|
||||
echo "🔍 Используемые параметры подключения:"
|
||||
echo "DB_HOST: $DB_HOST"
|
||||
echo "DB_PORT: $DB_PORT"
|
||||
echo "DB_NAME: $DB_NAME"
|
||||
echo "DB_USERNAME: $DB_USERNAME"
|
||||
echo "DB_PASSWORD: ********"
|
||||
|
||||
# Проверка подключения к базе данных
|
||||
echo "🔍 Проверка подключения к базе данных..."
|
||||
if command -v pg_isready >/dev/null; then
|
||||
pg_isready -h $DB_HOST -p $DB_PORT -U $DB_USERNAME
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "❌ Не удалось подключиться к базе данных!"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "⚠️ Утилита pg_isready не найдена, пропускаем проверку"
|
||||
fi
|
||||
|
||||
# Копирование миграций JS в отдельную директорию
|
||||
echo "📂 Копирование только JS-миграций во временную директорию..."
|
||||
mkdir -p temp_migrations
|
||||
find migrations -name "*.js" -exec cp {} temp_migrations/ \;
|
||||
|
||||
# Применение миграций
|
||||
echo "🔄 Применение миграций с помощью node-pg-migrate..."
|
||||
DATABASE_URL="postgres://$DB_USERNAME:$DB_PASSWORD@$DB_HOST:$DB_PORT/$DB_NAME" npx node-pg-migrate up --migrations-dir=temp_migrations
|
||||
|
||||
# Проверка результата
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✅ Миграции успешно применены!"
|
||||
else
|
||||
echo "❌ Ошибка при применении миграций!"
|
||||
echo "⚠️ Пытаемся применить миграции из других источников..."
|
||||
|
||||
# Попробуем применить SQL-миграции напрямую
|
||||
if [ -d "src/database/migrations" ]; then
|
||||
echo "📂 Найдены SQL-миграции. Пытаемся применить их напрямую..."
|
||||
for sql_file in src/database/migrations/*.sql; do
|
||||
if [ -f "$sql_file" ]; then
|
||||
echo "🔄 Применение миграции $sql_file..."
|
||||
PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST -p $DB_PORT -U $DB_USERNAME -d $DB_NAME -f "$sql_file" || echo "⚠️ Ошибка при применении $sql_file"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
# Очистка временных файлов
|
||||
echo "🧹 Очистка временных файлов..."
|
||||
rm -rf temp_migrations
|
||||
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
|
||||
52
bin/compile_ts_migrations.sh
Normal file
52
bin/compile_ts_migrations.sh
Normal file
@@ -0,0 +1,52 @@
|
||||
#!/bin/bash
|
||||
# compile_ts_migrations.sh - Скрипт для компиляции TS миграций в JS
|
||||
|
||||
echo "🔄 Компиляция TypeScript миграций в JavaScript..."
|
||||
|
||||
# Проверка наличия TypeScript файлов
|
||||
if [ ! -f "migrations/*.ts" ] && [ ! -d "node_modules/typescript" ]; then
|
||||
echo "📦 Установка TypeScript..."
|
||||
npm install --no-save typescript
|
||||
fi
|
||||
|
||||
# Создание временного tsconfig для миграций
|
||||
echo "📝 Создание временного tsconfig.json для миграций..."
|
||||
cat > migrations/tsconfig.json << EOL
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2020",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"outDir": "../temp_migrations",
|
||||
"baseUrl": "..",
|
||||
"paths": {
|
||||
"*": ["node_modules/*"]
|
||||
}
|
||||
},
|
||||
"include": ["./*.ts"]
|
||||
}
|
||||
EOL
|
||||
|
||||
# Компиляция TS файлов
|
||||
echo "🔄 Компиляция TypeScript миграций..."
|
||||
npx tsc -p migrations/tsconfig.json
|
||||
|
||||
# Подтверждение
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✅ Миграции успешно скомпилированы в директорию temp_migrations/"
|
||||
|
||||
# Проверка, были ли созданы файлы
|
||||
file_count=$(find temp_migrations -name "*.js" | wc -l)
|
||||
echo "📊 Скомпилировано файлов: $file_count"
|
||||
else
|
||||
echo "❌ Ошибка при компиляции миграций!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Очистка временных файлов
|
||||
rm migrations/tsconfig.json
|
||||
188
bin/create_consolidated_migration.sh
Normal file
188
bin/create_consolidated_migration.sh
Normal file
@@ -0,0 +1,188 @@
|
||||
#!/bin/bash
|
||||
# create_consolidated_migration.sh - Создание консолидированной миграции из всех источников
|
||||
|
||||
echo "🚀 Создание консолидированной миграции..."
|
||||
|
||||
# Создаем каталог для миграций если его нет
|
||||
mkdir -p migrations
|
||||
|
||||
# Текущее время для имени файла
|
||||
timestamp=$(date +%s)
|
||||
|
||||
# Путь к консолидированной миграции
|
||||
consolidated_file="migrations/${timestamp}_consolidated_migration.js"
|
||||
|
||||
echo "📝 Создание файла $consolidated_file..."
|
||||
|
||||
# Создаем JS-миграцию
|
||||
cat > "$consolidated_file" << EOL
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
exports.shorthands = undefined;
|
||||
|
||||
exports.up = pgm => {
|
||||
// Консолидированная миграция, созданная автоматически
|
||||
|
||||
// Создаем таблицу migrations, если её ещё нет
|
||||
pgm.sql(\`
|
||||
CREATE TABLE IF NOT EXISTS migrations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
\`);
|
||||
|
||||
// Создаем основную структуру базы данных
|
||||
pgm.sql(\`
|
||||
-- Таблица пользователей
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id BIGINT PRIMARY KEY,
|
||||
username VARCHAR(255),
|
||||
first_name VARCHAR(255),
|
||||
last_name VARCHAR(255),
|
||||
language_code VARCHAR(10),
|
||||
is_bot BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
state VARCHAR(50) DEFAULT 'START',
|
||||
state_data JSONB,
|
||||
gender VARCHAR(10),
|
||||
looking_for VARCHAR(10),
|
||||
bio TEXT,
|
||||
age INTEGER,
|
||||
location VARCHAR(255),
|
||||
photos JSONB DEFAULT '[]'::jsonb,
|
||||
interests TEXT[],
|
||||
premium BOOLEAN DEFAULT FALSE,
|
||||
premium_expires_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- Таблица профилей
|
||||
CREATE TABLE IF NOT EXISTS profiles (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255),
|
||||
age INTEGER,
|
||||
gender VARCHAR(10),
|
||||
bio TEXT,
|
||||
photos JSONB DEFAULT '[]'::jsonb,
|
||||
interests TEXT[],
|
||||
location VARCHAR(255),
|
||||
religion VARCHAR(50),
|
||||
education VARCHAR(255),
|
||||
job VARCHAR(255),
|
||||
height INTEGER,
|
||||
smoking VARCHAR(50),
|
||||
drinking VARCHAR(50),
|
||||
looking_for VARCHAR(10),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Таблица лайков
|
||||
CREATE TABLE IF NOT EXISTS likes (
|
||||
id SERIAL PRIMARY KEY,
|
||||
from_user_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
|
||||
to_user_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
|
||||
is_like BOOLEAN NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(from_user_id, to_user_id)
|
||||
);
|
||||
|
||||
-- Таблица матчей
|
||||
CREATE TABLE IF NOT EXISTS matches (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user1_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
|
||||
user2_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
active BOOLEAN DEFAULT TRUE,
|
||||
UNIQUE(user1_id, user2_id)
|
||||
);
|
||||
|
||||
-- Таблица сообщений
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id SERIAL PRIMARY KEY,
|
||||
match_id INTEGER REFERENCES matches(id) ON DELETE CASCADE,
|
||||
sender_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
|
||||
message_text TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Таблица просмотров профилей
|
||||
CREATE TABLE IF NOT EXISTS profile_views (
|
||||
id SERIAL PRIMARY KEY,
|
||||
viewer_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
|
||||
viewed_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
|
||||
viewed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(viewer_id, viewed_id)
|
||||
);
|
||||
|
||||
-- Таблица уведомлений
|
||||
CREATE TABLE IF NOT EXISTS notifications (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
|
||||
type VARCHAR(50) NOT NULL,
|
||||
data JSONB,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
scheduled_for TIMESTAMP,
|
||||
processed BOOLEAN DEFAULT FALSE
|
||||
);
|
||||
\`);
|
||||
|
||||
// Индексы для оптимизации запросов
|
||||
pgm.sql(\`
|
||||
-- Индексы для таблицы пользователей
|
||||
CREATE INDEX IF NOT EXISTS users_username_idx ON users(username);
|
||||
CREATE INDEX IF NOT EXISTS users_gender_idx ON users(gender);
|
||||
CREATE INDEX IF NOT EXISTS users_looking_for_idx ON users(looking_for);
|
||||
CREATE INDEX IF NOT EXISTS users_premium_idx ON users(premium);
|
||||
|
||||
-- Индексы для таблицы лайков
|
||||
CREATE INDEX IF NOT EXISTS likes_from_user_id_idx ON likes(from_user_id);
|
||||
CREATE INDEX IF NOT EXISTS likes_to_user_id_idx ON likes(to_user_id);
|
||||
CREATE INDEX IF NOT EXISTS likes_is_like_idx ON likes(is_like);
|
||||
|
||||
-- Индексы для таблицы матчей
|
||||
CREATE INDEX IF NOT EXISTS matches_user1_id_idx ON matches(user1_id);
|
||||
CREATE INDEX IF NOT EXISTS matches_user2_id_idx ON matches(user2_id);
|
||||
CREATE INDEX IF NOT EXISTS matches_active_idx ON matches(active);
|
||||
|
||||
-- Индексы для таблицы сообщений
|
||||
CREATE INDEX IF NOT EXISTS messages_match_id_idx ON messages(match_id);
|
||||
CREATE INDEX IF NOT EXISTS messages_sender_id_idx ON messages(sender_id);
|
||||
|
||||
-- Индексы для таблицы профилей
|
||||
CREATE INDEX IF NOT EXISTS profiles_user_id_idx ON profiles(user_id);
|
||||
CREATE INDEX IF NOT EXISTS profiles_gender_idx ON profiles(gender);
|
||||
CREATE INDEX IF NOT EXISTS profiles_looking_for_idx ON profiles(looking_for);
|
||||
|
||||
-- Индексы для таблицы просмотров профилей
|
||||
CREATE INDEX IF NOT EXISTS profile_views_viewer_id_idx ON profile_views(viewer_id);
|
||||
CREATE INDEX IF NOT EXISTS profile_views_viewed_id_idx ON profile_views(viewed_id);
|
||||
|
||||
-- Индексы для таблицы уведомлений
|
||||
CREATE INDEX IF NOT EXISTS notifications_user_id_idx ON notifications(user_id);
|
||||
CREATE INDEX IF NOT EXISTS notifications_scheduled_for_idx ON notifications(scheduled_for);
|
||||
CREATE INDEX IF NOT EXISTS notifications_processed_idx ON notifications(processed);
|
||||
\`);
|
||||
};
|
||||
|
||||
exports.down = pgm => {
|
||||
// Эта функция не будет фактически использоваться,
|
||||
// но для полноты оставляем возможность отката
|
||||
pgm.sql(\`
|
||||
DROP TABLE IF EXISTS notifications;
|
||||
DROP TABLE IF EXISTS profile_views;
|
||||
DROP TABLE IF EXISTS messages;
|
||||
DROP TABLE IF EXISTS matches;
|
||||
DROP TABLE IF EXISTS likes;
|
||||
DROP TABLE IF EXISTS profiles;
|
||||
DROP TABLE IF EXISTS users;
|
||||
\`);
|
||||
};
|
||||
EOL
|
||||
|
||||
echo "✅ Консолидированная миграция создана: $consolidated_file"
|
||||
echo ""
|
||||
echo "🚀 Для применения миграции выполните:"
|
||||
echo "DATABASE_URL=postgres://\${DB_USERNAME}:\${DB_PASSWORD}@\${DB_HOST}:\${DB_PORT}/\${DB_NAME} npx node-pg-migrate up"
|
||||
100
bin/fix_docker.bat
Normal file
100
bin/fix_docker.bat
Normal file
@@ -0,0 +1,100 @@
|
||||
@echo off
|
||||
REM fix_docker.bat - Скрипт для устранения проблемы ContainerConfig в Windows
|
||||
|
||||
echo 🔧 Устранение проблемы с Docker контейнерами...
|
||||
|
||||
REM Остановка всех контейнеров проекта
|
||||
echo 📥 Остановка всех контейнеров проекта...
|
||||
docker-compose down -v
|
||||
|
||||
REM Принудительное удаление контейнеров по имени
|
||||
echo 🗑️ Принудительное удаление оставшихся контейнеров...
|
||||
docker rm -f postgres-tinder adminer-tinder telegram-tinder-bot 2>NUL
|
||||
|
||||
REM Очистка неиспользуемых томов и сетей
|
||||
echo 🧹 Очистка неиспользуемых томов и сетей...
|
||||
docker system prune -f --volumes
|
||||
|
||||
REM Очистка кеша Docker
|
||||
echo 🧼 Очистка кеша Docker...
|
||||
docker builder prune -f
|
||||
|
||||
REM Исправление docker-compose.yml
|
||||
echo 📝 Создание обновленного docker-compose.yml...
|
||||
|
||||
REM Создаем обновленный docker-compose.yml с использованием PowerShell
|
||||
powershell -Command "& {
|
||||
$content = @'
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
bot:
|
||||
build: .
|
||||
container_name: telegram-tinder-bot
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- DB_HOST=${DB_HOST:-db}
|
||||
- DB_PORT=${DB_PORT:-5432}
|
||||
- DB_NAME=${DB_NAME:-telegram_tinder_bot}
|
||||
- DB_USERNAME=${DB_USERNAME:-postgres}
|
||||
- DB_PASSWORD=${DB_PASSWORD:-postgres}
|
||||
volumes:
|
||||
- ./uploads:/app/uploads:rw
|
||||
- ./logs:/app/logs:rw
|
||||
networks:
|
||||
- bot-network
|
||||
healthcheck:
|
||||
test: [\"CMD\", \"wget\", \"--no-verbose\", \"--tries=1\", \"--spider\", \"http://localhost:3000/health\"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
|
||||
db:
|
||||
image: postgres:15-alpine
|
||||
container_name: postgres-tinder
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- POSTGRES_DB=${DB_NAME:-telegram_tinder_bot}
|
||||
- POSTGRES_USER=${DB_USERNAME:-postgres}
|
||||
- POSTGRES_PASSWORD=${DB_PASSWORD:-postgres}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
ports:
|
||||
- \"5433:5432\"
|
||||
networks:
|
||||
- bot-network
|
||||
healthcheck:
|
||||
test: [\"CMD-SHELL\", \"pg_isready -U ${DB_USERNAME:-postgres} -d ${DB_NAME:-telegram_tinder_bot}\"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
|
||||
adminer:
|
||||
image: adminer:latest
|
||||
container_name: adminer-tinder
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- \"8080:8080\"
|
||||
networks:
|
||||
- bot-network
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
||||
networks:
|
||||
bot-network:
|
||||
driver: bridge
|
||||
'@
|
||||
Set-Content -Path 'docker-compose.yml' -Value $content
|
||||
}"
|
||||
|
||||
echo ✅ docker-compose.yml обновлен!
|
||||
|
||||
echo 🚀 Готово! Теперь вы можете запустить контейнеры снова с помощью команды:
|
||||
echo docker-compose up -d
|
||||
|
||||
pause
|
||||
117
bin/fix_docker.sh
Normal file
117
bin/fix_docker.sh
Normal file
@@ -0,0 +1,117 @@
|
||||
#!/bin/bash
|
||||
# fix_docker.sh - Скрипт для устранения проблемы ContainerConfig
|
||||
|
||||
echo "🔧 Устранение проблемы с Docker контейнерами..."
|
||||
|
||||
# Остановка всех контейнеров проекта
|
||||
echo "📥 Остановка всех контейнеров проекта..."
|
||||
docker-compose down -v
|
||||
|
||||
# Принудительное удаление контейнеров по имени
|
||||
echo "🗑️ Принудительное удаление оставшихся контейнеров..."
|
||||
docker rm -f postgres-tinder adminer-tinder telegram-tinder-bot 2>/dev/null || true
|
||||
|
||||
# Очистка неиспользуемых томов и сетей
|
||||
echo "🧹 Очистка неиспользуемых томов и сетей..."
|
||||
docker system prune -f --volumes
|
||||
|
||||
# Очистка кеша Docker
|
||||
echo "🧼 Очистка кеша Docker..."
|
||||
docker builder prune -f
|
||||
|
||||
# Исправление docker-compose.yml
|
||||
echo "📝 Создание обновленного docker-compose.yml..."
|
||||
|
||||
cat > docker-compose.yml << EOL
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
bot:
|
||||
build: .
|
||||
container_name: telegram-tinder-bot
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- DB_HOST=${DB_HOST:-db}
|
||||
- DB_PORT=${DB_PORT:-5432}
|
||||
- DB_NAME=${DB_NAME:-telegram_tinder_bot}
|
||||
- DB_USERNAME=${DB_USERNAME:-postgres}
|
||||
- DB_PASSWORD=${DB_PASSWORD:-postgres}
|
||||
volumes:
|
||||
- ./uploads:/app/uploads:rw
|
||||
- ./logs:/app/logs:rw
|
||||
networks:
|
||||
- bot-network
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
EOL
|
||||
|
||||
# Если используем внешнюю базу данных, добавляем только adminer
|
||||
if [ "${DB_HOST:-db}" != "db" ]; then
|
||||
cat >> docker-compose.yml << EOL
|
||||
|
||||
adminer:
|
||||
image: adminer:latest
|
||||
container_name: adminer-tinder
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:8080"
|
||||
networks:
|
||||
- bot-network
|
||||
EOL
|
||||
else
|
||||
# Если используем локальную базу данных, добавляем PostgreSQL и adminer
|
||||
cat >> docker-compose.yml << EOL
|
||||
|
||||
db:
|
||||
image: postgres:15-alpine
|
||||
container_name: postgres-tinder
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- POSTGRES_DB=\${DB_NAME:-telegram_tinder_bot}
|
||||
- POSTGRES_USER=\${DB_USERNAME:-postgres}
|
||||
- POSTGRES_PASSWORD=\${DB_PASSWORD:-postgres}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "5433:5432"
|
||||
networks:
|
||||
- bot-network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U \${DB_USERNAME:-postgres} -d \${DB_NAME:-telegram_tinder_bot}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
|
||||
adminer:
|
||||
image: adminer:latest
|
||||
container_name: adminer-tinder
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:8080"
|
||||
networks:
|
||||
- bot-network
|
||||
EOL
|
||||
fi
|
||||
|
||||
# Завершаем файл docker-compose.yml
|
||||
cat >> docker-compose.yml << EOL
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
||||
networks:
|
||||
bot-network:
|
||||
driver: bridge
|
||||
EOL
|
||||
|
||||
echo "✅ docker-compose.yml обновлен!"
|
||||
|
||||
echo "🚀 Готово! Теперь вы можете запустить контейнеры снова с помощью команды:"
|
||||
echo "docker-compose up -d"
|
||||
15
bin/fix_line_endings.sh
Normal file
15
bin/fix_line_endings.sh
Normal file
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
# fix_line_endings.sh - Script to fix line endings in shell scripts
|
||||
|
||||
echo "🔧 Fixing line endings in shell scripts..."
|
||||
|
||||
# Fix shell scripts
|
||||
for file in $(find . -name "*.sh"); do
|
||||
echo "📄 Processing $file..."
|
||||
tr -d '\r' < "$file" > "$file.fixed"
|
||||
mv "$file.fixed" "$file"
|
||||
chmod +x "$file"
|
||||
echo "✅ Fixed $file"
|
||||
done
|
||||
|
||||
echo "🚀 All shell scripts fixed!"
|
||||
17
bin/fix_permissions.sh
Normal file
17
bin/fix_permissions.sh
Normal file
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
# fix_permissions.sh - Устанавливает права на выполнение для всех скриптов
|
||||
|
||||
echo "🔧 Установка прав на выполнение для всех скриптов..."
|
||||
|
||||
# Находим все .sh файлы и устанавливаем права на выполнение
|
||||
find . -name "*.sh" -type f -exec chmod +x {} \;
|
||||
|
||||
echo "✅ Права на выполнение установлены!"
|
||||
|
||||
# Исправление переносов строк
|
||||
echo "🔧 Исправление переносов строк..."
|
||||
find . -name "*.sh" -type f -exec sh -c 'tr -d "\r" < "$1" > "$1.fixed" && mv "$1.fixed" "$1"' -- {} \;
|
||||
|
||||
echo "✅ Переносы строк исправлены!"
|
||||
|
||||
echo "🚀 Готово! Все скрипты готовы к использованию."
|
||||
20
bin/init_database.sh
Normal file
20
bin/init_database.sh
Normal file
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
# init_database.sh - Initialize database with schema
|
||||
|
||||
set -e
|
||||
|
||||
echo "🗄️ Initializing database..."
|
||||
|
||||
# Wait for database to be ready
|
||||
echo "⏳ Waiting for database..."
|
||||
sleep 3
|
||||
|
||||
# Create UUID extension
|
||||
echo "📦 Creating UUID extension..."
|
||||
docker compose exec -T db psql -U postgres -d telegram_tinder_bot -c "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";" || true
|
||||
|
||||
# Apply consolidated schema
|
||||
echo "📋 Applying database schema..."
|
||||
docker compose exec -T db psql -U postgres -d telegram_tinder_bot < sql/consolidated.sql
|
||||
|
||||
echo "✅ Database initialized successfully!"
|
||||
37
bin/run_full_migration.sh
Normal file
37
bin/run_full_migration.sh
Normal file
@@ -0,0 +1,37 @@
|
||||
#!/bin/bash
|
||||
# run_full_migration.sh - Полный процесс миграции с компиляцией TypeScript
|
||||
|
||||
echo "🚀 Запуск полного процесса миграции..."
|
||||
|
||||
# Проверка наличия файлов TS
|
||||
if find migrations -name "*.ts" -quit; then
|
||||
echo "📋 Обнаружены TypeScript миграции. Компилируем их..."
|
||||
|
||||
# Компиляция TS файлов
|
||||
./bin/compile_ts_migrations.sh
|
||||
|
||||
# Проверка результата
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "❌ Ошибка компиляции TS миграций!"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "ℹ️ TypeScript миграции не обнаружены, пропускаем компиляцию."
|
||||
mkdir -p temp_migrations
|
||||
fi
|
||||
|
||||
# Копирование JS миграций
|
||||
echo "📂 Копирование JS-миграций..."
|
||||
find migrations -name "*.js" -exec cp {} temp_migrations/ \;
|
||||
|
||||
# Запуск миграций
|
||||
echo "🔄 Применение всех миграций..."
|
||||
./bin/apply_migrations.sh
|
||||
|
||||
# Проверка результата
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✅ Процесс миграции успешно завершен!"
|
||||
else
|
||||
echo "❌ Ошибка в процессе миграции."
|
||||
exit 1
|
||||
fi
|
||||
101
bin/run_sql_migrations.sh
Normal file
101
bin/run_sql_migrations.sh
Normal file
@@ -0,0 +1,101 @@
|
||||
#!/bin/bash
|
||||
# run_sql_migrations.sh - Ручное применение SQL-миграций
|
||||
|
||||
echo "🚀 Запуск SQL-миграций..."
|
||||
|
||||
# Загрузка переменных окружения из .env
|
||||
if [ -f .env ]; then
|
||||
echo "📝 Загрузка переменных окружения из .env..."
|
||||
set -o allexport
|
||||
source .env
|
||||
set +o allexport
|
||||
else
|
||||
echo "⚠️ Файл .env не найден, используем значения по умолчанию"
|
||||
export DB_HOST="localhost"
|
||||
export DB_PORT="5432"
|
||||
export DB_NAME="telegram_tinder_bot"
|
||||
export DB_USERNAME="postgres"
|
||||
export DB_PASSWORD="postgres"
|
||||
fi
|
||||
|
||||
# Вывод информации о подключении
|
||||
echo "🔍 Используемые параметры подключения:"
|
||||
echo "DB_HOST: $DB_HOST"
|
||||
echo "DB_PORT: $DB_PORT"
|
||||
echo "DB_NAME: $DB_NAME"
|
||||
echo "DB_USERNAME: $DB_USERNAME"
|
||||
echo "DB_PASSWORD: ********"
|
||||
|
||||
# Функция для применения SQL файлов из директории
|
||||
apply_sql_files() {
|
||||
local directory=$1
|
||||
echo "🔍 Ищем SQL-файлы в директории $directory..."
|
||||
|
||||
if [ -d "$directory" ]; then
|
||||
# Получаем список файлов в порядке времени создания
|
||||
files=$(find "$directory" -name "*.sql" | sort)
|
||||
|
||||
if [ -z "$files" ]; then
|
||||
echo "⚠️ SQL-файлы не найдены в $directory"
|
||||
return 0
|
||||
fi
|
||||
|
||||
for sql_file in $files; do
|
||||
echo "🔄 Применение миграции $sql_file..."
|
||||
|
||||
# Проверяем, есть ли уже запись о миграции в таблице migrations
|
||||
filename=$(basename "$sql_file")
|
||||
exists=$(PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST -p $DB_PORT -U $DB_USERNAME -d $DB_NAME -t -c "SELECT EXISTS(SELECT 1 FROM migrations WHERE name='$filename')" 2>/dev/null)
|
||||
|
||||
# Если таблицы migrations не существует, создаем её
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "📝 Таблица migrations не найдена. Создаем..."
|
||||
PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST -p $DB_PORT -U $DB_USERNAME -d $DB_NAME -c "
|
||||
CREATE TABLE IF NOT EXISTS migrations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
" 2>/dev/null
|
||||
exists=" f"
|
||||
fi
|
||||
|
||||
# Если миграция уже применена, пропускаем
|
||||
if [[ "$exists" == *"t"* ]]; then
|
||||
echo "⏭️ Миграция $filename уже применена, пропускаем"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Применяем миграцию
|
||||
PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST -p $DB_PORT -U $DB_USERNAME -d $DB_NAME -f "$sql_file"
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✅ Миграция $filename успешно применена"
|
||||
# Записываем в таблицу migrations
|
||||
PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST -p $DB_PORT -U $DB_USERNAME -d $DB_NAME -c "
|
||||
INSERT INTO migrations (name) VALUES ('$filename')
|
||||
" 2>/dev/null
|
||||
else
|
||||
echo "❌ Ошибка при применении миграции $filename"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo "⚠️ Директория $directory не найдена"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Применяем SQL миграции из всех возможных папок
|
||||
echo "🔄 Применение SQL-миграций из src/database/migrations..."
|
||||
apply_sql_files "src/database/migrations"
|
||||
|
||||
echo "🔄 Применение SQL-миграций из migrations/sql..."
|
||||
apply_sql_files "migrations/sql"
|
||||
|
||||
echo "🔄 Применение SQL-миграций из migrations (если есть)..."
|
||||
apply_sql_files "migrations"
|
||||
|
||||
echo "✅ Процесс применения SQL-миграций завершен!"
|
||||
230
deploy.sh
230
deploy.sh
@@ -1,63 +1,217 @@
|
||||
#!/bin/bash
|
||||
# deploy.sh - Скрипт для деплоя Telegram Tinder Bot
|
||||
# deploy.sh - Улучшенный скрипт для деплоя Telegram Tinder Bot
|
||||
|
||||
echo "🚀 Деплой Telegram Tinder Bot..."
|
||||
set -e # Выход при ошибке
|
||||
|
||||
# Проверяем наличие Docker
|
||||
if ! command -v docker &> /dev/null || ! command -v docker-compose &> /dev/null; then
|
||||
echo "❌ Docker и Docker Compose должны быть установлены!"
|
||||
echo "Для установки на Ubuntu выполните:"
|
||||
echo "sudo apt update && sudo apt install -y docker.io docker-compose"
|
||||
exit 1
|
||||
fi
|
||||
# Определение цветов для вывода
|
||||
GREEN='\033[0;32m'
|
||||
BLUE='\033[0;34m'
|
||||
YELLOW='\033[0;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE} Telegram Tinder Bot Deploy ${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
|
||||
# Определяем рабочую директорию
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# Функция для проверки наличия команды
|
||||
check_command() {
|
||||
if ! command -v "$1" &> /dev/null; then
|
||||
echo -e "${RED}❌ Команда $1 не найдена!${NC}"
|
||||
return 1
|
||||
else
|
||||
echo -e "${GREEN}✓ Команда $1 найдена${NC}"
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
# Шаг 1: Проверка и установка зависимостей
|
||||
echo -e "\n${BLUE}Шаг 1: Проверка и установка зависимостей...${NC}"
|
||||
|
||||
# Проверяем наличие Docker и Docker Compose
|
||||
if ! check_command docker || ! check_command docker-compose; then
|
||||
echo -e "${YELLOW}Установка Docker и Docker Compose...${NC}"
|
||||
|
||||
# Проверяем, запущен ли скрипт от имени root
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo -e "${RED}❌ Этот скрипт должен быть запущен с правами root для установки Docker.${NC}"
|
||||
echo -e "Пожалуйста, запустите: ${YELLOW}sudo $0${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Устанавливаем Docker и Docker Compose
|
||||
if [ -f bin/install_docker.sh ]; then
|
||||
bash bin/install_docker.sh
|
||||
else
|
||||
echo -e "${YELLOW}Установка Docker с помощью apt...${NC}"
|
||||
apt update
|
||||
apt install -y docker.io docker-compose
|
||||
fi
|
||||
fi
|
||||
|
||||
# Проверяем наличие Git
|
||||
if ! check_command git; then
|
||||
echo -e "${YELLOW}Установка Git...${NC}"
|
||||
apt update && apt install -y git
|
||||
fi
|
||||
|
||||
# Проверяем наличие Node.js (для локальных операций)
|
||||
if ! check_command node || ! check_command npm; then
|
||||
echo -e "${YELLOW}Установка Node.js...${NC}"
|
||||
apt update
|
||||
apt install -y curl
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
|
||||
apt install -y nodejs
|
||||
fi
|
||||
|
||||
# Шаг 2: Получение последних изменений из репозитория
|
||||
echo -e "\n${BLUE}Шаг 2: Получение последних изменений из репозитория...${NC}"
|
||||
|
||||
# Сохраняем локальные изменения, если они есть
|
||||
git stash save "Auto-stash before deploy: $(date)" || true
|
||||
|
||||
# Получаем последние изменения
|
||||
echo "📥 Получение последних изменений..."
|
||||
git pull origin main
|
||||
git fetch --all
|
||||
git checkout main || git checkout master
|
||||
git pull origin "$(git rev-parse --abbrev-ref HEAD)"
|
||||
echo -e "${GREEN}✓ Получены последние изменения${NC}"
|
||||
|
||||
# Шаг 3: Проверка и создание файлов конфигурации
|
||||
echo -e "\n${BLUE}Шаг 3: Проверка и настройка конфигурационных файлов...${NC}"
|
||||
|
||||
# Проверяем наличие .env файла
|
||||
if [ ! -f .env ]; then
|
||||
echo "📝 Создание .env файла из .env.production..."
|
||||
cp .env.production .env
|
||||
echo "⚠️ Пожалуйста, отредактируйте файл .env и укажите свои настройки!"
|
||||
exit 1
|
||||
echo -e "${YELLOW}⚠️ Файл .env не найден!${NC}"
|
||||
|
||||
# Пытаемся найти шаблон .env файла
|
||||
if [ -f .env.production ]; then
|
||||
echo -e "${YELLOW}Создание .env файла из .env.production...${NC}"
|
||||
cp .env.production .env
|
||||
elif [ -f .env.example ]; then
|
||||
echo -e "${YELLOW}Создание .env файла из .env.example...${NC}"
|
||||
cp .env.example .env
|
||||
else
|
||||
echo -e "${RED}❌ Шаблон .env файла не найден! Создаем базовый .env файл...${NC}"
|
||||
cat > .env << EOL
|
||||
# Базовый .env файл
|
||||
NODE_ENV=production
|
||||
PORT=3000
|
||||
DB_HOST=db
|
||||
DB_PORT=5432
|
||||
DB_NAME=telegram_tinder_bot
|
||||
DB_USERNAME=postgres
|
||||
DB_PASSWORD=postgres
|
||||
TELEGRAM_BOT_TOKEN=YOUR_BOT_TOKEN
|
||||
EOL
|
||||
fi
|
||||
|
||||
echo -e "${YELLOW}⚠️ Пожалуйста, отредактируйте файл .env и укажите свои настройки!${NC}"
|
||||
echo -e "${YELLOW}⚠️ Особенно важно указать TELEGRAM_BOT_TOKEN${NC}"
|
||||
read -p "Продолжить деплой? (y/n): " continue_deploy
|
||||
if [[ ! $continue_deploy =~ ^[Yy]$ ]]; then
|
||||
echo -e "${RED}Деплой отменен. Пожалуйста, настройте .env файл и запустите скрипт снова.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Проверяем и исправляем проблему с командой сборки в Dockerfile
|
||||
echo "🔧 Проверка конфигурации Dockerfile..."
|
||||
if grep -q "RUN npm run build" Dockerfile; then
|
||||
echo "⚠️ Исправление команды сборки в Dockerfile для совместимости с Linux..."
|
||||
# Проверяем наличие docker-compose.override.yml
|
||||
if [ ! -f docker-compose.override.yml ] && [ -f docker-compose.override.yml.example ]; then
|
||||
echo -e "${YELLOW}Создание docker-compose.override.yml из примера...${NC}"
|
||||
cp docker-compose.override.yml.example docker-compose.override.yml
|
||||
fi
|
||||
|
||||
# Шаг 4: Исправление проблем с Docker
|
||||
echo -e "\n${BLUE}Шаг 4: Проверка и исправление проблем с Docker...${NC}"
|
||||
|
||||
# Исправляем проблему с командой сборки в Dockerfile
|
||||
if [ -f Dockerfile ] && grep -q "RUN npm run build" Dockerfile; then
|
||||
echo -e "${YELLOW}⚠️ Исправление команды сборки в Dockerfile для совместимости с Linux...${NC}"
|
||||
sed -i 's/RUN npm run build/RUN npm run build:linux/g' Dockerfile
|
||||
echo "✅ Dockerfile обновлен"
|
||||
echo -e "${GREEN}✓ Dockerfile обновлен${NC}"
|
||||
fi
|
||||
|
||||
# Запускаем Docker Compose
|
||||
echo "🐳 Сборка и запуск контейнеров Docker..."
|
||||
docker-compose down
|
||||
# Исправление прав доступа к файлам в Unix-системах
|
||||
if [ -f bin/fix_permissions.sh ]; then
|
||||
echo -e "${YELLOW}Исправление прав доступа к файлам...${NC}"
|
||||
bash bin/fix_permissions.sh
|
||||
fi
|
||||
|
||||
# Шаг 5: Запуск с Docker Compose
|
||||
echo -e "\n${BLUE}Шаг 5: Сборка и запуск Docker контейнеров...${NC}"
|
||||
|
||||
# Остановка и удаление старых контейнеров
|
||||
echo -e "${YELLOW}Остановка и удаление старых контейнеров...${NC}"
|
||||
docker-compose down || true
|
||||
|
||||
# Проверка наличия скрипта для исправления Docker
|
||||
if [ -f bin/fix_docker.sh ]; then
|
||||
echo -e "${YELLOW}Запуск скрипта исправления Docker...${NC}"
|
||||
bash bin/fix_docker.sh
|
||||
fi
|
||||
|
||||
# Создание необходимых директорий с правильными правами доступа
|
||||
echo -e "${YELLOW}Создание необходимых директорий...${NC}"
|
||||
mkdir -p logs uploads
|
||||
chmod -R 777 logs uploads
|
||||
|
||||
# Сборка и запуск контейнеров
|
||||
echo -e "${YELLOW}Сборка контейнеров...${NC}"
|
||||
docker-compose build
|
||||
|
||||
echo -e "${YELLOW}Запуск контейнеров...${NC}"
|
||||
docker-compose up -d
|
||||
|
||||
# Шаг 6: Применение миграций
|
||||
echo -e "\n${BLUE}Шаг 6: Применение миграций базы данных...${NC}"
|
||||
|
||||
# Ждем инициализации базы данных
|
||||
echo -e "${YELLOW}Ожидание инициализации базы данных...${NC}"
|
||||
sleep 10
|
||||
|
||||
# Выбор способа миграции
|
||||
if [ -f bin/run_full_migration.sh ]; then
|
||||
echo -e "${YELLOW}Запуск полной миграции базы данных...${NC}"
|
||||
docker-compose exec bot bash -c "cd /app && ./bin/run_full_migration.sh" || true
|
||||
elif [ -f bin/apply_migrations.sh ]; then
|
||||
echo -e "${YELLOW}Применение миграций базы данных...${NC}"
|
||||
docker-compose exec bot bash -c "cd /app && ./bin/apply_migrations.sh" || true
|
||||
else
|
||||
echo -e "${YELLOW}Миграционные скрипты не найдены, пропускаем этап миграции${NC}"
|
||||
fi
|
||||
|
||||
# Шаг 7: Проверка работоспособности
|
||||
echo -e "\n${BLUE}Шаг 7: Проверка работоспособности...${NC}"
|
||||
|
||||
# Проверяем статус контейнеров
|
||||
echo "🔍 Проверка статуса контейнеров..."
|
||||
echo -e "${YELLOW}Проверка статуса контейнеров...${NC}"
|
||||
docker-compose ps
|
||||
|
||||
echo "✅ Деплой успешно завершен! Бот должен быть доступен через Telegram."
|
||||
# Ждем запуска API
|
||||
echo -e "${YELLOW}Ожидание запуска API...${NC}"
|
||||
sleep 5
|
||||
docker-compose exec bot curl -s http://localhost:3000/health || echo "⚠️ Сервис не отвечает на проверку здоровья"
|
||||
|
||||
# Вывод информации о деплое
|
||||
echo -e "\n${GREEN}✅ Деплой успешно завершен!${NC}"
|
||||
echo -e "${GREEN}✅ Бот должен быть доступен через Telegram.${NC}"
|
||||
echo ""
|
||||
echo "📊 Полезные команды:"
|
||||
echo "- Просмотр логов: docker-compose logs -f"
|
||||
echo "- Перезапуск сервисов: docker-compose restart"
|
||||
echo "- Остановка всех сервисов: docker-compose down"
|
||||
echo "- Доступ к базе данных: docker-compose exec db psql -U postgres -d telegram_tinder_bot"
|
||||
echo "- Проверка состояния бота: curl http://localhost:3000/health"
|
||||
echo -e "${BLUE}📊 Полезные команды:${NC}"
|
||||
echo -e "- ${YELLOW}Просмотр логов:${NC} docker-compose logs -f bot"
|
||||
echo -e "- ${YELLOW}Перезапуск сервисов:${NC} docker-compose restart"
|
||||
echo -e "- ${YELLOW}Остановка всех сервисов:${NC} docker-compose down"
|
||||
echo -e "- ${YELLOW}Доступ к базе данных:${NC} docker-compose exec db psql -U postgres -d telegram_tinder_bot"
|
||||
echo -e "- ${YELLOW}Проверка состояния бота:${NC} curl http://localhost:3000/health"
|
||||
echo ""
|
||||
echo "🌟 Для администрирования базы данных:"
|
||||
echo "Adminer доступен по адресу: http://ваш_сервер:8080"
|
||||
echo " - Система: PostgreSQL"
|
||||
echo " - Сервер: db"
|
||||
echo " - Пользователь: postgres"
|
||||
echo " - Пароль: (из переменной DB_PASSWORD в .env)"
|
||||
echo " - База данных: telegram_tinder_bot"
|
||||
echo -e "${BLUE}🌟 Для администрирования базы данных:${NC}"
|
||||
echo -e "Adminer доступен по адресу: http://ваш_сервер:8080"
|
||||
echo -e " - ${YELLOW}Система:${NC} PostgreSQL"
|
||||
echo -e " - ${YELLOW}Сервер:${NC} db"
|
||||
echo -e " - ${YELLOW}Пользователь:${NC} postgres"
|
||||
echo -e " - ${YELLOW}Пароль:${NC} (из переменной DB_PASSWORD в .env)"
|
||||
echo -e " - ${YELLOW}База данных:${NC} telegram_tinder_bot"
|
||||
echo ""
|
||||
echo -e "${YELLOW}⚠️ При возникновении проблем проверьте файлы в директории bin/ для дополнительных утилит исправления.${NC}"
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
# Используем версию Docker Compose для локальной разработки
|
||||
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
bot:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
- NODE_ENV=development
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- DB_HOST=db
|
||||
- DB_PORT=5432
|
||||
- DB_NAME=telegram_tinder_bot
|
||||
- DB_USERNAME=postgres
|
||||
- DB_PASSWORD=dev_password
|
||||
volumes:
|
||||
# Монтируем исходный код для горячей перезагрузки
|
||||
- ./src:/app/src
|
||||
- ./dist:/app/dist
|
||||
- ./.env:/app/.env
|
||||
ports:
|
||||
# Открываем порт для отладки
|
||||
- "9229:9229"
|
||||
command: npm run dev
|
||||
networks:
|
||||
- bot-network
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
db:
|
||||
# Используем последнюю версию PostgreSQL для разработки
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
- POSTGRES_DB=telegram_tinder_bot
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=dev_password
|
||||
volumes:
|
||||
# Хранение данных локально для быстрого сброса
|
||||
- postgres_data_dev:/var/lib/postgresql/data
|
||||
# Монтируем скрипты инициализации
|
||||
- ./sql:/docker-entrypoint-initdb.d
|
||||
ports:
|
||||
# Открываем порт для доступа к БД напрямую
|
||||
- "5433:5432"
|
||||
networks:
|
||||
- bot-network
|
||||
|
||||
adminer:
|
||||
image: adminer:latest
|
||||
ports:
|
||||
- "8080:8080"
|
||||
networks:
|
||||
- bot-network
|
||||
depends_on:
|
||||
- db
|
||||
environment:
|
||||
- ADMINER_DEFAULT_SERVER=db
|
||||
- ADMINER_DEFAULT_USER=postgres
|
||||
- ADMINER_DEFAULT_PASSWORD=dev_password
|
||||
|
||||
volumes:
|
||||
postgres_data_dev:
|
||||
|
||||
networks:
|
||||
bot-network:
|
||||
driver: bridge
|
||||
@@ -1,45 +0,0 @@
|
||||
[36mВведите параметры подключения к внешней базе данных:[0m
|
||||
Хост (например, localhost): Порт (например, 5432): Имя базы данных: Имя пользователя: Пароль: [33mМодифицируем docker-compose.yml для работы с внешней базой данных...[0m
|
||||
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> 䠩<><E4A0A9><EFBFBD>: 0.
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
bot:
|
||||
build: .
|
||||
container_name: telegram-tinder-bot
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- DB_HOST=
|
||||
- DB_PORT=
|
||||
- DB_NAME=
|
||||
- DB_USERNAME=
|
||||
- DB_PASSWORD=
|
||||
volumes:
|
||||
- ./uploads:/app/uploads
|
||||
- ./logs:/app/logs
|
||||
networks:
|
||||
- bot-network
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
|
||||
adminer:
|
||||
image: adminer:latest
|
||||
container_name: adminer-tinder
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:8080"
|
||||
networks:
|
||||
- bot-network
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
||||
networks:
|
||||
bot-network:
|
||||
driver: bridge
|
||||
@@ -1,23 +1,19 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
bot:
|
||||
build: .
|
||||
container_name: telegram-tinder-bot
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
env_file: .env
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- DB_HOST=db
|
||||
- DB_PORT=5432
|
||||
- DB_NAME=telegram_tinder_bot
|
||||
- DB_USERNAME=postgres
|
||||
- DB_HOST=${DB_HOST:-db}
|
||||
- DB_PORT=${DB_PORT:-5432}
|
||||
- DB_NAME=${DB_NAME:-telegram_tinder_bot}
|
||||
- DB_USERNAME=${DB_USERNAME:-postgres}
|
||||
- DB_PASSWORD=${DB_PASSWORD:-postgres}
|
||||
volumes:
|
||||
- ./uploads:/app/uploads
|
||||
- ./logs:/app/logs
|
||||
- ./uploads:/app/uploads:rw
|
||||
- ./logs:/app/logs:rw
|
||||
networks:
|
||||
- bot-network
|
||||
healthcheck:
|
||||
@@ -32,9 +28,9 @@ services:
|
||||
container_name: postgres-tinder
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- POSTGRES_DB=telegram_tinder_bot
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=${DB_PASSWORD:-password123}
|
||||
- POSTGRES_DB=${DB_NAME:-telegram_tinder_bot}
|
||||
- POSTGRES_USER=${DB_USERNAME:-postgres}
|
||||
- POSTGRES_PASSWORD=${DB_PASSWORD:-postgres}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
ports:
|
||||
@@ -42,19 +38,18 @@ services:
|
||||
networks:
|
||||
- bot-network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 5s
|
||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME:-postgres} -d ${DB_NAME:-telegram_tinder_bot}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
start_period: 10s
|
||||
|
||||
adminer:
|
||||
image: adminer:latest
|
||||
container_name: adminer-tinder
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:8080"
|
||||
depends_on:
|
||||
- db
|
||||
networks:
|
||||
- bot-network
|
||||
|
||||
|
||||
176
docs/DATABASE_FIXES.md
Normal file
176
docs/DATABASE_FIXES.md
Normal file
@@ -0,0 +1,176 @@
|
||||
# Database Schema Fixes
|
||||
|
||||
## Обзор исправлений
|
||||
|
||||
Этот документ описывает исправления схемы базы данных, примененные к проекту Telegram Tinder Bot для устранения критических ошибок и предупреждений.
|
||||
|
||||
## Дата последнего обновления: 2025-11-06
|
||||
|
||||
---
|
||||
|
||||
## Исправление 1: Колонка `looking_for` и триггер создания профиля
|
||||
|
||||
### Проблема
|
||||
```
|
||||
ERROR: null value in column "looking_for" of relation "profiles" violates not-null constraint
|
||||
```
|
||||
|
||||
### Причина
|
||||
Триггер `create_initial_profile()` не устанавливал значение для обязательного поля `looking_for` при автоматическом создании профиля.
|
||||
|
||||
### Решение
|
||||
Применен патч: `sql/fix_looking_for_column.sql`
|
||||
|
||||
**Изменения:**
|
||||
1. Обновлен триггер для включения `looking_for = 'both'` и `interested_in = 'both'`
|
||||
2. Сделана колонка `looking_for` необязательной (nullable) с DEFAULT 'both'
|
||||
3. Добавлена колонка `interested_in` как современный синоним для `looking_for`
|
||||
4. Создан индекс для поиска: `idx_profiles_interested_in`
|
||||
|
||||
**Применение:**
|
||||
```bash
|
||||
PGPASSWORD='your_password' psql -h host -p 5432 -U user -d db_name -f sql/fix_looking_for_column.sql
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Исправление 2: Колонка `job` и `state`
|
||||
|
||||
### Проблема 1: Column "job" does not exist
|
||||
```
|
||||
ERROR: column "job" of relation "profiles" does not exist
|
||||
```
|
||||
|
||||
**Причина:** Код использует `job`, но в БД создана колонка `occupation`.
|
||||
|
||||
### Проблема 2: State column does not exist
|
||||
```
|
||||
WARNING: State column does not exist in users table. Skipping state check.
|
||||
```
|
||||
|
||||
**Причина:** Таблица `users` не содержит колонку `state` для отслеживания состояния диалога.
|
||||
|
||||
### Решение
|
||||
Применен патч: `sql/add_job_and_state_columns.sql`
|
||||
|
||||
**Изменения:**
|
||||
1. Добавлена колонка `job VARCHAR(255)` в таблицу `profiles`
|
||||
2. Скопированы данные из `occupation` → `job` для обратной совместимости
|
||||
3. Добавлена колонка `state VARCHAR(50)` в таблицу `users`
|
||||
4. Созданы индексы: `idx_profiles_job`, `idx_users_state`
|
||||
|
||||
**Применение:**
|
||||
```bash
|
||||
PGPASSWORD='your_password' psql -h host -p 5432 -U user -d db_name -f sql/add_job_and_state_columns.sql
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Полная последовательность применения патчей
|
||||
|
||||
Для нового развертывания применяйте патчи в следующем порядке:
|
||||
|
||||
```bash
|
||||
# 1. Основная схема (если еще не применена)
|
||||
PGPASSWORD='your_password' psql -h host -p 5432 -U user -d db_name -f sql/consolidated.sql
|
||||
|
||||
# 2. Исправление looking_for
|
||||
PGPASSWORD='your_password' psql -h host -p 5432 -U user -d db_name -f sql/fix_looking_for_column.sql
|
||||
|
||||
# 3. Добавление job и state
|
||||
PGPASSWORD='your_password' psql -h host -p 5432 -U user -d db_name -f sql/add_job_and_state_columns.sql
|
||||
```
|
||||
|
||||
Или используйте автоматизированную команду:
|
||||
```bash
|
||||
make migrate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Проверка применения исправлений
|
||||
|
||||
### Проверка триггера looking_for
|
||||
```sql
|
||||
SELECT proname, prosrc
|
||||
FROM pg_proc
|
||||
WHERE proname = 'create_initial_profile';
|
||||
```
|
||||
|
||||
Должен содержать: `looking_for = 'both', interested_in = 'both'`
|
||||
|
||||
### Проверка колонок
|
||||
```sql
|
||||
-- Проверка profiles
|
||||
SELECT column_name, data_type, is_nullable, column_default
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'profiles'
|
||||
AND column_name IN ('job', 'occupation', 'looking_for', 'interested_in')
|
||||
ORDER BY column_name;
|
||||
|
||||
-- Проверка users
|
||||
SELECT column_name, data_type, is_nullable
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'users'
|
||||
AND column_name = 'state';
|
||||
```
|
||||
|
||||
### Проверка индексов
|
||||
```sql
|
||||
SELECT indexname, tablename, indexdef
|
||||
FROM pg_indexes
|
||||
WHERE tablename IN ('profiles', 'users')
|
||||
AND indexname IN ('idx_profiles_job', 'idx_users_state', 'idx_profiles_interested_in')
|
||||
ORDER BY tablename, indexname;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Известные предупреждения (неопасные)
|
||||
|
||||
### ES Module в миграциях
|
||||
```
|
||||
SyntaxError: Unexpected token 'export'
|
||||
```
|
||||
|
||||
**Статус:** Не критично. Миграции применяются через psql напрямую, а не через node-pg-migrate.
|
||||
|
||||
**Причина:** Файлы миграций в `/migrations` используют ES6 синтаксис, несовместимый с node-pg-migrate в режиме CommonJS.
|
||||
|
||||
**Решение:** Используйте `make migrate` или применяйте SQL патчи напрямую через psql.
|
||||
|
||||
### DEEPSEEK_API_KEY not found
|
||||
```
|
||||
⚠️ DEEPSEEK_API_KEY not found in environment variables
|
||||
```
|
||||
|
||||
**Статус:** Не критично. Это опциональная AI-функция.
|
||||
|
||||
**Решение:** Добавьте `DEEPSEEK_API_KEY=your_key` в `.env` если хотите использовать AI-фичи.
|
||||
|
||||
---
|
||||
|
||||
## Mapping колонок (для справки)
|
||||
|
||||
| Код (TypeScript) | База данных | Комментарий |
|
||||
|------------------|-------------|-------------|
|
||||
| `job` | `job` | Основная колонка (новая) |
|
||||
| `job` | `occupation` | Устаревшая, оставлена для совместимости |
|
||||
| `interestedIn` | `interested_in` | Основная колонка (новая) |
|
||||
| `lookingFor` | `looking_for` | Устаревшая, nullable |
|
||||
| - | `state` | Новая колонка для users.state |
|
||||
|
||||
---
|
||||
|
||||
## Контакты для поддержки
|
||||
|
||||
При возникновении проблем с миграциями:
|
||||
|
||||
1. Проверьте логи бота: `docker compose logs bot --tail 50`
|
||||
2. Проверьте применение всех патчей (см. раздел "Проверка")
|
||||
3. Убедитесь, что `.env` содержит правильные DB_* переменные
|
||||
4. Попробуйте применить патчи вручную через psql
|
||||
|
||||
**Версия документа:** 1.0
|
||||
**Автор:** GitHub Copilot
|
||||
**Дата:** 2025-11-06
|
||||
173
docs/FIXES_SUMMARY_2025-11-06.md
Normal file
173
docs/FIXES_SUMMARY_2025-11-06.md
Normal file
@@ -0,0 +1,173 @@
|
||||
# Сводка исправлений от 2025-11-06
|
||||
|
||||
## 🎯 Цель
|
||||
Устранить критические ошибки базы данных, блокирующие создание профилей пользователей.
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Исправленные ошибки
|
||||
|
||||
### 1. ❌ Column "job" does not exist
|
||||
**Ошибка:**
|
||||
```
|
||||
ERROR: column "job" of relation "profiles" does not exist
|
||||
Code: 42703
|
||||
```
|
||||
|
||||
**Причина:** Код использует поле `job`, но в БД существует только `occupation`.
|
||||
|
||||
**Решение:** Добавлена колонка `job` в таблицу `profiles`.
|
||||
|
||||
**Файл патча:** `sql/add_job_and_state_columns.sql`
|
||||
|
||||
**Статус:** ✅ ИСПРАВЛЕНО
|
||||
|
||||
---
|
||||
|
||||
### 2. ⚠️ State column does not exist in users table
|
||||
**Предупреждение:**
|
||||
```
|
||||
State column does not exist in users table. Skipping state check.
|
||||
```
|
||||
|
||||
**Причина:** Код пытается проверить состояние диалога пользователя через колонку `state`, которой нет в таблице `users`.
|
||||
|
||||
**Решение:** Добавлена колонка `state VARCHAR(50)` в таблицу `users`.
|
||||
|
||||
**Файл патча:** `sql/add_job_and_state_columns.sql`
|
||||
|
||||
**Статус:** ✅ ИСПРАВЛЕНО
|
||||
|
||||
---
|
||||
|
||||
### 3. ❌ null value in column "looking_for" violates not-null constraint
|
||||
**Ошибка:**
|
||||
```
|
||||
ERROR: null value in column "looking_for" of relation "profiles" violates not-null constraint
|
||||
Code: 23502
|
||||
```
|
||||
|
||||
**Причина:** Триггер `create_initial_profile()` не устанавливал значение для обязательного поля `looking_for`.
|
||||
|
||||
**Решение:**
|
||||
- Обновлен триггер для включения `looking_for = 'both'`
|
||||
- Колонка сделана nullable с DEFAULT 'both'
|
||||
- Добавлена колонка `interested_in` как современный синоним
|
||||
|
||||
**Файл патча:** `sql/fix_looking_for_column.sql`
|
||||
|
||||
**Статус:** ✅ ИСПРАВЛЕНО
|
||||
|
||||
---
|
||||
|
||||
## 📦 Применённые патчи
|
||||
|
||||
| # | Файл | Описание | Дата |
|
||||
|---|------|----------|------|
|
||||
| 1 | `sql/fix_looking_for_column.sql` | Исправление триггера и колонки looking_for | 2025-11-06 |
|
||||
| 2 | `sql/add_job_and_state_columns.sql` | Добавление колонок job и state | 2025-11-06 |
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Применение патчей
|
||||
|
||||
### Автоматически (рекомендуется):
|
||||
```bash
|
||||
./bin/apply_all_patches.sh
|
||||
```
|
||||
|
||||
### Вручную:
|
||||
```bash
|
||||
# Патч 1: looking_for
|
||||
PGPASSWORD='password' psql -h host -p 5432 -U user -d db_name \
|
||||
-f sql/fix_looking_for_column.sql
|
||||
|
||||
# Патч 2: job и state
|
||||
PGPASSWORD='password' psql -h host -p 5432 -U user -d db_name \
|
||||
-f sql/add_job_and_state_columns.sql
|
||||
|
||||
# Перезапуск бота
|
||||
docker compose restart bot
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Результаты после исправлений
|
||||
|
||||
### Проверка логов (нет критичных ошибок):
|
||||
```bash
|
||||
docker compose logs bot --since 5m | grep -E "(State column|column.*job|does not exist)"
|
||||
```
|
||||
**Результат:** Пусто (0 строк) ✅
|
||||
|
||||
### Проверка структуры БД:
|
||||
```
|
||||
table_name | column_name | data_type | nullable | column_default
|
||||
------------+---------------+-------------------+----------+--------------------
|
||||
profiles | interested_in | character varying | NULL | 'both'
|
||||
profiles | job | character varying | NULL |
|
||||
profiles | looking_for | character varying | NULL | 'both'
|
||||
profiles | occupation | character varying | NULL |
|
||||
users | state | character varying | NULL |
|
||||
```
|
||||
**Результат:** Все колонки присутствуют ✅
|
||||
|
||||
### Статус бота:
|
||||
```
|
||||
🎉 Bot initialized successfully!
|
||||
🤖 Bot is running and ready to match people!
|
||||
📱 Bot username: @seoulmate_officialbot
|
||||
```
|
||||
**Результат:** Бот запущен успешно ✅
|
||||
|
||||
---
|
||||
|
||||
## 📊 Статистика изменений
|
||||
|
||||
- **Добавлено колонок:** 3 (`job`, `state`, `interested_in`)
|
||||
- **Обновлено триггеров:** 1 (`create_initial_profile`)
|
||||
- **Создано индексов:** 3 (`idx_profiles_job`, `idx_users_state`, `idx_profiles_interested_in`)
|
||||
- **Файлов патчей:** 2
|
||||
- **Создано документации:** 3 файла (DATABASE_FIXES.md, HEALTH_CHECK.md, этот файл)
|
||||
- **Создано утилит:** 1 (`bin/apply_all_patches.sh`)
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Дальнейшие действия
|
||||
|
||||
### Обязательно:
|
||||
- ✅ Протестировать создание профиля через бота
|
||||
- ✅ Проверить обновление профиля (поле job)
|
||||
- ✅ Убедиться что свайпы работают
|
||||
|
||||
### Опционально:
|
||||
- ⚠️ Рассмотреть объединение `job` и `occupation` в одну колонку
|
||||
- ⚠️ Рассмотреть объединение `looking_for` и `interested_in` в одну колонку
|
||||
- ⚠️ Исправить ES module warnings в миграциях (низкий приоритет)
|
||||
- ⚠️ Настроить DEEPSEEK_API_KEY если нужны AI-фичи
|
||||
|
||||
---
|
||||
|
||||
## 📚 Связанная документация
|
||||
|
||||
- `/docs/DATABASE_FIXES.md` - Подробное описание всех исправлений БД
|
||||
- `/docs/HEALTH_CHECK.md` - Чеклист проверки здоровья бота
|
||||
- `/bin/README.md` - Описание утилит и скриптов
|
||||
- `/bin/apply_all_patches.sh` - Скрипт автоматического применения патчей
|
||||
|
||||
---
|
||||
|
||||
## 👤 Авторство
|
||||
|
||||
**Дата:** 2025-11-06
|
||||
**Автор:** GitHub Copilot
|
||||
**Проект:** Telegram Tinder Bot
|
||||
**Версия:** 2.0
|
||||
|
||||
---
|
||||
|
||||
## ✨ Заключение
|
||||
|
||||
Все критические ошибки устранены. Бот готов к работе в production-среде с внешним PostgreSQL сервером (192.168.0.102:5432).
|
||||
|
||||
**Статус проекта:** 🟢 РАБОТОСПОСОБЕН
|
||||
199
docs/HEALTH_CHECK.md
Normal file
199
docs/HEALTH_CHECK.md
Normal file
@@ -0,0 +1,199 @@
|
||||
# Быстрая проверка здоровья бота
|
||||
|
||||
Используйте этот чеклист после развёртывания или обновления бота.
|
||||
|
||||
## ✅ Чеклист проверки
|
||||
|
||||
### 1. Проверка контейнера
|
||||
```bash
|
||||
docker compose ps
|
||||
# Ожидается: telegram-tinder-bot в состоянии "running" (healthy)
|
||||
```
|
||||
|
||||
### 2. Проверка логов (нет критичных ошибок)
|
||||
```bash
|
||||
docker compose logs bot --tail 50
|
||||
# ✅ Должно быть: "Bot initialized successfully"
|
||||
# ✅ Должно быть: "Bot username: @your_bot_name"
|
||||
# ❌ НЕ должно быть: "column X does not exist" (критическая ошибка)
|
||||
```
|
||||
|
||||
### 3. Проверка схемы БД
|
||||
```bash
|
||||
# Проверка критичных колонок
|
||||
PGPASSWORD='your_password' psql -h host -p 5432 -U user -d db_name << 'EOF'
|
||||
SELECT
|
||||
table_name,
|
||||
column_name,
|
||||
data_type,
|
||||
is_nullable
|
||||
FROM information_schema.columns
|
||||
WHERE table_name IN ('users', 'profiles')
|
||||
AND column_name IN ('state', 'job', 'looking_for', 'interested_in', 'occupation')
|
||||
ORDER BY table_name, column_name;
|
||||
EOF
|
||||
```
|
||||
|
||||
**Ожидаемый результат:**
|
||||
```
|
||||
table_name | column_name | data_type | is_nullable
|
||||
------------+----------------+--------------------+-------------
|
||||
profiles | interested_in | character varying | YES
|
||||
profiles | job | character varying | YES
|
||||
profiles | looking_for | character varying | YES
|
||||
profiles | occupation | character varying | YES
|
||||
users | state | character varying | YES
|
||||
```
|
||||
|
||||
### 4. Проверка триггера
|
||||
```bash
|
||||
PGPASSWORD='your_password' psql -h host -p 5432 -U user -d db_name -c \
|
||||
"SELECT proname FROM pg_proc WHERE proname = 'create_initial_profile';"
|
||||
```
|
||||
|
||||
**Ожидается:** `create_initial_profile` (1 строка)
|
||||
|
||||
### 5. Проверка индексов
|
||||
```bash
|
||||
PGPASSWORD='your_password' psql -h host -p 5432 -U user -d db_name << 'EOF'
|
||||
SELECT indexname
|
||||
FROM pg_indexes
|
||||
WHERE indexname IN ('idx_profiles_job', 'idx_users_state', 'idx_profiles_interested_in');
|
||||
EOF
|
||||
```
|
||||
|
||||
**Ожидается:** 3 строки с названиями индексов
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Типичные проблемы и решения
|
||||
|
||||
### Проблема: "column job does not exist"
|
||||
**Решение:**
|
||||
```bash
|
||||
./bin/apply_all_patches.sh
|
||||
docker compose restart bot
|
||||
```
|
||||
|
||||
### Проблема: "State column does not exist" (много раз)
|
||||
**Решение:**
|
||||
```bash
|
||||
PGPASSWORD='password' psql -h host -p 5432 -U user -d db_name \
|
||||
-c "ALTER TABLE users ADD COLUMN IF NOT EXISTS state VARCHAR(50);"
|
||||
docker compose restart bot
|
||||
```
|
||||
|
||||
### Проблема: "looking_for violates not-null constraint"
|
||||
**Решение:**
|
||||
```bash
|
||||
PGPASSWORD='password' psql -h host -p 5432 -U user -d db_name \
|
||||
-f sql/fix_looking_for_column.sql
|
||||
docker compose restart bot
|
||||
```
|
||||
|
||||
### Проблема: Бот не запускается (exit code 1)
|
||||
**Диагностика:**
|
||||
```bash
|
||||
docker compose logs bot --tail 100
|
||||
# Ищите строки с ERROR или "does not exist"
|
||||
```
|
||||
|
||||
**Решения:**
|
||||
1. Проверьте `.env` - все переменные DB_* заданы?
|
||||
2. Проверьте подключение к БД: `PGPASSWORD='password' psql -h host -p 5432 -U user -d db_name -c "SELECT 1;"`
|
||||
3. Примените все патчи: `./bin/apply_all_patches.sh`
|
||||
4. Пересоберите контейнер: `make update`
|
||||
|
||||
---
|
||||
|
||||
## 📊 Быстрая диагностика одной командой
|
||||
|
||||
```bash
|
||||
# Создайте alias для удобства
|
||||
alias bot-health='docker compose ps && echo "=== LOGS ===" && docker compose logs bot --tail 20'
|
||||
|
||||
# Использование
|
||||
bot-health
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Команды для разработки
|
||||
|
||||
```bash
|
||||
# Полное обновление (pull + rebuild + migrate + restart)
|
||||
make update
|
||||
|
||||
# Применение миграций
|
||||
make migrate
|
||||
|
||||
# Только перезапуск
|
||||
docker compose restart bot
|
||||
|
||||
# Пересборка с нуля
|
||||
make clean && make install
|
||||
|
||||
# Проверка синтаксиса TypeScript
|
||||
npm run build
|
||||
|
||||
# Запуск в режиме разработки (локально)
|
||||
npm run dev
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Переменные окружения (.env)
|
||||
|
||||
Обязательные:
|
||||
```env
|
||||
DB_HOST=192.168.0.102
|
||||
DB_PORT=5432
|
||||
DB_NAME=telegram_tinder_bot
|
||||
DB_USERNAME=trevor
|
||||
DB_PASSWORD=your_secure_password
|
||||
|
||||
TELEGRAM_BOT_TOKEN=your_bot_token
|
||||
JWT_SECRET=your_jwt_secret
|
||||
APP_SECRET=your_app_secret
|
||||
|
||||
NODE_ENV=production
|
||||
PORT=3000
|
||||
```
|
||||
|
||||
Опциональные:
|
||||
```env
|
||||
DEEPSEEK_API_KEY=your_deepseek_key # Для AI фич
|
||||
LOG_LEVEL=info # debug | info | warn | error
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🆘 Экстренное восстановление
|
||||
|
||||
Если бот полностью сломан:
|
||||
|
||||
```bash
|
||||
# 1. Остановить всё
|
||||
docker compose down
|
||||
|
||||
# 2. Сделать бэкап БД
|
||||
./bin/backup_db.sh
|
||||
|
||||
# 3. Откатить к последнему коммиту
|
||||
git reset --hard HEAD
|
||||
|
||||
# 4. Применить все патчи заново
|
||||
./bin/apply_all_patches.sh
|
||||
|
||||
# 5. Пересобрать
|
||||
make install
|
||||
|
||||
# 6. Запустить
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Версия:** 1.0
|
||||
**Дата:** 2025-11-06
|
||||
**Для:** Telegram Tinder Bot v2.0
|
||||
80
docs/docker_fix.md
Normal file
80
docs/docker_fix.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# Решение проблемы с Docker-контейнерами
|
||||
|
||||
## Проблема
|
||||
При запуске контейнеров через Docker Compose возникает ошибка `KeyError: 'ContainerConfig'`. Эта ошибка появляется из-за несовместимости между версиями Docker, Docker Compose и структурой docker-compose.yml.
|
||||
|
||||
## Решение
|
||||
|
||||
### 1. Очистка окружения Docker
|
||||
|
||||
На сервере выполните следующие команды, чтобы полностью очистить окружение Docker:
|
||||
|
||||
```bash
|
||||
# Остановка и удаление контейнеров
|
||||
docker-compose down -v
|
||||
|
||||
# Принудительное удаление контейнеров по имени
|
||||
docker rm -f postgres-tinder adminer-tinder telegram-tinder-bot
|
||||
|
||||
# Очистка неиспользуемых томов и сетей
|
||||
docker system prune -f --volumes
|
||||
|
||||
# Очистка кеша Docker
|
||||
docker builder prune -f
|
||||
```
|
||||
|
||||
### 2. Исправление проблем с переносами строк
|
||||
|
||||
Файлы, созданные в Windows и перенесенные в Linux, могут содержать неправильные символы переноса строки.
|
||||
|
||||
```bash
|
||||
# Исправление переносов строк в shell-скриптах
|
||||
find . -name "*.sh" -type f -exec sh -c 'tr -d "\r" < "$1" > "$1.fixed" && mv "$1.fixed" "$1" && chmod +x "$1"' -- {} \;
|
||||
```
|
||||
|
||||
### 3. Обновление docker-compose.yml
|
||||
|
||||
Создайте новый docker-compose.yml с исправленной структурой:
|
||||
|
||||
```bash
|
||||
# Запустите скрипт для исправления проблем с Docker
|
||||
./bin/fix_docker.sh
|
||||
```
|
||||
|
||||
### 4. Запуск с полностью чистым окружением
|
||||
|
||||
После выполнения всех исправлений запустите контейнеры заново:
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## Альтернативное решение
|
||||
|
||||
Если проблема сохраняется, можно попробовать запустить контейнеры по отдельности:
|
||||
|
||||
```bash
|
||||
# Сначала запустить базу данных (если она нужна)
|
||||
docker-compose up -d db
|
||||
|
||||
# Дождаться запуска базы данных
|
||||
sleep 10
|
||||
|
||||
# Запустить бота
|
||||
docker-compose up -d bot
|
||||
|
||||
# Запустить adminer
|
||||
docker-compose up -d adminer
|
||||
```
|
||||
|
||||
## Проверка работы миграций
|
||||
|
||||
После запуска контейнеров проверьте, что миграции базы данных применяются правильно:
|
||||
|
||||
```bash
|
||||
# Просмотр логов контейнера бота
|
||||
docker logs telegram-tinder-bot
|
||||
|
||||
# Если миграции не применяются, можно запустить их вручную внутри контейнера
|
||||
docker exec -it telegram-tinder-bot sh -c "DATABASE_URL=postgres://$DB_USERNAME:$DB_PASSWORD@$DB_HOST:$DB_PORT/$DB_NAME npx node-pg-migrate up"
|
||||
```
|
||||
88
docs/migrations_fix.md
Normal file
88
docs/migrations_fix.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# Решение проблемы с миграциями базы данных
|
||||
|
||||
## Проблемы
|
||||
|
||||
При попытке применить миграции были обнаружены следующие проблемы:
|
||||
|
||||
1. **Ошибка с TypeScript файлами**: Node.js не может напрямую выполнять файлы `.ts` без компиляции их в JavaScript.
|
||||
2. **Предупреждения о ES модулях**: Файлы используют синтаксис ES модулей, но не имеют расширения `.mjs` или настроек в package.json.
|
||||
3. **Неверный порядок миграций**: Миграции могут выполняться в неправильном порядке.
|
||||
|
||||
## Решения
|
||||
|
||||
### Для быстрого применения миграций
|
||||
|
||||
Используйте один из следующих сценариев:
|
||||
|
||||
```bash
|
||||
# Полный процесс миграции с компиляцией TypeScript
|
||||
./bin/run_full_migration.sh
|
||||
|
||||
# Только SQL-миграции (минуя node-pg-migrate)
|
||||
./bin/run_sql_migrations.sh
|
||||
```
|
||||
|
||||
### Пошаговое решение
|
||||
|
||||
1. **Компиляция TypeScript миграций в JavaScript**:
|
||||
```bash
|
||||
./bin/compile_ts_migrations.sh
|
||||
```
|
||||
|
||||
2. **Применение JS-миграций**:
|
||||
```bash
|
||||
./bin/apply_migrations.sh
|
||||
```
|
||||
|
||||
3. **Ручное применение SQL-миграций**:
|
||||
```bash
|
||||
./bin/run_sql_migrations.sh
|
||||
```
|
||||
|
||||
## Описание скриптов
|
||||
|
||||
- **run_full_migration.sh**: Полный процесс миграции, включающий компиляцию TypeScript и применение всех миграций.
|
||||
- **compile_ts_migrations.sh**: Только компиляция TypeScript миграций в JavaScript.
|
||||
- **apply_migrations.sh**: Применение JS-миграций через node-pg-migrate.
|
||||
- **run_sql_migrations.sh**: Прямое применение SQL-миграций через psql.
|
||||
|
||||
## Проверка результатов
|
||||
|
||||
После выполнения миграций проверьте состояние базы данных:
|
||||
|
||||
```bash
|
||||
# Подключение к базе данных
|
||||
PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST -p $DB_PORT -U $DB_USERNAME -d $DB_NAME
|
||||
|
||||
# Проверка таблиц
|
||||
\dt
|
||||
|
||||
# Проверка примененных миграций
|
||||
SELECT * FROM migrations ORDER BY executed_at;
|
||||
```
|
||||
|
||||
## Если проблемы сохраняются
|
||||
|
||||
1. **Очистить директорию миграций**:
|
||||
```bash
|
||||
# Создание резервной копии
|
||||
mkdir -p backup_migrations
|
||||
cp -r migrations/* backup_migrations/
|
||||
|
||||
# Оставить только JS-миграции
|
||||
rm -f migrations/*.ts
|
||||
```
|
||||
|
||||
2. **Инициализировать миграции заново**:
|
||||
```bash
|
||||
npx node-pg-migrate init
|
||||
```
|
||||
|
||||
3. **Применить специальную консолидированную миграцию**:
|
||||
```bash
|
||||
# Создание консолидированной миграции
|
||||
cat src/database/migrations/*.sql > consolidated.sql
|
||||
|
||||
# Применение консолидированной миграции
|
||||
PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST -p $DB_PORT -U $DB_USERNAME -d $DB_NAME -f consolidated.sql
|
||||
```
|
||||
BIN
new_docker-keyring.gpg
Normal file
BIN
new_docker-keyring.gpg
Normal file
Binary file not shown.
@@ -1,15 +1,16 @@
|
||||
// add-hobbies-column.js
|
||||
// Скрипт для добавления колонки hobbies в таблицу profiles
|
||||
|
||||
require('dotenv').config();
|
||||
const { Pool } = require('pg');
|
||||
|
||||
// Настройки подключения к базе данных
|
||||
// Настройки подключения к базе данных из переменных окружения
|
||||
const pool = new Pool({
|
||||
host: '192.168.0.102',
|
||||
port: 5432,
|
||||
database: 'telegram_tinder_bot',
|
||||
user: 'trevor',
|
||||
password: 'Cl0ud_1985!'
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: parseInt(process.env.DB_PORT) || 5432,
|
||||
database: process.env.DB_NAME || 'telegram_tinder_bot',
|
||||
user: process.env.DB_USERNAME || 'postgres',
|
||||
password: process.env.DB_PASSWORD
|
||||
});
|
||||
|
||||
async function addHobbiesColumn() {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Исправленный код для создания профиля
|
||||
require('dotenv').config();
|
||||
const { Client } = require('pg');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
|
||||
@@ -18,13 +19,13 @@ if (!telegramId || !name || !age || !gender || !city || !bio || !photoFileId) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Устанавливаем соединение с базой данных
|
||||
// Устанавливаем соединение с базой данных из переменных окружения
|
||||
const client = new Client({
|
||||
host: '192.168.0.102',
|
||||
port: 5432,
|
||||
user: 'trevor',
|
||||
password: 'Cl0ud_1985!',
|
||||
database: 'telegram_tinder_bot'
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: parseInt(process.env.DB_PORT) || 5432,
|
||||
user: process.env.DB_USERNAME || 'postgres',
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME || 'telegram_tinder_bot'
|
||||
});
|
||||
|
||||
async function createProfile() {
|
||||
|
||||
@@ -2,17 +2,18 @@
|
||||
// Этот скрипт создает записи о миграциях в таблице pgmigrations без применения изменений
|
||||
// Используется для синхронизации существующей базы с миграциями
|
||||
|
||||
require('dotenv').config();
|
||||
const { Client } = require('pg');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Подключение к базе данных
|
||||
// Подключение к базе данных из переменных окружения
|
||||
const client = new Client({
|
||||
host: '192.168.0.102',
|
||||
port: 5432,
|
||||
database: 'telegram_tinder_bot',
|
||||
user: 'trevor',
|
||||
password: 'Cl0ud_1985!'
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: parseInt(process.env.DB_PORT) || 5432,
|
||||
database: process.env.DB_NAME || 'telegram_tinder_bot',
|
||||
user: process.env.DB_USERNAME || 'postgres',
|
||||
password: process.env.DB_PASSWORD
|
||||
});
|
||||
|
||||
async function syncMigrations() {
|
||||
|
||||
@@ -27,7 +27,56 @@ sleep 5
|
||||
|
||||
# Run database migrations
|
||||
echo "🔄 Running database migrations..."
|
||||
node dist/database/migrateOnStartup.js
|
||||
|
||||
# Create migrations directory structure
|
||||
mkdir -p dist/database/migrations
|
||||
|
||||
# Copy any available migrations
|
||||
if [ -d "src/database/migrations" ]; then
|
||||
echo "<22> Found SQL migrations. Copying..."
|
||||
cp -R src/database/migrations/* dist/database/migrations/ 2>/dev/null || echo "No SQL migrations to copy"
|
||||
fi
|
||||
|
||||
# Copy JS migrations if available
|
||||
if [ -d "migrations" ]; then
|
||||
echo "📂 Found JS migrations. Copying..."
|
||||
mkdir -p migrations-temp
|
||||
cp migrations/*.js migrations-temp/ 2>/dev/null || echo "No JS migrations to copy"
|
||||
# Move JS migrations to dist/database/migrations
|
||||
cp migrations-temp/*.js dist/database/migrations/ 2>/dev/null || echo "No JS migrations to copy to dist"
|
||||
fi
|
||||
|
||||
# Display environment variables for debugging (without passwords)
|
||||
echo "🔍 Environment variables for database connection:"
|
||||
echo "DB_HOST: $DB_HOST"
|
||||
echo "DB_PORT: $DB_PORT"
|
||||
echo "DB_NAME: $DB_NAME"
|
||||
echo "DB_USERNAME: $DB_USERNAME"
|
||||
|
||||
# Run migrations using node-pg-migrate
|
||||
echo "🔄 Running migrations with node-pg-migrate..."
|
||||
DATABASE_URL="postgres://$DB_USERNAME:$DB_PASSWORD@$DB_HOST:$DB_PORT/$DB_NAME" npx node-pg-migrate up
|
||||
|
||||
# Verify connection to database
|
||||
echo "🔍 Verifying database connection..."
|
||||
node -e "
|
||||
const { Pool } = require('pg');
|
||||
const pool = new Pool({
|
||||
host: process.env.DB_HOST,
|
||||
port: process.env.DB_PORT,
|
||||
database: process.env.DB_NAME,
|
||||
user: process.env.DB_USERNAME,
|
||||
password: process.env.DB_PASSWORD
|
||||
});
|
||||
pool.query('SELECT NOW()', (err, res) => {
|
||||
if (err) {
|
||||
console.error('❌ Database connection failed:', err.message);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('✅ Database connection successful:', res.rows[0].now);
|
||||
pool.end();
|
||||
}
|
||||
});" || echo "❌ Failed to verify database connection"
|
||||
|
||||
# Start the bot
|
||||
echo "✅ Starting the bot..."
|
||||
|
||||
36
sql/add_job_and_state_columns.sql
Normal file
36
sql/add_job_and_state_columns.sql
Normal file
@@ -0,0 +1,36 @@
|
||||
-- Добавление колонок job и state
|
||||
-- Дата: 2025-11-06
|
||||
-- Исправляет ошибки: "column job does not exist" и "State column does not exist"
|
||||
|
||||
-- 1. Добавляем колонку job в таблицу profiles (синоним для occupation)
|
||||
ALTER TABLE profiles ADD COLUMN IF NOT EXISTS job VARCHAR(255);
|
||||
|
||||
-- 2. Копируем существующие данные из occupation в job
|
||||
UPDATE profiles SET job = occupation WHERE occupation IS NOT NULL AND job IS NULL;
|
||||
|
||||
-- 3. Добавляем колонку state в таблицу users для отслеживания состояния диалога
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS state VARCHAR(50);
|
||||
|
||||
-- 4. Создаём индексы для производительности
|
||||
CREATE INDEX IF NOT EXISTS idx_profiles_job ON profiles(job);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_state ON users(state);
|
||||
|
||||
-- 5. Добавляем комментарии для документации
|
||||
COMMENT ON COLUMN profiles.job IS 'Профессия/работа пользователя (синоним для occupation)';
|
||||
COMMENT ON COLUMN profiles.occupation IS 'Профессия/работа пользователя (устаревшее, используйте job)';
|
||||
COMMENT ON COLUMN users.state IS 'Текущее состояние пользователя в диалоге с ботом';
|
||||
|
||||
-- Проверка результата
|
||||
SELECT
|
||||
'profiles.job' as column_name,
|
||||
CASE WHEN EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'profiles' AND column_name = 'job'
|
||||
) THEN '✅ Существует' ELSE '❌ Не найдена' END as status
|
||||
UNION ALL
|
||||
SELECT
|
||||
'users.state' as column_name,
|
||||
CASE WHEN EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'users' AND column_name = 'state'
|
||||
) THEN '✅ Существует' ELSE '❌ Не найдена' END as status;
|
||||
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;
|
||||
43
sql/fix_looking_for_column.sql
Normal file
43
sql/fix_looking_for_column.sql
Normal file
@@ -0,0 +1,43 @@
|
||||
-- Исправление триггера create_initial_profile и колонки looking_for
|
||||
-- Дата: 2025-11-06
|
||||
|
||||
-- 1. Удаляем старую функцию триггера
|
||||
DROP FUNCTION IF EXISTS create_initial_profile() CASCADE;
|
||||
|
||||
-- 2. Создаём исправленную функцию с полем looking_for
|
||||
CREATE OR REPLACE FUNCTION create_initial_profile()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
INSERT INTO profiles (user_id, name, age, gender, looking_for, interested_in)
|
||||
VALUES (NEW.id, COALESCE(NEW.first_name, 'User'), 18, 'other', 'both', 'both');
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- 3. Пересоздаём триггер
|
||||
DROP TRIGGER IF EXISTS create_profile_on_user_insert ON users;
|
||||
DROP TRIGGER IF EXISTS create_profile_trigger ON users;
|
||||
|
||||
CREATE TRIGGER create_profile_on_user_insert
|
||||
AFTER INSERT ON users
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION create_initial_profile();
|
||||
|
||||
-- 4. Делаем looking_for необязательным с дефолтным значением
|
||||
ALTER TABLE profiles ALTER COLUMN looking_for DROP NOT NULL;
|
||||
ALTER TABLE profiles ALTER COLUMN looking_for SET DEFAULT 'both';
|
||||
|
||||
-- 5. Добавляем interested_in как синоним для looking_for
|
||||
ALTER TABLE profiles ADD COLUMN IF NOT EXISTS interested_in VARCHAR(20) DEFAULT 'both'
|
||||
CHECK (interested_in IN ('male', 'female', 'both'));
|
||||
|
||||
-- 6. Обновляем существующие записи
|
||||
UPDATE profiles SET looking_for = 'both' WHERE looking_for IS NULL;
|
||||
UPDATE profiles SET interested_in = COALESCE(looking_for, 'both') WHERE interested_in IS NULL;
|
||||
|
||||
-- 7. Создаём индекс для поиска по interested_in
|
||||
CREATE INDEX IF NOT EXISTS idx_profiles_interested_in ON profiles(interested_in);
|
||||
|
||||
COMMENT ON COLUMN profiles.looking_for IS 'Предпочитаемый пол для знакомства (устаревшее, используйте interested_in)';
|
||||
COMMENT ON COLUMN profiles.interested_in IS 'Предпочитаемый пол для знакомства: male, female, both';
|
||||
82
sql/fix_match_trigger.sql
Normal file
82
sql/fix_match_trigger.sql
Normal file
@@ -0,0 +1,82 @@
|
||||
-- Исправление триггера create_match_on_mutual_like
|
||||
-- Дата: 2025-11-06
|
||||
-- Проблемы:
|
||||
-- 1. Использовались неправильные имена полей: target_id вместо target_user_id, action вместо type
|
||||
-- 2. Использовались несуществующие колонки в notifications: content и reference_id вместо data
|
||||
|
||||
-- Удаляем старую функцию
|
||||
DROP FUNCTION IF EXISTS create_match_on_mutual_like() CASCADE;
|
||||
|
||||
-- Создаем исправленную функцию
|
||||
CREATE OR REPLACE FUNCTION create_match_on_mutual_like()
|
||||
RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
reverse_like_exists BOOLEAN;
|
||||
match_id_var UUID;
|
||||
BEGIN
|
||||
-- Проверяем только лайки и суперлайки (игнорируем pass)
|
||||
IF NEW.type != 'like' AND NEW.type != 'superlike' THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
-- Проверяем есть ли обратный лайк (правильные имена полей: target_user_id, type)
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM swipes
|
||||
WHERE user_id = NEW.target_user_id
|
||||
AND target_user_id = NEW.user_id
|
||||
AND type IN ('like', 'superlike')
|
||||
) INTO reverse_like_exists;
|
||||
|
||||
-- Если есть взаимный лайк, создаем матч
|
||||
IF reverse_like_exists THEN
|
||||
-- Создаем матч и получаем его ID
|
||||
INSERT INTO matches (user_id_1, user_id_2, created_at, is_active)
|
||||
VALUES (
|
||||
LEAST(NEW.user_id, NEW.target_user_id),
|
||||
GREATEST(NEW.user_id, NEW.target_user_id),
|
||||
NOW(),
|
||||
true
|
||||
)
|
||||
ON CONFLICT (user_id_1, user_id_2) DO UPDATE SET is_active = true
|
||||
RETURNING id INTO match_id_var;
|
||||
|
||||
-- Создаем уведомления для обоих пользователей
|
||||
-- Используем data (jsonb) вместо content и reference_id
|
||||
INSERT INTO notifications (user_id, type, data, is_read, created_at)
|
||||
VALUES (
|
||||
NEW.user_id,
|
||||
'new_match',
|
||||
jsonb_build_object('message', 'У вас новый матч!', 'match_id', match_id_var),
|
||||
false,
|
||||
NOW()
|
||||
);
|
||||
|
||||
INSERT INTO notifications (user_id, type, data, is_read, created_at)
|
||||
VALUES (
|
||||
NEW.target_user_id,
|
||||
'new_match',
|
||||
jsonb_build_object('message', 'У вас новый матч!', 'match_id', match_id_var),
|
||||
false,
|
||||
NOW()
|
||||
);
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Пересоздаем триггер
|
||||
DROP TRIGGER IF EXISTS create_match_trigger ON swipes;
|
||||
CREATE TRIGGER create_match_trigger
|
||||
AFTER INSERT ON swipes
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION create_match_on_mutual_like();
|
||||
|
||||
-- Проверка
|
||||
SELECT 'Триггер create_match_on_mutual_like успешно исправлен!' as status;
|
||||
|
||||
COMMENT ON FUNCTION create_match_on_mutual_like() IS
|
||||
'Триггер автоматически создает матч при взаимном лайке.
|
||||
Исправлено: использование правильных имен полей (target_user_id, type)
|
||||
и правильной структуры notifications (data jsonb).';
|
||||
49
sql/fix_notify_message_trigger.sql
Normal file
49
sql/fix_notify_message_trigger.sql
Normal file
@@ -0,0 +1,49 @@
|
||||
-- Исправление триггера notify_new_message для использования правильной схемы notifications
|
||||
-- Проблема: триггер использует content и reference_id вместо data (jsonb)
|
||||
|
||||
-- Удаляем старый триггер и функцию
|
||||
DROP TRIGGER IF EXISTS notify_new_message_trigger ON messages;
|
||||
DROP FUNCTION IF EXISTS notify_new_message();
|
||||
|
||||
-- Создаём новую функцию с правильной схемой
|
||||
CREATE OR REPLACE FUNCTION notify_new_message()
|
||||
RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
recipient_id UUID;
|
||||
BEGIN
|
||||
-- Определяем получателя сообщения (второго участника матча)
|
||||
SELECT CASE
|
||||
WHEN m.user_id_1 = NEW.sender_id THEN m.user_id_2
|
||||
ELSE m.user_id_1
|
||||
END INTO recipient_id
|
||||
FROM matches m
|
||||
WHERE m.id = NEW.match_id;
|
||||
|
||||
-- Создаём уведомление с правильной структурой (data jsonb)
|
||||
IF recipient_id IS NOT NULL THEN
|
||||
INSERT INTO notifications (user_id, type, data, created_at)
|
||||
VALUES (
|
||||
recipient_id,
|
||||
'new_message',
|
||||
jsonb_build_object(
|
||||
'message_id', NEW.id,
|
||||
'match_id', NEW.match_id,
|
||||
'sender_id', NEW.sender_id,
|
||||
'content_preview', LEFT(NEW.content, 50)
|
||||
),
|
||||
NOW()
|
||||
);
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Создаём триггер заново
|
||||
CREATE TRIGGER notify_new_message_trigger
|
||||
AFTER INSERT ON messages
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION notify_new_message();
|
||||
|
||||
-- Проверка
|
||||
SELECT 'Trigger notify_new_message fixed successfully' AS status;
|
||||
46
sql/fix_update_last_active_trigger.sql
Normal file
46
sql/fix_update_last_active_trigger.sql
Normal file
@@ -0,0 +1,46 @@
|
||||
-- Исправление триггера update_last_active для работы с messages и swipes
|
||||
-- Проблема: в messages есть sender_id, а в swipes есть user_id
|
||||
|
||||
-- Удаляем старые триггеры
|
||||
DROP TRIGGER IF EXISTS update_last_active_on_message ON messages;
|
||||
DROP TRIGGER IF EXISTS update_last_active_on_swipe ON swipes;
|
||||
DROP FUNCTION IF EXISTS update_last_active();
|
||||
|
||||
-- Создаём функцию для обновления last_active для отправителя сообщения
|
||||
CREATE OR REPLACE FUNCTION update_last_active_on_message()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
UPDATE profiles
|
||||
SET last_active = CURRENT_TIMESTAMP
|
||||
WHERE user_id = NEW.sender_id;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Создаём функцию для обновления last_active при свайпе
|
||||
CREATE OR REPLACE FUNCTION update_last_active_on_swipe()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
UPDATE profiles
|
||||
SET last_active = CURRENT_TIMESTAMP
|
||||
WHERE user_id = NEW.user_id;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Создаём триггер для messages
|
||||
CREATE TRIGGER update_last_active_on_message
|
||||
AFTER INSERT ON messages
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_last_active_on_message();
|
||||
|
||||
-- Создаём триггер для swipes
|
||||
CREATE TRIGGER update_last_active_on_swipe
|
||||
AFTER INSERT ON swipes
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_last_active_on_swipe();
|
||||
|
||||
-- Проверка
|
||||
SELECT 'Triggers update_last_active fixed successfully' AS status;
|
||||
@@ -34,7 +34,7 @@ class TelegramTinderBot {
|
||||
this.localizationService = LocalizationService.getInstance();
|
||||
|
||||
this.commandHandlers = new CommandHandlers(this.bot);
|
||||
this.messageHandlers = new MessageHandlers(this.bot);
|
||||
this.messageHandlers = new MessageHandlers(this.bot, this.notificationService);
|
||||
this.callbackHandlers = new CallbackHandlers(this.bot, this.messageHandlers);
|
||||
this.notificationHandlers = new NotificationHandlers(this.bot);
|
||||
|
||||
|
||||
@@ -31,7 +31,9 @@ export class CallbackHandlers {
|
||||
this.bot = bot;
|
||||
this.profileService = new ProfileService();
|
||||
this.matchingService = new MatchingService();
|
||||
this.chatService = new ChatService();
|
||||
// Получаем notificationService из messageHandlers (если есть)
|
||||
const notificationService = (messageHandlers as any).notificationService;
|
||||
this.chatService = new ChatService(notificationService);
|
||||
this.messageHandlers = messageHandlers;
|
||||
this.profileEditController = new ProfileEditController(this.profileService);
|
||||
this.enhancedChatHandlers = new EnhancedChatHandlers(bot);
|
||||
@@ -86,6 +88,112 @@ export class CallbackHandlers {
|
||||
await this.handleEditHobbies(chatId, telegramId);
|
||||
} else if (data === 'edit_city') {
|
||||
await this.handleEditCity(chatId, telegramId);
|
||||
} else if (data === 'confirm_city') {
|
||||
try {
|
||||
const states = (this.messageHandlers as any).userStates as Map<string, any>;
|
||||
const userState = states ? states.get(telegramId) : null;
|
||||
if (userState && userState.step === 'confirm_city') {
|
||||
// Подтверждаем город и переводим пользователя к вводу био
|
||||
userState.step = 'waiting_bio';
|
||||
console.log(`User ${telegramId} confirmed city: ${userState.data.city}`);
|
||||
// Убираем inline-кнопки из сообщения с подтверждением
|
||||
try {
|
||||
// clear inline keyboard
|
||||
await this.bot.editMessageReplyMarkup({ inline_keyboard: [] } as any, { chat_id: chatId, message_id: query.message?.message_id });
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
await this.bot.sendMessage(chatId, `✅ Город подтверждён: *${userState.data.city}*\n\n📝 Теперь расскажите немного о себе (био):`, { parse_mode: 'Markdown' });
|
||||
} else {
|
||||
await this.bot.answerCallbackQuery(query.id, { text: 'Контекст не найден. Повторите, пожалуйста.' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error confirming city via callback:', error);
|
||||
await this.bot.answerCallbackQuery(query.id, { text: 'Ошибка при подтверждении города' });
|
||||
}
|
||||
} else if (data === 'edit_city_manual') {
|
||||
try {
|
||||
const states = (this.messageHandlers as any).userStates as Map<string, any>;
|
||||
const userState = states ? states.get(telegramId) : null;
|
||||
if (userState) {
|
||||
userState.step = 'waiting_city';
|
||||
console.log(`User ${telegramId} chose to enter city manually`);
|
||||
try {
|
||||
// clear inline keyboard
|
||||
await this.bot.editMessageReplyMarkup({ inline_keyboard: [] } as any, { chat_id: chatId, message_id: query.message?.message_id });
|
||||
} catch (e) { }
|
||||
await this.bot.sendMessage(chatId, '✏️ Введите название вашего города вручную:', { reply_markup: { remove_keyboard: true } });
|
||||
} else {
|
||||
await this.bot.answerCallbackQuery(query.id, { text: 'Контекст не найден. Повторите, пожалуйста.' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error switching to manual city input via callback:', error);
|
||||
await this.bot.answerCallbackQuery(query.id, { text: 'Ошибка' });
|
||||
}
|
||||
} else if (data === 'confirm_city_edit') {
|
||||
try {
|
||||
const editState = this.messageHandlers.profileEditStates.get(telegramId);
|
||||
if (editState && editState.field === 'city' && editState.tempCity) {
|
||||
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-кнопки
|
||||
try {
|
||||
await this.bot.editMessageReplyMarkup({ inline_keyboard: [] } as any, { chat_id: chatId, message_id: query.message?.message_id });
|
||||
} catch (e) { }
|
||||
await this.bot.sendMessage(chatId, '✅ Город успешно обновлён!');
|
||||
setTimeout(async () => {
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{ text: '✏️ Продолжить редактирование', callback_data: 'edit_profile' },
|
||||
{ text: '👀 Предпросмотр', callback_data: 'preview_profile' }
|
||||
],
|
||||
[{ text: '🏠 Главное меню', callback_data: 'main_menu' }]
|
||||
]
|
||||
};
|
||||
await this.bot.sendMessage(chatId, 'Выберите действие:', { reply_markup: keyboard });
|
||||
}, 500);
|
||||
} else {
|
||||
await this.bot.answerCallbackQuery(query.id, { text: 'Контекст не найден. Повторите, пожалуйста.' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error confirming city edit via callback:', error);
|
||||
await this.bot.answerCallbackQuery(query.id, { text: 'Ошибка при подтверждении города' });
|
||||
}
|
||||
} else if (data === 'edit_city_manual_edit') {
|
||||
try {
|
||||
const editState = this.messageHandlers.profileEditStates.get(telegramId);
|
||||
if (editState && editState.field === 'city') {
|
||||
console.log(`User ${telegramId} chose to re-enter city during edit`);
|
||||
// Очищаем временный город, но оставляем состояние редактирования
|
||||
delete editState.tempCity;
|
||||
try {
|
||||
await this.bot.editMessageReplyMarkup({ inline_keyboard: [] } as any, { chat_id: chatId, message_id: query.message?.message_id });
|
||||
} catch (e) { }
|
||||
await this.bot.sendMessage(chatId, '✏️ Введите название вашего города вручную или отправьте геолокацию:', {
|
||||
reply_markup: {
|
||||
keyboard: [
|
||||
[{ text: '📍 Отправить геолокацию', request_location: true }]
|
||||
],
|
||||
resize_keyboard: true,
|
||||
one_time_keyboard: true
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await this.bot.answerCallbackQuery(query.id, { text: 'Контекст не найден. Повторите, пожалуйста.' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error switching to re-enter city during edit via callback:', error);
|
||||
await this.bot.answerCallbackQuery(query.id, { text: 'Ошибка' });
|
||||
}
|
||||
} else if (data === 'edit_job') {
|
||||
await this.handleEditJob(chatId, telegramId);
|
||||
} else if (data === 'edit_education') {
|
||||
@@ -184,6 +292,12 @@ export class CallbackHandlers {
|
||||
await this.likeBackHandler.handleLikeBack(chatId, telegramId, targetUserId);
|
||||
}
|
||||
|
||||
// Быстрый переход в чат из уведомлений
|
||||
else if (data.startsWith('open_chat:')) {
|
||||
const matchId = data.replace('open_chat:', '');
|
||||
await this.enhancedChatHandlers.openNativeChat(chatId, telegramId, matchId);
|
||||
}
|
||||
|
||||
// Матчи и чаты
|
||||
else if (data === 'view_matches') {
|
||||
await this.handleViewMatches(chatId, telegramId);
|
||||
@@ -952,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';
|
||||
@@ -975,14 +1102,22 @@ export class CallbackHandlers {
|
||||
profileText += '\n📝 ' + (profile.bio || 'Описание не указано') + '\n';
|
||||
|
||||
// Хобби с хэштегами
|
||||
if (profile.hobbies && profile.hobbies.trim()) {
|
||||
const hobbiesArray = profile.hobbies.split(',').map(hobby => hobby.trim()).filter(hobby => hobby);
|
||||
const formattedHobbies = hobbiesArray.map(hobby => '#' + hobby).join(' ');
|
||||
profileText += '\n🎯 ' + formattedHobbies + '\n';
|
||||
if (profile.hobbies) {
|
||||
let hobbiesArray: string[] = [];
|
||||
if (typeof profile.hobbies === 'string') {
|
||||
hobbiesArray = profile.hobbies.split(',').map(hobby => hobby.trim()).filter(hobby => hobby);
|
||||
} else if (Array.isArray(profile.hobbies)) {
|
||||
hobbiesArray = (profile.hobbies as string[]).filter((hobby: string) => hobby && hobby.trim());
|
||||
}
|
||||
|
||||
if (hobbiesArray.length > 0) {
|
||||
const formattedHobbies = hobbiesArray.map(hobby => '#' + hobby).join(' ');
|
||||
profileText += '\n🎯 ' + formattedHobbies + '\n';
|
||||
}
|
||||
}
|
||||
|
||||
if (profile.interests.length > 0) {
|
||||
profileText += '\n<EFBFBD> Интересы: ' + profile.interests.join(', ');
|
||||
profileText += '\n💡 Интересы: ' + profile.interests.join(', ');
|
||||
}
|
||||
|
||||
let keyboard: InlineKeyboardMarkup;
|
||||
@@ -1087,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';
|
||||
@@ -1192,8 +1340,15 @@ export class CallbackHandlers {
|
||||
// Редактирование города
|
||||
async handleEditCity(chatId: number, telegramId: string): Promise<void> {
|
||||
this.messageHandlers.setWaitingForInput(parseInt(telegramId), 'city');
|
||||
await this.bot.sendMessage(chatId, '🏙️ *Введите ваш город:*\n\nНапример: Москва', {
|
||||
parse_mode: 'Markdown'
|
||||
await this.bot.sendMessage(chatId, '🏙️ *Укажите ваш город:*\n\nВыберите один из вариантов:', {
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: {
|
||||
keyboard: [
|
||||
[{ text: '📍 Отправить геолокацию', request_location: true }]
|
||||
],
|
||||
resize_keyboard: true,
|
||||
one_time_keyboard: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1762,10 +1917,18 @@ export class CallbackHandlers {
|
||||
candidateText += '\n📝 ' + (candidate.bio || 'Описание отсутствует') + '\n';
|
||||
|
||||
// Хобби с хэштегами
|
||||
if (candidate.hobbies && candidate.hobbies.trim()) {
|
||||
const hobbiesArray = candidate.hobbies.split(',').map(hobby => hobby.trim()).filter(hobby => hobby);
|
||||
const formattedHobbies = hobbiesArray.map(hobby => '#' + hobby).join(' ');
|
||||
candidateText += '\n🎯 ' + formattedHobbies + '\n';
|
||||
if (candidate.hobbies) {
|
||||
let hobbiesArray: string[] = [];
|
||||
if (typeof candidate.hobbies === 'string') {
|
||||
hobbiesArray = candidate.hobbies.split(',').map(hobby => hobby.trim()).filter(hobby => hobby);
|
||||
} else if (Array.isArray(candidate.hobbies)) {
|
||||
hobbiesArray = (candidate.hobbies as string[]).filter((hobby: string) => hobby && hobby.trim());
|
||||
}
|
||||
|
||||
if (hobbiesArray.length > 0) {
|
||||
const formattedHobbies = hobbiesArray.map(hobby => '#' + hobby).join(' ');
|
||||
candidateText += '\n🎯 ' + formattedHobbies + '\n';
|
||||
}
|
||||
}
|
||||
|
||||
if (candidate.interests.length > 0) {
|
||||
|
||||
@@ -11,9 +11,9 @@ export class EnhancedChatHandlers {
|
||||
|
||||
constructor(bot: TelegramBot) {
|
||||
this.bot = bot;
|
||||
this.chatService = new ChatService();
|
||||
this.profileService = new ProfileService();
|
||||
this.notificationService = new NotificationService(bot);
|
||||
this.chatService = new ChatService(this.notificationService);
|
||||
}
|
||||
|
||||
// ===== НАТИВНЫЙ ИНТЕРФЕЙС ЧАТОВ =====
|
||||
|
||||
@@ -2,6 +2,7 @@ import TelegramBot, { Message, InlineKeyboardMarkup } from 'node-telegram-bot-ap
|
||||
import { ProfileService } from '../services/profileService';
|
||||
import { ChatService } from '../services/chatService';
|
||||
import { EnhancedChatHandlers } from './enhancedChatHandlers';
|
||||
import { KakaoMapService } from '../services/kakaoMapService';
|
||||
|
||||
// Состояния пользователей для создания профилей
|
||||
interface UserState {
|
||||
@@ -19,6 +20,8 @@ interface ChatState {
|
||||
interface ProfileEditState {
|
||||
waitingForInput: boolean;
|
||||
field: string;
|
||||
tempCity?: string; // Временное хранение города для подтверждения
|
||||
tempLocation?: { latitude: number; longitude: number }; // Временное хранение координат
|
||||
}
|
||||
|
||||
export class MessageHandlers {
|
||||
@@ -26,15 +29,27 @@ export class MessageHandlers {
|
||||
private profileService: ProfileService;
|
||||
private chatService: ChatService;
|
||||
private enhancedChatHandlers: EnhancedChatHandlers;
|
||||
private notificationService: any;
|
||||
private kakaoMapService: KakaoMapService | null = null;
|
||||
private userStates: Map<string, UserState> = new Map();
|
||||
private chatStates: Map<string, ChatState> = new Map();
|
||||
private profileEditStates: Map<string, ProfileEditState> = new Map();
|
||||
public profileEditStates: Map<string, ProfileEditState> = new Map();
|
||||
|
||||
constructor(bot: TelegramBot) {
|
||||
constructor(bot: TelegramBot, notificationService?: any) {
|
||||
this.bot = bot;
|
||||
this.profileService = new ProfileService();
|
||||
this.chatService = new ChatService();
|
||||
this.notificationService = notificationService;
|
||||
this.chatService = new ChatService(notificationService);
|
||||
this.enhancedChatHandlers = new EnhancedChatHandlers(bot);
|
||||
|
||||
// Инициализируем Kakao Maps, если есть API ключ
|
||||
const kakaoApiKey = process.env.KAKAO_REST_API_KEY || process.env.KAKAO_MAP_REST_KEY;
|
||||
if (kakaoApiKey) {
|
||||
this.kakaoMapService = new KakaoMapService(kakaoApiKey);
|
||||
console.log('✅ Kakao Maps service initialized');
|
||||
} else {
|
||||
console.warn('⚠️ KAKAO_REST_API_KEY or KAKAO_MAP_REST_KEY not found, location features will be limited');
|
||||
}
|
||||
}
|
||||
|
||||
register(): void {
|
||||
@@ -55,7 +70,7 @@ export class MessageHandlers {
|
||||
const profileEditState = this.profileEditStates.get(userId);
|
||||
|
||||
// Проверяем на нативные чаты (прямые сообщения в контексте чата)
|
||||
if (msg.text && await this.enhancedChatHandlers.handleIncomingChatMessage(msg.chat.id, msg.text)) {
|
||||
if (await this.enhancedChatHandlers.handleIncomingChatMessage(msg, userId)) {
|
||||
return; // Сообщение обработано как сообщение в чате
|
||||
}
|
||||
|
||||
@@ -127,22 +142,68 @@ export class MessageHandlers {
|
||||
userState.data.age = age;
|
||||
userState.step = 'waiting_city';
|
||||
|
||||
await this.bot.sendMessage(chatId, '📍 Прекрасно! В каком городе вы живете?');
|
||||
// Запрашиваем геолокацию или текст
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
'📍 Прекрасно! В каком городе вы живете?\n\n' +
|
||||
'💡 Вы можете:\n' +
|
||||
'• Отправить геолокацию 📍 (кнопка ниже)\n' +
|
||||
'• Написать название города вручную',
|
||||
{
|
||||
reply_markup: {
|
||||
keyboard: [
|
||||
[{ text: '📍 Отправить геолокацию', request_location: true }]
|
||||
],
|
||||
one_time_keyboard: true,
|
||||
resize_keyboard: true
|
||||
}
|
||||
}
|
||||
);
|
||||
break;
|
||||
|
||||
case 'waiting_city':
|
||||
if (!msg.text) {
|
||||
await this.bot.sendMessage(chatId, '❌ Пожалуйста, отправьте название города');
|
||||
// Обработка геолокации
|
||||
if (msg.location) {
|
||||
await this.handleLocationForCity(msg, userId, userState);
|
||||
return;
|
||||
}
|
||||
|
||||
userState.data.city = msg.text.trim();
|
||||
userState.step = 'waiting_bio';
|
||||
// Обработка текста
|
||||
if (!msg.text) {
|
||||
await this.bot.sendMessage(chatId, '❌ Пожалуйста, отправьте название города или геолокацию');
|
||||
return;
|
||||
}
|
||||
|
||||
const cityInput = msg.text.trim();
|
||||
console.log(`User ${userId} entered city manually: ${cityInput}`);
|
||||
|
||||
// Временно сохраняем город и запрашиваем подтверждение
|
||||
userState.data.city = cityInput;
|
||||
userState.step = 'confirm_city';
|
||||
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
'📝 Теперь расскажите немного о себе (био):\n\n' +
|
||||
'💡 Например: хобби, интересы, что ищете в отношениях и т.д.'
|
||||
chatId,
|
||||
`<EFBFBD> Вы указали город: *${cityInput}*\n\n` +
|
||||
'Подтвердите или введите заново.',
|
||||
{
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{ text: '✅ Подтвердить', callback_data: 'confirm_city' },
|
||||
{ text: '✏️ Ввести заново', callback_data: 'edit_city_manual' }
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
);
|
||||
break;
|
||||
|
||||
case 'confirm_city':
|
||||
// Этот случай обрабатывается через callback_data, но на случай если пользователь напишет текст
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
'⚠️ Пожалуйста, используйте кнопки для подтверждения города.'
|
||||
);
|
||||
break;
|
||||
|
||||
@@ -208,6 +269,7 @@ export class MessageHandlers {
|
||||
interestedIn: interestedIn,
|
||||
bio: profileData.bio,
|
||||
city: profileData.city,
|
||||
location: profileData.location, // Добавляем координаты
|
||||
photos: profileData.photos,
|
||||
interests: [],
|
||||
searchPreferences: {
|
||||
@@ -428,6 +490,47 @@ export class MessageHandlers {
|
||||
}
|
||||
value = distance;
|
||||
break;
|
||||
|
||||
case 'hobbies':
|
||||
// Разбиваем строку с запятыми на массив
|
||||
if (typeof value === 'string') {
|
||||
value = value.split(',').map(hobby => hobby.trim()).filter(hobby => hobby.length > 0);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'city':
|
||||
// Обработка города: поддержка геолокации и текстового ввода
|
||||
if (msg.location) {
|
||||
// Обработка геолокации
|
||||
await this.handleLocationForCityEdit(msg, userId);
|
||||
return; // Выходим из функции, так как требуется подтверждение
|
||||
} else if (msg.text) {
|
||||
// Обработка текстового ввода города
|
||||
const cityInput = msg.text.trim();
|
||||
console.log(`User ${userId} entered city manually during edit: ${cityInput}`);
|
||||
// Сохраняем временно в состояние редактирования
|
||||
const editState = this.profileEditStates.get(userId);
|
||||
if (editState) {
|
||||
editState.tempCity = cityInput;
|
||||
}
|
||||
// Требуем подтверждения
|
||||
await this.bot.sendMessage(chatId, `📍 Вы указали город: *${cityInput}*\n\nПодтвердите или введите заново.`, {
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{ text: '✅ Подтвердить', callback_data: 'confirm_city_edit' },
|
||||
{ text: '✏️ Ввести заново', callback_data: 'edit_city_manual_edit' }
|
||||
]
|
||||
]
|
||||
}
|
||||
});
|
||||
return; // Выходим, ждем подтверждения
|
||||
} else {
|
||||
isValid = false;
|
||||
errorMessage = '❌ Пожалуйста, отправьте название города или геолокацию';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
@@ -497,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;
|
||||
@@ -537,4 +644,202 @@ export class MessageHandlers {
|
||||
await this.profileService.updateProfile(profile.userId, updates);
|
||||
}
|
||||
}
|
||||
|
||||
// Обработка геолокации для определения города
|
||||
private async handleLocationForCity(msg: Message, userId: string, userState: UserState): Promise<void> {
|
||||
const chatId = msg.chat.id;
|
||||
|
||||
if (!msg.location) {
|
||||
await this.bot.sendMessage(chatId, '❌ Не удалось получить геолокацию');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Показываем индикатор загрузки
|
||||
await this.bot.sendChatAction(chatId, 'typing');
|
||||
|
||||
if (!this.kakaoMapService) {
|
||||
// Если Kakao Maps не настроен, используем координаты как есть
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
'⚠️ Автоматическое определение города недоступно.\n' +
|
||||
'Пожалуйста, введите название вашего города вручную.',
|
||||
{ reply_markup: { remove_keyboard: true } }
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Показываем индикатор загрузки
|
||||
await this.bot.sendChatAction(chatId, 'typing');
|
||||
|
||||
if (!this.kakaoMapService) {
|
||||
// Если Kakao Maps не настроен, используем координаты как есть
|
||||
console.warn(`KakaoMaps not configured - user ${userId} sent coords: ${msg.location.latitude},${msg.location.longitude}`);
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
'⚠️ Автоматическое определение города недоступно.\n' +
|
||||
'Пожалуйста, введите название вашего города вручную.',
|
||||
{ reply_markup: { remove_keyboard: true } }
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Логируем входные координаты
|
||||
console.log(`Processing coordinates for user ${userId}: lat=${msg.location.latitude}, lon=${msg.location.longitude}`);
|
||||
|
||||
// Получаем адрес через Kakao Maps
|
||||
const address = await this.kakaoMapService.getAddressFromCoordinates(
|
||||
msg.location.latitude,
|
||||
msg.location.longitude
|
||||
);
|
||||
|
||||
if (!address) {
|
||||
console.warn(`KakaoMaps returned no address for user ${userId} coords: ${msg.location.latitude},${msg.location.longitude}`);
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
'❌ Не удалось определить город по вашей геолокации.\n' +
|
||||
'Пожалуйста, введите название города вручную.',
|
||||
{ reply_markup: { remove_keyboard: true } }
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Получаем название города для сохранения
|
||||
const cityName = this.kakaoMapService.getCityNameForProfile(address);
|
||||
const displayAddress = this.kakaoMapService.formatAddressForDisplay(address);
|
||||
|
||||
// Логируем результат
|
||||
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';
|
||||
|
||||
// Отправляем пользователю информацию с кнопками подтверждения
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
`✅ Мы распознали местоположение: *${displayAddress}*\n\n` +
|
||||
'Пожалуйста, подтвердите город проживания или введите название вручную.',
|
||||
{
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{ text: '✅ Подтвердить', callback_data: 'confirm_city' },
|
||||
{ text: '✏️ Ввести вручную', callback_data: 'edit_city_manual' }
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
);
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
'📝 Теперь расскажите немного о себе (био):\n\n' +
|
||||
'💡 Например: хобби, интересы, что ищете в отношениях и т.д.'
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error handling location for city:', error);
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
'❌ Произошла ошибка при определении города.\n' +
|
||||
'Пожалуйста, введите название города вручную.',
|
||||
{ reply_markup: { remove_keyboard: true } }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Обработка геолокации для определения города при редактировании профиля
|
||||
private async handleLocationForCityEdit(msg: Message, userId: string): Promise<void> {
|
||||
const chatId = msg.chat.id;
|
||||
|
||||
if (!msg.location) {
|
||||
await this.bot.sendMessage(chatId, '❌ Не удалось получить геолокацию');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Показываем индикатор загрузки
|
||||
await this.bot.sendChatAction(chatId, 'typing');
|
||||
|
||||
if (!this.kakaoMapService) {
|
||||
console.warn(`KakaoMaps not configured - user ${userId} sent coords during edit: ${msg.location.latitude},${msg.location.longitude}`);
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
'⚠️ Автоматическое определение города недоступно.\n' +
|
||||
'Пожалуйста, введите название вашего города вручную.',
|
||||
{ reply_markup: { remove_keyboard: true } }
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Логируем входные координаты
|
||||
console.log(`Processing coordinates for user ${userId} during edit: lat=${msg.location.latitude}, lon=${msg.location.longitude}`);
|
||||
|
||||
// Получаем адрес через Kakao Maps
|
||||
const address = await this.kakaoMapService.getAddressFromCoordinates(
|
||||
msg.location.latitude,
|
||||
msg.location.longitude
|
||||
);
|
||||
|
||||
if (!address) {
|
||||
console.warn(`KakaoMaps returned no address for user ${userId} during edit coords: ${msg.location.latitude},${msg.location.longitude}`);
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
'❌ Не удалось определить город по вашей геолокации.\n' +
|
||||
'Пожалуйста, введите название города вручную.',
|
||||
{ reply_markup: { remove_keyboard: true } }
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Получаем название города для сохранения
|
||||
const cityName = this.kakaoMapService.getCityNameForProfile(address);
|
||||
const displayAddress = this.kakaoMapService.formatAddressForDisplay(address);
|
||||
|
||||
// Логируем результат
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
// Отправляем пользователю информацию с кнопками подтверждения
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
`✅ Мы распознали местоположение: *${displayAddress}*\n\n` +
|
||||
'Пожалуйста, подтвердите город проживания или введите название вручную.',
|
||||
{
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{ text: '✅ Подтвердить', callback_data: 'confirm_city_edit' },
|
||||
{ text: '✏️ Ввести вручную', callback_data: 'edit_city_manual_edit' }
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error handling location for city during edit:', error);
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
'❌ Произошла ошибка при определении города.\n' +
|
||||
'Пожалуйста, введите название города вручную.',
|
||||
{ reply_markup: { remove_keyboard: true } }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,11 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export class ChatService {
|
||||
private profileService: ProfileService;
|
||||
private notificationService: any; // Добавим позже правильный тип
|
||||
|
||||
constructor() {
|
||||
constructor(notificationService?: any) {
|
||||
this.profileService = new ProfileService();
|
||||
this.notificationService = notificationService;
|
||||
}
|
||||
|
||||
// Получить все чаты (матчи) пользователя
|
||||
@@ -149,7 +151,7 @@ export class ChatService {
|
||||
}
|
||||
|
||||
const row = messageResult.rows[0];
|
||||
return new Message({
|
||||
const message = new Message({
|
||||
id: row.id,
|
||||
matchId: row.match_id,
|
||||
senderId: row.sender_id,
|
||||
@@ -158,6 +160,26 @@ export class ChatService {
|
||||
isRead: row.is_read,
|
||||
createdAt: new Date(row.created_at)
|
||||
});
|
||||
|
||||
// Определяем получателя (второго участника матча)
|
||||
const match = matchResult.rows[0];
|
||||
const receiverId = match.user_id_1 === senderId ? match.user_id_2 : match.user_id_1;
|
||||
|
||||
// Отправляем уведомление получателю о новом сообщении
|
||||
if (this.notificationService) {
|
||||
try {
|
||||
await this.notificationService.sendMessageNotification(
|
||||
receiverId,
|
||||
senderId,
|
||||
content,
|
||||
matchId
|
||||
);
|
||||
} catch (notifError) {
|
||||
console.error('Error sending message notification:', notifError);
|
||||
}
|
||||
}
|
||||
|
||||
return message;
|
||||
} catch (error) {
|
||||
console.error('Error sending message:', error);
|
||||
return null;
|
||||
|
||||
162
src/services/kakaoMapService.ts
Normal file
162
src/services/kakaoMapService.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import axios from 'axios';
|
||||
|
||||
export interface KakaoCoordinates {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}
|
||||
|
||||
export interface KakaoAddress {
|
||||
addressName: string; // Полный адрес
|
||||
region1: string; // Область (예: 서울특별시)
|
||||
region2: string; // Район (예: 강남구)
|
||||
region3: string; // Подрайон (예: 역삼동)
|
||||
city?: string; // Город (обработанный)
|
||||
}
|
||||
|
||||
export class KakaoMapService {
|
||||
private readonly apiKey: string;
|
||||
private readonly baseUrl = 'https://dapi.kakao.com/v2/local';
|
||||
|
||||
constructor(apiKey: string) {
|
||||
if (!apiKey) {
|
||||
throw new Error('Kakao Maps API key is required');
|
||||
}
|
||||
this.apiKey = apiKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить адрес по координатам (Reverse Geocoding)
|
||||
*/
|
||||
async getAddressFromCoordinates(latitude: number, longitude: number): Promise<KakaoAddress | null> {
|
||||
try {
|
||||
const response = await axios.get(`${this.baseUrl}/geo/coord2address.json`, {
|
||||
headers: {
|
||||
'Authorization': `KakaoAK ${this.apiKey}`
|
||||
},
|
||||
params: {
|
||||
x: longitude,
|
||||
y: latitude,
|
||||
input_coord: 'WGS84'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data && response.data.documents && response.data.documents.length > 0) {
|
||||
const doc = response.data.documents[0];
|
||||
const address = doc.address || doc.road_address;
|
||||
|
||||
if (!address) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Извлекаем город из региона
|
||||
const city = this.extractCity(address.region_1depth_name, address.region_2depth_name);
|
||||
|
||||
return {
|
||||
addressName: address.address_name || '',
|
||||
region1: address.region_1depth_name || '',
|
||||
region2: address.region_2depth_name || '',
|
||||
region3: address.region_3depth_name || '',
|
||||
city
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Error getting address from Kakao Maps:', error);
|
||||
if (axios.isAxiosError(error)) {
|
||||
console.error('Response:', error.response?.data);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Извлечь название города из регионов
|
||||
* Примеры:
|
||||
* - 서울특별시 → Seoul
|
||||
* - 경기도 수원시 → Suwon
|
||||
* - 부산광역시 → Busan
|
||||
*/
|
||||
private extractCity(region1: string, region2: string): string {
|
||||
// Убираем суффиксы типов административных единиц
|
||||
const cleanRegion1 = region1
|
||||
.replace('특별시', '') // Особый город (Сеул)
|
||||
.replace('광역시', '') // Метрополитен
|
||||
.replace('특별자치시', '') // Особый автономный город
|
||||
.replace('도', '') // Провинция
|
||||
.trim();
|
||||
|
||||
const cleanRegion2 = region2
|
||||
.replace('시', '') // Город
|
||||
.replace('군', '') // Округ
|
||||
.trim();
|
||||
|
||||
// Если region1 - это город (Сеул, Пусан, и т.д.), возвращаем его
|
||||
const specialCities = ['서울', '부산', '대구', '인천', '광주', '대전', '울산', '세종'];
|
||||
if (specialCities.some(city => region1.includes(city))) {
|
||||
return this.translateCityName(cleanRegion1);
|
||||
}
|
||||
|
||||
// Иначе возвращаем region2 (город в провинции)
|
||||
if (cleanRegion2) {
|
||||
return this.translateCityName(cleanRegion2);
|
||||
}
|
||||
|
||||
// Если не удалось определить, возвращаем очищенный region1
|
||||
return this.translateCityName(cleanRegion1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Перевод названий городов на английский/латиницу
|
||||
*/
|
||||
private translateCityName(koreanName: string): string {
|
||||
const cityMap: { [key: string]: string } = {
|
||||
'서울': 'Seoul',
|
||||
'부산': 'Busan',
|
||||
'대구': 'Daegu',
|
||||
'인천': 'Incheon',
|
||||
'광주': 'Gwangju',
|
||||
'대전': 'Daejeon',
|
||||
'울산': 'Ulsan',
|
||||
'세종': 'Sejong',
|
||||
'수원': 'Suwon',
|
||||
'성남': 'Seongnam',
|
||||
'고양': 'Goyang',
|
||||
'용인': 'Yongin',
|
||||
'창원': 'Changwon',
|
||||
'청주': 'Cheongju',
|
||||
'전주': 'Jeonju',
|
||||
'천안': 'Cheonan',
|
||||
'안산': 'Ansan',
|
||||
'안양': 'Anyang',
|
||||
'제주': 'Jeju',
|
||||
'평택': 'Pyeongtaek',
|
||||
'화성': 'Hwaseong',
|
||||
'포항': 'Pohang',
|
||||
'진주': 'Jinju',
|
||||
'강릉': 'Gangneung',
|
||||
'김해': 'Gimhae',
|
||||
'춘천': 'Chuncheon',
|
||||
'원주': 'Wonju'
|
||||
};
|
||||
|
||||
return cityMap[koreanName] || koreanName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Форматировать адрес для отображения пользователю
|
||||
*/
|
||||
formatAddressForDisplay(address: KakaoAddress): string {
|
||||
if (address.city) {
|
||||
return `${address.city}, ${address.region1}`;
|
||||
}
|
||||
return `${address.region2}, ${address.region1}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить краткое название города для сохранения в профиле
|
||||
*/
|
||||
getCityNameForProfile(address: KakaoAddress): string {
|
||||
return address.city || address.region2;
|
||||
}
|
||||
}
|
||||
@@ -427,15 +427,20 @@ export class MatchingService {
|
||||
const userId = userProfile.userId;
|
||||
|
||||
// Определяем, каким должен быть пол показываемых профилей
|
||||
let targetGender: string;
|
||||
let targetGender: string | null;
|
||||
let genderCondition: string;
|
||||
|
||||
if (userProfile.interestedIn === 'male' || userProfile.interestedIn === 'female') {
|
||||
// Конкретный пол
|
||||
targetGender = userProfile.interestedIn;
|
||||
genderCondition = `AND p.gender = '${targetGender}'`;
|
||||
} else {
|
||||
// Если "both" или другое значение, показываем противоположный пол
|
||||
targetGender = userProfile.gender === 'male' ? 'female' : 'male';
|
||||
// Если "both" - показываем всех кроме своего пола
|
||||
targetGender = null;
|
||||
genderCondition = `AND p.gender != '${userProfile.gender}'`;
|
||||
}
|
||||
|
||||
console.log(`[DEBUG] Определен целевой пол для поиска: ${targetGender}`);
|
||||
console.log(`[DEBUG] Определен целевой пол для поиска: ${targetGender || 'any (except self)'}, условие: ${genderCondition}`);
|
||||
|
||||
// Получаем список просмотренных профилей из новой таблицы profile_views
|
||||
// и добавляем также профили из свайпов для полной совместимости
|
||||
@@ -490,7 +495,7 @@ export class MatchingService {
|
||||
FROM profiles p
|
||||
JOIN users u ON p.user_id = u.id
|
||||
WHERE p.is_visible = true
|
||||
AND p.gender = '${targetGender}'
|
||||
${genderCondition}
|
||||
${excludeCondition}
|
||||
`;
|
||||
|
||||
@@ -502,14 +507,14 @@ export class MatchingService {
|
||||
console.log(`[DEBUG] Найдено ${availableProfilesCount} доступных профилей`);
|
||||
|
||||
// Используем определенный ранее targetGender для поиска
|
||||
console.log(`[DEBUG] Поиск кандидата для gender=${targetGender}, возраст: ${userProfile.searchPreferences.minAge}-${userProfile.searchPreferences.maxAge}`);
|
||||
console.log(`[DEBUG] Поиск кандидата для gender=${targetGender || 'any'}, возраст: ${userProfile.searchPreferences.minAge}-${userProfile.searchPreferences.maxAge}`);
|
||||
|
||||
const candidateQuery = `
|
||||
SELECT p.*, u.telegram_id, u.username, u.first_name, u.last_name
|
||||
FROM profiles p
|
||||
JOIN users u ON p.user_id = u.id
|
||||
WHERE p.is_visible = true
|
||||
AND p.gender = '${targetGender}'
|
||||
${genderCondition}
|
||||
AND p.age BETWEEN ${userProfile.searchPreferences.minAge} AND ${userProfile.searchPreferences.maxAge}
|
||||
${excludeCondition}
|
||||
ORDER BY RANDOM()
|
||||
|
||||
@@ -1118,10 +1118,10 @@ export class NotificationService {
|
||||
|
||||
const lastMessageTime = new Date(result.rows[0].created_at);
|
||||
const now = new Date();
|
||||
const hoursSinceLastMessage = (now.getTime() - lastMessageTime.getTime()) / (1000 * 60 * 60);
|
||||
const secondsSinceLastMessage = (now.getTime() - lastMessageTime.getTime()) / 1000;
|
||||
|
||||
// Считаем активным если последнее сообщение было менее 10 минут назад
|
||||
return hoursSinceLastMessage < (10 / 60);
|
||||
// Считаем активным если последнее сообщение было менее 30 секунд назад
|
||||
return secondsSinceLastMessage < 30;
|
||||
} catch (error) {
|
||||
console.error('Error checking user activity:', error);
|
||||
return false;
|
||||
|
||||
@@ -46,17 +46,38 @@ export class ProfileService {
|
||||
updatedAt: now
|
||||
});
|
||||
|
||||
// Сохранение в базу данных
|
||||
// Сохранение в базу данных (UPSERT для избежания конфликта с триггером)
|
||||
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,
|
||||
gender = EXCLUDED.gender,
|
||||
interested_in = EXCLUDED.interested_in,
|
||||
bio = EXCLUDED.bio,
|
||||
photos = EXCLUDED.photos,
|
||||
city = EXCLUDED.city,
|
||||
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,
|
||||
is_visible = EXCLUDED.is_visible,
|
||||
updated_at = EXCLUDED.updated_at
|
||||
`, [
|
||||
profileId, userId, profile.name, profile.age, profile.gender, profile.interestedIn,
|
||||
profile.bio, JSON.stringify(profile.photos), profile.city, profile.education, profile.job,
|
||||
profile.height, profile.religion, profile.datingGoal,
|
||||
profile.bio, profile.photos, profile.city, profile.education, profile.job,
|
||||
profile.height,
|
||||
profile.location?.latitude || null,
|
||||
profile.location?.longitude || null,
|
||||
profile.religion, profile.datingGoal,
|
||||
profile.isVerified, profile.isVisible, profile.createdAt, profile.updatedAt
|
||||
]);
|
||||
|
||||
@@ -168,13 +189,20 @@ export class ProfileService {
|
||||
switch (key) {
|
||||
case 'photos':
|
||||
case 'interests':
|
||||
case 'hobbies':
|
||||
updateFields.push(`${this.camelToSnake(key)} = $${paramIndex++}`);
|
||||
// Для PostgreSQL массивы должны быть преобразованы в JSON-строку
|
||||
updateValues.push(JSON.stringify(value));
|
||||
// PostgreSQL принимает нативные массивы
|
||||
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 больше не хранятся в БД, пропускаем
|
||||
@@ -458,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,
|
||||
|
||||
59
src/services/userLanguageService.ts
Normal file
59
src/services/userLanguageService.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { query } from '../database/connection';
|
||||
|
||||
export class UserLanguageService {
|
||||
// Получить язык пользователя
|
||||
async getUserLanguage(telegramId: string): Promise<string> {
|
||||
try {
|
||||
const result = await query(
|
||||
'SELECT language FROM users WHERE telegram_id = $1',
|
||||
[telegramId]
|
||||
);
|
||||
|
||||
if (result.rows.length > 0) {
|
||||
return result.rows[0].language || 'ru';
|
||||
}
|
||||
|
||||
return 'ru'; // Язык по умолчанию
|
||||
} catch (error) {
|
||||
console.error('Error getting user language:', error);
|
||||
return 'ru';
|
||||
}
|
||||
}
|
||||
|
||||
// Установить язык пользователя
|
||||
async setUserLanguage(telegramId: string, language: string): Promise<boolean> {
|
||||
try {
|
||||
await query(
|
||||
'UPDATE users SET language = $1 WHERE telegram_id = $2',
|
||||
[language, telegramId]
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error setting user language:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Получить поддерживаемые языки
|
||||
getSupportedLanguages(): { [key: string]: string } {
|
||||
return {
|
||||
'ru': '🇷🇺 Русский',
|
||||
'en': '🇺🇸 English',
|
||||
'es': '🇪🇸 Español',
|
||||
'fr': '🇫🇷 Français',
|
||||
'de': '🇩🇪 Deutsch',
|
||||
'it': '🇮🇹 Italiano',
|
||||
'pt': '🇵🇹 Português',
|
||||
'zh': '🇨🇳 中文',
|
||||
'ja': '🇯🇵 日本語',
|
||||
'ko': '🇰🇷 한국어',
|
||||
'uz': '🇺🇿 O\'zbekcha',
|
||||
'kk': '🇰🇿 Қазақша'
|
||||
};
|
||||
}
|
||||
|
||||
// Проверить, поддерживается ли язык
|
||||
isLanguageSupported(language: string): boolean {
|
||||
return Object.keys(this.getSupportedLanguages()).includes(language);
|
||||
}
|
||||
}
|
||||
241
start.bat
241
start.bat
@@ -1,241 +0,0 @@
|
||||
@echo off
|
||||
:: start.bat - Скрипт для запуска Telegram Tinder Bot на Windows
|
||||
:: Позволяет выбрать между локальной БД в контейнере или внешней БД
|
||||
|
||||
echo ================================================
|
||||
echo Запуск Telegram Tinder Bot
|
||||
echo ================================================
|
||||
|
||||
:: Проверка наличия Docker и Docker Compose
|
||||
WHERE docker >nul 2>&1
|
||||
IF %ERRORLEVEL% NEQ 0 (
|
||||
echo [31mОШИБКА: Docker не установлен![0m
|
||||
echo Установите Docker Desktop для Windows: https://docs.docker.com/desktop/install/windows-install/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
:: Проверяем наличие .env файла
|
||||
IF NOT EXIST .env (
|
||||
echo [33mФайл .env не найден. Создаем из шаблона...[0m
|
||||
IF EXIST .env.example (
|
||||
copy .env.example .env
|
||||
echo [32mФайл .env создан из шаблона. Пожалуйста, отредактируйте его с вашими настройками.[0m
|
||||
) ELSE (
|
||||
echo [31mОШИБКА: Файл .env.example не найден. Создайте файл .env вручную.[0m
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
|
||||
:: Спрашиваем про запуск базы данных
|
||||
set /p use_container_db="Запустить базу данных PostgreSQL в контейнере? (y/n): "
|
||||
|
||||
:: Функции для работы с docker-compose
|
||||
IF /I "%use_container_db%" NEQ "y" (
|
||||
:: Запрашиваем параметры подключения к внешней БД
|
||||
echo [36mВведите параметры подключения к внешней базе данных:[0m
|
||||
set /p db_host="Хост (например, localhost): "
|
||||
set /p db_port="Порт (например, 5432): "
|
||||
set /p db_name="Имя базы данных: "
|
||||
set /p db_user="Имя пользователя: "
|
||||
set /p db_password="Пароль: "
|
||||
|
||||
:: Модифицируем docker-compose.yml
|
||||
echo [33mМодифицируем docker-compose.yml для работы с внешней базой данных...[0m
|
||||
|
||||
:: Сохраняем оригинальную версию файла
|
||||
copy docker-compose.yml docker-compose.yml.bak
|
||||
|
||||
:: Создаем временный файл с модифицированным содержимым
|
||||
(
|
||||
echo version: '3.8'
|
||||
echo.
|
||||
echo services:
|
||||
echo bot:
|
||||
echo build: .
|
||||
echo container_name: telegram-tinder-bot
|
||||
echo restart: unless-stopped
|
||||
echo env_file: .env
|
||||
echo environment:
|
||||
echo - NODE_ENV=production
|
||||
echo - DB_HOST=%db_host%
|
||||
echo - DB_PORT=%db_port%
|
||||
echo - DB_NAME=%db_name%
|
||||
echo - DB_USERNAME=%db_user%
|
||||
echo - DB_PASSWORD=%db_password%
|
||||
echo volumes:
|
||||
echo - ./uploads:/app/uploads
|
||||
echo - ./logs:/app/logs
|
||||
echo networks:
|
||||
echo - bot-network
|
||||
echo healthcheck:
|
||||
echo test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health"]
|
||||
echo interval: 30s
|
||||
echo timeout: 5s
|
||||
echo retries: 3
|
||||
echo start_period: 10s
|
||||
echo.
|
||||
echo adminer:
|
||||
echo image: adminer:latest
|
||||
echo container_name: adminer-tinder
|
||||
echo restart: unless-stopped
|
||||
echo ports:
|
||||
echo - "8080:8080"
|
||||
echo networks:
|
||||
echo - bot-network
|
||||
echo.
|
||||
echo volumes:
|
||||
echo postgres_data:
|
||||
echo.
|
||||
echo networks:
|
||||
echo bot-network:
|
||||
echo driver: bridge
|
||||
) > docker-compose.temp.yml
|
||||
|
||||
:: Заменяем оригинальный файл
|
||||
move /y docker-compose.temp.yml docker-compose.yml
|
||||
|
||||
echo [32mdocker-compose.yml обновлен для работы с внешней базой данных[0m
|
||||
|
||||
:: Обновляем .env файл
|
||||
echo [33mОбновляем файл .env с параметрами внешней базы данных...[0m
|
||||
|
||||
:: Создаем временный файл
|
||||
type NUL > .env.temp
|
||||
|
||||
:: Читаем .env построчно и заменяем нужные строки
|
||||
for /f "tokens=*" %%a in (.env) do (
|
||||
set line=%%a
|
||||
set line=!line:DB_HOST=*!
|
||||
if "!line:~0,1!" == "*" (
|
||||
echo DB_HOST=%db_host%>> .env.temp
|
||||
) else (
|
||||
set line=!line:DB_PORT=*!
|
||||
if "!line:~0,1!" == "*" (
|
||||
echo DB_PORT=%db_port%>> .env.temp
|
||||
) else (
|
||||
set line=!line:DB_NAME=*!
|
||||
if "!line:~0,1!" == "*" (
|
||||
echo DB_NAME=%db_name%>> .env.temp
|
||||
) else (
|
||||
set line=!line:DB_USERNAME=*!
|
||||
if "!line:~0,1!" == "*" (
|
||||
echo DB_USERNAME=%db_user%>> .env.temp
|
||||
) else (
|
||||
set line=!line:DB_PASSWORD=*!
|
||||
if "!line:~0,1!" == "*" (
|
||||
echo DB_PASSWORD=%db_password%>> .env.temp
|
||||
) else (
|
||||
echo %%a>> .env.temp
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
:: Заменяем оригинальный файл
|
||||
move /y .env.temp .env
|
||||
|
||||
echo [32mФайл .env обновлен с параметрами внешней базы данных[0m
|
||||
|
||||
:: Запускаем только контейнер с ботом
|
||||
echo [36mЗапускаем Telegram Bot без контейнера базы данных...[0m
|
||||
docker-compose up -d bot adminer
|
||||
|
||||
echo [32mБот запущен и использует внешнюю базу данных: %db_host%:%db_port%/%db_name%[0m
|
||||
echo [33mAdminer доступен по адресу: http://localhost:8080/[0m
|
||||
echo [33mДанные для входа в Adminer:[0m
|
||||
echo [33mСистема: PostgreSQL[0m
|
||||
echo [33mСервер: %db_host%[0m
|
||||
echo [33mПользователь: %db_user%[0m
|
||||
echo [33mПароль: (введенный вами)[0m
|
||||
echo [33mБаза данных: %db_name%[0m
|
||||
) ELSE (
|
||||
:: Восстанавливаем оригинальный docker-compose.yml если есть бэкап
|
||||
if exist docker-compose.yml.bak (
|
||||
move /y docker-compose.yml.bak docker-compose.yml
|
||||
echo [32mdocker-compose.yml восстановлен из резервной копии[0m
|
||||
)
|
||||
|
||||
echo [36mЗапускаем Telegram Bot с контейнером базы данных...[0m
|
||||
|
||||
:: Проверка, запущены ли контейнеры
|
||||
docker ps -q -f name=telegram-tinder-bot > tmp_containers.txt
|
||||
set /p containers=<tmp_containers.txt
|
||||
del tmp_containers.txt
|
||||
|
||||
if not "%containers%" == "" (
|
||||
set /p restart_containers="Контейнеры уже запущены. Перезапустить? (y/n): "
|
||||
if /I "%restart_containers%" == "y" (
|
||||
docker-compose down
|
||||
docker-compose up -d
|
||||
echo [32mКонтейнеры перезапущены[0m
|
||||
) else (
|
||||
echo [36mПродолжаем работу с уже запущенными контейнерами[0m
|
||||
)
|
||||
) else (
|
||||
docker-compose up -d
|
||||
echo [32mКонтейнеры запущены[0m
|
||||
)
|
||||
|
||||
:: Проверка наличия пароля для БД в .env
|
||||
findstr /C:"DB_PASSWORD=" .env > tmp_db_password.txt
|
||||
set /p db_password_line=<tmp_db_password.txt
|
||||
del tmp_db_password.txt
|
||||
|
||||
:: Проверяем, есть ли значение после DB_PASSWORD=
|
||||
for /f "tokens=2 delims==" %%i in ("%db_password_line%") do (
|
||||
set db_password=%%i
|
||||
)
|
||||
|
||||
if "%db_password%" == "" (
|
||||
:: Генерируем случайный пароль
|
||||
set chars=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789
|
||||
set random_password=
|
||||
for /L %%i in (1,1,16) do (
|
||||
set /a random_index=!random! %% 62
|
||||
for %%j in (!random_index!) do set random_password=!random_password!!chars:~%%j,1!
|
||||
)
|
||||
|
||||
:: Обновляем .env файл с новым паролем
|
||||
:: Создаем временный файл
|
||||
type NUL > .env.temp
|
||||
|
||||
:: Читаем .env построчно и заменяем строку с паролем
|
||||
for /f "tokens=*" %%a in (.env) do (
|
||||
set line=%%a
|
||||
set line=!line:DB_PASSWORD=*!
|
||||
if "!line:~0,1!" == "*" (
|
||||
echo DB_PASSWORD=%random_password%>> .env.temp
|
||||
) else (
|
||||
echo %%a>> .env.temp
|
||||
)
|
||||
)
|
||||
|
||||
:: Заменяем оригинальный файл
|
||||
move /y .env.temp .env
|
||||
|
||||
echo [33mСгенерирован случайный пароль для базы данных и сохранен в .env[0m
|
||||
)
|
||||
|
||||
echo [32mTelegram Bot запущен с локальной базой данных[0m
|
||||
echo [33mAdminer доступен по адресу: http://localhost:8080/[0m
|
||||
echo [33mДанные для входа в Adminer:[0m
|
||||
echo [33mСистема: PostgreSQL[0m
|
||||
echo [33mСервер: db[0m
|
||||
echo [33mПользователь: postgres[0m
|
||||
echo [33mПароль: (из переменной DB_PASSWORD в .env)[0m
|
||||
echo [33mБаза данных: telegram_tinder_bot[0m
|
||||
)
|
||||
|
||||
:: Проверка статуса контейнеров
|
||||
echo [36mПроверка статуса контейнеров:[0m
|
||||
docker-compose ps
|
||||
|
||||
echo ================================================
|
||||
echo [32mПроцесс запуска Telegram Tinder Bot завершен![0m
|
||||
echo ================================================
|
||||
echo [33mДля просмотра логов используйте: docker-compose logs -f bot[0m
|
||||
echo [33mДля остановки: docker-compose down[0m
|
||||
|
||||
pause
|
||||
229
start.ps1
229
start.ps1
@@ -1,229 +0,0 @@
|
||||
function createModifiedDockerCompose {
|
||||
param (
|
||||
[string]$dbHost,
|
||||
[string]$dbPort,
|
||||
[string]$dbName,
|
||||
[string]$dbUser,
|
||||
[string]$dbPassword
|
||||
)
|
||||
|
||||
$dockerComposeContent = @"
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
bot:
|
||||
build: .
|
||||
container_name: telegram-tinder-bot
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- DB_HOST=$dbHost
|
||||
- DB_PORT=$dbPort
|
||||
- DB_NAME=$dbName
|
||||
- DB_USERNAME=$dbUser
|
||||
- DB_PASSWORD=$dbPassword
|
||||
volumes:
|
||||
- ./uploads:/app/uploads
|
||||
- ./logs:/app/logs
|
||||
networks:
|
||||
- bot-network
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
|
||||
adminer:
|
||||
image: adminer:latest
|
||||
container_name: adminer-tinder
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:8080"
|
||||
networks:
|
||||
- bot-network
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
||||
networks:
|
||||
bot-network:
|
||||
driver: bridge
|
||||
"@
|
||||
|
||||
return $dockerComposeContent
|
||||
}
|
||||
|
||||
function restoreDockerCompose {
|
||||
if (Test-Path -Path "docker-compose.yml.bak") {
|
||||
Copy-Item -Path "docker-compose.yml.bak" -Destination "docker-compose.yml" -Force
|
||||
Write-Host "docker-compose.yml восстановлен из резервной копии" -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
|
||||
function updateEnvFile {
|
||||
param (
|
||||
[string]$dbHost,
|
||||
[string]$dbPort,
|
||||
[string]$dbName,
|
||||
[string]$dbUser,
|
||||
[string]$dbPassword
|
||||
)
|
||||
|
||||
$envContent = Get-Content -Path ".env" -Raw
|
||||
|
||||
$envContent = $envContent -replace "DB_HOST=.*", "DB_HOST=$dbHost"
|
||||
$envContent = $envContent -replace "DB_PORT=.*", "DB_PORT=$dbPort"
|
||||
$envContent = $envContent -replace "DB_NAME=.*", "DB_NAME=$dbName"
|
||||
$envContent = $envContent -replace "DB_USERNAME=.*", "DB_USERNAME=$dbUser"
|
||||
$envContent = $envContent -replace "DB_PASSWORD=.*", "DB_PASSWORD=$dbPassword"
|
||||
|
||||
Set-Content -Path ".env" -Value $envContent
|
||||
|
||||
Write-Host "Файл .env обновлен с параметрами внешней базы данных" -ForegroundColor Green
|
||||
}
|
||||
|
||||
function generateRandomPassword {
|
||||
$length = 16
|
||||
$chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
$bytes = New-Object Byte[] $length
|
||||
$rng = [System.Security.Cryptography.RNGCryptoServiceProvider]::new()
|
||||
$rng.GetBytes($bytes)
|
||||
|
||||
$password = ""
|
||||
for ($i = 0; $i -lt $length; $i++) {
|
||||
$password += $chars[$bytes[$i] % $chars.Length]
|
||||
}
|
||||
|
||||
return $password
|
||||
}
|
||||
|
||||
# Начало основного скрипта
|
||||
Write-Host "==================================================" -ForegroundColor Cyan
|
||||
Write-Host " Запуск Telegram Tinder Bot" -ForegroundColor Cyan
|
||||
Write-Host "==================================================" -ForegroundColor Cyan
|
||||
|
||||
# Проверка наличия Docker
|
||||
try {
|
||||
docker --version | Out-Null
|
||||
} catch {
|
||||
Write-Host "ОШИБКА: Docker не установлен!" -ForegroundColor Red
|
||||
Write-Host "Установите Docker Desktop для Windows: https://docs.docker.com/desktop/install/windows-install/"
|
||||
exit
|
||||
}
|
||||
|
||||
# Проверяем наличие .env файла
|
||||
if (-not (Test-Path -Path ".env")) {
|
||||
Write-Host "Файл .env не найден. Создаем из шаблона..." -ForegroundColor Yellow
|
||||
|
||||
if (Test-Path -Path ".env.example") {
|
||||
Copy-Item -Path ".env.example" -Destination ".env"
|
||||
Write-Host "Файл .env создан из шаблона. Пожалуйста, отредактируйте его с вашими настройками." -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "ОШИБКА: Файл .env.example не найден. Создайте файл .env вручную." -ForegroundColor Red
|
||||
exit
|
||||
}
|
||||
}
|
||||
|
||||
# Спрашиваем про запуск базы данных
|
||||
$useContainerDb = Read-Host "Запустить базу данных PostgreSQL в контейнере? (y/n)"
|
||||
|
||||
if ($useContainerDb -ne "y") {
|
||||
# Запрашиваем параметры подключения к внешней БД
|
||||
Write-Host "Введите параметры подключения к внешней базе данных:" -ForegroundColor Cyan
|
||||
$dbHost = Read-Host "Хост (например, localhost)"
|
||||
$dbPort = Read-Host "Порт (например, 5432)"
|
||||
$dbName = Read-Host "Имя базы данных"
|
||||
$dbUser = Read-Host "Имя пользователя"
|
||||
$dbPassword = Read-Host "Пароль" -AsSecureString
|
||||
$dbPasswordText = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($dbPassword))
|
||||
|
||||
# Модифицируем docker-compose.yml
|
||||
Write-Host "Модифицируем docker-compose.yml для работы с внешней базой данных..." -ForegroundColor Yellow
|
||||
|
||||
# Сохраняем оригинальную версию файла
|
||||
Copy-Item -Path "docker-compose.yml" -Destination "docker-compose.yml.bak" -Force
|
||||
|
||||
# Создаем модифицированный docker-compose.yml
|
||||
$dockerComposeContent = createModifiedDockerCompose -dbHost $dbHost -dbPort $dbPort -dbName $dbName -dbUser $dbUser -dbPassword $dbPasswordText
|
||||
Set-Content -Path "docker-compose.yml" -Value $dockerComposeContent
|
||||
|
||||
Write-Host "docker-compose.yml обновлен для работы с внешней базой данных" -ForegroundColor Green
|
||||
|
||||
# Обновляем .env файл
|
||||
Write-Host "Обновляем файл .env с параметрами внешней базы данных..." -ForegroundColor Yellow
|
||||
updateEnvFile -dbHost $dbHost -dbPort $dbPort -dbName $dbName -dbUser $dbUser -dbPassword $dbPasswordText
|
||||
|
||||
# Запускаем только контейнер с ботом
|
||||
Write-Host "Запускаем Telegram Bot без контейнера базы данных..." -ForegroundColor Cyan
|
||||
docker-compose up -d bot adminer
|
||||
|
||||
Write-Host "Бот запущен и использует внешнюю базу данных: $dbHost`:$dbPort/$dbName" -ForegroundColor Green
|
||||
Write-Host "Adminer доступен по адресу: http://localhost:8080/" -ForegroundColor Yellow
|
||||
Write-Host "Данные для входа в Adminer:" -ForegroundColor Yellow
|
||||
Write-Host "Система: PostgreSQL" -ForegroundColor Yellow
|
||||
Write-Host "Сервер: $dbHost" -ForegroundColor Yellow
|
||||
Write-Host "Пользователь: $dbUser" -ForegroundColor Yellow
|
||||
Write-Host "Пароль: (введенный вами)" -ForegroundColor Yellow
|
||||
Write-Host "База данных: $dbName" -ForegroundColor Yellow
|
||||
} else {
|
||||
# Восстанавливаем оригинальный docker-compose.yml если есть бэкап
|
||||
restoreDockerCompose
|
||||
|
||||
Write-Host "Запускаем Telegram Bot с контейнером базы данных..." -ForegroundColor Cyan
|
||||
|
||||
# Проверка, запущены ли контейнеры
|
||||
$containers = docker ps -q -f name=telegram-tinder-bot -f name=postgres-tinder
|
||||
|
||||
if ($containers) {
|
||||
$restartContainers = Read-Host "Контейнеры уже запущены. Перезапустить? (y/n)"
|
||||
if ($restartContainers -eq "y") {
|
||||
docker-compose down
|
||||
docker-compose up -d
|
||||
Write-Host "Контейнеры перезапущены" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "Продолжаем работу с уже запущенными контейнерами" -ForegroundColor Cyan
|
||||
}
|
||||
} else {
|
||||
docker-compose up -d
|
||||
Write-Host "Контейнеры запущены" -ForegroundColor Green
|
||||
}
|
||||
|
||||
# Проверка наличия пароля для БД в .env
|
||||
$envContent = Get-Content -Path ".env" -Raw
|
||||
$match = [Regex]::Match($envContent, "DB_PASSWORD=(.*)(\r?\n|$)")
|
||||
$dbPassword = if ($match.Success) { $match.Groups[1].Value.Trim() } else { "" }
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($dbPassword)) {
|
||||
# Генерируем случайный пароль
|
||||
$randomPassword = generateRandomPassword
|
||||
|
||||
# Обновляем .env файл
|
||||
$envContent = $envContent -replace "DB_PASSWORD=.*", "DB_PASSWORD=$randomPassword"
|
||||
Set-Content -Path ".env" -Value $envContent
|
||||
|
||||
Write-Host "Сгенерирован случайный пароль для базы данных и сохранен в .env" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
Write-Host "Telegram Bot запущен с локальной базой данных" -ForegroundColor Green
|
||||
Write-Host "Adminer доступен по адресу: http://localhost:8080/" -ForegroundColor Yellow
|
||||
Write-Host "Данные для входа в Adminer:" -ForegroundColor Yellow
|
||||
Write-Host "Система: PostgreSQL" -ForegroundColor Yellow
|
||||
Write-Host "Сервер: db" -ForegroundColor Yellow
|
||||
Write-Host "Пользователь: postgres" -ForegroundColor Yellow
|
||||
Write-Host "Пароль: (из переменной DB_PASSWORD в .env)" -ForegroundColor Yellow
|
||||
Write-Host "База данных: telegram_tinder_bot" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# Проверка статуса контейнеров
|
||||
Write-Host "Проверка статуса контейнеров:" -ForegroundColor Cyan
|
||||
docker-compose ps
|
||||
|
||||
Write-Host "==================================================" -ForegroundColor Cyan
|
||||
Write-Host "Процесс запуска Telegram Tinder Bot завершен!" -ForegroundColor Green
|
||||
Write-Host "==================================================" -ForegroundColor Cyan
|
||||
Write-Host "Для просмотра логов используйте: docker-compose logs -f bot" -ForegroundColor Yellow
|
||||
Write-Host "Для остановки: docker-compose down" -ForegroundColor Yellow
|
||||
|
||||
Read-Host "Нажмите Enter для выхода"
|
||||
225
start.sh
225
start.sh
@@ -1,225 +0,0 @@
|
||||
#!/bin/bash
|
||||
# start.sh - Скрипт для запуска Telegram Tinder Bot
|
||||
# Позволяет выбрать между локальной БД в контейнере или внешней БД
|
||||
|
||||
# Цвета для вывода
|
||||
GREEN='\033[0;32m'
|
||||
BLUE='\033[0;34m'
|
||||
YELLOW='\033[0;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${BLUE}==================================================${NC}"
|
||||
echo -e "${BLUE} Запуск Telegram Tinder Bot ${NC}"
|
||||
echo -e "${BLUE}==================================================${NC}"
|
||||
|
||||
# Проверка наличия Docker и Docker Compose
|
||||
if ! command -v docker &> /dev/null || ! command -v docker-compose &> /dev/null; then
|
||||
echo -e "${RED}ОШИБКА: Docker и/или Docker Compose не установлены!${NC}"
|
||||
echo -e "Для установки Docker следуйте инструкции на: https://docs.docker.com/get-docker/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Проверяем наличие .env файла
|
||||
if [ ! -f .env ]; then
|
||||
echo -e "${YELLOW}Файл .env не найден. Создаем из шаблона...${NC}"
|
||||
if [ -f .env.example ]; then
|
||||
cp .env.example .env
|
||||
echo -e "${GREEN}Файл .env создан из шаблона. Пожалуйста, отредактируйте его с вашими настройками.${NC}"
|
||||
else
|
||||
echo -e "${RED}ОШИБКА: Файл .env.example не найден. Создайте файл .env вручную.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Проверяем и исправляем проблему с командой сборки в Dockerfile
|
||||
echo -e "${YELLOW}Проверка конфигурации Dockerfile...${NC}"
|
||||
if grep -q "RUN npm run build" Dockerfile && ! grep -q "RUN npm run build:linux" Dockerfile; then
|
||||
echo -e "${YELLOW}⚠️ Исправление команды сборки в Dockerfile для совместимости с Linux...${NC}"
|
||||
sed -i "s/RUN npm run build/RUN npm run build:linux/g" Dockerfile
|
||||
echo -e "${GREEN}✅ Dockerfile обновлен для использования команды сборки совместимой с Linux${NC}"
|
||||
fi
|
||||
|
||||
# Спрашиваем про запуск базы данных
|
||||
read -p "Запустить базу данных PostgreSQL в контейнере? (y/n): " use_container_db
|
||||
|
||||
# Функция для изменения docker-compose.yml
|
||||
modify_docker_compose() {
|
||||
local host=$1
|
||||
local port=$2
|
||||
local user=$3
|
||||
local password=$4
|
||||
local db_name=$5
|
||||
|
||||
echo -e "${YELLOW}Модифицируем docker-compose.yml для работы с внешней базой данных...${NC}"
|
||||
|
||||
# Сохраняем оригинальную версию файла
|
||||
cp docker-compose.yml docker-compose.yml.bak
|
||||
|
||||
# Создаем временный файл с модифицированным содержимым
|
||||
cat > docker-compose.temp.yml << EOL
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
bot:
|
||||
build: .
|
||||
container_name: telegram-tinder-bot
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- DB_HOST=${host}
|
||||
- DB_PORT=${port}
|
||||
- DB_NAME=${db_name}
|
||||
- DB_USERNAME=${user}
|
||||
- DB_PASSWORD=${password}
|
||||
volumes:
|
||||
- ./uploads:/app/uploads
|
||||
- ./logs:/app/logs
|
||||
networks:
|
||||
- bot-network
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
|
||||
adminer:
|
||||
image: adminer:latest
|
||||
container_name: adminer-tinder
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:8080"
|
||||
networks:
|
||||
- bot-network
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
||||
networks:
|
||||
bot-network:
|
||||
driver: bridge
|
||||
EOL
|
||||
|
||||
# Заменяем оригинальный файл
|
||||
mv docker-compose.temp.yml docker-compose.yml
|
||||
|
||||
echo -e "${GREEN}docker-compose.yml обновлен для работы с внешней базой данных${NC}"
|
||||
}
|
||||
|
||||
# Функция для восстановления docker-compose.yml
|
||||
restore_docker_compose() {
|
||||
if [ -f docker-compose.yml.bak ]; then
|
||||
mv docker-compose.yml.bak docker-compose.yml
|
||||
echo -e "${GREEN}docker-compose.yml восстановлен из резервной копии${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Обработка выбора
|
||||
if [[ "$use_container_db" =~ ^[Nn]$ ]]; then
|
||||
# Запрашиваем параметры подключения к внешней БД
|
||||
echo -e "${BLUE}Введите параметры подключения к внешней базе данных:${NC}"
|
||||
read -p "Хост (например, localhost): " db_host
|
||||
read -p "Порт (например, 5432): " db_port
|
||||
read -p "Имя базы данных: " db_name
|
||||
read -p "Имя пользователя: " db_user
|
||||
read -p "Пароль: " db_password
|
||||
|
||||
# Модифицируем docker-compose.yml
|
||||
modify_docker_compose "$db_host" "$db_port" "$db_user" "$db_password" "$db_name"
|
||||
|
||||
# Обновляем .env файл
|
||||
echo -e "${YELLOW}Обновляем файл .env с параметрами внешней базы данных...${NC}"
|
||||
|
||||
# Используем sed для замены переменных в .env
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
# macOS требует другой синтаксис для sed
|
||||
sed -i '' "s/DB_HOST=.*/DB_HOST=${db_host}/" .env
|
||||
sed -i '' "s/DB_PORT=.*/DB_PORT=${db_port}/" .env
|
||||
sed -i '' "s/DB_NAME=.*/DB_NAME=${db_name}/" .env
|
||||
sed -i '' "s/DB_USERNAME=.*/DB_USERNAME=${db_user}/" .env
|
||||
sed -i '' "s/DB_PASSWORD=.*/DB_PASSWORD=${db_password}/" .env
|
||||
else
|
||||
# Linux и другие системы
|
||||
sed -i "s/DB_HOST=.*/DB_HOST=${db_host}/" .env
|
||||
sed -i "s/DB_PORT=.*/DB_PORT=${db_port}/" .env
|
||||
sed -i "s/DB_NAME=.*/DB_NAME=${db_name}/" .env
|
||||
sed -i "s/DB_USERNAME=.*/DB_USERNAME=${db_user}/" .env
|
||||
sed -i "s/DB_PASSWORD=.*/DB_PASSWORD=${db_password}/" .env
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}Файл .env обновлен с параметрами внешней базы данных${NC}"
|
||||
|
||||
# Запускаем только контейнер с ботом
|
||||
echo -e "${BLUE}Запускаем Telegram Bot без контейнера базы данных...${NC}"
|
||||
docker-compose up -d bot adminer
|
||||
|
||||
echo -e "${GREEN}Бот запущен и использует внешнюю базу данных: ${db_host}:${db_port}/${db_name}${NC}"
|
||||
echo -e "${YELLOW}Adminer доступен по адресу: http://localhost:8080/${NC}"
|
||||
echo -e "${YELLOW}Данные для входа в Adminer:${NC}"
|
||||
echo -e "${YELLOW}Система: PostgreSQL${NC}"
|
||||
echo -e "${YELLOW}Сервер: ${db_host}${NC}"
|
||||
echo -e "${YELLOW}Пользователь: ${db_user}${NC}"
|
||||
echo -e "${YELLOW}Пароль: (введенный вами)${NC}"
|
||||
echo -e "${YELLOW}База данных: ${db_name}${NC}"
|
||||
else
|
||||
# Восстанавливаем оригинальный docker-compose.yml если есть бэкап
|
||||
restore_docker_compose
|
||||
|
||||
echo -e "${BLUE}Запускаем Telegram Bot с контейнером базы данных...${NC}"
|
||||
|
||||
# Проверка, запущены ли контейнеры
|
||||
containers=$(docker ps -q -f name=telegram-tinder-bot -f name=postgres-tinder)
|
||||
if [ -n "$containers" ]; then
|
||||
echo -e "${YELLOW}Контейнеры уже запущены. Перезапустить? (y/n): ${NC}"
|
||||
read restart_containers
|
||||
if [[ "$restart_containers" =~ ^[Yy]$ ]]; then
|
||||
docker-compose down
|
||||
docker-compose up -d
|
||||
echo -e "${GREEN}Контейнеры перезапущены${NC}"
|
||||
else
|
||||
echo -e "${BLUE}Продолжаем работу с уже запущенными контейнерами${NC}"
|
||||
fi
|
||||
else
|
||||
docker-compose up -d
|
||||
echo -e "${GREEN}Контейнеры запущены${NC}"
|
||||
fi
|
||||
|
||||
# Проверка наличия пароля для БД в .env
|
||||
db_password=$(grep DB_PASSWORD .env | cut -d '=' -f2)
|
||||
if [ -z "$db_password" ]; then
|
||||
# Генерируем случайный пароль
|
||||
random_password=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 16)
|
||||
|
||||
# Обновляем .env файл
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
# macOS требует другой синтаксис для sed
|
||||
sed -i '' "s/DB_PASSWORD=.*/DB_PASSWORD=${random_password}/" .env
|
||||
else
|
||||
# Linux и другие системы
|
||||
sed -i "s/DB_PASSWORD=.*/DB_PASSWORD=${random_password}/" .env
|
||||
fi
|
||||
|
||||
echo -e "${YELLOW}Сгенерирован случайный пароль для базы данных и сохранен в .env${NC}"
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}Telegram Bot запущен с локальной базой данных${NC}"
|
||||
echo -e "${YELLOW}Adminer доступен по адресу: http://localhost:8080/${NC}"
|
||||
echo -e "${YELLOW}Данные для входа в Adminer:${NC}"
|
||||
echo -e "${YELLOW}Система: PostgreSQL${NC}"
|
||||
echo -e "${YELLOW}Сервер: db${NC}"
|
||||
echo -e "${YELLOW}Пользователь: postgres${NC}"
|
||||
echo -e "${YELLOW}Пароль: (из переменной DB_PASSWORD в .env)${NC}"
|
||||
echo -e "${YELLOW}База данных: telegram_tinder_bot${NC}"
|
||||
fi
|
||||
|
||||
# Проверка статуса контейнеров
|
||||
echo -e "${BLUE}Проверка статуса контейнеров:${NC}"
|
||||
docker-compose ps
|
||||
|
||||
echo -e "${BLUE}==================================================${NC}"
|
||||
echo -e "${GREEN}Процесс запуска Telegram Tinder Bot завершен!${NC}"
|
||||
echo -e "${BLUE}==================================================${NC}"
|
||||
echo -e "${YELLOW}Для просмотра логов используйте: docker-compose logs -f bot${NC}"
|
||||
echo -e "${YELLOW}Для остановки: docker-compose down${NC}"
|
||||
152
temp_migrations/1758144488937_initial-schema.js
Normal file
152
temp_migrations/1758144488937_initial-schema.js
Normal file
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
|
||||
*/
|
||||
export const shorthands = undefined;
|
||||
|
||||
/**
|
||||
* @param pgm {import('node-pg-migrate').MigrationBuilder}
|
||||
* @param run {() => void | undefined}
|
||||
* @returns {Promise<void> | void}
|
||||
*/
|
||||
export const up = (pgm) => {
|
||||
// Создание расширения для генерации UUID
|
||||
pgm.createExtension('pgcrypto', { ifNotExists: true });
|
||||
|
||||
// Таблица пользователей
|
||||
pgm.createTable('users', {
|
||||
id: { type: 'uuid', primaryKey: true, default: pgm.func('gen_random_uuid()') },
|
||||
telegram_id: { type: 'bigint', notNull: true, unique: true },
|
||||
username: { type: 'varchar(255)' },
|
||||
first_name: { type: 'varchar(255)' },
|
||||
last_name: { type: 'varchar(255)' },
|
||||
language_code: { type: 'varchar(10)', default: 'en' },
|
||||
is_active: { type: 'boolean', default: true },
|
||||
created_at: { type: 'timestamp', default: pgm.func('NOW()') },
|
||||
last_active_at: { type: 'timestamp', default: pgm.func('NOW()') },
|
||||
updated_at: { type: 'timestamp', default: pgm.func('NOW()') },
|
||||
premium: { type: 'boolean', default: false },
|
||||
premium_expires_at: { type: 'timestamp' }
|
||||
});
|
||||
|
||||
// Таблица профилей
|
||||
pgm.createTable('profiles', {
|
||||
id: { type: 'uuid', primaryKey: true, default: pgm.func('gen_random_uuid()') },
|
||||
user_id: { type: 'uuid', references: 'users(id)', onDelete: 'CASCADE' },
|
||||
name: { type: 'varchar(255)', notNull: true },
|
||||
age: {
|
||||
type: 'integer',
|
||||
notNull: true,
|
||||
check: 'age >= 18 AND age <= 100'
|
||||
},
|
||||
gender: {
|
||||
type: 'varchar(10)',
|
||||
notNull: true,
|
||||
check: "gender IN ('male', 'female', 'other')"
|
||||
},
|
||||
interested_in: {
|
||||
type: 'varchar(10)',
|
||||
notNull: true,
|
||||
check: "interested_in IN ('male', 'female', 'both')"
|
||||
},
|
||||
looking_for: {
|
||||
type: 'varchar(20)',
|
||||
default: 'both',
|
||||
check: "looking_for IN ('male', 'female', 'both')"
|
||||
},
|
||||
bio: { type: 'text' },
|
||||
photos: { type: 'jsonb', default: '[]' },
|
||||
interests: { type: 'jsonb', default: '[]' },
|
||||
city: { type: 'varchar(255)' },
|
||||
education: { type: 'varchar(255)' },
|
||||
job: { type: 'varchar(255)' },
|
||||
height: { type: 'integer' },
|
||||
religion: { type: 'varchar(255)' },
|
||||
dating_goal: { type: 'varchar(255)' },
|
||||
smoking: { type: 'boolean' },
|
||||
drinking: { type: 'boolean' },
|
||||
has_kids: { type: 'boolean' },
|
||||
location_lat: { type: 'decimal(10,8)' },
|
||||
location_lon: { type: 'decimal(11,8)' },
|
||||
search_min_age: { type: 'integer', default: 18 },
|
||||
search_max_age: { type: 'integer', default: 50 },
|
||||
search_max_distance: { type: 'integer', default: 50 },
|
||||
is_verified: { type: 'boolean', default: false },
|
||||
is_visible: { type: 'boolean', default: true },
|
||||
created_at: { type: 'timestamp', default: pgm.func('NOW()') },
|
||||
updated_at: { type: 'timestamp', default: pgm.func('NOW()') }
|
||||
});
|
||||
|
||||
// Таблица свайпов
|
||||
pgm.createTable('swipes', {
|
||||
id: { type: 'uuid', primaryKey: true, default: pgm.func('gen_random_uuid()') },
|
||||
user_id: { type: 'uuid', references: 'users(id)', onDelete: 'CASCADE' },
|
||||
target_user_id: { type: 'uuid', references: 'users(id)', onDelete: 'CASCADE' },
|
||||
type: {
|
||||
type: 'varchar(20)',
|
||||
notNull: true,
|
||||
check: "type IN ('like', 'pass', 'superlike')"
|
||||
},
|
||||
created_at: { type: 'timestamp', default: pgm.func('NOW()') },
|
||||
is_match: { type: 'boolean', default: false }
|
||||
});
|
||||
pgm.addConstraint('swipes', 'unique_swipe', {
|
||||
unique: ['user_id', 'target_user_id']
|
||||
});
|
||||
|
||||
// Таблица матчей
|
||||
pgm.createTable('matches', {
|
||||
id: { type: 'uuid', primaryKey: true, default: pgm.func('gen_random_uuid()') },
|
||||
user_id_1: { type: 'uuid', references: 'users(id)', onDelete: 'CASCADE' },
|
||||
user_id_2: { type: 'uuid', references: 'users(id)', onDelete: 'CASCADE' },
|
||||
created_at: { type: 'timestamp', default: pgm.func('NOW()') },
|
||||
last_message_at: { type: 'timestamp' },
|
||||
is_active: { type: 'boolean', default: true },
|
||||
is_super_match: { type: 'boolean', default: false },
|
||||
unread_count_1: { type: 'integer', default: 0 },
|
||||
unread_count_2: { type: 'integer', default: 0 }
|
||||
});
|
||||
pgm.addConstraint('matches', 'unique_match', {
|
||||
unique: ['user_id_1', 'user_id_2']
|
||||
});
|
||||
|
||||
// Таблица сообщений
|
||||
pgm.createTable('messages', {
|
||||
id: { type: 'uuid', primaryKey: true, default: pgm.func('gen_random_uuid()') },
|
||||
match_id: { type: 'uuid', references: 'matches(id)', onDelete: 'CASCADE' },
|
||||
sender_id: { type: 'uuid', references: 'users(id)', onDelete: 'CASCADE' },
|
||||
receiver_id: { type: 'uuid', references: 'users(id)', onDelete: 'CASCADE' },
|
||||
content: { type: 'text', notNull: true },
|
||||
message_type: {
|
||||
type: 'varchar(20)',
|
||||
default: 'text',
|
||||
check: "message_type IN ('text', 'photo', 'gif', 'sticker')"
|
||||
},
|
||||
created_at: { type: 'timestamp', default: pgm.func('NOW()') },
|
||||
is_read: { type: 'boolean', default: false }
|
||||
});
|
||||
|
||||
// Создание индексов
|
||||
pgm.createIndex('users', 'telegram_id');
|
||||
pgm.createIndex('profiles', 'user_id');
|
||||
pgm.createIndex('profiles', ['location_lat', 'location_lon'], {
|
||||
where: 'location_lat IS NOT NULL AND location_lon IS NOT NULL'
|
||||
});
|
||||
pgm.createIndex('profiles', ['age', 'gender', 'interested_in']);
|
||||
pgm.createIndex('swipes', ['user_id', 'target_user_id']);
|
||||
pgm.createIndex('matches', ['user_id_1', 'user_id_2']);
|
||||
pgm.createIndex('messages', ['match_id', 'created_at']);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param pgm {import('node-pg-migrate').MigrationBuilder}
|
||||
* @param run {() => void | undefined}
|
||||
* @returns {Promise<void> | void}
|
||||
*/
|
||||
export const down = (pgm) => {
|
||||
pgm.dropTable('messages');
|
||||
pgm.dropTable('matches');
|
||||
pgm.dropTable('swipes');
|
||||
pgm.dropTable('profiles');
|
||||
pgm.dropTable('users');
|
||||
pgm.dropExtension('pgcrypto');
|
||||
};
|
||||
25
temp_migrations/1758144618548_add-missing-profile-columns.js
Normal file
25
temp_migrations/1758144618548_add-missing-profile-columns.js
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
|
||||
*/
|
||||
export const shorthands = undefined;
|
||||
|
||||
/**
|
||||
* @param pgm {import('node-pg-migrate').MigrationBuilder}
|
||||
* @param run {() => void | undefined}
|
||||
* @returns {Promise<void> | void}
|
||||
*/
|
||||
export const up = (pgm) => {
|
||||
// Добавляем колонки, которые могли быть пропущены в схеме
|
||||
pgm.addColumns('profiles', {
|
||||
hobbies: { type: 'text' }
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param pgm {import('node-pg-migrate').MigrationBuilder}
|
||||
* @param run {() => void | undefined}
|
||||
* @returns {Promise<void> | void}
|
||||
*/
|
||||
export const down = (pgm) => {
|
||||
pgm.dropColumns('profiles', ['hobbies']);
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
|
||||
*/
|
||||
export const shorthands = undefined;
|
||||
|
||||
/**
|
||||
* @param pgm {import('node-pg-migrate').MigrationBuilder}
|
||||
* @param run {() => void | undefined}
|
||||
* @returns {Promise<void> | void}
|
||||
*/
|
||||
export const up = (pgm) => {};
|
||||
|
||||
/**
|
||||
* @param pgm {import('node-pg-migrate').MigrationBuilder}
|
||||
* @param run {() => void | undefined}
|
||||
* @returns {Promise<void> | void}
|
||||
*/
|
||||
export const down = (pgm) => {};
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
|
||||
*/
|
||||
export const shorthands = undefined;
|
||||
|
||||
/**
|
||||
* @param pgm {import('node-pg-migrate').MigrationBuilder}
|
||||
* @param run {() => void | undefined}
|
||||
* @returns {Promise<void> | void}
|
||||
*/
|
||||
export const up = (pgm) => {
|
||||
// Добавляем отсутствующие колонки в таблицу profiles
|
||||
pgm.addColumns('profiles', {
|
||||
religion: { type: 'varchar(255)' },
|
||||
dating_goal: { type: 'varchar(255)' },
|
||||
smoking: { type: 'boolean' },
|
||||
drinking: { type: 'boolean' },
|
||||
has_kids: { type: 'boolean' }
|
||||
}, { ifNotExists: true });
|
||||
};
|
||||
|
||||
/**
|
||||
* @param pgm {import('node-pg-migrate').MigrationBuilder}
|
||||
* @param run {() => void | undefined}
|
||||
* @returns {Promise<void> | void}
|
||||
*/
|
||||
export const down = (pgm) => {
|
||||
pgm.dropColumns('profiles', ['religion', 'dating_goal', 'smoking', 'drinking', 'has_kids'], { ifExists: true });
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
|
||||
*/
|
||||
export const shorthands = undefined;
|
||||
|
||||
/**
|
||||
* @param pgm {import('node-pg-migrate').MigrationBuilder}
|
||||
* @param run {() => void | undefined}
|
||||
* @returns {Promise<void> | void}
|
||||
*/
|
||||
export const up = (pgm) => {};
|
||||
|
||||
/**
|
||||
* @param pgm {import('node-pg-migrate').MigrationBuilder}
|
||||
* @param run {() => void | undefined}
|
||||
* @returns {Promise<void> | void}
|
||||
*/
|
||||
export const down = (pgm) => {};
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
|
||||
*/
|
||||
export const shorthands = undefined;
|
||||
|
||||
/**
|
||||
* @param pgm {import('node-pg-migrate').MigrationBuilder}
|
||||
* @param run {() => void | undefined}
|
||||
* @returns {Promise<void> | void}
|
||||
*/
|
||||
export const up = (pgm) => {};
|
||||
|
||||
/**
|
||||
* @param pgm {import('node-pg-migrate').MigrationBuilder}
|
||||
* @param run {() => void | undefined}
|
||||
* @returns {Promise<void> | void}
|
||||
*/
|
||||
export const down = (pgm) => {};
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
|
||||
*/
|
||||
export const shorthands = undefined;
|
||||
|
||||
/**
|
||||
* @param pgm {import('node-pg-migrate').MigrationBuilder}
|
||||
* @param run {() => void | undefined}
|
||||
* @returns {Promise<void> | void}
|
||||
*/
|
||||
export const up = (pgm) => {
|
||||
// Изменяем тип столбцов с boolean на varchar для хранения строковых значений
|
||||
pgm.alterColumn('profiles', 'smoking', {
|
||||
type: 'varchar(50)',
|
||||
using: 'smoking::text'
|
||||
});
|
||||
|
||||
pgm.alterColumn('profiles', 'drinking', {
|
||||
type: 'varchar(50)',
|
||||
using: 'drinking::text'
|
||||
});
|
||||
|
||||
// has_kids оставляем boolean, так как у него всего два состояния
|
||||
};
|
||||
|
||||
/**
|
||||
* @param pgm {import('node-pg-migrate').MigrationBuilder}
|
||||
* @param run {() => void | undefined}
|
||||
* @returns {Promise<void> | void}
|
||||
*/
|
||||
export const down = (pgm) => {
|
||||
// Возвращаем столбцы к типу boolean
|
||||
pgm.alterColumn('profiles', 'smoking', {
|
||||
type: 'boolean',
|
||||
using: "CASE WHEN smoking = 'regularly' OR smoking = 'sometimes' THEN true ELSE false END"
|
||||
});
|
||||
|
||||
pgm.alterColumn('profiles', 'drinking', {
|
||||
type: 'boolean',
|
||||
using: "CASE WHEN drinking = 'regularly' OR drinking = 'sometimes' THEN true ELSE false END"
|
||||
});
|
||||
};
|
||||
50
temp_migrations/1758149087361_add-column-synonyms.js
Normal file
50
temp_migrations/1758149087361_add-column-synonyms.js
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
|
||||
*/
|
||||
export const shorthands = undefined;
|
||||
|
||||
/**
|
||||
* @param pgm {import('node-pg-migrate').MigrationBuilder}
|
||||
* @param run {() => void | undefined}
|
||||
* @returns {Promise<void> | void}
|
||||
*/
|
||||
export const up = (pgm) => {
|
||||
// Создание представления для совместимости со старым кодом (swipes)
|
||||
pgm.sql(`
|
||||
CREATE OR REPLACE VIEW swipes_view AS
|
||||
SELECT
|
||||
id,
|
||||
user_id AS swiper_id,
|
||||
target_user_id AS swiped_id,
|
||||
type AS direction,
|
||||
created_at,
|
||||
is_match
|
||||
FROM swipes;
|
||||
`);
|
||||
|
||||
// Создание представления для совместимости со старым кодом (matches)
|
||||
pgm.sql(`
|
||||
CREATE OR REPLACE VIEW matches_view AS
|
||||
SELECT
|
||||
id,
|
||||
user_id_1 AS user1_id,
|
||||
user_id_2 AS user2_id,
|
||||
created_at AS matched_at,
|
||||
is_active AS status,
|
||||
last_message_at,
|
||||
is_super_match,
|
||||
unread_count_1,
|
||||
unread_count_2
|
||||
FROM matches;
|
||||
`);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param pgm {import('node-pg-migrate').MigrationBuilder}
|
||||
* @param run {() => void | undefined}
|
||||
* @returns {Promise<void> | void}
|
||||
*/
|
||||
export const down = (pgm) => {
|
||||
pgm.sql(`DROP VIEW IF EXISTS swipes_view;`);
|
||||
pgm.sql(`DROP VIEW IF EXISTS matches_view;`);
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
exports.shorthands = undefined;
|
||||
|
||||
exports.up = pgm => {
|
||||
// Проверяем существование таблицы scheduled_notifications
|
||||
pgm.sql(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_name = 'scheduled_notifications'
|
||||
) THEN
|
||||
-- Проверяем, нет ли уже столбца processed
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'scheduled_notifications' AND column_name = 'processed'
|
||||
) THEN
|
||||
-- Добавляем столбец processed
|
||||
ALTER TABLE scheduled_notifications ADD COLUMN processed BOOLEAN DEFAULT FALSE;
|
||||
END IF;
|
||||
ELSE
|
||||
-- Создаем таблицу, если она не существует
|
||||
CREATE TABLE scheduled_notifications (
|
||||
id UUID PRIMARY KEY,
|
||||
user_id UUID REFERENCES users(id),
|
||||
type VARCHAR(50) NOT NULL,
|
||||
data JSONB,
|
||||
scheduled_at TIMESTAMP NOT NULL,
|
||||
processed BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
`);
|
||||
};
|
||||
|
||||
exports.down = pgm => {
|
||||
pgm.sql(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'scheduled_notifications' AND column_name = 'processed'
|
||||
) THEN
|
||||
ALTER TABLE scheduled_notifications DROP COLUMN processed;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
`);
|
||||
};
|
||||
Reference in New Issue
Block a user