Компактные hero секции и улучшенная инициализация БД

🎨 UI улучшения:
- Уменьшена высота синих панелей с 100vh до 70vh на главной
- Добавлен класс .compact (25vh) для всех остальных страниц
- Улучшена адаптивность для мобильных устройств
- Обновлены все шаблоны с hero секциями

🚀 Инфраструктура:
- Автоматическая инициализация базы данных при деплое
- Улучшены мокапные данные (больше отзывов, бронирований, сообщений)
- Добавлены настройки сайта в базу данных
- Создан скрипт автоматического деплоя deploy.sh

📦 Система сборки:
- Обновлен .gitignore с полным покрытием файлов
- Добавлена папка для загрузок с .gitkeep
- Улучшен README с инструкциями по запуску
- ES модули для инициализации базы данных

🐛 Исправления:
- Совместимость с ES модулями в Node.js
- Правильная обработка ошибок инициализации БД
- Корректные SQL запросы для PostgreSQL
This commit is contained in:
2025-11-29 18:47:42 +09:00
parent 409e6c146b
commit a461fea9d9
24 changed files with 1442 additions and 84 deletions

77
database/init-database.js Normal file
View File

@@ -0,0 +1,77 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import db from '../src/config/database.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
export async function initDatabase() {
try {
console.log('🚀 Starting complete database initialization...');
// Check if database is connected
await db.query('SELECT 1');
console.log('✅ Database connection successful');
// 1. Create schema
console.log('📋 Creating database schema...');
const schemaPath = path.join(__dirname, 'schema.sql');
const schema = fs.readFileSync(schemaPath, 'utf8');
await db.query(schema);
console.log('✅ Database schema created successfully');
// 2. Check if tables are empty (first run)
const checkResult = await db.query('SELECT COUNT(*) FROM admins');
const isEmpty = parseInt(checkResult.rows[0].count) === 0;
if (isEmpty) {
console.log('📝 Database is empty, inserting mock data...');
// Insert mock data
const mockDataPath = path.join(__dirname, 'mock-data.sql');
const mockData = fs.readFileSync(mockDataPath, 'utf8');
await db.query(mockData);
console.log('✅ Mock data inserted successfully');
} else {
console.log(' Database already contains data, skipping mock data insertion');
}
// 3. Run any pending migrations
console.log('🔄 Checking for pending migrations...');
// Check if rating system exists
try {
await db.query('SELECT 1 FROM route_ratings LIMIT 1');
console.log(' Rating system already exists');
} catch (error) {
console.log('📈 Installing rating system...');
const ratingMigrationPath = path.join(__dirname, 'rating-system-migration.sql');
if (fs.existsSync(ratingMigrationPath)) {
const ratingMigration = fs.readFileSync(ratingMigrationPath, 'utf8');
await db.query(ratingMigration);
console.log('✅ Rating system installed successfully');
}
}
console.log('✨ Database initialization completed successfully!');
} catch (error) {
console.error('❌ Database initialization failed:', error);
throw error;
}
}
// Run if called directly
if (process.argv[1] === fileURLToPath(import.meta.url)) {
initDatabase()
.then(() => {
console.log('🎉 All done!');
process.exit(0);
})
.catch((error) => {
console.error('💥 Initialization failed:', error);
process.exit(1);
});
}

View File

@@ -80,24 +80,44 @@ INSERT INTO reviews (route_id, customer_name, customer_email, rating, comment, i
(7, 'Михаил Рыбаков', 'mikhail.rybakov@email.com', 4, 'Два дня рыбалки у берегов Пусана - это было здорово! Поймали много рыбы, капитан Ли очень опытный. Единственный минус - качка была сильная, но это природа. Улов приготовили прямо на борту - вкуснее не ел никогда!', true, NOW() - INTERVAL '8 days'),
(2, 'Елена Иванова', 'elena.ivanova@email.com', 5, 'Морской воздух, свежие морепродукты, красивые пляжи - всё было идеально! Гид Чой Ю На показала лучшие места для фото и рассказала много интересного о жизни в портовом городе.', true, NOW() - INTERVAL '15 days'),
(1, 'Сергей Морозов', 'sergey.morozov@email.com', 4, 'Тур по Сеулу организован очень хорошо. Группа небольшая, гид внимательный. Единственное - хотелось бы больше свободного времени для шопинга в Мёндоне. В целом очень доволен!', true, NOW() - INTERVAL '20 days'),
(6, 'Ольга Кузнецова', 'olga.kuznetsova@email.com', 5, 'Халласан покорился легко благодаря отличной подготовке группы. Остров Чеджу вообще сказочный - вулканические пляжи, мандариновые рощи, дружелюбные местные жители. Хочу вернуться!', true, NOW() - INTERVAL '25 days');
(6, 'Ольга Кузнецова', 'olga.kuznetsova@email.com', 5, 'Халласан покорился легко благодаря отличной подготовке группы. Остров Чеджу вообще сказочный - вулканические пляжи, мандариновые рощи, дружелюбные местные жители. Хочу вернуться!', true, NOW() - INTERVAL '25 days'),
(3, 'Виктор Николаев', 'viktor.nikolaev@email.com', 5, 'Кёнджу - это путешествие во времени! Юн Тэ Гу просто энциклопедия корейской истории. Каждый камень в Пульгуксе имеет свою историю. Грот Соккурам поражает мастерством древних зодчих.', true, NOW() - INTERVAL '30 days'),
(5, 'Татьяна Смирнова', 'tatyana.smirnova@email.com', 4, 'Чирисан - это духовное очищение. Ночёвки в храмах, медитации на рассвете, звенящая тишина гор. Физически тяжело, но душевно очень важно. Рекомендую всем, кто ищет смысл жизни.', true, NOW() - INTERVAL '35 days'),
(8, 'Алексей Рыбников', 'alexey.rybnikov@email.com', 5, 'Три дня на рыбацком судне в Восточном море - незабываемо! Ночная рыбалка на кальмаров, рассвет над морем, свежайшая рыба на завтрак. Капитан Ли и команда - настоящие профессионалы!', true, NOW() - INTERVAL '40 days');
-- Настройки сайта
INSERT INTO site_settings (setting_key, setting_value, description, updated_at) VALUES
('site_name', 'Корея Тур Агентство', 'Название сайта', NOW()),
('site_description', 'Откройте для себя красоту Кореи с нашими профессиональными турами', 'Описание сайта для SEO', NOW()),
('contact_email', 'info@koreatour.ru', 'Email для связи', NOW()),
('contact_phone', '+7 (495) 123-45-67', 'Телефон для связи', NOW()),
('contact_address', 'Москва, ул. Примерная, д. 123', 'Адрес офиса', NOW()),
('social_facebook', 'https://facebook.com/koreatour', 'Ссылка на Facebook', NOW()),
('social_instagram', 'https://instagram.com/koreatour', 'Ссылка на Instagram', NOW()),
('social_youtube', 'https://youtube.com/koreatour', 'Ссылка на YouTube', NOW()),
('booking_email', 'booking@koreatour.ru', 'Email для бронирования', NOW()),
('emergency_phone', '+82-10-911-1234', 'Экстренный телефон в Корее', NOW());
INSERT INTO site_settings (setting_key, setting_value, setting_type, description, updated_at) VALUES
('site_name', 'Корея Тур Агентство', 'text', 'Название сайта', NOW()),
('site_description', 'Откройте для себя красоту Кореи с нашими профессиональными турами', 'text', 'Описание сайта для SEO', NOW()),
('contact_email', 'info@koreatour.ru', 'text', 'Email для связи', NOW()),
('contact_phone', '+7 (495) 123-45-67', 'text', 'Телефон для связи', NOW()),
('contact_address', 'Москва, ул. Примерная, д. 123', 'text', 'Адрес офиса', NOW()),
('social_facebook', 'https://facebook.com/koreatour', 'text', 'Ссылка на Facebook', NOW()),
('social_instagram', 'https://instagram.com/koreatour', 'text', 'Ссылка на Instagram', NOW()),
('social_youtube', 'https://youtube.com/koreatour', 'text', 'Ссылка на YouTube', NOW()),
('booking_email', 'booking@koreatour.ru', 'text', 'Email для бронирования', NOW()),
('emergency_phone', '+82-10-911-1234', 'text', 'Экстренный телефон в Корее', NOW()),
('booking_enabled', 'true', 'boolean', 'Включить онлайн бронирование', NOW()),
('maintenance_mode', 'false', 'boolean', 'Режим обслуживания', NOW()),
('max_group_size', '15', 'number', 'Максимальный размер группы', NOW()),
('default_currency', 'KRW', 'text', 'Валюта по умолчанию', NOW()),
('facebook_url', 'https://facebook.com/koreatour', 'text', 'URL Facebook страницы', NOW()),
('instagram_url', 'https://instagram.com/koreatour', 'text', 'URL Instagram профиля', NOW()),
('twitter_url', 'https://twitter.com/koreatour', 'text', 'URL Twitter профиля', NOW());
-- Примеры бронирований
INSERT INTO bookings (route_id, customer_name, customer_email, customer_phone, preferred_date, status, notes, created_at) VALUES
(1, 'Иван Петров', 'ivan.petrov@email.com', '+7-915-123-4567', '2024-04-15', 'confirmed', 'Оплата получена, гид назначен', NOW() - INTERVAL '3 days'),
INSERT INTO bookings (route_id, guide_id, customer_name, customer_email, customer_phone, preferred_date, group_size, total_price, status, notes, created_at) VALUES
(1, 1, 'Иван Петров', 'ivan.petrov@email.com', '+7-915-123-4567', '2025-12-15', 2, 900000, 'confirmed', 'Оплата получена, гид назначен', NOW() - INTERVAL '3 days'),
(4, 2, 'Мария Сидорова', 'maria.sidorova@email.com', '+7-916-234-5678', '2025-12-20', 4, 2720000, 'pending', 'Ожидает подтверждения оплаты', NOW() - INTERVAL '1 day'),
(7, 3, 'Андрей Козлов', 'andrey.kozlov@email.com', '+7-917-345-6789', '2025-12-25', 3, 1260000, 'confirmed', 'Группа из трёх друзей, все опытные рыбаки', NOW() - INTERVAL '5 days'),
(2, 4, 'Светлана Попова', 'svetlana.popova@email.com', '+7-918-456-7890', '2026-01-05', 1, 320000, 'pending', 'Первый раз в Корее, нужна подробная консультация', NOW() - INTERVAL '2 days');
-- Контактные сообщения
INSERT INTO contact_messages (name, email, phone, subject, message, status, created_at) VALUES
('Алексей Иванов', 'alexey.ivanov@email.com', '+7-921-123-4567', 'Вопрос о горных турах', 'Здравствуйте! Интересуют походы в горы для начинающих. Какой тур посоветуете для первого раза? Опыт походов минимальный, но физическая подготовка хорошая.', 'unread', NOW() - INTERVAL '2 hours'),
('Наталья Кузнецова', 'natasha.kuznetsova@email.com', '+7-925-234-5678', 'Групповой тур в Сеул', 'Планируем поездку компанией из 8 человек в апреле. Возможно ли организовать индивидуальный тур по дворцам Сеула? Интересуют цены и программа.', 'read', NOW() - INTERVAL '1 day'),
('Михаил Петров', 'mikhail.petrov@email.com', '+7-926-345-6789', 'Рыбалка для новичков', 'Никогда не рыбачил в море, но очень хочется попробовать. Есть ли туры для абсолютных новичков? Нужно ли своё снаряжение?', 'replied', NOW() - INTERVAL '3 days'),
('Елена Смирнова', 'elena.smirnova@email.com', '+7-927-456-7890', 'Сезонные особенности', 'Какое время года лучше для посещения храмов и дворцов? Планируем поездку с детьми 12 и 15 лет. Спасибо!', 'unread', NOW() - INTERVAL '5 hours');
(4, 'Мария Сидорова', 'maria.sidorova@email.com', '+7-926-234-5678', '2024-05-20', 'pending', 'Ожидаем подтверждение доступности адаптированного маршрута', NOW() - INTERVAL '1 day'),
(7, 'Алексей Рыбак', 'alexey.rybak@email.com', '+7-903-345-6789', '2024-03-25', 'confirmed', 'Забронировано судно для опытных рыболовов', NOW() - INTERVAL '7 days'),
(2, 'Екатерина Новикова', 'ekaterina.novikova@email.com', '+7-985-456-7890', '2024-04-08', 'completed', 'Тур завершён, клиент оставил отличный отзыв', NOW() - INTERVAL '14 days');

View File

@@ -0,0 +1,73 @@
-- Система лайков/дизлайков для туров, гидов и статей
CREATE TABLE ratings (
id SERIAL PRIMARY KEY,
user_ip VARCHAR(45) NOT NULL, -- IP адрес для анонимных пользователей
target_id INTEGER NOT NULL,
target_type VARCHAR(20) NOT NULL CHECK (target_type IN ('route', 'guide', 'article')),
rating INTEGER NOT NULL CHECK (rating IN (1, -1)), -- 1 = лайк, -1 = дизлайк
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_ip, target_id, target_type) -- Один пользователь - один голос за объект
);
-- График работы гидов
CREATE TABLE guide_schedules (
id SERIAL PRIMARY KEY,
guide_id INTEGER NOT NULL REFERENCES guides(id) ON DELETE CASCADE,
monday BOOLEAN DEFAULT true,
tuesday BOOLEAN DEFAULT true,
wednesday BOOLEAN DEFAULT true,
thursday BOOLEAN DEFAULT true,
friday BOOLEAN DEFAULT true,
saturday BOOLEAN DEFAULT false,
sunday BOOLEAN DEFAULT false,
start_time TIME DEFAULT '09:00',
end_time TIME DEFAULT '18:00',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(guide_id)
);
-- Выходные дни (общие и индивидуальные)
CREATE TABLE holidays (
id SERIAL PRIMARY KEY,
date DATE NOT NULL,
title VARCHAR(255) NOT NULL,
type VARCHAR(20) NOT NULL CHECK (type IN ('public', 'guide_personal')), -- публичный или персональный
guide_id INTEGER REFERENCES guides(id) ON DELETE CASCADE, -- NULL для публичных выходных
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(date, guide_id) -- Один выходной день для одного гида
);
-- Обновляем таблицу бронирований для связи с гидом
ALTER TABLE bookings ADD COLUMN guide_id INTEGER REFERENCES guides(id);
ALTER TABLE bookings ADD COLUMN route_id INTEGER REFERENCES routes(id);
-- Добавляем индексы для производительности
CREATE INDEX idx_ratings_target ON ratings(target_type, target_id);
CREATE INDEX idx_ratings_user_ip ON ratings(user_ip);
CREATE INDEX idx_bookings_date ON bookings(preferred_date);
CREATE INDEX idx_bookings_guide ON bookings(guide_id);
CREATE INDEX idx_holidays_date ON holidays(date);
-- Функция для подсчета рейтинга
CREATE OR REPLACE FUNCTION calculate_rating(target_type_param VARCHAR, target_id_param INTEGER)
RETURNS TABLE(
likes_count BIGINT,
dislikes_count BIGINT,
total_votes BIGINT,
rating_percentage NUMERIC(5,2)
) AS $$
BEGIN
RETURN QUERY
SELECT
COUNT(CASE WHEN rating = 1 THEN 1 END) as likes_count,
COUNT(CASE WHEN rating = -1 THEN 1 END) as dislikes_count,
COUNT(*) as total_votes,
CASE
WHEN COUNT(*) = 0 THEN 0
ELSE ROUND((COUNT(CASE WHEN rating = 1 THEN 1 END)::NUMERIC / COUNT(*)::NUMERIC) * 100, 2)
END as rating_percentage
FROM ratings
WHERE target_type = target_type_param AND target_id = target_id_param;
END;
$$ LANGUAGE plpgsql;

106
database/run-migration.js Normal file
View File

@@ -0,0 +1,106 @@
import pool from '../src/config/database.js';
const migrateRatingSystem = async () => {
try {
console.log('🔄 Выполняю миграцию для системы рейтингов...');
// Создание таблицы ratings
await pool.query(`
CREATE TABLE IF NOT EXISTS ratings (
id SERIAL PRIMARY KEY,
user_ip VARCHAR(45) NOT NULL,
target_id INTEGER NOT NULL,
target_type VARCHAR(20) NOT NULL CHECK (target_type IN ('route', 'guide', 'article')),
rating INTEGER NOT NULL CHECK (rating IN (1, -1)),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_ip, target_id, target_type)
);
`);
// Создание таблицы guide_schedules
await pool.query(`
CREATE TABLE IF NOT EXISTS guide_schedules (
id SERIAL PRIMARY KEY,
guide_id INTEGER NOT NULL REFERENCES guides(id) ON DELETE CASCADE,
monday BOOLEAN DEFAULT true,
tuesday BOOLEAN DEFAULT true,
wednesday BOOLEAN DEFAULT true,
thursday BOOLEAN DEFAULT true,
friday BOOLEAN DEFAULT true,
saturday BOOLEAN DEFAULT false,
sunday BOOLEAN DEFAULT false,
start_time TIME DEFAULT '09:00',
end_time TIME DEFAULT '18:00',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(guide_id)
);
`);
// Создание таблицы holidays
await pool.query(`
CREATE TABLE IF NOT EXISTS holidays (
id SERIAL PRIMARY KEY,
date DATE NOT NULL,
title VARCHAR(255) NOT NULL,
type VARCHAR(20) NOT NULL CHECK (type IN ('public', 'guide_personal')),
guide_id INTEGER REFERENCES guides(id) ON DELETE CASCADE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(date, guide_id)
);
`);
// Добавление колонок в bookings (если не существуют)
try {
await pool.query('ALTER TABLE bookings ADD COLUMN guide_id INTEGER REFERENCES guides(id);');
} catch (e) {
console.log('Колонка guide_id уже существует');
}
try {
await pool.query('ALTER TABLE bookings ADD COLUMN route_id INTEGER REFERENCES routes(id);');
} catch (e) {
console.log('Колонка route_id уже существует');
}
// Создание индексов
await pool.query('CREATE INDEX IF NOT EXISTS idx_ratings_target ON ratings(target_type, target_id);');
await pool.query('CREATE INDEX IF NOT EXISTS idx_ratings_user_ip ON ratings(user_ip);');
await pool.query('CREATE INDEX IF NOT EXISTS idx_bookings_date ON bookings(preferred_date);');
await pool.query('CREATE INDEX IF NOT EXISTS idx_bookings_guide ON bookings(guide_id);');
await pool.query('CREATE INDEX IF NOT EXISTS idx_holidays_date ON holidays(date);');
// Функция для подсчета рейтинга
await pool.query(`
CREATE OR REPLACE FUNCTION calculate_rating(target_type_param VARCHAR, target_id_param INTEGER)
RETURNS TABLE(
likes_count BIGINT,
dislikes_count BIGINT,
total_votes BIGINT,
rating_percentage NUMERIC(5,2)
) AS $$
BEGIN
RETURN QUERY
SELECT
COUNT(CASE WHEN rating = 1 THEN 1 END) as likes_count,
COUNT(CASE WHEN rating = -1 THEN 1 END) as dislikes_count,
COUNT(*) as total_votes,
CASE
WHEN COUNT(*) = 0 THEN 0
ELSE ROUND((COUNT(CASE WHEN rating = 1 THEN 1 END)::NUMERIC / COUNT(*)::NUMERIC) * 100, 2)
END as rating_percentage
FROM ratings
WHERE target_type = target_type_param AND target_id = target_id_param;
END;
$$ LANGUAGE plpgsql;
`);
console.log('✅ Миграция выполнена успешно!');
process.exit(0);
} catch (error) {
console.error('❌ Ошибка миграции:', error);
process.exit(1);
}
};
migrateRatingSystem();

148
database/seed-guide-data.js Normal file
View File

@@ -0,0 +1,148 @@
import pool from '../src/config/database.js';
const seedGuideData = async () => {
try {
console.log('🌱 Добавляю тестовых гидов и их расписания...');
// Добавляем гидов, если их нет
const guidesCount = await pool.query('SELECT COUNT(*) FROM guides');
if (parseInt(guidesCount.rows[0].count) === 0) {
await pool.query(`
INSERT INTO guides (name, email, phone, languages, specialization, bio, experience, hourly_rate, is_active) VALUES
('Ли Мин Хо', 'lee@korea-tours.com', '+82-10-1234-5678', 'Корейский, Английский, Русский', 'city', 'Опытный гид по Сеулу с 8-летним стажем. Знаю все исторические места и современные достопримечательности столицы.', 8, 45000, true),
('Пак Со Ён', 'park@korea-tours.com', '+82-10-2345-6789', 'Корейский, Английский, Китайский', 'mountain', 'Профессиональный горный гид. Провожу походы в Сораксан, Чириксан и другие национальные парки Кореи.', 12, 55000, true),
('Ким Джун Су', 'kim@korea-tours.com', '+82-10-3456-7890', 'Корейский, Японский, Английский', 'fishing', 'Эксперт по рыбалке в морских и пресных водах. Организую туры на лучшие рыболовные места Кореи.', 15, 60000, true),
('Чой Хе Ран', 'choi@korea-tours.com', '+82-10-4567-8901', 'Корейский, Английский, Русский', 'general', 'Универсальный гид с широким спектром знаний о корейской культуре, истории и природе.', 6, 40000, true),
('Юн Тэ Хён', 'yoon@korea-tours.com', '+82-10-5678-9012', 'Корейский, Английский', 'city', 'Специалист по культурным турам и K-pop маршрутам. Покажу вам современную Корею глазами молодежи.', 4, 38000, true)
`);
}
// Получаем ID гидов
const guides = await pool.query('SELECT id FROM guides ORDER BY id');
// Добавляем расписания для каждого гида
for (const guide of guides.rows) {
const existingSchedule = await pool.query('SELECT * FROM guide_schedules WHERE guide_id = $1', [guide.id]);
if (existingSchedule.rows.length === 0) {
// Разные расписания для разных гидов
const schedules = [
{ monday: true, tuesday: true, wednesday: true, thursday: true, friday: true, saturday: true, sunday: false }, // Работает 6 дней
{ monday: true, tuesday: false, wednesday: true, thursday: true, friday: true, saturday: true, sunday: true }, // Выходной вторник
{ monday: true, tuesday: true, wednesday: true, thursday: true, friday: true, saturday: false, sunday: false }, // Только будни
{ monday: true, tuesday: true, wednesday: false, thursday: true, friday: true, saturday: true, sunday: true }, // Выходной среда
{ monday: false, tuesday: true, wednesday: true, thursday: true, friday: true, saturday: true, sunday: true } // Выходной понедельник
];
const schedule = schedules[(guide.id - 1) % schedules.length];
await pool.query(`
INSERT INTO guide_schedules (guide_id, monday, tuesday, wednesday, thursday, friday, saturday, sunday, start_time, end_time)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, '09:00', '18:00')
`, [
guide.id,
schedule.monday,
schedule.tuesday,
schedule.wednesday,
schedule.thursday,
schedule.friday,
schedule.saturday,
schedule.sunday
]);
}
}
// Добавляем несколько общих выходных дней
const holidays = [
{ date: '2025-01-01', title: 'Новый год', type: 'public' },
{ date: '2025-02-10', title: 'Лунный Новый год', type: 'public' },
{ date: '2025-03-01', title: 'День независимости', type: 'public' },
{ date: '2025-05-05', title: 'День детей', type: 'public' },
{ date: '2025-05-15', title: 'День Будды', type: 'public' },
{ date: '2025-06-06', title: 'День памяти', type: 'public' },
{ date: '2025-08-15', title: 'День освобождения', type: 'public' },
{ date: '2025-10-03', title: 'День основания государства', type: 'public' },
{ date: '2025-10-09', title: 'День корейского алфавита', type: 'public' },
{ date: '2025-12-25', title: 'Рождество', type: 'public' }
];
for (const holiday of holidays) {
const existing = await pool.query('SELECT * FROM holidays WHERE date = $1 AND type = $2', [holiday.date, holiday.type]);
if (existing.rows.length === 0) {
await pool.query(
'INSERT INTO holidays (date, title, type) VALUES ($1, $2, $3)',
[holiday.date, holiday.title, holiday.type]
);
}
}
// Добавляем несколько индивидуальных выходных
if (guides.rows.length > 0) {
const personalHolidays = [
{ date: '2025-12-01', title: 'Личный отпуск', guide_id: guides.rows[0].id },
{ date: '2025-12-15', title: 'Семейное мероприятие', guide_id: guides.rows[1].id },
{ date: '2025-11-30', title: 'Медицинский осмотр', guide_id: guides.rows[2].id }
];
for (const holiday of personalHolidays) {
const existing = await pool.query('SELECT * FROM holidays WHERE date = $1 AND guide_id = $2', [holiday.date, holiday.guide_id]);
if (existing.rows.length === 0) {
await pool.query(
'INSERT INTO holidays (date, title, type, guide_id) VALUES ($1, $2, $3, $4)',
[holiday.date, holiday.title, 'guide_personal', holiday.guide_id]
);
}
}
}
// Добавляем тестовые рейтинги
const routes = await pool.query('SELECT id FROM routes LIMIT 3');
const articles = await pool.query('SELECT id FROM articles LIMIT 3');
const testRatings = [
// Рейтинги для гидов
...guides.rows.slice(0, 3).map((guide, index) => ({
target_type: 'guide',
target_id: guide.id,
ratings: index === 0 ? [1, 1, 1, 1, -1] : index === 1 ? [1, 1, -1] : [1, 1, 1, 1, 1, 1, -1, -1]
})),
// Рейтинги для маршрутов
...routes.rows.map((route, index) => ({
target_type: 'route',
target_id: route.id,
ratings: index === 0 ? [1, 1, 1] : index === 1 ? [1, -1, 1, 1] : [1, 1, 1, 1, 1]
})),
// Рейтинги для статей
...articles.rows.map((article, index) => ({
target_type: 'article',
target_id: article.id,
ratings: index === 0 ? [1, 1] : index === 1 ? [1, 1, 1, -1] : [1]
}))
];
for (const item of testRatings) {
for (let i = 0; i < item.ratings.length; i++) {
const userIp = `192.168.1.${100 + i}`;
const existing = await pool.query(
'SELECT * FROM ratings WHERE user_ip = $1 AND target_type = $2 AND target_id = $3',
[userIp, item.target_type, item.target_id]
);
if (existing.rows.length === 0) {
await pool.query(
'INSERT INTO ratings (user_ip, target_type, target_id, rating) VALUES ($1, $2, $3, $4)',
[userIp, item.target_type, item.target_id, item.ratings[i]]
);
}
}
}
console.log('✅ Тестовые данные добавлены успешно!');
process.exit(0);
} catch (error) {
console.error('❌ Ошибка добавления тестовых данных:', error);
process.exit(1);
}
};
seedGuideData();