Initial commit: Korea Tourism Agency website with AdminJS
- Full-stack Node.js/Express application with PostgreSQL - Modern ES modules architecture - AdminJS admin panel with Sequelize ORM - Tourism routes, guides, articles, bookings management - Responsive Bootstrap 5 frontend - Docker containerization with docker-compose - Complete database schema with migrations - Authentication system for admin panel - Dynamic placeholder images for tour categories
This commit is contained in:
75
database/migrate.js
Normal file
75
database/migrate.js
Normal file
@@ -0,0 +1,75 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const db = require('../src/config/database');
|
||||
|
||||
async function runMigrations() {
|
||||
try {
|
||||
console.log('💾 Starting database migration...');
|
||||
|
||||
// Check if database is connected
|
||||
await db.query('SELECT 1');
|
||||
console.log('✅ Database connection successful');
|
||||
|
||||
// Read and execute schema
|
||||
const schemaPath = path.join(__dirname, 'schema.sql');
|
||||
const schema = fs.readFileSync(schemaPath, 'utf8');
|
||||
|
||||
await db.query(schema);
|
||||
console.log('✅ Database schema created successfully');
|
||||
|
||||
// Insert default admin user
|
||||
const bcrypt = require('bcryptjs');
|
||||
const hashedPassword = await bcrypt.hash('admin123', 10);
|
||||
|
||||
try {
|
||||
await db.query(`
|
||||
INSERT INTO admins (username, password, name, email, role)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (username) DO NOTHING
|
||||
`, ['admin', hashedPassword, 'Administrator', 'admin@example.com', 'admin']);
|
||||
console.log('✅ Default admin user created');
|
||||
} catch (error) {
|
||||
console.log('ℹ️ Admin user already exists');
|
||||
}
|
||||
|
||||
// Insert default site settings
|
||||
const defaultSettings = [
|
||||
['site_name', 'Korea Tourism Agency', 'text', 'Website name'],
|
||||
['site_description', 'Discover Korea\'s hidden gems with our guided tours', 'text', 'Site description'],
|
||||
['contact_email', 'info@koreatourism.com', 'text', 'Contact email'],
|
||||
['contact_phone', '+82-2-1234-5678', 'text', 'Contact phone'],
|
||||
['contact_address', 'Seoul, South Korea', 'text', 'Contact address'],
|
||||
['facebook_url', '', 'text', 'Facebook page URL'],
|
||||
['instagram_url', '', 'text', 'Instagram profile URL'],
|
||||
['twitter_url', '', 'text', 'Twitter profile URL'],
|
||||
['booking_enabled', 'true', 'boolean', 'Enable online booking'],
|
||||
['maintenance_mode', 'false', 'boolean', 'Maintenance mode']
|
||||
];
|
||||
|
||||
for (const [key, value, type, description] of defaultSettings) {
|
||||
try {
|
||||
await db.query(`
|
||||
INSERT INTO site_settings (setting_key, setting_value, setting_type, description)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (setting_key) DO NOTHING
|
||||
`, [key, value, type, description]);
|
||||
} catch (error) {
|
||||
console.log(`ℹ️ Setting ${key} already exists`);
|
||||
}
|
||||
}
|
||||
console.log('✅ Default site settings inserted');
|
||||
|
||||
console.log('✨ Migration completed successfully!');
|
||||
process.exit(0);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Migration failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
runMigrations();
|
||||
}
|
||||
|
||||
module.exports = { runMigrations };
|
||||
109
database/mock-data.sql
Normal file
109
database/mock-data.sql
Normal file
@@ -0,0 +1,109 @@
|
||||
-- Мокапные данные для туристического сайта
|
||||
-- Удаляем старые данные и заполняем новыми
|
||||
|
||||
-- Очистка таблиц
|
||||
TRUNCATE TABLE bookings, reviews, routes, guides, articles, admins, contact_messages, site_settings RESTART IDENTITY CASCADE;
|
||||
|
||||
-- Админы
|
||||
INSERT INTO admins (username, password, name, email, role, created_at) VALUES
|
||||
('admin', '$2b$12$LQv3c1yqBwEHbLn5F2x/3OlzqXrJQ9vSf9Gm7ZwTsYcAb3DeF4gHi', 'Администратор', 'admin@koreatour.com', 'super_admin', NOW()),
|
||||
('manager', '$2b$12$LQv3c1yqBwEHbLn5F2x/3OlzqXrJQ9vSf9Gm7ZwTsYcAb3DeF4gHi', 'Менеджер', 'manager@koreatour.com', 'admin', NOW());
|
||||
|
||||
-- Гиды
|
||||
INSERT INTO guides (name, email, phone, languages, specialization, bio, image_url, hourly_rate, is_active, created_at) VALUES
|
||||
('Ким Мин Джун', 'kim.minjun@guide.com', '+82-10-1234-5678', ARRAY['русский', 'корейский', 'английский'], 'city', 'Опытный гид со знанием истории и культуры Кореи. Специализируется на дворцах Сеула и традиционных деревнях.', '/images/guides/kim-minjun.jpg', 25000, true, NOW()),
|
||||
('Пак Со Ён', 'park.soyeon@guide.com', '+82-10-2345-6789', ARRAY['русский', 'корейский', 'китайский'], 'mountain', 'Инструктор по горному туризму. Знает все тропы национальных парков и безопасные маршруты.', '/images/guides/park-soyeon.jpg', 20000, true, NOW()),
|
||||
('Ли Дон Хёк', 'lee.donhyuk@guide.com', '+82-10-3456-7890', ARRAY['корейский', 'английский', 'японский'], 'fishing', 'Капитан с большим опытом морской рыбалки. Знает лучшие места для ловли у берегов Пусана.', '/images/guides/lee-donhyuk.jpg', 30000, true, NOW()),
|
||||
('Чой Ю На', 'choi.yuna@guide.com', '+82-10-4567-8901', ARRAY['русский', 'корейский'], 'city', 'Эксперт корейской кухни. Проводит кулинарные мастер-классы и дегустационные туры по Сеулу.', '/images/guides/choi-yuna.jpg', 22000, true, NOW()),
|
||||
('Юн Тэ Гу', 'yun.taegu@guide.com', '+82-10-5678-9012', ARRAY['корейский', 'английский'], 'city', 'Историк и археолог. Специализируется на древних памятниках и буддийских храмах Кореи.', '/images/guides/yun-taegu.jpg', 24000, true, NOW());
|
||||
|
||||
-- Маршруты
|
||||
INSERT INTO routes (title, description, type, duration, price, difficulty_level, max_group_size, guide_id, image_url, content, included_services, is_active, is_featured, created_at) VALUES
|
||||
('Сеул: Дворцы и традиции', 'Познакомьтесь с королевскими дворцами Сеула, традиционными районами и современной культурой K-pop.', 'city', 3, 450000, 'easy', 12, 1, '/images/tours/seoul-palaces-1.jpg',
|
||||
'День 1: Дворец Кёнбоккун, смена караула, район Букчон Ханок. День 2: Дворец Чхандоккун, Тайный сад, рынок Инсадон. День 3: Намдэмун, Мёндон, башня Намсан, шоу K-pop.',
|
||||
ARRAY['Входные билеты в дворцы', 'Трансфер', 'Гид', 'Обеды'], true, true, NOW()),
|
||||
|
||||
('Пусан: Морской город', 'Откройте для себя главный порт Кореи, его пляжи, рыбные рынки и современную архитектуру.', 'city', 2, 320000, 'easy', 10, 4, '/images/tours/busan-1.jpg',
|
||||
'День 1: Пляж Хэундэ, храм Хэдон Ёнгунса, рыбный рынок Чагальчи. День 2: Остров Орюкдо, деревня Камчон, башня Пусан.',
|
||||
ARRAY['Трансфер', 'Гид', 'Входные билеты', 'Обеды'], true, true, NOW()),
|
||||
|
||||
('Кёнджу: Древняя столица', 'Посетите древнюю столицу династии Силла с её храмами, гробницами и археологическими памятниками.', 'city', 2, 380000, 'easy', 8, 5, '/images/tours/gyeongju-1.jpg',
|
||||
'День 1: Грот Соккурам, храм Пульгукса, музей Кёнджу. День 2: Парк Тумули, пруд Анапчи, обсерватория Чхомсондэ.',
|
||||
ARRAY['Трансфер', 'Гид-историк', 'Входные билеты', 'Обеды'], true, false, NOW()),
|
||||
|
||||
('Сораксан: Горные вершины', 'Треккинг в одном из красивейших национальных парков Кореи с водопадами и горными храмами.', 'mountain', 4, 680000, 'moderate', 8, 2, '/images/tours/seoraksan-1.jpg',
|
||||
'День 1: Прибытие, водопад Юктам, храм Синхынса. День 2: Подъём на Ульсанбави, канатная дорога. День 3: Треккинг к пику Тэчхонбон. День 4: Долина Пэктам, возвращение.',
|
||||
ARRAY['Проживание в горной хижине', 'Все питание', 'Гид-инструктор', 'Снаряжение'], true, true, NOW()),
|
||||
|
||||
('Чирисан: Духовный путь', 'Поход по священным тропам самого высокого материкового пика Кореи с посещением древних храмов.', 'mountain', 5, 750000, 'moderate', 6, 2, '/images/tours/jirisan-1.jpg',
|
||||
'День 1: Храм Хваомса, начало восхождения. День 2: Восхождение на пик Чонванбон. День 3: Переход через хребет. День 4: Храм Ссанггеса, медитация. День 5: Спуск, завершение маршрута.',
|
||||
ARRAY['Проживание в храмах и хижинах', 'Вегетарианское питание', 'Гид', 'Церемонии'], true, false, NOW()),
|
||||
|
||||
('Халласан (остров Чеджу)', 'Восхождение на высочайшую вершину Кореи на вулканическом острове Чеджу.', 'mountain', 3, 580000, 'moderate', 10, 2, '/images/tours/hallasan-1.jpg',
|
||||
'День 1: Прибытие на Чеджу, тропа Сонпанак. День 2: Восхождение на Халласан, кратерное озеро. День 3: Водопад Чонбан, базальтовые колонны.',
|
||||
ARRAY['Авиаперелёт', 'Проживание', 'Гид', 'Трансфер'], true, true, NOW()),
|
||||
|
||||
('Рыбалка у берегов Пусана', 'Морская рыбалка с опытным капитаном в богатых рыбой водах Корейского пролива.', 'fishing', 2, 420000, 'easy', 6, 3, '/images/tours/fishing-busan-1.jpg',
|
||||
'День 1: Инструктаж, выход в море, рыбалка на морского леща. День 2: Глубоководная рыбалка, приготовление улова.',
|
||||
ARRAY['Катер', 'Снасти', 'Инструктор', 'Приготовление рыбы'], true, true, NOW()),
|
||||
|
||||
('Рыбалка на Восточном море', 'Рыбалка в водах Восточного моря с ночёвкой на рыбацком судне и изучением традиционных методов ловли.', 'fishing', 3, 650000, 'moderate', 4, 3, '/images/tours/east-sea-fishing-1.jpg',
|
||||
'День 1: Выход из порта Сокчо, дневная рыбалка. День 2: Ночная рыбалка на кальмаров, ночёвка на судне. День 3: Утренняя рыбалка, возвращение в порт.',
|
||||
ARRAY['Судно', 'Все снасти', 'Питание на борту', 'Опытная команда'], true, false, NOW());
|
||||
|
||||
-- Статьи
|
||||
INSERT INTO articles (title, content, excerpt, category, image_url, author_id, views, is_published, created_at, updated_at) VALUES
|
||||
('Лучшее время для посещения Кореи',
|
||||
'Корея прекрасна в любое время года, но каждый сезон имеет свои особенности. Весна (март-май) - один из лучших периодов для посещения Кореи. Температура комфортная (15-20°C), цветёт сакура, особенно красиво в парках Сеула и Пусана. Лето жаркое и влажное с частыми дождями. Осень - самый популярный сезон среди туристов с потрясающими красками в горах. Зима холодная, но отличное время для горнолыжного спорта.',
|
||||
'Узнайте, когда лучше всего посещать Корею в зависимости от ваших предпочтений и планируемых активностей.',
|
||||
'travel-tips', '/images/articles/korea-seasons.jpg', 1, 1247, true, NOW(), NOW()),
|
||||
|
||||
('Корейская кухня для туристов',
|
||||
'Корейская кухня - это больше чем просто кимчи. Кимчи - ферментированные овощи, главное блюдо корейской кухни. Пибимпап - рис с овощами, мясом и яйцом. Пульгоги - маринованная говядина-гриль. Самгёпсаль - свиная грудинка на гриле. Соджу - корейская водка, самый популярный алкогольный напиток. Лучшие рестораны находятся в районах Мёндон и Хонгдэ в Сеуле.',
|
||||
'Полный гид по корейской кухне: что попробовать, где поесть и как заказывать.',
|
||||
'food', '/images/articles/korean-food.jpg', 1, 892, true, NOW(), NOW()),
|
||||
|
||||
('Топ-10 храмов Кореи',
|
||||
'Буддийские храмы Кореи - архитектурные шедевры. Пульгукса (Кёнджу) - объект ЮНЕСКО. Хэинса - хранилище Трипитаки Кореаны. Чогеса - главный храм в Сеуле. Синхынса - храм в Сораксане с гигантской статуей Будды. При посещении снимайте обувь, говорите тихо, фотографируйте без вспышки.',
|
||||
'Откройте для себя духовное наследие Кореи через посещение её самых значимых буддийских храмов.',
|
||||
'culture', '/images/articles/korean-temples.jpg', 2, 634, true, NOW(), NOW()),
|
||||
|
||||
('Что взять с собой в поход по корейским горам',
|
||||
'Для походов в корейские горы нужно: треккинговые ботинки с хорошим протектором, дождевик (погода меняется быстро), слои одежды, головной убор, солнцезащитный крем. Тропы хорошо размечены, есть места отдыха каждые 1-2 км, питьевая вода в хижинах. Обязательно регистрируйтесь на входе в парк и не сходите с троп.',
|
||||
'Полный список снаряжения и советы по безопасности для походов в корейские горы.',
|
||||
'nature', '/images/articles/hiking-gear.jpg', 2, 423, true, NOW(), NOW());
|
||||
|
||||
-- Отзывы
|
||||
INSERT INTO reviews (route_id, customer_name, customer_email, rating, comment, is_approved, created_at) VALUES
|
||||
(1, 'Анна Петрова', 'anna.petrova@email.com', 5, 'Три дня в Сеуле пролетели как один день. Гид Ким Мин Джун просто потрясающий - знает историю каждого камня во дворцах. Особенно понравился Тайный сад и шоу K-pop. Обязательно вернёмся!', true, NOW() - INTERVAL '5 days'),
|
||||
(4, 'Дмитрий Соколов', 'dmitry.sokolov@email.com', 5, 'Сораксан превзошёл все ожидания! Пак Со Ён - профессиональный гид, который заботится о безопасности группы. Виды с Ульсанбави просто космические. Рекомендую всем любителям гор!', true, NOW() - INTERVAL '12 days'),
|
||||
(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');
|
||||
|
||||
-- Настройки сайта
|
||||
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 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'),
|
||||
(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');
|
||||
|
||||
-- Сообщения от посетителей
|
||||
INSERT INTO contact_messages (name, email, phone, subject, message, status, created_at) VALUES
|
||||
('Анна Смирнова', 'anna.smirnova@email.com', '+7-912-567-8901', 'Вопрос о групповой скидке', 'Здравствуйте! Планируем поездку группой из 15 человек. Есть ли групповые скидки на туры по Сеулу?', 'unread', NOW() - INTERVAL '2 hours'),
|
||||
('Петр Козлов', 'petr.kozlov@email.com', '', 'Безопасность горных походов', 'Интересует безопасность походов в Сораксан. Какое снаряжение предоставляете? Есть ли медицинская поддержка?', 'read', NOW() - INTERVAL '1 day'),
|
||||
('Ольга Волкова', 'olga.volkova@email.com', '+7-967-678-9012', 'Индивидуальный тур', 'Можете ли организовать индивидуальный тур по Кёнджу на 5 дней с посещением всех исторических мест?', 'unread', NOW() - INTERVAL '6 hours');
|
||||
155
database/schema.sql
Normal file
155
database/schema.sql
Normal file
@@ -0,0 +1,155 @@
|
||||
-- Korea Tourism Agency Database Schema
|
||||
-- Создание основных таблиц для туристического агентства
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
|
||||
-- Таблица администраторов
|
||||
CREATE TABLE IF NOT EXISTS admins (
|
||||
id SERIAL PRIMARY KEY,
|
||||
username VARCHAR(50) UNIQUE NOT NULL,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
email VARCHAR(100) UNIQUE NOT NULL,
|
||||
role VARCHAR(20) DEFAULT 'admin',
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Таблица гидов
|
||||
CREATE TABLE IF NOT EXISTS guides (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
email VARCHAR(100) UNIQUE,
|
||||
phone VARCHAR(20),
|
||||
bio TEXT,
|
||||
specialization VARCHAR(20) NOT NULL CHECK (specialization IN ('city', 'mountain', 'fishing')),
|
||||
languages TEXT[], -- Массив языков
|
||||
experience INTEGER DEFAULT 0, -- Опыт в годах
|
||||
image_url VARCHAR(255),
|
||||
hourly_rate DECIMAL(8,2),
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Таблица маршрутов/туров
|
||||
CREATE TABLE IF NOT EXISTS routes (
|
||||
id SERIAL PRIMARY KEY,
|
||||
title VARCHAR(200) NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
content TEXT, -- Полное описание
|
||||
type VARCHAR(20) NOT NULL CHECK (type IN ('city', 'mountain', 'fishing')),
|
||||
price DECIMAL(10,2) NOT NULL,
|
||||
duration INTEGER NOT NULL, -- Длительность в часах
|
||||
difficulty_level VARCHAR(10) DEFAULT 'easy' CHECK (difficulty_level IN ('easy', 'moderate', 'hard')),
|
||||
max_group_size INTEGER DEFAULT 10,
|
||||
included_services TEXT[],
|
||||
meeting_point TEXT,
|
||||
image_url VARCHAR(255),
|
||||
guide_id INTEGER REFERENCES guides(id),
|
||||
is_featured BOOLEAN DEFAULT false,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Таблица статей
|
||||
CREATE TABLE IF NOT EXISTS articles (
|
||||
id SERIAL PRIMARY KEY,
|
||||
title VARCHAR(200) NOT NULL,
|
||||
excerpt TEXT,
|
||||
content TEXT NOT NULL,
|
||||
category VARCHAR(50) NOT NULL CHECK (category IN ('travel-tips', 'culture', 'food', 'nature', 'history')),
|
||||
image_url VARCHAR(255),
|
||||
author_id INTEGER REFERENCES admins(id),
|
||||
views INTEGER DEFAULT 0,
|
||||
is_published BOOLEAN DEFAULT false,
|
||||
meta_description TEXT,
|
||||
meta_keywords TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Таблица бронирований
|
||||
CREATE TABLE IF NOT EXISTS bookings (
|
||||
id SERIAL PRIMARY KEY,
|
||||
route_id INTEGER REFERENCES routes(id) NOT NULL,
|
||||
guide_id INTEGER REFERENCES guides(id),
|
||||
customer_name VARCHAR(100) NOT NULL,
|
||||
customer_email VARCHAR(100) NOT NULL,
|
||||
customer_phone VARCHAR(20),
|
||||
preferred_date DATE,
|
||||
group_size INTEGER DEFAULT 1,
|
||||
total_price DECIMAL(10,2),
|
||||
special_requirements TEXT,
|
||||
status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'confirmed', 'cancelled', 'completed')),
|
||||
notes TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Таблица отзывов
|
||||
CREATE TABLE IF NOT EXISTS reviews (
|
||||
id SERIAL PRIMARY KEY,
|
||||
route_id INTEGER REFERENCES routes(id),
|
||||
guide_id INTEGER REFERENCES guides(id),
|
||||
booking_id INTEGER REFERENCES bookings(id),
|
||||
customer_name VARCHAR(100) NOT NULL,
|
||||
customer_email VARCHAR(100),
|
||||
rating INTEGER CHECK (rating >= 1 AND rating <= 5) NOT NULL,
|
||||
comment TEXT,
|
||||
is_approved BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Таблица сообщений с формы контактов
|
||||
CREATE TABLE IF NOT EXISTS contact_messages (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
email VARCHAR(100) NOT NULL,
|
||||
phone VARCHAR(20),
|
||||
subject VARCHAR(200),
|
||||
message TEXT NOT NULL,
|
||||
status VARCHAR(20) DEFAULT 'unread' CHECK (status IN ('unread', 'read', 'replied')),
|
||||
admin_notes TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Таблица настроек сайта
|
||||
CREATE TABLE IF NOT EXISTS site_settings (
|
||||
id SERIAL PRIMARY KEY,
|
||||
setting_key VARCHAR(100) UNIQUE NOT NULL,
|
||||
setting_value TEXT,
|
||||
setting_type VARCHAR(20) DEFAULT 'text' CHECK (setting_type IN ('text', 'number', 'boolean', 'json')),
|
||||
description TEXT,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Создание индексов для производительности
|
||||
CREATE INDEX IF NOT EXISTS idx_routes_type ON routes(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_routes_active ON routes(is_active);
|
||||
CREATE INDEX IF NOT EXISTS idx_routes_featured ON routes(is_featured);
|
||||
CREATE INDEX IF NOT EXISTS idx_guides_specialization ON guides(specialization);
|
||||
CREATE INDEX IF NOT EXISTS idx_guides_active ON guides(is_active);
|
||||
CREATE INDEX IF NOT EXISTS idx_articles_category ON articles(category);
|
||||
CREATE INDEX IF NOT EXISTS idx_articles_published ON articles(is_published);
|
||||
CREATE INDEX IF NOT EXISTS idx_bookings_status ON bookings(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_bookings_date ON bookings(preferred_date);
|
||||
CREATE INDEX IF NOT EXISTS idx_reviews_rating ON reviews(rating);
|
||||
|
||||
-- Создание триггеров для автоматического обновления updated_at
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
CREATE TRIGGER update_admins_updated_at BEFORE UPDATE ON admins FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
CREATE TRIGGER update_guides_updated_at BEFORE UPDATE ON guides FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
CREATE TRIGGER update_routes_updated_at BEFORE UPDATE ON routes FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
CREATE TRIGGER update_articles_updated_at BEFORE UPDATE ON articles FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
CREATE TRIGGER update_bookings_updated_at BEFORE UPDATE ON bookings FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
CREATE TRIGGER update_site_settings_updated_at BEFORE UPDATE ON site_settings FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
250
database/seed.js
Normal file
250
database/seed.js
Normal file
@@ -0,0 +1,250 @@
|
||||
const db = require('../src/config/database');
|
||||
|
||||
async function seedDatabase() {
|
||||
try {
|
||||
console.log('🌱 Starting database seeding...');
|
||||
|
||||
// Seed guides
|
||||
const guides = [
|
||||
{
|
||||
name: 'Kim Min-jun',
|
||||
email: 'minjun@koreatourism.com',
|
||||
phone: '+82-10-1234-5678',
|
||||
bio: 'Experienced Seoul city guide with 8 years of expertise in Korean history and culture. Fluent in English, Japanese, and Chinese.',
|
||||
specialization: 'city',
|
||||
languages: ['Korean', 'English', 'Japanese', 'Chinese'],
|
||||
experience: 8,
|
||||
hourly_rate: 50000
|
||||
},
|
||||
{
|
||||
name: 'Park So-young',
|
||||
email: 'soyoung@koreatourism.com',
|
||||
phone: '+82-10-2345-6789',
|
||||
bio: 'Mountain hiking specialist with deep knowledge of Korean national parks. Safety-certified guide with wilderness first aid training.',
|
||||
specialization: 'mountain',
|
||||
languages: ['Korean', 'English'],
|
||||
experience: 6,
|
||||
hourly_rate: 45000
|
||||
},
|
||||
{
|
||||
name: 'Lee Sung-ho',
|
||||
email: 'sungho@koreatourism.com',
|
||||
phone: '+82-10-3456-7890',
|
||||
bio: 'Professional fishing guide specializing in coastal and river fishing. 10 years of experience with traditional Korean fishing techniques.',
|
||||
specialization: 'fishing',
|
||||
languages: ['Korean', 'English'],
|
||||
experience: 10,
|
||||
hourly_rate: 60000
|
||||
},
|
||||
{
|
||||
name: 'Choi Yeon-seo',
|
||||
email: 'yeonseo@koreatourism.com',
|
||||
phone: '+82-10-4567-8901',
|
||||
bio: 'Cultural heritage expert specializing in traditional Korean architecture and temples. PhD in Korean History.',
|
||||
specialization: 'city',
|
||||
languages: ['Korean', 'English', 'Mandarin'],
|
||||
experience: 12,
|
||||
hourly_rate: 55000
|
||||
}
|
||||
];
|
||||
|
||||
for (const guide of guides) {
|
||||
await db.query(`
|
||||
INSERT INTO guides (name, email, phone, bio, specialization, languages, experience, hourly_rate)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
ON CONFLICT (email) DO NOTHING
|
||||
`, [guide.name, guide.email, guide.phone, guide.bio, guide.specialization, guide.languages, guide.experience, guide.hourly_rate]);
|
||||
}
|
||||
console.log('✅ Guides seeded successfully');
|
||||
|
||||
// Get guide IDs for routes
|
||||
const guideRows = await db.query('SELECT id, name, specialization FROM guides ORDER BY id');
|
||||
const guideMap = {};
|
||||
guideRows.rows.forEach(guide => {
|
||||
if (!guideMap[guide.specialization]) guideMap[guide.specialization] = [];
|
||||
guideMap[guide.specialization].push(guide.id);
|
||||
});
|
||||
|
||||
// Seed routes
|
||||
const routes = [
|
||||
{
|
||||
title: 'Historic Seoul Walking Tour',
|
||||
description: 'Explore the ancient palaces and traditional markets of Seoul with our expert guide.',
|
||||
content: 'Discover the rich history of Seoul through a comprehensive walking tour that covers Gyeongbokgung Palace, Bukchon Hanok Village, and Insadong traditional market. Learn about Korean royal history, traditional architecture, and sample authentic Korean street food.',
|
||||
type: 'city',
|
||||
price: 75000,
|
||||
duration: 4,
|
||||
difficulty_level: 'easy',
|
||||
max_group_size: 15,
|
||||
included_services: ['Professional guide', 'Traditional tea ceremony', 'Palace entrance fees'],
|
||||
meeting_point: 'Gyeongbokgung Palace Main Gate',
|
||||
guide_id: guideMap.city ? guideMap.city[0] : null,
|
||||
is_featured: true
|
||||
},
|
||||
{
|
||||
title: 'Seoraksan National Park Hiking',
|
||||
description: 'Experience the breathtaking beauty of Seoraksan with guided mountain hiking.',
|
||||
content: 'Join us for an unforgettable hiking experience in Seoraksan National Park. This moderate-level hike takes you through stunning mountain landscapes, ancient temples, and offers spectacular views from the peaks. Perfect for nature lovers and photography enthusiasts.',
|
||||
type: 'mountain',
|
||||
price: 120000,
|
||||
duration: 8,
|
||||
difficulty_level: 'moderate',
|
||||
max_group_size: 8,
|
||||
included_services: ['Certified mountain guide', 'Safety equipment', 'Traditional lunch', 'Transportation'],
|
||||
meeting_point: 'Sokcho Bus Terminal',
|
||||
guide_id: guideMap.mountain ? guideMap.mountain[0] : null,
|
||||
is_featured: true
|
||||
},
|
||||
{
|
||||
title: 'East Sea Fishing Adventure',
|
||||
description: 'Traditional Korean fishing experience in the beautiful East Sea.',
|
||||
content: 'Learn traditional Korean fishing techniques while enjoying the scenic beauty of the East Sea. Our experienced guide will teach you about local marine life, traditional fishing methods, and you\'ll enjoy a fresh seafood lunch prepared from your catch.',
|
||||
type: 'fishing',
|
||||
price: 95000,
|
||||
duration: 6,
|
||||
difficulty_level: 'easy',
|
||||
max_group_size: 6,
|
||||
included_services: ['Fishing equipment', 'Boat charter', 'Fresh seafood lunch', 'Professional guide'],
|
||||
meeting_point: 'Gangneung Harbor',
|
||||
guide_id: guideMap.fishing ? guideMap.fishing[0] : null,
|
||||
is_featured: true
|
||||
},
|
||||
{
|
||||
title: 'Busan Coastal City Tour',
|
||||
description: 'Discover the vibrant coastal city of Busan with its beaches, temples, and markets.',
|
||||
content: 'Explore Busan\'s highlights including Haeundae Beach, Jagalchi Fish Market, Gamcheon Culture Village, and Beomeosa Temple. Experience the unique culture of Korea\'s largest port city.',
|
||||
type: 'city',
|
||||
price: 85000,
|
||||
duration: 6,
|
||||
difficulty_level: 'easy',
|
||||
max_group_size: 12,
|
||||
included_services: ['Professional guide', 'Market tasting', 'Temple entrance', 'Local transportation'],
|
||||
meeting_point: 'Busan Station',
|
||||
guide_id: guideMap.city ? guideMap.city[1] : null,
|
||||
is_featured: false
|
||||
},
|
||||
{
|
||||
title: 'Jirisan Mountain Expedition',
|
||||
description: 'Challenge yourself with a multi-day trek through Korea\'s largest national park.',
|
||||
content: 'Experience the wilderness of Jirisan National Park on this challenging multi-day expedition. Trek through ancient forests, visit remote temples, and enjoy spectacular mountain vistas.',
|
||||
type: 'mountain',
|
||||
price: 250000,
|
||||
duration: 16,
|
||||
difficulty_level: 'hard',
|
||||
max_group_size: 4,
|
||||
included_services: ['Expert guide', 'Camping equipment', 'All meals', 'Emergency support'],
|
||||
meeting_point: 'Jirisan National Park Visitor Center',
|
||||
guide_id: guideMap.mountain ? guideMap.mountain[0] : null,
|
||||
is_featured: false
|
||||
},
|
||||
{
|
||||
title: 'Jeju Island Fishing Charter',
|
||||
description: 'Deep sea fishing adventure around the beautiful Jeju Island.',
|
||||
content: 'Experience world-class deep sea fishing in the waters around Jeju Island. Target species include tuna, marlin, and various local fish while enjoying the stunning volcanic island scenery.',
|
||||
type: 'fishing',
|
||||
price: 180000,
|
||||
duration: 10,
|
||||
difficulty_level: 'moderate',
|
||||
max_group_size: 8,
|
||||
included_services: ['Charter boat', 'Professional crew', 'Equipment', 'Lunch', 'Fish preparation'],
|
||||
meeting_point: 'Jeju Harbor',
|
||||
guide_id: guideMap.fishing ? guideMap.fishing[0] : null,
|
||||
is_featured: false
|
||||
}
|
||||
];
|
||||
|
||||
for (const route of routes) {
|
||||
await db.query(`
|
||||
INSERT INTO routes (
|
||||
title, description, content, type, price, duration,
|
||||
difficulty_level, max_group_size, included_services,
|
||||
meeting_point, guide_id, is_featured
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||
`, [
|
||||
route.title, route.description, route.content, route.type,
|
||||
route.price, route.duration, route.difficulty_level,
|
||||
route.max_group_size, route.included_services,
|
||||
route.meeting_point, route.guide_id, route.is_featured
|
||||
]);
|
||||
}
|
||||
console.log('✅ Routes seeded successfully');
|
||||
|
||||
// Seed articles
|
||||
const adminResult = await db.query('SELECT id FROM admins LIMIT 1');
|
||||
const adminId = adminResult.rows[0]?.id;
|
||||
|
||||
const articles = [
|
||||
{
|
||||
title: 'Best Time to Visit Korea: A Seasonal Guide',
|
||||
excerpt: 'Discover when to visit Korea for the perfect weather and experiences throughout the year.',
|
||||
content: `Korea offers something special in every season, making it a year-round destination...
|
||||
|
||||
Spring (March-May): Cherry blossoms bloom across the country, creating stunning pink canopies. This is perhaps the most popular time to visit Korea. The weather is mild and perfect for outdoor activities.
|
||||
|
||||
Summer (June-August): Hot and humid with occasional monsoons. Great for mountain hiking and coastal activities. Many festivals happen during this time.
|
||||
|
||||
Autumn (September-November): Spectacular fall foliage and comfortable temperatures. Ideal for hiking and sightseeing. Clear skies offer great mountain views.
|
||||
|
||||
Winter (December-February): Cold but beautiful, especially with snow. Perfect for winter sports and enjoying hot springs. Christmas and New Year celebrations add to the charm.`,
|
||||
category: 'travel-tips',
|
||||
author_id: adminId,
|
||||
is_published: true
|
||||
},
|
||||
{
|
||||
title: 'Korean Temple Etiquette: Respectful Visiting Tips',
|
||||
excerpt: 'Learn the proper way to visit Korean Buddhist temples and show respect for local customs.',
|
||||
content: `Visiting Korean temples can be a deeply spiritual and cultural experience. Here are essential tips for respectful temple visits...
|
||||
|
||||
Dress Code: Wear modest clothing that covers shoulders and knees. Avoid revealing or tight clothing.
|
||||
|
||||
Behavior: Maintain quiet voices and respectful demeanor. Remove hats and sunglasses before entering temple halls.
|
||||
|
||||
Photography: Ask permission before taking photos of people, especially monks. Some areas may prohibit photography entirely.
|
||||
|
||||
Temple Stay: Many temples offer overnight experiences where you can participate in meditation and daily temple life.`,
|
||||
category: 'culture',
|
||||
author_id: adminId,
|
||||
is_published: true
|
||||
},
|
||||
{
|
||||
title: 'Korean Street Food You Must Try',
|
||||
excerpt: 'A delicious guide to Korea\'s most popular street foods and where to find them.',
|
||||
content: `Korean street food is an adventure for your taste buds. Here are the must-try dishes...
|
||||
|
||||
Tteokbokki: Spicy rice cakes in gochujang sauce, a Korean comfort food classic.
|
||||
|
||||
Hotteok: Sweet pancakes filled with sugar, nuts, and cinnamon, perfect for cold days.
|
||||
|
||||
Bungeoppang: Fish-shaped pastries filled with sweet red bean paste.
|
||||
|
||||
Kimbap: Korean rice rolls with various fillings, perfect for a quick meal.
|
||||
|
||||
Odeng: Fish cake soup served hot from street vendors, especially popular in winter.`,
|
||||
category: 'food',
|
||||
author_id: adminId,
|
||||
is_published: true
|
||||
}
|
||||
];
|
||||
|
||||
for (const article of articles) {
|
||||
await db.query(`
|
||||
INSERT INTO articles (title, excerpt, content, category, author_id, is_published)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
`, [article.title, article.excerpt, article.content, article.category, article.author_id, article.is_published]);
|
||||
}
|
||||
console.log('✅ Articles seeded successfully');
|
||||
|
||||
console.log('✨ Database seeding completed successfully!');
|
||||
process.exit(0);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Seeding failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
seedDatabase();
|
||||
}
|
||||
|
||||
module.exports = { seedDatabase };
|
||||
Reference in New Issue
Block a user