geo detection
This commit is contained in:
@@ -46,6 +46,13 @@ JWT_SECRET=your_jwt_secret_here
|
|||||||
# Encryption key for sensitive data
|
# Encryption key for sensitive data
|
||||||
ENCRYPTION_KEY=your_encryption_key_here
|
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 ===
|
# === ADVANCED SETTINGS ===
|
||||||
|
|
||||||
# Notification check interval in milliseconds (default: 60000 - 1 minute)
|
# Notification check interval in milliseconds (default: 60000 - 1 minute)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ COPY src/ ./src/
|
|||||||
COPY .env.example ./
|
COPY .env.example ./
|
||||||
|
|
||||||
# Build the application (using Linux-compatible build command)
|
# Build the application (using Linux-compatible build command)
|
||||||
RUN npm run build:linux:linux
|
RUN npm run build:linux
|
||||||
|
|
||||||
# Production stage
|
# Production stage
|
||||||
FROM node:18-alpine AS production
|
FROM node:18-alpine AS production
|
||||||
|
|||||||
53
Makefile
53
Makefile
@@ -21,9 +21,20 @@ help:
|
|||||||
|
|
||||||
install:
|
install:
|
||||||
@echo "Установка зависимостей..."
|
@echo "Установка зависимостей..."
|
||||||
@if ! command -v docker &> /dev/null || ! command -v docker-compose &> /dev/null; then \
|
@if ! command -v docker &> /dev/null; then \
|
||||||
echo "Установка Docker..."; \
|
echo "Установка Docker..."; \
|
||||||
sudo apt update && sudo apt install -y docker.io docker-compose; \
|
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
|
fi
|
||||||
@if [ ! -f .env ]; then \
|
@if [ ! -f .env ]; then \
|
||||||
echo "Создание .env файла..."; \
|
echo "Создание .env файла..."; \
|
||||||
@@ -37,6 +48,14 @@ update:
|
|||||||
@git fetch --all
|
@git fetch --all
|
||||||
@git pull origin main || git pull origin master || echo "Не удалось обновить код"
|
@git pull origin main || git pull origin master || echo "Не удалось обновить код"
|
||||||
@if [ -f package.json ]; then npm ci || npm install; fi
|
@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:
|
run:
|
||||||
@echo "Запуск бота..."
|
@echo "Запуск бота..."
|
||||||
@@ -47,18 +66,26 @@ run:
|
|||||||
@echo "Бот запущен! Для просмотра логов: docker-compose logs -f"
|
@echo "Бот запущен! Для просмотра логов: docker-compose logs -f"
|
||||||
|
|
||||||
migrate:
|
migrate:
|
||||||
@echo "Применение миграций..."
|
@echo "Применение миграций к базе данных..."
|
||||||
@if [ -d migrations ]; then \
|
@if [ ! -f .env ]; then \
|
||||||
mkdir -p temp_migrations; \
|
echo "❌ Файл .env не найден! Создайте его перед применением миграций."; \
|
||||||
find migrations -name "*.js" -exec cp {} temp_migrations/ \; 2>/dev/null || true; \
|
exit 1; \
|
||||||
DATABASE_URL=postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME} \
|
|
||||||
npx node-pg-migrate up --migrations-dir=migrations || true; \
|
|
||||||
fi
|
|
||||||
@if [ -d sql ]; then \
|
|
||||||
for sql_file in sql/*.sql; do \
|
|
||||||
[ -f "$${sql_file}" ] && docker-compose exec -T db psql -U ${DB_USERNAME} -d ${DB_NAME} -f "/app/$${sql_file}" || true; \
|
|
||||||
done; \
|
|
||||||
fi
|
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:
|
fix-docker:
|
||||||
@echo "Исправление Docker конфигурации..."
|
@echo "Исправление Docker конфигурации..."
|
||||||
|
|||||||
0
bin/apply_direct_sql.sh
Executable file → Normal file
0
bin/apply_direct_sql.sh
Executable file → Normal file
0
bin/apply_migrations.sh
Executable file → Normal file
0
bin/apply_migrations.sh
Executable file → Normal file
0
bin/backup_db.sh
Executable file → Normal file
0
bin/backup_db.sh
Executable file → Normal file
0
bin/compile_ts_migrations.sh
Executable file → Normal file
0
bin/compile_ts_migrations.sh
Executable file → Normal file
0
bin/create_consolidated_migration.sh
Executable file → Normal file
0
bin/create_consolidated_migration.sh
Executable file → Normal file
0
bin/create_release.sh
Executable file → Normal file
0
bin/create_release.sh
Executable file → Normal file
0
bin/fix_docker.sh
Executable file → Normal file
0
bin/fix_docker.sh
Executable file → Normal file
0
bin/fix_line_endings.sh
Executable file → Normal file
0
bin/fix_line_endings.sh
Executable file → Normal file
0
bin/fix_permissions.sh
Executable file → Normal file
0
bin/fix_permissions.sh
Executable file → Normal file
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!"
|
||||||
0
bin/install_docker.sh
Executable file → Normal file
0
bin/install_docker.sh
Executable file → Normal file
0
bin/install_ubuntu.sh
Executable file → Normal file
0
bin/install_ubuntu.sh
Executable file → Normal file
0
bin/run_full_migration.sh
Executable file → Normal file
0
bin/run_full_migration.sh
Executable file → Normal file
0
bin/run_sql_migrations.sh
Executable file → Normal file
0
bin/run_sql_migrations.sh
Executable file → Normal file
0
bin/setup.sh
Executable file → Normal file
0
bin/setup.sh
Executable file → Normal file
0
bin/start_bot.sh
Executable file → Normal file
0
bin/start_bot.sh
Executable file → Normal file
0
bin/update.sh
Executable file → Normal file
0
bin/update.sh
Executable file → Normal file
@@ -1,23 +1,19 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
bot:
|
bot:
|
||||||
build: .
|
build: .
|
||||||
container_name: telegram-tinder-bot
|
container_name: telegram-tinder-bot
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
|
||||||
db:
|
|
||||||
condition: service_healthy
|
|
||||||
env_file: .env
|
env_file: .env
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
- DB_HOST=db
|
- DB_HOST=${DB_HOST:-db}
|
||||||
- DB_PORT=5432
|
- DB_PORT=${DB_PORT:-5432}
|
||||||
- DB_NAME=telegram_tinder_bot
|
- DB_NAME=${DB_NAME:-telegram_tinder_bot}
|
||||||
- DB_USERNAME=postgres
|
- DB_USERNAME=${DB_USERNAME:-postgres}
|
||||||
|
- DB_PASSWORD=${DB_PASSWORD:-postgres}
|
||||||
volumes:
|
volumes:
|
||||||
- ./uploads:/app/uploads
|
- ./uploads:/app/uploads:rw
|
||||||
- ./logs:/app/logs
|
- ./logs:/app/logs:rw
|
||||||
networks:
|
networks:
|
||||||
- bot-network
|
- bot-network
|
||||||
healthcheck:
|
healthcheck:
|
||||||
@@ -32,9 +28,9 @@ services:
|
|||||||
container_name: postgres-tinder
|
container_name: postgres-tinder
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_DB=telegram_tinder_bot
|
- POSTGRES_DB=${DB_NAME:-telegram_tinder_bot}
|
||||||
- POSTGRES_USER=postgres
|
- POSTGRES_USER=${DB_USERNAME:-postgres}
|
||||||
- POSTGRES_PASSWORD=${DB_PASSWORD:-password123}
|
- POSTGRES_PASSWORD=${DB_PASSWORD:-postgres}
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
ports:
|
ports:
|
||||||
@@ -42,10 +38,11 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- bot-network
|
- bot-network
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME:-postgres} -d ${DB_NAME:-telegram_tinder_bot}"]
|
||||||
interval: 5s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
start_period: 10s
|
||||||
|
|
||||||
adminer:
|
adminer:
|
||||||
image: adminer:latest
|
image: adminer:latest
|
||||||
@@ -53,8 +50,6 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
depends_on:
|
|
||||||
- db
|
|
||||||
networks:
|
networks:
|
||||||
- bot-network
|
- bot-network
|
||||||
|
|
||||||
|
|||||||
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
|
// add-hobbies-column.js
|
||||||
// Скрипт для добавления колонки hobbies в таблицу profiles
|
// Скрипт для добавления колонки hobbies в таблицу profiles
|
||||||
|
|
||||||
|
require('dotenv').config();
|
||||||
const { Pool } = require('pg');
|
const { Pool } = require('pg');
|
||||||
|
|
||||||
// Настройки подключения к базе данных
|
// Настройки подключения к базе данных из переменных окружения
|
||||||
const pool = new Pool({
|
const pool = new Pool({
|
||||||
host: '192.168.0.102',
|
host: process.env.DB_HOST || 'localhost',
|
||||||
port: 5432,
|
port: parseInt(process.env.DB_PORT) || 5432,
|
||||||
database: 'telegram_tinder_bot',
|
database: process.env.DB_NAME || 'telegram_tinder_bot',
|
||||||
user: 'trevor',
|
user: process.env.DB_USERNAME || 'postgres',
|
||||||
password: 'Cl0ud_1985!'
|
password: process.env.DB_PASSWORD
|
||||||
});
|
});
|
||||||
|
|
||||||
async function addHobbiesColumn() {
|
async function addHobbiesColumn() {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// Исправленный код для создания профиля
|
// Исправленный код для создания профиля
|
||||||
|
require('dotenv').config();
|
||||||
const { Client } = require('pg');
|
const { Client } = require('pg');
|
||||||
const { v4: uuidv4 } = require('uuid');
|
const { v4: uuidv4 } = require('uuid');
|
||||||
|
|
||||||
@@ -18,13 +19,13 @@ if (!telegramId || !name || !age || !gender || !city || !bio || !photoFileId) {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Устанавливаем соединение с базой данных
|
// Устанавливаем соединение с базой данных из переменных окружения
|
||||||
const client = new Client({
|
const client = new Client({
|
||||||
host: '192.168.0.102',
|
host: process.env.DB_HOST || 'localhost',
|
||||||
port: 5432,
|
port: parseInt(process.env.DB_PORT) || 5432,
|
||||||
user: 'trevor',
|
user: process.env.DB_USERNAME || 'postgres',
|
||||||
password: 'Cl0ud_1985!',
|
password: process.env.DB_PASSWORD,
|
||||||
database: 'telegram_tinder_bot'
|
database: process.env.DB_NAME || 'telegram_tinder_bot'
|
||||||
});
|
});
|
||||||
|
|
||||||
async function createProfile() {
|
async function createProfile() {
|
||||||
|
|||||||
@@ -2,17 +2,18 @@
|
|||||||
// Этот скрипт создает записи о миграциях в таблице pgmigrations без применения изменений
|
// Этот скрипт создает записи о миграциях в таблице pgmigrations без применения изменений
|
||||||
// Используется для синхронизации существующей базы с миграциями
|
// Используется для синхронизации существующей базы с миграциями
|
||||||
|
|
||||||
|
require('dotenv').config();
|
||||||
const { Client } = require('pg');
|
const { Client } = require('pg');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
// Подключение к базе данных
|
// Подключение к базе данных из переменных окружения
|
||||||
const client = new Client({
|
const client = new Client({
|
||||||
host: '192.168.0.102',
|
host: process.env.DB_HOST || 'localhost',
|
||||||
port: 5432,
|
port: parseInt(process.env.DB_PORT) || 5432,
|
||||||
database: 'telegram_tinder_bot',
|
database: process.env.DB_NAME || 'telegram_tinder_bot',
|
||||||
user: 'trevor',
|
user: process.env.DB_USERNAME || 'postgres',
|
||||||
password: 'Cl0ud_1985!'
|
password: process.env.DB_PASSWORD
|
||||||
});
|
});
|
||||||
|
|
||||||
async function syncMigrations() {
|
async function syncMigrations() {
|
||||||
|
|||||||
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.localizationService = LocalizationService.getInstance();
|
||||||
|
|
||||||
this.commandHandlers = new CommandHandlers(this.bot);
|
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.callbackHandlers = new CallbackHandlers(this.bot, this.messageHandlers);
|
||||||
this.notificationHandlers = new NotificationHandlers(this.bot);
|
this.notificationHandlers = new NotificationHandlers(this.bot);
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,9 @@ export class CallbackHandlers {
|
|||||||
this.bot = bot;
|
this.bot = bot;
|
||||||
this.profileService = new ProfileService();
|
this.profileService = new ProfileService();
|
||||||
this.matchingService = new MatchingService();
|
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.messageHandlers = messageHandlers;
|
||||||
this.profileEditController = new ProfileEditController(this.profileService);
|
this.profileEditController = new ProfileEditController(this.profileService);
|
||||||
this.enhancedChatHandlers = new EnhancedChatHandlers(bot);
|
this.enhancedChatHandlers = new EnhancedChatHandlers(bot);
|
||||||
@@ -86,6 +88,107 @@ export class CallbackHandlers {
|
|||||||
await this.handleEditHobbies(chatId, telegramId);
|
await this.handleEditHobbies(chatId, telegramId);
|
||||||
} else if (data === 'edit_city') {
|
} else if (data === 'edit_city') {
|
||||||
await this.handleEditCity(chatId, telegramId);
|
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);
|
||||||
|
// Очищаем состояние
|
||||||
|
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') {
|
} else if (data === 'edit_job') {
|
||||||
await this.handleEditJob(chatId, telegramId);
|
await this.handleEditJob(chatId, telegramId);
|
||||||
} else if (data === 'edit_education') {
|
} else if (data === 'edit_education') {
|
||||||
@@ -184,6 +287,12 @@ export class CallbackHandlers {
|
|||||||
await this.likeBackHandler.handleLikeBack(chatId, telegramId, targetUserId);
|
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') {
|
else if (data === 'view_matches') {
|
||||||
await this.handleViewMatches(chatId, telegramId);
|
await this.handleViewMatches(chatId, telegramId);
|
||||||
@@ -975,14 +1084,22 @@ export class CallbackHandlers {
|
|||||||
profileText += '\n📝 ' + (profile.bio || 'Описание не указано') + '\n';
|
profileText += '\n📝 ' + (profile.bio || 'Описание не указано') + '\n';
|
||||||
|
|
||||||
// Хобби с хэштегами
|
// Хобби с хэштегами
|
||||||
if (profile.hobbies && profile.hobbies.trim()) {
|
if (profile.hobbies) {
|
||||||
const hobbiesArray = profile.hobbies.split(',').map(hobby => hobby.trim()).filter(hobby => hobby);
|
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(' ');
|
const formattedHobbies = hobbiesArray.map(hobby => '#' + hobby).join(' ');
|
||||||
profileText += '\n🎯 ' + formattedHobbies + '\n';
|
profileText += '\n🎯 ' + formattedHobbies + '\n';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (profile.interests.length > 0) {
|
if (profile.interests.length > 0) {
|
||||||
profileText += '\n<EFBFBD> Интересы: ' + profile.interests.join(', ');
|
profileText += '\n💡 Интересы: ' + profile.interests.join(', ');
|
||||||
}
|
}
|
||||||
|
|
||||||
let keyboard: InlineKeyboardMarkup;
|
let keyboard: InlineKeyboardMarkup;
|
||||||
@@ -1192,8 +1309,15 @@ export class CallbackHandlers {
|
|||||||
// Редактирование города
|
// Редактирование города
|
||||||
async handleEditCity(chatId: number, telegramId: string): Promise<void> {
|
async handleEditCity(chatId: number, telegramId: string): Promise<void> {
|
||||||
this.messageHandlers.setWaitingForInput(parseInt(telegramId), 'city');
|
this.messageHandlers.setWaitingForInput(parseInt(telegramId), 'city');
|
||||||
await this.bot.sendMessage(chatId, '🏙️ *Введите ваш город:*\n\nНапример: Москва', {
|
await this.bot.sendMessage(chatId, '🏙️ *Укажите ваш город:*\n\nВыберите один из вариантов:', {
|
||||||
parse_mode: 'Markdown'
|
parse_mode: 'Markdown',
|
||||||
|
reply_markup: {
|
||||||
|
keyboard: [
|
||||||
|
[{ text: '📍 Отправить геолокацию', request_location: true }]
|
||||||
|
],
|
||||||
|
resize_keyboard: true,
|
||||||
|
one_time_keyboard: true
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1762,11 +1886,19 @@ export class CallbackHandlers {
|
|||||||
candidateText += '\n📝 ' + (candidate.bio || 'Описание отсутствует') + '\n';
|
candidateText += '\n📝 ' + (candidate.bio || 'Описание отсутствует') + '\n';
|
||||||
|
|
||||||
// Хобби с хэштегами
|
// Хобби с хэштегами
|
||||||
if (candidate.hobbies && candidate.hobbies.trim()) {
|
if (candidate.hobbies) {
|
||||||
const hobbiesArray = candidate.hobbies.split(',').map(hobby => hobby.trim()).filter(hobby => hobby);
|
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(' ');
|
const formattedHobbies = hobbiesArray.map(hobby => '#' + hobby).join(' ');
|
||||||
candidateText += '\n🎯 ' + formattedHobbies + '\n';
|
candidateText += '\n🎯 ' + formattedHobbies + '\n';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (candidate.interests.length > 0) {
|
if (candidate.interests.length > 0) {
|
||||||
candidateText += '\n<> Интересы: ' + candidate.interests.join(', ');
|
candidateText += '\n<> Интересы: ' + candidate.interests.join(', ');
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ export class EnhancedChatHandlers {
|
|||||||
|
|
||||||
constructor(bot: TelegramBot) {
|
constructor(bot: TelegramBot) {
|
||||||
this.bot = bot;
|
this.bot = bot;
|
||||||
this.chatService = new ChatService();
|
|
||||||
this.profileService = new ProfileService();
|
this.profileService = new ProfileService();
|
||||||
this.notificationService = new NotificationService(bot);
|
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 { ProfileService } from '../services/profileService';
|
||||||
import { ChatService } from '../services/chatService';
|
import { ChatService } from '../services/chatService';
|
||||||
import { EnhancedChatHandlers } from './enhancedChatHandlers';
|
import { EnhancedChatHandlers } from './enhancedChatHandlers';
|
||||||
|
import { KakaoMapService } from '../services/kakaoMapService';
|
||||||
|
|
||||||
// Состояния пользователей для создания профилей
|
// Состояния пользователей для создания профилей
|
||||||
interface UserState {
|
interface UserState {
|
||||||
@@ -19,6 +20,7 @@ interface ChatState {
|
|||||||
interface ProfileEditState {
|
interface ProfileEditState {
|
||||||
waitingForInput: boolean;
|
waitingForInput: boolean;
|
||||||
field: string;
|
field: string;
|
||||||
|
tempCity?: string; // Временное хранение города для подтверждения
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MessageHandlers {
|
export class MessageHandlers {
|
||||||
@@ -26,15 +28,27 @@ export class MessageHandlers {
|
|||||||
private profileService: ProfileService;
|
private profileService: ProfileService;
|
||||||
private chatService: ChatService;
|
private chatService: ChatService;
|
||||||
private enhancedChatHandlers: EnhancedChatHandlers;
|
private enhancedChatHandlers: EnhancedChatHandlers;
|
||||||
|
private notificationService: any;
|
||||||
|
private kakaoMapService: KakaoMapService | null = null;
|
||||||
private userStates: Map<string, UserState> = new Map();
|
private userStates: Map<string, UserState> = new Map();
|
||||||
private chatStates: Map<string, ChatState> = 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.bot = bot;
|
||||||
this.profileService = new ProfileService();
|
this.profileService = new ProfileService();
|
||||||
this.chatService = new ChatService();
|
this.notificationService = notificationService;
|
||||||
|
this.chatService = new ChatService(notificationService);
|
||||||
this.enhancedChatHandlers = new EnhancedChatHandlers(bot);
|
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 {
|
register(): void {
|
||||||
@@ -55,7 +69,7 @@ export class MessageHandlers {
|
|||||||
const profileEditState = this.profileEditStates.get(userId);
|
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; // Сообщение обработано как сообщение в чате
|
return; // Сообщение обработано как сообщение в чате
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,22 +141,68 @@ export class MessageHandlers {
|
|||||||
userState.data.age = age;
|
userState.data.age = age;
|
||||||
userState.step = 'waiting_city';
|
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;
|
break;
|
||||||
|
|
||||||
case 'waiting_city':
|
case 'waiting_city':
|
||||||
if (!msg.text) {
|
// Обработка геолокации
|
||||||
await this.bot.sendMessage(chatId, '❌ Пожалуйста, отправьте название города');
|
if (msg.location) {
|
||||||
|
await this.handleLocationForCity(msg, userId, userState);
|
||||||
return;
|
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(
|
await this.bot.sendMessage(
|
||||||
chatId,
|
chatId,
|
||||||
'📝 Теперь расскажите немного о себе (био):\n\n' +
|
`<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;
|
break;
|
||||||
|
|
||||||
@@ -428,6 +488,47 @@ export class MessageHandlers {
|
|||||||
}
|
}
|
||||||
value = distance;
|
value = distance;
|
||||||
break;
|
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) {
|
if (!isValid) {
|
||||||
@@ -537,4 +638,194 @@ export class MessageHandlers {
|
|||||||
await this.profileService.updateProfile(profile.userId, updates);
|
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.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отправляем пользователю информацию с кнопками подтверждения
|
||||||
|
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 {
|
export class ChatService {
|
||||||
private profileService: ProfileService;
|
private profileService: ProfileService;
|
||||||
|
private notificationService: any; // Добавим позже правильный тип
|
||||||
|
|
||||||
constructor() {
|
constructor(notificationService?: any) {
|
||||||
this.profileService = new ProfileService();
|
this.profileService = new ProfileService();
|
||||||
|
this.notificationService = notificationService;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Получить все чаты (матчи) пользователя
|
// Получить все чаты (матчи) пользователя
|
||||||
@@ -149,7 +151,7 @@ export class ChatService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const row = messageResult.rows[0];
|
const row = messageResult.rows[0];
|
||||||
return new Message({
|
const message = new Message({
|
||||||
id: row.id,
|
id: row.id,
|
||||||
matchId: row.match_id,
|
matchId: row.match_id,
|
||||||
senderId: row.sender_id,
|
senderId: row.sender_id,
|
||||||
@@ -158,6 +160,26 @@ export class ChatService {
|
|||||||
isRead: row.is_read,
|
isRead: row.is_read,
|
||||||
createdAt: new Date(row.created_at)
|
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) {
|
} catch (error) {
|
||||||
console.error('Error sending message:', error);
|
console.error('Error sending message:', error);
|
||||||
return null;
|
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;
|
const userId = userProfile.userId;
|
||||||
|
|
||||||
// Определяем, каким должен быть пол показываемых профилей
|
// Определяем, каким должен быть пол показываемых профилей
|
||||||
let targetGender: string;
|
let targetGender: string | null;
|
||||||
|
let genderCondition: string;
|
||||||
|
|
||||||
if (userProfile.interestedIn === 'male' || userProfile.interestedIn === 'female') {
|
if (userProfile.interestedIn === 'male' || userProfile.interestedIn === 'female') {
|
||||||
|
// Конкретный пол
|
||||||
targetGender = userProfile.interestedIn;
|
targetGender = userProfile.interestedIn;
|
||||||
|
genderCondition = `AND p.gender = '${targetGender}'`;
|
||||||
} else {
|
} else {
|
||||||
// Если "both" или другое значение, показываем противоположный пол
|
// Если "both" - показываем всех кроме своего пола
|
||||||
targetGender = userProfile.gender === 'male' ? 'female' : 'male';
|
targetGender = null;
|
||||||
|
genderCondition = `AND p.gender != '${userProfile.gender}'`;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[DEBUG] Определен целевой пол для поиска: ${targetGender}`);
|
console.log(`[DEBUG] Определен целевой пол для поиска: ${targetGender || 'any (except self)'}, условие: ${genderCondition}`);
|
||||||
|
|
||||||
// Получаем список просмотренных профилей из новой таблицы profile_views
|
// Получаем список просмотренных профилей из новой таблицы profile_views
|
||||||
// и добавляем также профили из свайпов для полной совместимости
|
// и добавляем также профили из свайпов для полной совместимости
|
||||||
@@ -490,7 +495,7 @@ export class MatchingService {
|
|||||||
FROM profiles p
|
FROM profiles p
|
||||||
JOIN users u ON p.user_id = u.id
|
JOIN users u ON p.user_id = u.id
|
||||||
WHERE p.is_visible = true
|
WHERE p.is_visible = true
|
||||||
AND p.gender = '${targetGender}'
|
${genderCondition}
|
||||||
${excludeCondition}
|
${excludeCondition}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -502,14 +507,14 @@ export class MatchingService {
|
|||||||
console.log(`[DEBUG] Найдено ${availableProfilesCount} доступных профилей`);
|
console.log(`[DEBUG] Найдено ${availableProfilesCount} доступных профилей`);
|
||||||
|
|
||||||
// Используем определенный ранее targetGender для поиска
|
// Используем определенный ранее 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 = `
|
const candidateQuery = `
|
||||||
SELECT p.*, u.telegram_id, u.username, u.first_name, u.last_name
|
SELECT p.*, u.telegram_id, u.username, u.first_name, u.last_name
|
||||||
FROM profiles p
|
FROM profiles p
|
||||||
JOIN users u ON p.user_id = u.id
|
JOIN users u ON p.user_id = u.id
|
||||||
WHERE p.is_visible = true
|
WHERE p.is_visible = true
|
||||||
AND p.gender = '${targetGender}'
|
${genderCondition}
|
||||||
AND p.age BETWEEN ${userProfile.searchPreferences.minAge} AND ${userProfile.searchPreferences.maxAge}
|
AND p.age BETWEEN ${userProfile.searchPreferences.minAge} AND ${userProfile.searchPreferences.maxAge}
|
||||||
${excludeCondition}
|
${excludeCondition}
|
||||||
ORDER BY RANDOM()
|
ORDER BY RANDOM()
|
||||||
|
|||||||
@@ -1118,10 +1118,10 @@ export class NotificationService {
|
|||||||
|
|
||||||
const lastMessageTime = new Date(result.rows[0].created_at);
|
const lastMessageTime = new Date(result.rows[0].created_at);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const hoursSinceLastMessage = (now.getTime() - lastMessageTime.getTime()) / (1000 * 60 * 60);
|
const secondsSinceLastMessage = (now.getTime() - lastMessageTime.getTime()) / 1000;
|
||||||
|
|
||||||
// Считаем активным если последнее сообщение было менее 10 минут назад
|
// Считаем активным если последнее сообщение было менее 30 секунд назад
|
||||||
return hoursSinceLastMessage < (10 / 60);
|
return secondsSinceLastMessage < 30;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error checking user activity:', error);
|
console.error('Error checking user activity:', error);
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -46,16 +46,32 @@ export class ProfileService {
|
|||||||
updatedAt: now
|
updatedAt: now
|
||||||
});
|
});
|
||||||
|
|
||||||
// Сохранение в базу данных
|
// Сохранение в базу данных (UPSERT для избежания конфликта с триггером)
|
||||||
await query(`
|
await query(`
|
||||||
INSERT INTO profiles (
|
INSERT INTO profiles (
|
||||||
id, user_id, name, age, gender, interested_in, bio, photos,
|
id, user_id, name, age, gender, interested_in, bio, photos,
|
||||||
city, education, job, height, religion, dating_goal,
|
city, education, job, height, religion, dating_goal,
|
||||||
is_verified, is_visible, created_at, updated_at
|
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)
|
||||||
|
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,
|
||||||
|
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,
|
profileId, userId, profile.name, profile.age, profile.gender, profile.interestedIn,
|
||||||
profile.bio, JSON.stringify(profile.photos), profile.city, profile.education, profile.job,
|
profile.bio, profile.photos, profile.city, profile.education, profile.job,
|
||||||
profile.height, profile.religion, profile.datingGoal,
|
profile.height, profile.religion, profile.datingGoal,
|
||||||
profile.isVerified, profile.isVisible, profile.createdAt, profile.updatedAt
|
profile.isVerified, profile.isVisible, profile.createdAt, profile.updatedAt
|
||||||
]);
|
]);
|
||||||
@@ -168,9 +184,10 @@ export class ProfileService {
|
|||||||
switch (key) {
|
switch (key) {
|
||||||
case 'photos':
|
case 'photos':
|
||||||
case 'interests':
|
case 'interests':
|
||||||
|
case 'hobbies':
|
||||||
updateFields.push(`${this.camelToSnake(key)} = $${paramIndex++}`);
|
updateFields.push(`${this.camelToSnake(key)} = $${paramIndex++}`);
|
||||||
// Для PostgreSQL массивы должны быть преобразованы в JSON-строку
|
// PostgreSQL принимает нативные массивы
|
||||||
updateValues.push(JSON.stringify(value));
|
updateValues.push(Array.isArray(value) ? value : [value]);
|
||||||
break;
|
break;
|
||||||
case 'location':
|
case 'location':
|
||||||
// Пропускаем обработку местоположения, так как колонки location нет
|
// Пропускаем обработку местоположения, так как колонки location нет
|
||||||
|
|||||||
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