Планирование проекта

Погрузись в мир мистики: смотри эпичные битвы экстрасенсов, ставь оценки и определяй, кто достоин звания сильнейшего. Твой голос решает исход битвы!

  1. Определить основные сущности:
    • Пользователь (участник)
    • Профиль экстрасенса (фото, биография, статус)
    • Эпизод/битва (запись видео, описание)
    • Рейтинг (звёзды от зрителей)
    • Статус (автоматически рассчитывается по сумме баллов)
  2. Требования к функционалу:
    • Регистрация/авторизация
    • Личный кабинет
    • Страница битвы с видеоплеером
    • Система голосования (звёздный рейтинг)
    • Отображение текущего статуса и общего рейтинга
    • Защита от накрутки (cookie + IP‑check)
  3. Технический стек:
    • Backend – PHP 8+, MySQL 8+
    • Web‑сервер – Apache/Nginx

Структура базы данных

Ниже пример минимального набора таблиц. Схема совместима с любой современной СУБД MySQL.

CREATE DATABASE battle_extrasens;
USE battle_extrasens;

-- 1. Пользователи
CREATE TABLE users (
 id INT AUTO_INCREMENT PRIMARY KEY,
 username VARCHAR(30) NOT NULL UNIQUE,
 email VARCHAR(100) NOT NULL UNIQUE,
 password VARCHAR(255) NOT NULL,
 created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
-- 2. Профили экстрасенсов
CREATE TABLE profiles (
 user_id INT PRIMARY KEY,
 avatar VARCHAR(255) DEFAULT 'default.jpg',
 bio TEXT,
 status ENUM('Новичок','Ученик','Мастер','Легенда') DEFAULT 'Новичок',
 total_score INT DEFAULT 0,
 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB;

-- 3. Битвы (эпизоды)
CREATE TABLE battles (
 id INT AUTO_INCREMENT PRIMARY KEY,
 title VARCHAR(150) NOT NULL,
 description TEXT,
 video_url VARCHAR(255) NOT NULL,
 created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;

-- 4. Оценки зрителей (звёздный рейтинг)
CREATE TABLE ratings (
 id INT AUTO_INCREMENT PRIMARY KEY,
 battle_id INT NOT NULL,
 user_id INT NOT NULL,
 stars TINYINT NOT NULL CHECK (stars BETWEEN 1 AND 5),
 created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
 UNIQUE KEY uniq_vote (battle_id, user_id), -- один голос от одного пользователя
 FOREIGN KEY (battle_id) REFERENCES battles(id) ON DELETE CASCADE,
 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB;

После создания таблиц импортируйте их в базу (например, через phpMyAdmin или консоль mysql -u root -p < schema.sql).


Настройка соединения с БД

Создайте файл config.php и поместите в него параметры подключения. Не забудьте изменить права доступа к файлу (chmod 600).

<?php
// config.php
define('DB_HOST', 'localhost');
define('DB_NAME', 'battle_extrasens');
define('DB_USER', 'your_user');
define('DB_PASS', 'your_password');

try {
 $pdo = new PDO(
 "mysql:host=".DB_HOST.";dbname=".DB_NAME.";charset=utf8",
 DB_USER,
 DB_PASS,
 [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
 );
} catch (PDOException $e) {
 die('Ошибка подключения к базе данных: ' . $e->getMessage);
}
?>

Регистрация и авторизация

Для простоты используем готовый шаблон PHPMailer для подтверждения e‑mail и password_hash для хранения пароля.

register.php

<?php
require 'config.php';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
 $username = trim($_POST['username']);
 $email = trim($_POST['email']);
 $pass = $_POST['password'];
 $hash = password_hash($pass, PASSWORD_DEFAULT);

 $stmt = $pdo->prepare('INSERT INTO users (username,email,password) VALUES (?,?,?)');
 $stmt->execute([$username,$email,$hash]);

 // сразу создаём профиль
 $userId = $pdo->lastInsertId;
 $pdo->prepare('INSERT INTO profiles (user_id) VALUES (?)')->execute([$userId]);

 header('Location: login.php');
 exit;
}
?><html lang="ru">
<head>
 <meta charset="UTF-8">
 <title>Регистрация</title>
</head>
<body>
<h2>Регистрация</h2>
<form method="post">
 <label>Логин: <input type="text" name="username" required></label><br>
 <label>E‑mail: <input type="email" name="email" required></label><br>
 <label>Пароль: <input type="password" name="password" required></label><br>
 <button type="submit">Зарегистрироваться</button>
</form>
</body>

login.php

<?php
require 'config.php';
session_start;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
 $stmt = $pdo->prepare('SELECT * FROM users WHERE email = ?');
 $stmt->execute([$_POST['email']]);
 $user = $stmt->fetch(PDO::FETCH_ASSOC);
 if ($user && password_verify($_POST['password'], $user['password'])) {
 $_SESSION['user_id'] = $user['id'];
 header('Location: dashboard.php');
 exit;
 } else {
 $error = 'Неверный e‑mail или пароль';
 }
}
?><html lang="ru">
<head>
 <meta charset="UTF-8">
 <title>Вход</title>
</head>
<body>
<h2>Вход</h2>
<?php if (!empty($error)): ?>
 <p style="color:red;"><?= $error ?></p>
<?php endif; ?>
<form method="post">
 <label>E‑mail: <input type="email" name="email" required></label><br>
 <label>Пароль: <input type="password" name="password" required></label><br>
 <button type="submit">Войти</button>
</form>
</body>

Страница битвы (видеоплеер + рейтинг)

battle.php?id=12

<?php
require 'config.php';
$battleId = (int)$_GET['id'] ?? 0;

$stmt = $pdo->prepare('SELECT * FROM battles WHERE id = ?');
$stmt->execute([$battleId]);
$battle = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$battle) { die('Битва не найдена'); }

$stmt = $pdo->prepare('SELECT AVG(stars) AS avg_rating, COUNT(*) AS votes FROM ratings WHERE battle_id = ?');
$stmt->execute([$battleId]);
$ratingInfo = $stmt->fetch(PDO::FETCH_ASSOC);
$avg = round($ratingInfo['avg_rating'], 1);
$votes = $ratingInfo['votes'];
?><html lang="ru">
<head>
 <meta charset="UTF-8"> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
 <style>
 .star {font-size:2rem; color:#ccc; cursor:pointer;}
 .star.selected, .star.hover {color:#f39c12;}
 </style>
</head>
<body> <video width="640" controls> Ваш браузер не поддерживает HTML5‑видео.
 </video>

 <h3>Оценка зрителей</h3>
 <div id="rating">
 <!-- 5 звёзд -->
 <?php for ($i=1;$i<=5;$i++): ?>
 <span class="star" data-value="<?= $i ?>">★</span>
 <?php endfor; ?>
 <span id="avgRating">(<?= $avg ?> из 5, <?= $votes ?> голосов)</span>
 </div>

 <script>
 $(function{
 const battleId = ;
 const $stars = $('.star');
 // подсветка при наведении
 $stars.on('mouseenter', function{
 const val = $(this).data('value');
 $stars.each(function(i){
 $(this);toggleClass('hover', i < val);
 });
 }).on('mouseleave', function{
 $stars.removeClass('hover');
 });

 // отправка оценки
 $stars.on('click', function{
 const stars = $(this).data('value');
 $.post('rate.php', {battle_id: battleId, stars: stars}, function(resp){
 if (resp.success){
 $('#avgRating').text(`(${resp.avg} из 5, ${resp.votes} голосов)`);
 $stars.removeClass('selected');
 $stars.each(function(i){
 if (i < stars) $(this).addClass('selected');
 });
 } else {
 alert(resp.message);
 }
 }, 'json');
 });
 });
 </script>
</body>

rate.php – обработка AJAX‑запроса

<?php
require 'config.php';
session_start;

header('Content-Type: application/json');

if (!isset($_SESSION['user_id'])) {
 echo json_encode(['success'=>false,'message'=>'Только авторизованные пользователи могут голосовать']);
 exit;
}

$battleId = (int)$_POST['battle_id'] ?? 0;
$stars = (int)$_POST['stars'] ?? 0;
$userId = $_SESSION['user_id'];

if ($stars < 1 || $stars > 5) {
 echo json_encode(['success'=>false,'message'=>'Недопустимое значение']);
 exit;
}
/* Защита от двойного голоса – UNIQUE KEY в таблице ratings */
$stmt = $pdo->prepare('INSERT INTO ratings (battle_id,user_id,stars) VALUES (?,?,?) 
 ON DUPLICATE KEY UPDATE stars = VALUES(stars)');
$stmt->execute([$battleId,$userId,$stars]);

// пересчитаем средний рейтинг
$stmt = $pdo->prepare('SELECT AVG(stars) AS avg_rating, COUNT(*) AS votes FROM ratings WHERE battle_id = ?');
$stmt->execute([$battleId]);
$res = $stmt->fetch(PDO::FETCH_ASSOC);

echo json_encode([
 'success'=>true,
 'avg' => round($res['avg_rating'],1),
 'votes' => $res['votes']
]);
?>
Важно: В примере используется простая защита от накрутки – уникальный ключ (battle_id, user_id) и проверка сессии. Для более надёжного решения добавьте проверку Cookie и/или reCAPTCHA.

Система статусов

Статус экстрасенса будет зависеть от общего количества набранных баллов (сумма всех звёзд). Добавим небольшую функцию, которая будет вызываться после каждой новой оценки.

update_status.php

<?php
require 'config.php';

function recalcUserStatus(int $userId, PDO $pdo): void {
 // Считаем суммарные звёзды за все битвы, где пользователь был участником
 $stmt = $pdo->prepare('
 SELECT SUM(r.stars) AS total
 FROM ratings r
 JOIN battles b ON r.battle_id = b.id
 WHERE b.id IN (SELECT battle_id FROM participants WHERE user_id = ?)
 ');
 $stmt->execute([$userId]);
 $total = (int)$stmt->fetchColumn;

 // Определяем статус
 if ($total >= 200) $status = 'Легенда';
 elseif ($total >= 100) $status = 'Мастер';
 elseif ($total >= 50) $status = 'Ученик';
 else $status = 'Новичок';
 // Сохраняем в профиле
 $pdo->prepare('UPDATE profiles SET total_score = ?, status = ? WHERE user_id = ?')
 ->execute([$total,$status,$userId]);
}
?>

После успешного сохранения оценки в rate.php добавьте вызов функции:

// после пересчёта среднего рейтинга
require_once 'update_status.php';
recalcUserStatus($userId, $pdo);

Отображение статуса в профиле

<?php
require 'config.php';
session_start;
$userId = $_SESSION['user_id'] ?? 0;
$stmt = $pdo->prepare('SELECT u.username, p.avatar, p.bio, p.status, p.total_score FROM users u 
 JOIN profiles p ON u.id = p.user_id WHERE u.id = ?');
$stmt->execute([$userId]);
$profile = $stmt->fetch(PDO::FETCH_ASSOC);
?><html lang="ru">
<head>
 <meta charset="UTF-8">
 <title>Личный кабинет</title>
</head>
<body> <img src="avatars/<?= $profile['avatar'] ?>" alt="Аватар" width="150">
 <p><b>Статус: <span style="color:#e67e22"><?= $profile['status'] ?></span></p>
 <p><b>Всего баллов: <?= $profile['total_score'] ?></p></body>

Защита от накрутки и безопасность

  • SQL‑инъекции: используем подготовленные запросы (PDO).
  • CSRF: добавляем токен в формы (пример: $_SESSION['csrf_token']).
  • Накрутка голосов:
    • UNIQUE KEY (battle_id, user_id) – один голос от одного пользователя.
    • Записываем IP и timestamp в отдельную таблицу vote_log и ограничиваем частоту (например, 1 голос/30 сек).
    • Включаем reCAPTCHA v3 для анонимных голосов.
  • Пароли: password_hash + password_verify.
  • HTTPS: обязательный протокол для всех страниц.

Дизайн и адаптивность

Для быстрого прототипа можно подключить готовый CSS‑фреймворк Tailwind CSS или Bootstrap 5. Ниже пример простого блока рейтинга с Tailwind:

<link href="https://cdn.jsdelivr.net/npm/tailwindcss@3.4.0/dist/tailwind.min.css" rel="stylesheet">

<div class="flex items-center space-x-2">
 <!-- звёзды -->
 <span class="star text-2xl cursor-pointer" data-value="1">★</span>
 <span class="star text-2xl cursor-pointer" data-value="2">★</span>
 <span class="star text-2xl cursor-pointer" data-value="3">★</span>
 <span class="star text-2xl cursor-pointer" data-value="4">★</span>
 <span class="star text-2xl cursor-pointer" data-value="5">★</span>

 <span id="avgRating" class="ml-2 text-gray-600">(0 из 5, 0 голосов)</span>
</div>

Tailwind позволяет быстро менять цвета, отступы и адаптировать интерфейс под мобильные устройства.


Развёртывание проекта

  1. Подготовьте сервер с LAMP/LEMP‑стеком.
  2. Создайте базу данных и импортируйте schema.sql.
  3. Установите права:chmod -R 750 /var/www/html/battle
  4. Настройте виртуальный хост (Apache пример):
    <VirtualHost *:80>
     ServerName battle.example;com <Directory /var/www/html/battle>
     AllowOverride All
     Require all granted
     </Directory>
    </VirtualHost>
  5. Перезапустите веб‑сервер: systemctl restart apache2 (или nginx).
  6. Обязательно включите HTTPS (Let’s Encrypt: certbot --apache -d battle.example.com).

Что дальше?

  • Добавить модуль комментариев к каждой битве.
  • Реализовать таблицу лидеров (по сумме баллов).
  • Интегрировать социальный вход (Google, VK, Facebook).
  • Разработать модерацию контента и систему жалоб.
  • Оптимизировать запросы (индексы, кеширование).

Следуя этим шагам, вы получите полностью функционирующий сайт «Битва экстрасенсов», где участники могут демонстрировать свои способности, зрители, голосовать, а система автоматически присваивает статус в зависимости от набранных баллов.


Удачной разработки!