BIG_UPDATE deleted vps, added s3 infrastructure.
This commit is contained in:
65
ospabhost/backend/prisma/FOREIGN_KEY_FIX.md
Normal file
65
ospabhost/backend/prisma/FOREIGN_KEY_FIX.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# Решение проблемы Foreign Key при удалении тарифов
|
||||
|
||||
## Проблема
|
||||
При попытке удалить тариф через Prisma Studio появляется ошибка:
|
||||
```
|
||||
Foreign key constraint violated on the fields: (`tariffId`)
|
||||
```
|
||||
|
||||
## Причина
|
||||
Тариф используется серверами. MySQL не позволяет удалить тариф, если на него ссылаются записи в таблице `server`.
|
||||
|
||||
## Решение
|
||||
|
||||
### Способ 1: Безопасный (рекомендуется)
|
||||
Удаляет только неиспользуемые тарифы, сохраняет серверы.
|
||||
|
||||
```bash
|
||||
mysql -u root -p ospabhost < backend/prisma/safe_tariff_migration.sql
|
||||
```
|
||||
|
||||
### Способ 2: Полная очистка (только для dev!)
|
||||
Удаляет ВСЕ серверы и тарифы.
|
||||
|
||||
```bash
|
||||
# Сначала бэкап!
|
||||
mysqldump -u root -p ospabhost > backup.sql
|
||||
|
||||
# Потом очистка
|
||||
mysql -u root -p ospabhost < backend/prisma/clean_slate_migration.sql
|
||||
```
|
||||
|
||||
### Способ 3: Ручное удаление через SQL
|
||||
|
||||
```sql
|
||||
-- 1. Найти тарифы без серверов
|
||||
SELECT t.id, t.name, COUNT(s.id) as servers
|
||||
FROM tariff t
|
||||
LEFT JOIN server s ON s.tariffId = t.id
|
||||
GROUP BY t.id
|
||||
HAVING servers = 0;
|
||||
|
||||
-- 2. Удалить только неиспользуемые
|
||||
DELETE FROM tariff WHERE id IN (
|
||||
SELECT id FROM (
|
||||
SELECT t.id FROM tariff t
|
||||
LEFT JOIN server s ON s.tariffId = t.id
|
||||
GROUP BY t.id
|
||||
HAVING COUNT(s.id) = 0
|
||||
) as unused
|
||||
);
|
||||
|
||||
-- 3. Добавить категорию
|
||||
ALTER TABLE tariff ADD COLUMN category VARCHAR(50) NOT NULL DEFAULT 'vps';
|
||||
|
||||
-- 4. Добавить новые тарифы (см. safe_tariff_migration.sql)
|
||||
```
|
||||
|
||||
## Перезапуск backend
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
npm start
|
||||
```
|
||||
|
||||
Готово! 🎉
|
||||
43
ospabhost/backend/prisma/add_tariff_categories.sql
Normal file
43
ospabhost/backend/prisma/add_tariff_categories.sql
Normal file
@@ -0,0 +1,43 @@
|
||||
-- Добавление поля category в таблицу tariff
|
||||
ALTER TABLE `tariff` ADD COLUMN `category` VARCHAR(50) NOT NULL DEFAULT 'vps' AFTER `description`;
|
||||
|
||||
-- Удаление старых тарифов (если нужно)
|
||||
-- DELETE FROM `tariff`;
|
||||
|
||||
-- ============================================
|
||||
-- VPS/VDS Тарифы
|
||||
-- ============================================
|
||||
|
||||
INSERT INTO `tariff` (`name`, `price`, `description`, `category`) VALUES
|
||||
('VPS Starter', 299, '1 vCore, 1 GB RAM, 10 GB SSD, 1 TB трафик', 'vps'),
|
||||
('VPS Basic', 499, '2 vCore, 2 GB RAM, 20 GB SSD, 2 TB трафик', 'vps'),
|
||||
('VPS Standard', 899, '4 vCore, 4 GB RAM, 40 GB SSD, 3 TB трафик', 'vps'),
|
||||
('VPS Advanced', 1499, '6 vCore, 8 GB RAM, 80 GB SSD, 5 TB трафик', 'vps'),
|
||||
('VPS Pro', 2499, '8 vCore, 16 GB RAM, 160 GB SSD, Unlimited трафик', 'vps'),
|
||||
('VPS Enterprise', 4999, '16 vCore, 32 GB RAM, 320 GB SSD, Unlimited трафик', 'vps');
|
||||
|
||||
-- ============================================
|
||||
-- Хостинг для сайтов
|
||||
-- ============================================
|
||||
|
||||
INSERT INTO `tariff` (`name`, `price`, `description`, `category`) VALUES
|
||||
('Хостинг Lite', 149, '1 сайт, 5 GB SSD, 10 GB трафик, 1 БД MySQL', 'hosting'),
|
||||
('Хостинг Start', 249, '3 сайта, 10 GB SSD, 50 GB трафик, 3 БД MySQL', 'hosting'),
|
||||
('Хостинг Plus', 449, '10 сайтов, 25 GB SSD, 100 GB трафик, 10 БД MySQL', 'hosting'),
|
||||
('Хостинг Business', 799, 'Безлимит сайтов, 50 GB SSD, 500 GB трафик, Безлимит БД', 'hosting'),
|
||||
('Хостинг Premium', 1299, 'Безлимит сайтов, 100 GB SSD, Unlimited трафик, Безлимит БД, SSL', 'hosting');
|
||||
|
||||
-- ============================================
|
||||
-- S3 Хранилище
|
||||
-- ============================================
|
||||
|
||||
INSERT INTO `tariff` (`name`, `price`, `description`, `category`) VALUES
|
||||
('S3 Mini', 99, '10 GB хранилище, 50 GB трафик, API доступ', 's3'),
|
||||
('S3 Basic', 199, '50 GB хранилище, 200 GB трафик, API доступ', 's3'),
|
||||
('S3 Standard', 399, '200 GB хранилище, 1 TB трафик, API доступ, CDN', 's3'),
|
||||
('S3 Advanced', 799, '500 GB хранилище, 3 TB трафик, API доступ, CDN', 's3'),
|
||||
('S3 Pro', 1499, '1 TB хранилище, 10 TB трафик, API доступ, CDN, Priority Support', 's3'),
|
||||
('S3 Enterprise', 2999, '5 TB хранилище, Unlimited трафик, API доступ, CDN, Priority Support', 's3');
|
||||
|
||||
-- Проверка добавленных тарифов
|
||||
SELECT * FROM `tariff` ORDER BY `category`, `price`;
|
||||
57
ospabhost/backend/prisma/apply-migration.ts
Normal file
57
ospabhost/backend/prisma/apply-migration.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { prisma } from '../src/prisma/client';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
async function applyMigration() {
|
||||
try {
|
||||
const sqlPath = path.join(__dirname, 'migrations_manual', 'add_sessions_qr_tickets_features.sql');
|
||||
const sql = fs.readFileSync(sqlPath, 'utf-8');
|
||||
|
||||
// Удаляем комментарии и разделяем по точке с запятой
|
||||
const cleaned = sql
|
||||
.split('\n')
|
||||
.filter(line => !line.trim().startsWith('--'))
|
||||
.join('\n');
|
||||
|
||||
const statements = cleaned
|
||||
.split(';')
|
||||
.map(s => s.trim())
|
||||
.filter(s => s.length > 0);
|
||||
|
||||
console.log(`🚀 Применяю миграцию: ${statements.length} запросов...`);
|
||||
|
||||
for (let i = 0; i < statements.length; i++) {
|
||||
const statement = statements[i];
|
||||
const preview = statement.replace(/\s+/g, ' ').substring(0, 150);
|
||||
console.log(`\n[${i + 1}/${statements.length}] Выполняю:`);
|
||||
console.log(preview + '...');
|
||||
|
||||
try {
|
||||
await prisma.$executeRawUnsafe(statement);
|
||||
console.log('✅ Успешно');
|
||||
} catch (error: any) {
|
||||
// Игнорируем ошибки "duplicate column" и "table already exists"
|
||||
if (
|
||||
error.message.includes('Duplicate column') ||
|
||||
error.message.includes('already exists') ||
|
||||
error.message.includes('Duplicate key')
|
||||
) {
|
||||
console.log('⚠️ Уже существует, пропускаю...');
|
||||
} else {
|
||||
console.error('❌ Ошибка:', error.message);
|
||||
// Не выбрасываем ошибку, продолжаем выполнение
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n✅ Миграция завершена!');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('❌ Критическая ошибка:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
applyMigration();
|
||||
73
ospabhost/backend/prisma/clean_slate_migration.sql
Normal file
73
ospabhost/backend/prisma/clean_slate_migration.sql
Normal file
@@ -0,0 +1,73 @@
|
||||
-- ============================================
|
||||
-- ВНИМАНИЕ! Этот скрипт удалит ВСЕ серверы и тарифы
|
||||
-- Используйте только если хотите начать с чистого листа
|
||||
-- ============================================
|
||||
|
||||
-- Шаг 1: Проверяем, что будет удалено
|
||||
SELECT 'Servers to delete:' as info, COUNT(*) as count FROM `server`;
|
||||
SELECT 'Tariffs to delete:' as info, COUNT(*) as count FROM `tariff`;
|
||||
|
||||
-- Шаг 2: Удаляем все связанные данные (в правильном порядке)
|
||||
|
||||
-- Удаляем метрики серверов
|
||||
DELETE FROM `server_metrics`;
|
||||
|
||||
-- Удаляем платежи
|
||||
DELETE FROM `payment`;
|
||||
|
||||
-- Удаляем серверы (это разрешит удаление тарифов)
|
||||
DELETE FROM `server`;
|
||||
|
||||
-- Удаляем все тарифы
|
||||
DELETE FROM `tariff`;
|
||||
|
||||
-- Шаг 3: Добавляем поле category
|
||||
ALTER TABLE `tariff`
|
||||
ADD COLUMN IF NOT EXISTS `category` VARCHAR(50) NOT NULL DEFAULT 'vps'
|
||||
AFTER `description`;
|
||||
|
||||
-- Шаг 4: Добавляем новые тарифы
|
||||
-- ============================================
|
||||
-- VPS/VDS Тарифы
|
||||
-- ============================================
|
||||
|
||||
INSERT INTO `tariff` (`name`, `price`, `description`, `category`) VALUES
|
||||
('VPS Starter', 299, '1 vCore, 1 GB RAM, 10 GB SSD, 1 TB трафик', 'vps'),
|
||||
('VPS Basic', 499, '2 vCore, 2 GB RAM, 20 GB SSD, 2 TB трафик', 'vps'),
|
||||
('VPS Standard', 899, '4 vCore, 4 GB RAM, 40 GB SSD, 3 TB трафик', 'vps'),
|
||||
('VPS Advanced', 1499, '6 vCore, 8 GB RAM, 80 GB SSD, 5 TB трафик', 'vps'),
|
||||
('VPS Pro', 2499, '8 vCore, 16 GB RAM, 160 GB SSD, Unlimited трафик', 'vps'),
|
||||
('VPS Enterprise', 4999, '16 vCore, 32 GB RAM, 320 GB SSD, Unlimited трафик', 'vps');
|
||||
|
||||
-- ============================================
|
||||
-- Хостинг для сайтов
|
||||
-- ============================================
|
||||
|
||||
INSERT INTO `tariff` (`name`, `price`, `description`, `category`) VALUES
|
||||
('Хостинг Lite', 149, '1 сайт, 5 GB SSD, 10 GB трафик, 1 БД MySQL', 'hosting'),
|
||||
('Хостинг Start', 249, '3 сайта, 10 GB SSD, 50 GB трафик, 3 БД MySQL', 'hosting'),
|
||||
('Хостинг Plus', 449, '10 сайтов, 25 GB SSD, 100 GB трафик, 10 БД MySQL', 'hosting'),
|
||||
('Хостинг Business', 799, 'Безлимит сайтов, 50 GB SSD, 500 GB трафик, Безлимит БД', 'hosting'),
|
||||
('Хостинг Premium', 1299, 'Безлимит сайтов, 100 GB SSD, Unlimited трафик, Безлимит БД, SSL', 'hosting');
|
||||
|
||||
-- ============================================
|
||||
-- S3 Хранилище
|
||||
-- ============================================
|
||||
|
||||
INSERT INTO `tariff` (`name`, `price`, `description`, `category`) VALUES
|
||||
('S3 Mini', 99, '10 GB хранилище, 50 GB трафик, API доступ', 's3'),
|
||||
('S3 Basic', 199, '50 GB хранилище, 200 GB трафик, API доступ', 's3'),
|
||||
('S3 Standard', 399, '200 GB хранилище, 1 TB трафик, API доступ, CDN', 's3'),
|
||||
('S3 Advanced', 799, '500 GB хранилище, 3 TB трафик, API доступ, CDN', 's3'),
|
||||
('S3 Pro', 1499, '1 TB хранилище, 10 TB трафик, API доступ, CDN, Priority Support', 's3'),
|
||||
('S3 Enterprise', 2999, '5 TB хранилище, Unlimited трафик, API доступ, CDN, Priority Support', 's3');
|
||||
|
||||
-- Шаг 5: Проверка
|
||||
SELECT 'New tariffs:' as info;
|
||||
SELECT * FROM `tariff` ORDER BY `category`, `price`;
|
||||
|
||||
-- Сброс AUTO_INCREMENT (опционально)
|
||||
ALTER TABLE `tariff` AUTO_INCREMENT = 1;
|
||||
ALTER TABLE `server` AUTO_INCREMENT = 1;
|
||||
ALTER TABLE `payment` AUTO_INCREMENT = 1;
|
||||
ALTER TABLE `server_metrics` AUTO_INCREMENT = 1;
|
||||
18
ospabhost/backend/prisma/manual_migration_category.sql
Normal file
18
ospabhost/backend/prisma/manual_migration_category.sql
Normal file
@@ -0,0 +1,18 @@
|
||||
-- ============================================
|
||||
-- Миграция: Добавление category к тарифам
|
||||
-- Дата: 8 ноября 2025
|
||||
-- ============================================
|
||||
|
||||
-- Шаг 1: Добавление поля category (если ещё не существует)
|
||||
ALTER TABLE `tariff`
|
||||
ADD COLUMN IF NOT EXISTS `category` VARCHAR(50) NOT NULL DEFAULT 'vps'
|
||||
AFTER `description`;
|
||||
|
||||
-- Шаг 2: Обновление существующих тарифов (если есть)
|
||||
UPDATE `tariff` SET `category` = 'vps' WHERE `category` IS NULL OR `category` = '';
|
||||
|
||||
-- Проверка структуры таблицы
|
||||
DESCRIBE `tariff`;
|
||||
|
||||
-- Показать текущие тарифы
|
||||
SELECT * FROM `tariff` ORDER BY `category`, `price`;
|
||||
25
ospabhost/backend/prisma/migrations/add_server_metrics.sql
Normal file
25
ospabhost/backend/prisma/migrations/add_server_metrics.sql
Normal file
@@ -0,0 +1,25 @@
|
||||
-- Миграция для добавления таблицы метрик серверов
|
||||
|
||||
CREATE TABLE `server_metrics` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`serverId` INT NOT NULL,
|
||||
`cpuUsage` DOUBLE NOT NULL DEFAULT 0,
|
||||
`memoryUsage` DOUBLE NOT NULL DEFAULT 0,
|
||||
`memoryUsed` BIGINT NOT NULL DEFAULT 0,
|
||||
`memoryMax` BIGINT NOT NULL DEFAULT 0,
|
||||
`diskUsage` DOUBLE NOT NULL DEFAULT 0,
|
||||
`diskUsed` BIGINT NOT NULL DEFAULT 0,
|
||||
`diskMax` BIGINT NOT NULL DEFAULT 0,
|
||||
`networkIn` BIGINT NOT NULL DEFAULT 0,
|
||||
`networkOut` BIGINT NOT NULL DEFAULT 0,
|
||||
`status` VARCHAR(191) NOT NULL DEFAULT 'unknown',
|
||||
`uptime` BIGINT NOT NULL DEFAULT 0,
|
||||
`timestamp` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `server_metrics_serverId_timestamp_idx` (`serverId`, `timestamp`),
|
||||
|
||||
CONSTRAINT `server_metrics_serverId_fkey`
|
||||
FOREIGN KEY (`serverId`) REFERENCES `server`(`id`)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
@@ -0,0 +1,125 @@
|
||||
-- Миграция: Добавление системы сессий, QR-авторизации и улучшенных тикетов
|
||||
-- Дата: 2025-11-09
|
||||
|
||||
-- 1. Обновление таблицы ticket (добавление новых полей)
|
||||
ALTER TABLE `ticket`
|
||||
MODIFY COLUMN `message` TEXT NOT NULL,
|
||||
ADD COLUMN `priority` VARCHAR(20) DEFAULT 'normal' AFTER `status`,
|
||||
ADD COLUMN `category` VARCHAR(50) DEFAULT 'general' AFTER `priority`,
|
||||
ADD COLUMN `assignedTo` INT NULL AFTER `category`,
|
||||
ADD COLUMN `closedAt` DATETIME NULL AFTER `updatedAt`;
|
||||
|
||||
-- 2. Обновление таблицы response
|
||||
ALTER TABLE `response`
|
||||
MODIFY COLUMN `message` TEXT NOT NULL,
|
||||
ADD COLUMN `isInternal` BOOLEAN DEFAULT FALSE AFTER `message`;
|
||||
|
||||
-- 3. Создание таблицы для прикреплённых файлов к тикетам
|
||||
CREATE TABLE IF NOT EXISTS `ticket_attachment` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`ticketId` INT NOT NULL,
|
||||
`filename` VARCHAR(255) NOT NULL,
|
||||
`fileUrl` VARCHAR(500) NOT NULL,
|
||||
`fileSize` INT NOT NULL,
|
||||
`mimeType` VARCHAR(100) NOT NULL,
|
||||
`createdAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `ticketId_idx` (`ticketId`),
|
||||
CONSTRAINT `ticket_attachment_ticketId_fkey`
|
||||
FOREIGN KEY (`ticketId`)
|
||||
REFERENCES `ticket`(`id`)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 4. Создание таблицы для прикреплённых файлов к ответам
|
||||
CREATE TABLE IF NOT EXISTS `response_attachment` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`responseId` INT NOT NULL,
|
||||
`filename` VARCHAR(255) NOT NULL,
|
||||
`fileUrl` VARCHAR(500) NOT NULL,
|
||||
`fileSize` INT NOT NULL,
|
||||
`mimeType` VARCHAR(100) NOT NULL,
|
||||
`createdAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `responseId_idx` (`responseId`),
|
||||
CONSTRAINT `response_attachment_responseId_fkey`
|
||||
FOREIGN KEY (`responseId`)
|
||||
REFERENCES `response`(`id`)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 5. Создание таблицы QR-авторизации
|
||||
CREATE TABLE IF NOT EXISTS `qr_login_request` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`code` VARCHAR(128) NOT NULL,
|
||||
`userId` INT NULL,
|
||||
`status` VARCHAR(20) DEFAULT 'pending',
|
||||
`ipAddress` VARCHAR(45) NULL,
|
||||
`userAgent` TEXT NULL,
|
||||
`createdAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`expiresAt` DATETIME NOT NULL,
|
||||
`confirmedAt` DATETIME NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `code_unique` (`code`),
|
||||
INDEX `code_idx` (`code`),
|
||||
INDEX `status_expiresAt_idx` (`status`, `expiresAt`),
|
||||
INDEX `userId_idx` (`userId`),
|
||||
CONSTRAINT `qr_login_request_userId_fkey`
|
||||
FOREIGN KEY (`userId`)
|
||||
REFERENCES `user`(`id`)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 6. Проверка и создание таблицы session (если не существует)
|
||||
CREATE TABLE IF NOT EXISTS `session` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`userId` INT NOT NULL,
|
||||
`token` VARCHAR(500) NOT NULL,
|
||||
`ipAddress` VARCHAR(45) NULL,
|
||||
`userAgent` TEXT NULL,
|
||||
`device` VARCHAR(50) NULL,
|
||||
`browser` VARCHAR(50) NULL,
|
||||
`location` VARCHAR(200) NULL,
|
||||
`lastActivity` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`createdAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`expiresAt` DATETIME NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `token_unique` (`token`),
|
||||
INDEX `userId_idx` (`userId`),
|
||||
CONSTRAINT `session_userId_fkey`
|
||||
FOREIGN KEY (`userId`)
|
||||
REFERENCES `user`(`id`)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 7. Проверка и создание таблицы login_history (если не существует)
|
||||
CREATE TABLE IF NOT EXISTS `login_history` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`userId` INT NOT NULL,
|
||||
`ipAddress` VARCHAR(45) NOT NULL,
|
||||
`userAgent` TEXT NULL,
|
||||
`device` VARCHAR(50) NULL,
|
||||
`browser` VARCHAR(50) NULL,
|
||||
`location` VARCHAR(200) NULL,
|
||||
`success` BOOLEAN DEFAULT TRUE,
|
||||
`createdAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `userId_idx` (`userId`),
|
||||
INDEX `createdAt_idx` (`createdAt`),
|
||||
CONSTRAINT `login_history_userId_fkey`
|
||||
FOREIGN KEY (`userId`)
|
||||
REFERENCES `user`(`id`)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 8. Обновление статусов тикетов для существующих записей
|
||||
UPDATE `ticket` SET `priority` = 'normal' WHERE `priority` IS NULL;
|
||||
UPDATE `ticket` SET `category` = 'general' WHERE `category` IS NULL;
|
||||
|
||||
-- Готово!
|
||||
SELECT 'Migration completed successfully!' as status;
|
||||
86
ospabhost/backend/prisma/safe_tariff_migration.sql
Normal file
86
ospabhost/backend/prisma/safe_tariff_migration.sql
Normal file
@@ -0,0 +1,86 @@
|
||||
-- ============================================
|
||||
-- Безопасная миграция тарифов
|
||||
-- Удаление старых тарифов с учётом foreign key
|
||||
-- ============================================
|
||||
|
||||
-- Шаг 1: Проверяем, какие тарифы используются серверами
|
||||
SELECT
|
||||
t.id,
|
||||
t.name,
|
||||
COUNT(s.id) as servers_count
|
||||
FROM `tariff` t
|
||||
LEFT JOIN `server` s ON s.tariffId = t.id
|
||||
GROUP BY t.id, t.name
|
||||
ORDER BY t.name;
|
||||
|
||||
-- Шаг 2: Добавляем поле category (если ещё нет)
|
||||
ALTER TABLE `tariff`
|
||||
ADD COLUMN IF NOT EXISTS `category` VARCHAR(50) NOT NULL DEFAULT 'vps'
|
||||
AFTER `description`;
|
||||
|
||||
-- Шаг 3: ВАРИАНТ А - Обновить существующие тарифы вместо удаления
|
||||
-- Присваиваем категории существующим тарифам
|
||||
UPDATE `tariff` SET `category` = 'vps' WHERE `category` IS NULL OR `category` = '';
|
||||
|
||||
-- Шаг 4: ВАРИАНТ Б - Удалить только неиспользуемые тарифы
|
||||
-- Создаём временную таблицу с ID используемых тарифов
|
||||
CREATE TEMPORARY TABLE used_tariffs AS
|
||||
SELECT DISTINCT tariffId FROM `server`;
|
||||
|
||||
-- Удаляем только те тарифы, которые НЕ используются
|
||||
DELETE FROM `tariff`
|
||||
WHERE id NOT IN (SELECT tariffId FROM used_tariffs);
|
||||
|
||||
-- Удаляем временную таблицу
|
||||
DROP TEMPORARY TABLE used_tariffs;
|
||||
|
||||
-- Шаг 5: Добавляем новые тарифы
|
||||
-- ============================================
|
||||
-- VPS/VDS Тарифы
|
||||
-- ============================================
|
||||
|
||||
INSERT INTO `tariff` (`name`, `price`, `description`, `category`) VALUES
|
||||
('VPS Starter', 299, '1 vCore, 1 GB RAM, 10 GB SSD, 1 TB трафик', 'vps'),
|
||||
('VPS Basic', 499, '2 vCore, 2 GB RAM, 20 GB SSD, 2 TB трафик', 'vps'),
|
||||
('VPS Standard', 899, '4 vCore, 4 GB RAM, 40 GB SSD, 3 TB трафик', 'vps'),
|
||||
('VPS Advanced', 1499, '6 vCore, 8 GB RAM, 80 GB SSD, 5 TB трафик', 'vps'),
|
||||
('VPS Pro', 2499, '8 vCore, 16 GB RAM, 160 GB SSD, Unlimited трафик', 'vps'),
|
||||
('VPS Enterprise', 4999, '16 vCore, 32 GB RAM, 320 GB SSD, Unlimited трафик', 'vps');
|
||||
|
||||
-- ============================================
|
||||
-- Хостинг для сайтов
|
||||
-- ============================================
|
||||
|
||||
INSERT INTO `tariff` (`name`, `price`, `description`, `category`) VALUES
|
||||
('Хостинг Lite', 149, '1 сайт, 5 GB SSD, 10 GB трафик, 1 БД MySQL', 'hosting'),
|
||||
('Хостинг Start', 249, '3 сайта, 10 GB SSD, 50 GB трафик, 3 БД MySQL', 'hosting'),
|
||||
('Хостинг Plus', 449, '10 сайтов, 25 GB SSD, 100 GB трафик, 10 БД MySQL', 'hosting'),
|
||||
('Хостинг Business', 799, 'Безлимит сайтов, 50 GB SSD, 500 GB трафик, Безлимит БД', 'hosting'),
|
||||
('Хостинг Premium', 1299, 'Безлимит сайтов, 100 GB SSD, Unlimited трафик, Безлимит БД, SSL', 'hosting');
|
||||
|
||||
-- ============================================
|
||||
-- S3 Хранилище
|
||||
-- ============================================
|
||||
|
||||
INSERT INTO `tariff` (`name`, `price`, `description`, `category`) VALUES
|
||||
('S3 Mini', 99, '10 GB хранилище, 50 GB трафик, API доступ', 's3'),
|
||||
('S3 Basic', 199, '50 GB хранилище, 200 GB трафик, API доступ', 's3'),
|
||||
('S3 Standard', 399, '200 GB хранилище, 1 TB трафик, API доступ, CDN', 's3'),
|
||||
('S3 Advanced', 799, '500 GB хранилище, 3 TB трафик, API доступ, CDN', 's3'),
|
||||
('S3 Pro', 1499, '1 TB хранилище, 10 TB трафик, API доступ, CDN, Priority Support', 's3'),
|
||||
('S3 Enterprise', 2999, '5 TB хранилище, Unlimited трафик, API доступ, CDN, Priority Support', 's3');
|
||||
|
||||
-- Проверка добавленных тарифов
|
||||
SELECT * FROM `tariff` ORDER BY `category`, `price`;
|
||||
|
||||
-- Показать количество серверов для каждого тарифа
|
||||
SELECT
|
||||
t.id,
|
||||
t.name,
|
||||
t.category,
|
||||
t.price,
|
||||
COUNT(s.id) as active_servers
|
||||
FROM `tariff` t
|
||||
LEFT JOIN `server` s ON s.tariffId = t.id
|
||||
GROUP BY t.id, t.name, t.category, t.price
|
||||
ORDER BY t.category, t.price;
|
||||
@@ -10,71 +10,7 @@ datasource db {
|
||||
}
|
||||
|
||||
// This is your Prisma schema file,
|
||||
model Tariff {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @unique
|
||||
price Float
|
||||
description String?
|
||||
createdAt DateTime @default(now())
|
||||
servers Server[]
|
||||
|
||||
@@map("tariff")
|
||||
}
|
||||
|
||||
|
||||
|
||||
model OperatingSystem {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @unique
|
||||
type String // linux, windows, etc
|
||||
template String? // путь к шаблону для контейнера
|
||||
createdAt DateTime @default(now())
|
||||
servers Server[]
|
||||
|
||||
@@map("operatingsystem")
|
||||
}
|
||||
|
||||
model Server {
|
||||
id Int @id @default(autoincrement())
|
||||
userId Int
|
||||
tariffId Int
|
||||
osId Int
|
||||
status String @default("creating") // creating, running, stopped, suspended, error
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
tariff Tariff @relation(fields: [tariffId], references: [id])
|
||||
os OperatingSystem @relation(fields: [osId], references: [id])
|
||||
|
||||
// Proxmox данные
|
||||
node String?
|
||||
diskTemplate String?
|
||||
proxmoxId Int?
|
||||
|
||||
// Сетевые настройки
|
||||
ipAddress String? // Локальный IP адрес
|
||||
macAddress String? // MAC адрес
|
||||
|
||||
// Доступы
|
||||
rootPassword String? // Зашифрованный root пароль
|
||||
sshPublicKey String? // SSH публичный ключ (опционально)
|
||||
|
||||
// Мониторинг
|
||||
lastPing DateTime?
|
||||
cpuUsage Float? @default(0)
|
||||
memoryUsage Float? @default(0)
|
||||
diskUsage Float? @default(0)
|
||||
networkIn Float? @default(0)
|
||||
networkOut Float? @default(0)
|
||||
|
||||
// Автоматические платежи
|
||||
nextPaymentDate DateTime? // Дата следующего списания
|
||||
autoRenew Boolean @default(true) // Автопродление
|
||||
|
||||
payments Payment[]
|
||||
|
||||
@@map("server")
|
||||
}
|
||||
// VPS/Server models removed - moving to S3 storage
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
@@ -89,10 +25,20 @@ model User {
|
||||
responses Response[] @relation("OperatorResponses")
|
||||
checks Check[] @relation("UserChecks")
|
||||
balance Float @default(0)
|
||||
servers Server[]
|
||||
notifications Notification[]
|
||||
payments Payment[]
|
||||
pushSubscriptions PushSubscription[]
|
||||
transactions Transaction[] // История всех транзакций
|
||||
posts Post[] @relation("PostAuthor") // Статьи блога
|
||||
comments Comment[] @relation("UserComments") // Комментарии
|
||||
buckets StorageBucket[] // S3 хранилища пользователя
|
||||
|
||||
// Новые relations для расширенных настроек
|
||||
sessions Session[]
|
||||
loginHistory LoginHistory[]
|
||||
apiKeys APIKey[]
|
||||
notificationSettings NotificationSettings?
|
||||
profile UserProfile?
|
||||
qrLoginRequests QrLoginRequest[]
|
||||
|
||||
@@map("user")
|
||||
}
|
||||
@@ -136,55 +82,86 @@ model Service {
|
||||
model Ticket {
|
||||
id Int @id @default(autoincrement())
|
||||
title String
|
||||
message String
|
||||
message String @db.Text
|
||||
userId Int
|
||||
status String @default("open")
|
||||
status String @default("open") // open, in_progress, awaiting_reply, resolved, closed
|
||||
priority String @default("normal") // low, normal, high, urgent
|
||||
category String @default("general") // general, technical, billing, other
|
||||
assignedTo Int? // ID оператора, которому назначен тикет
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
closedAt DateTime?
|
||||
responses Response[] @relation("TicketResponses")
|
||||
attachments TicketAttachment[]
|
||||
user User? @relation("UserTickets", fields: [userId], references: [id])
|
||||
|
||||
@@map("ticket")
|
||||
@@map("ticket")
|
||||
}
|
||||
|
||||
model Response {
|
||||
id Int @id @default(autoincrement())
|
||||
ticketId Int
|
||||
operatorId Int
|
||||
message String
|
||||
message String @db.Text
|
||||
isInternal Boolean @default(false) // Внутренний комментарий (виден только операторам)
|
||||
createdAt DateTime @default(now())
|
||||
ticket Ticket @relation("TicketResponses", fields: [ticketId], references: [id])
|
||||
ticket Ticket @relation("TicketResponses", fields: [ticketId], references: [id], onDelete: Cascade)
|
||||
operator User @relation("OperatorResponses", fields: [operatorId], references: [id])
|
||||
attachments ResponseAttachment[]
|
||||
|
||||
@@map("response")
|
||||
|
||||
|
||||
}
|
||||
model Notification {
|
||||
id Int @id @default(autoincrement())
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId Int
|
||||
title String
|
||||
message String
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@map("notification")
|
||||
@@map("response")
|
||||
}
|
||||
|
||||
// Автоматические платежи за серверы
|
||||
model Payment {
|
||||
// Прикреплённые файлы к тикетам
|
||||
model TicketAttachment {
|
||||
id Int @id @default(autoincrement())
|
||||
userId Int
|
||||
serverId Int
|
||||
amount Float
|
||||
status String @default("pending") // pending, success, failed
|
||||
type String // subscription, manual
|
||||
ticketId Int
|
||||
ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
|
||||
|
||||
filename String
|
||||
fileUrl String
|
||||
fileSize Int // Размер в байтах
|
||||
mimeType String
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
processedAt DateTime?
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
server Server @relation(fields: [serverId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("ticket_attachment")
|
||||
}
|
||||
|
||||
@@map("payment")
|
||||
// Прикреплённые файлы к ответам
|
||||
model ResponseAttachment {
|
||||
id Int @id @default(autoincrement())
|
||||
responseId Int
|
||||
response Response @relation(fields: [responseId], references: [id], onDelete: Cascade)
|
||||
|
||||
filename String
|
||||
fileUrl String
|
||||
fileSize Int
|
||||
mimeType String
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@map("response_attachment")
|
||||
}
|
||||
|
||||
// QR-код авторизация (как в Telegram Web)
|
||||
model QrLoginRequest {
|
||||
id Int @id @default(autoincrement())
|
||||
code String @unique @db.VarChar(128) // Уникальный код QR
|
||||
userId Int? // После подтверждения - ID пользователя
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
status String @default("pending") // pending, confirmed, expired, rejected
|
||||
ipAddress String?
|
||||
userAgent String? @db.Text
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
expiresAt DateTime // Через 60 секунд
|
||||
confirmedAt DateTime?
|
||||
|
||||
@@index([code])
|
||||
@@index([status, expiresAt])
|
||||
@@map("qr_login_request")
|
||||
}
|
||||
|
||||
// История всех транзакций (пополнения, списания, возвраты)
|
||||
@@ -201,4 +178,242 @@ model Transaction {
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
@@map("transaction")
|
||||
}
|
||||
|
||||
// Блог
|
||||
model Post {
|
||||
id Int @id @default(autoincrement())
|
||||
title String
|
||||
content String @db.Text // Rich text content (HTML)
|
||||
excerpt String? @db.Text // Краткое описание для ленты
|
||||
coverImage String? // URL обложки
|
||||
url String @unique // Пользовательский URL (blog_name)
|
||||
status String @default("draft") // draft, published, archived
|
||||
authorId Int
|
||||
author User @relation("PostAuthor", fields: [authorId], references: [id])
|
||||
views Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
publishedAt DateTime?
|
||||
comments Comment[]
|
||||
|
||||
@@map("post")
|
||||
}
|
||||
|
||||
// Комментарии к статьям блога
|
||||
model Comment {
|
||||
id Int @id @default(autoincrement())
|
||||
postId Int
|
||||
userId Int? // null если комментарий от гостя
|
||||
authorName String? // Имя автора (для гостей)
|
||||
content String @db.Text
|
||||
status String @default("pending") // pending, approved, rejected
|
||||
createdAt DateTime @default(now())
|
||||
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
|
||||
user User? @relation("UserComments", fields: [userId], references: [id])
|
||||
|
||||
@@map("comment")
|
||||
}
|
||||
|
||||
// Модель для уведомлений
|
||||
model Notification {
|
||||
id Int @id @default(autoincrement())
|
||||
userId Int
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
type String // server_created, payment_charged, tariff_expiring, ticket_reply, payment_received, balance_low
|
||||
title String
|
||||
message String @db.Text
|
||||
|
||||
// Связанные сущности (опционально)
|
||||
ticketId Int?
|
||||
checkId Int?
|
||||
|
||||
// Метаданные
|
||||
actionUrl String? // URL для перехода при клике
|
||||
icon String? // Иконка (emoji или path)
|
||||
color String? // Цвет (green, blue, orange, red, purple)
|
||||
|
||||
isRead Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([userId, isRead])
|
||||
@@index([userId, createdAt])
|
||||
@@map("notification")
|
||||
}
|
||||
|
||||
// Модель для Push-подписок (Web Push API)
|
||||
model PushSubscription {
|
||||
id Int @id @default(autoincrement())
|
||||
userId Int
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
endpoint String @db.VarChar(512)
|
||||
p256dh String @db.Text // Публичный ключ для шифрования
|
||||
auth String @db.Text // Токен аутентификации
|
||||
|
||||
userAgent String? @db.Text // Браузер/устройство
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
lastUsed DateTime @default(now())
|
||||
|
||||
@@unique([userId, endpoint])
|
||||
@@index([userId])
|
||||
@@map("push_subscription")
|
||||
}
|
||||
|
||||
// Активные сеансы пользователя
|
||||
model Session {
|
||||
id Int @id @default(autoincrement())
|
||||
userId Int
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
token String @unique @db.VarChar(500) // JWT refresh token
|
||||
ipAddress String?
|
||||
userAgent String? @db.Text
|
||||
device String? // Desktop, Mobile, Tablet
|
||||
browser String? // Chrome, Firefox, Safari, etc.
|
||||
location String? // Город/страна по IP
|
||||
|
||||
lastActivity DateTime @default(now())
|
||||
createdAt DateTime @default(now())
|
||||
expiresAt DateTime
|
||||
|
||||
@@index([userId])
|
||||
@@map("session")
|
||||
}
|
||||
|
||||
// История входов
|
||||
model LoginHistory {
|
||||
id Int @id @default(autoincrement())
|
||||
userId Int
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
ipAddress String
|
||||
userAgent String? @db.Text
|
||||
device String?
|
||||
browser String?
|
||||
location String?
|
||||
|
||||
success Boolean @default(true) // true = успешный вход, false = неудачная попытка
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([userId])
|
||||
@@index([createdAt])
|
||||
@@map("login_history")
|
||||
}
|
||||
|
||||
// API ключи для разработчиков
|
||||
model APIKey {
|
||||
id Int @id @default(autoincrement())
|
||||
userId Int
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
name String // Название (например, "Production API")
|
||||
key String @unique @db.VarChar(64) // Сам API ключ
|
||||
prefix String @db.VarChar(16) // Префикс для отображения (ospab_xxxx)
|
||||
|
||||
permissions String? @db.Text // JSON массив разрешений ["servers:read", "servers:create", etc.]
|
||||
|
||||
lastUsed DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
expiresAt DateTime?
|
||||
|
||||
@@index([userId])
|
||||
@@index([key])
|
||||
@@map("api_key")
|
||||
}
|
||||
|
||||
// Настройки уведомлений пользователя
|
||||
model NotificationSettings {
|
||||
id Int @id @default(autoincrement())
|
||||
userId Int @unique
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
// Email уведомления
|
||||
emailBalanceLow Boolean @default(true)
|
||||
emailPaymentCharged Boolean @default(true)
|
||||
emailTicketReply Boolean @default(true)
|
||||
emailNewsletter Boolean @default(false)
|
||||
|
||||
// Push уведомления
|
||||
pushBalanceLow Boolean @default(true)
|
||||
pushPaymentCharged Boolean @default(true)
|
||||
pushTicketReply Boolean @default(true)
|
||||
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("notification_settings")
|
||||
}
|
||||
|
||||
// Настройки профиля
|
||||
model UserProfile {
|
||||
id Int @id @default(autoincrement())
|
||||
userId Int @unique
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
avatarUrl String? // Путь к аватару
|
||||
phoneNumber String?
|
||||
timezone String? @default("Europe/Moscow")
|
||||
language String? @default("ru")
|
||||
|
||||
// Настройки приватности
|
||||
profilePublic Boolean @default(false)
|
||||
showEmail Boolean @default(false)
|
||||
|
||||
// 2FA
|
||||
twoFactorEnabled Boolean @default(false)
|
||||
twoFactorSecret String? @db.Text
|
||||
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("user_profile")
|
||||
}
|
||||
|
||||
// S3 Bucket модель
|
||||
model StorageBucket {
|
||||
id Int @id @default(autoincrement())
|
||||
userId Int
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
name String // Уникальное имя бакета в рамках пользователя
|
||||
plan String // Выбранный тариф (basic, standard, plus, pro, enterprise)
|
||||
quotaGb Int // Лимит включённого объёма в GB
|
||||
usedBytes BigInt @default(0) // Текущий объём хранения в байтах
|
||||
objectCount Int @default(0)
|
||||
storageClass String @default("standard") // standard, infrequent, archive
|
||||
region String @default("ru-central-1")
|
||||
public Boolean @default(false)
|
||||
versioning Boolean @default(false)
|
||||
status String @default("active") // active, grace, suspended
|
||||
monthlyPrice Float
|
||||
nextBillingDate DateTime?
|
||||
lastBilledAt DateTime?
|
||||
autoRenew Boolean @default(true)
|
||||
usageSyncedAt DateTime?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
accessKeys StorageAccessKey[]
|
||||
|
||||
@@index([userId])
|
||||
@@unique([userId, name]) // Имя уникально в рамках пользователя
|
||||
@@map("storage_bucket")
|
||||
}
|
||||
|
||||
model StorageAccessKey {
|
||||
id Int @id @default(autoincrement())
|
||||
bucketId Int
|
||||
bucket StorageBucket @relation(fields: [bucketId], references: [id], onDelete: Cascade)
|
||||
|
||||
accessKey String @unique
|
||||
secretKey String // хранится в зашифрованном виде
|
||||
label String?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
lastUsedAt DateTime?
|
||||
|
||||
@@index([bucketId])
|
||||
@@map("storage_access_key")
|
||||
}
|
||||
Reference in New Issue
Block a user