add files

This commit is contained in:
Andrey Kuvshinov
2025-10-17 12:23:41 +03:00
commit 84f7b44926
7 changed files with 706 additions and 0 deletions

73
.gitignore vendored Normal file
View File

@@ -0,0 +1,73 @@
# Временные файлы редакторов и ОС
.DS_Store
Thumbs.db
*.swp
*.swo
*~
# Логи приложения
*.log
logs/
# Файлы окружения (не коммитим чувствительные данные)
.env
.env.local
.env.*.local
# Зависимости Composer (можно коммитить, но обычно нет)
/vendor/
/composer.lock
# Кэш и скомпилированные файлы
/cache/
/tmp/
/var/cache/
/var/tmp/
# Файлы IDE
.idea/
.phpstorm.meta.php
*.buildpath
*.project
*.settings/
.vscode/
# Unit тесты и покрытие кода
/coverage/
.phpunit.result.cache
# Деплой и билд
/build/
/dist/
# Файлы операционной системы
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
[Tt]humbs.db
# Apache/Nginx
.htaccess
nginx.conf.local
# Сессии PHP
/sessions/
# Загружаемые файлы (если они не должны быть в репозитории)
/uploads/
/storage/app/public/
/public/storage/
# Файлы пакетов
*.tar.gz
*.zip
# SQLite базы данных (если используются)
*.sqlite
*.db
# Xdebug
xdebug.log

20
composer.json Normal file
View File

@@ -0,0 +1,20 @@
{
"require": {
"php":">=5.3.0",
"masterforweb/kuri":"dev-master",
"masterforweb/db_lite":"dev-master"
},
"repositories":[
{
"type":"git",
"url":"https://github.com/masterforweb/kuri"
},
{
"type":"git",
"url":"https://github.com/masterforweb/db_lite"
}
]
}

5
composer.sh Normal file
View File

@@ -0,0 +1,5 @@
#!/bin/bash
docker run --rm --interactive --tty \
--volume $PWD:/app \
composer update

8
config.php Normal file
View File

@@ -0,0 +1,8 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
db_config('argumentiru', 'mysql:host=87.249.36.139;dbname=argumentiru', 'argumentiru', 'hjYu78kl*90-Uio23');
define('SITE', 'https://exdv.argumenti.ru/');

76
exdv.js Normal file
View File

@@ -0,0 +1,76 @@
function trackAdvViews() {
// Находим все изображения с data-picture="ex_1109"
const images = document.querySelectorAll('img[data-picture^="ex_"]');
// Получаем данные пользователя
const userData = {
page_url: window.location.href,
user_agent: navigator.userAgent,
viewed_at: new Date().toISOString()
};
// Отслеживаем только уникальные adv_id на этой странице
const trackedIds = new Set();
images.forEach(img => {
const pictureData = img.getAttribute('data-picture');
const advId = pictureData.replace('ex_', '');
// Проверяем, не отслеживали ли мы уже этот adv_id на этой странице
if (!trackedIds.has(advId)) {
trackedIds.add(advId);
const data = {
adv_id: parseInt(advId),
...userData
};
// Отправляем запрос на сервер
sendViewData(data);
}
});
}
function sendViewData(data) {
const url = 'https://exdv.argumenti.ru';
console.log('📤 Отправка JSON данных:', data);
fetch(url, {
method: 'POST',
mode: 'no-cors',
headers: {
'Content-Type': 'application/json', // Важно для JSON!
},
body: JSON.stringify(data) // Отправляем как JSON
})
.then(response => {
console.log('📥 Статус ответа:', response.status);
if (!response.ok) {
return response.text().then(text => {
throw new Error(`HTTP ${response.status}: ${text}`);
});
}
return response.json();
})
.then(result => {
console.log('✅ Успешный ответ:', result);
})
.catch(error => {
console.error('❌ Ошибка:', error.message);
});
}
// Дебаунсинг для избежания множественных вызовов
let trackTimeout = null;
function debouncedTrackAdvViews() {
if (trackTimeout) {
clearTimeout(trackTimeout);
}
trackTimeout = setTimeout(trackAdvViews, 100);
}
// Запускаем отслеживание при загрузке страницы
document.addEventListener('DOMContentLoaded', debouncedTrackAdvViews);

96
index.php Normal file
View File

@@ -0,0 +1,96 @@
<?php
// exdv/add endpoint
//$pdo = new PDO('mysql:host=87.249.36.139;dbname=argumentiru;charset=utf8mb4', 'argumentiru', 'hjYu78kl*90-Uio23');
// exdv/add endpoint
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
// ОБРАБОТКА OPTIONS ЗАПРОСА (preflight)
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
http_response_code(200);
exit;
}
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
// Получаем сырые JSON данные
$jsonInput = file_get_contents('php://input');
$input = json_decode($jsonInput, true);
// Логируем для отладки
error_log("Raw JSON input: " . $jsonInput);
error_log("Decoded data: " . print_r($input, true));
// Проверяем что JSON распарсился
if (json_last_error() !== JSON_ERROR_NONE) {
http_response_code(400);
echo json_encode(['error' => 'Invalid JSON: ' . json_last_error_msg()]);
exit;
}
// Валидация данных
if (!isset($input['adv_id']) || !is_numeric($input['adv_id'])) {
error_log("Ошибка: invalid adv_id");
http_response_code(400);
echo json_encode(['error' => 'Invalid adv_id', 'received_data' => $input]);
exit;
}
// Подготовка данных
$advId = (int)$input['adv_id'];
$ipAddress = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? 'unknown';
$userAgent = $input['user_agent'] ?? '';
$pageUrl = $input['page_url'] ?? '';
$viewedAt = date('Y-m-d H:i:s');
// Создаем хэш на основе adv_id, ip, user_agent и текущей минуты
$minuteWindow = date('Y-m-d H:i');
$hashData = $advId . $ipAddress . $userAgent . $minuteWindow;
$viewHash = md5($hashData);
try {
$pdo = new PDO('mysql:host=87.249.36.139;dbname=argumentiru;charset=utf8mb4', 'argumentiru', 'hjYu78kl*90-Uio23');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// Проверяем существующий просмотр по хэшу
$checkStmt = $pdo->prepare("SELECT view_id FROM `adv_views` WHERE view_hash = ? LIMIT 1");
$checkStmt->execute([$viewHash]);
if ($checkStmt->fetch()) {
echo json_encode([
'success' => true,
'duplicate' => true,
'message' => 'View already recorded in this minute'
]);
exit;
}
// Сохраняем новый просмотр с хэшем
$stmt = $pdo->prepare("INSERT INTO `adv_views` (`view_hash`, `adv_id`, `ip_address`, `user_agent`, `viewed_at`, `page_url`) VALUES (?, ?, ?, ?, ?, ?)");
$stmt->execute([$viewHash, $advId, $ipAddress, $userAgent, $viewedAt, $pageUrl]);
$viewId = $pdo->lastInsertId();
http_response_code(200);
echo json_encode([
'success' => true,
'view_id' => $viewId,
'view_hash' => $viewHash,
'message' => 'View recorded successfully'
]);
} catch (PDOException $e) {
http_response_code(500);
error_log("Database error: " . $e->getMessage());
echo json_encode(['error' => 'Database error: ' . $e->getMessage()]);
}
} else {
http_response_code(200);
echo json_encode(['error' => 'Method not allowed']);
}
?>

428
report.php Normal file
View File

@@ -0,0 +1,428 @@
<?php
// report.php
session_start();
// Проверка пароля
$correct_password = 'sdf7-jKl89w'; // Замените на ваш пароль
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
if (isset($_POST['password'])) {
if ($_POST['password'] === $correct_password) {
$_SESSION['authenticated'] = true;
} else {
$error = "Неверный пароль";
}
}
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
?>
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Доступ к отчету</title>
<style>
body { font-family: Arial, sans-serif; background: #f4f4f4; display: flex; justify-content: center; align-items: center; height: 100vh; }
.login-form { background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); text-align: center; }
input[type="password"] { padding: 10px; margin: 10px 0; width: 200px; border: 1px solid #ddd; border-radius: 4px; }
button { background: #3498db; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; }
.error { color: #e74c3c; margin-top: 10px; }
</style>
</head>
<body>
<div class="login-form">
<h2>🔒 Доступ к отчету</h2>
<form method="POST">
<input type="password" name="password" placeholder="Введите пароль" required>
<br>
<button type="submit">Войти</button>
<?php if (isset($error)): ?>
<div class="error"><?= $error ?></div>
<?php endif; ?>
</form>
</div>
</body>
</html>
<?php
exit;
}
}
// Основной код отчета
header('Content-Type: text/html; charset=utf-8');
// Подключение к базе данных
try {
$pdo = new PDO('mysql:host=87.249.36.139;dbname=argumentiru;charset=utf8mb4', 'argumentiru', 'hjYu78kl*90-Uio23');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
die('Database connection failed: ' . $e->getMessage());
}
// Обработка параметров периода
$start_date = $_GET['start_date'] ?? date('Y-m-01');
$end_date = $_GET['end_date'] ?? date('Y-m-d');
$adv_id = $_GET['adv_id'] ?? null;
$space_id = $_GET['space_id'] ?? null;
// Валидация дат
if (!strtotime($start_date) || !strtotime($end_date)) {
$start_date = date('Y-m-01');
$end_date = date('Y-m-d');
}
// Получение статистики - ТОЛЬКО баннеры с показами за период
function getAdvStats($pdo, $start_date, $end_date, $adv_id = null, $space_id = null) {
$sql = "
SELECT
ai.item_id,
ai.itemname,
ai.erid,
ai.adv_link,
ai.space_id,
ai.adv_file,
ai.desktop,
ai.phone,
ai.tablet,
ai.android,
ai.adv_active,
COUNT(av.view_id) as views_count,
COUNT(DISTINCT av.ip_address) as unique_views,
MIN(av.viewed_at) as first_view,
MAX(av.viewed_at) as last_view
FROM adv_views av
INNER JOIN adv_items2 ai ON av.adv_id = ai.item_id
WHERE av.viewed_at BETWEEN :start_date AND DATE_ADD(:end_date, INTERVAL 1 DAY)
";
$params = [
'start_date' => $start_date,
'end_date' => $end_date
];
// Фильтр по ID баннера
if ($adv_id) {
$sql .= " AND ai.item_id = :adv_id";
$params['adv_id'] = $adv_id;
}
// Фильтр по space_id
if ($space_id) {
$sql .= " AND ai.space_id = :space_id";
$params['space_id'] = $space_id;
}
$sql .= " GROUP BY ai.item_id, ai.itemname, ai.erid
HAVING views_count > 0
ORDER BY views_count DESC, ai.itemname ASC";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// Получение списка баннеров для фильтра - ТОЛЬКО те, у которых есть показы за период
function getAdvList($pdo, $start_date, $end_date) {
$sql = "
SELECT DISTINCT
ai.item_id,
ai.itemname,
ai.erid,
ai.space_id
FROM adv_views av
INNER JOIN adv_items2 ai ON av.adv_id = ai.item_id
WHERE av.viewed_at BETWEEN :start_date AND DATE_ADD(:end_date, INTERVAL 1 DAY)
ORDER BY ai.itemname
";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'start_date' => $start_date,
'end_date' => $end_date
]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// Получение списка space_id для фильтра - ТОЛЬКО те, у которых есть показы за период
function getSpaceList($pdo, $start_date, $end_date) {
$sql = "
SELECT DISTINCT ai.space_id
FROM adv_views av
INNER JOIN adv_items2 ai ON av.adv_id = ai.item_id
WHERE av.viewed_at BETWEEN :start_date AND DATE_ADD(:end_date, INTERVAL 1 DAY)
ORDER BY ai.space_id
";
$stmt = $pdo->prepare($sql);
$stmt->execute([
'start_date' => $start_date,
'end_date' => $end_date
]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
$stats = getAdvStats($pdo, $start_date, $end_date, $adv_id, $space_id);
$adv_list = getAdvList($pdo, $start_date, $end_date);
$space_list = getSpaceList($pdo, $start_date, $end_date);
// Общая статистика
$total_views = array_sum(array_column($stats, 'views_count'));
$total_unique = array_sum(array_column($stats, 'unique_views'));
$total_banners = count($stats);
// Топ баннеры
$top_banners = array_slice($stats, 0, 5);
?>
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Отчет по показам баннеров</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; background: #f4f4f4; padding: 20px; }
.container { max-width: 1400px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
h1 { color: #2c3e50; margin-bottom: 20px; border-bottom: 2px solid #3498db; padding-bottom: 10px; }
.filters { background: #f8f9fa; padding: 20px; border-radius: 6px; margin-bottom: 20px; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; font-weight: bold; color: #555; }
input, select, button { padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; }
button { background: #3498db; color: white; border: none; cursor: pointer; transition: background 0.3s; }
button:hover { background: #2980b9; }
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 20px; }
.stat-card { background: white; padding: 15px; border-radius: 6px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); text-align: center; border-left: 4px solid #3498db; }
.top-banners { background: #fff3cd; padding: 15px; border-radius: 6px; margin-bottom: 20px; border-left: 4px solid #ffc107; }
.top-banner-item { padding: 5px 0; border-bottom: 1px solid #ffeaa7; }
.stat-number { font-size: 24px; font-weight: bold; color: #2c3e50; }
.stat-label { font-size: 14px; color: #7f8c8d; }
.table-container { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
th { background: #34495e; color: white; position: sticky; top: 0; }
tr:hover { background: #f5f5f5; }
.status-active { color: #27ae60; font-weight: bold; }
.status-inactive { color: #e74c3c; font-weight: bold; }
.device-icon { display: inline-block; width: 20px; text-align: center; }
.no-data { text-align: center; padding: 40px; color: #7f8c8d; font-style: italic; }
.export-btn { background: #27ae60; margin-left: 10px; }
.export-btn:hover { background: #219a52; }
.logout-btn { background: #e74c3c; margin-left: 10px; }
.logout-btn:hover { background: #c0392b; }
.filter-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; }
.header-actions { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.erid-column { max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
</style>
</head>
<body>
<div class="container">
<div class="header-actions">
<h1>📊 Отчет по показам баннеров</h1>
<div>
<button type="button" class="export-btn" onclick="exportToCSV()">Экспорт в CSV</button>
<button type="button" class="logout-btn" onclick="logout()">Выйти</button>
</div>
</div>
<!-- Фильтры -->
<div class="filters">
<form method="GET" action="">
<div class="filter-row">
<div class="form-group">
<label for="start_date">Дата с:</label>
<input type="date" id="start_date" name="start_date" value="<?= htmlspecialchars($start_date) ?>" required>
</div>
<div class="form-group">
<label for="end_date">Дата по:</label>
<input type="date" id="end_date" name="end_date" value="<?= htmlspecialchars($end_date) ?>" required>
</div>
<div class="form-group">
<label for="adv_id">Баннер:</label>
<select id="adv_id" name="adv_id">
<option value="">Все баннеры</option>
<?php foreach ($adv_list as $adv): ?>
<option value="<?= $adv['item_id'] ?>" <?= $adv_id == $adv['item_id'] ? 'selected' : '' ?>>
<?= htmlspecialchars($adv['item_id'] . ' - ' . $adv['itemname'] . ($adv['erid'] ? ' (' . $adv['erid'] . ')' : '')) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label for="space_id">ID пространства:</label>
<select id="space_id" name="space_id">
<option value="">Все пространства</option>
<?php foreach ($space_list as $space): ?>
<option value="<?= $space['space_id'] ?>" <?= $space_id == $space['space_id'] ? 'selected' : '' ?>>
<?= $space['space_id'] ?>
</option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="form-group" style="margin-top: 15px;">
<button type="submit">Применить фильтры</button>
<button type="button" onclick="resetFilters()">Сбросить</button>
</div>
</form>
</div>
<!-- Общая статистика -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-number"><?= number_format($total_banners) ?></div>
<div class="stat-label">Баннеров с показами</div>
</div>
<div class="stat-card">
<div class="stat-number"><?= number_format($total_views) ?></div>
<div class="stat-label">Всего показов</div>
</div>
<div class="stat-card">
<div class="stat-number"><?= number_format($total_unique) ?></div>
<div class="stat-label">Уникальные показы</div>
</div>
<div class="stat-card">
<div class="stat-number">
<?= $total_banners > 0 ? number_format($total_views / $total_banners, 1) : 0 ?>
</div>
<div class="stat-label">Среднее на баннер</div>
</div>
</div>
<!-- Топ баннеры -->
<?php if (!empty($top_banners)): ?>
<div class="top-banners">
<h3>🏆 Топ-5 баннеров по показам</h3>
<?php foreach ($top_banners as $index => $banner): ?>
<div class="top-banner-item">
<strong>#<?= $index + 1 ?></strong>
ID <?= $banner['item_id'] ?> - <?= htmlspecialchars($banner['itemname']) ?>
<?php if ($banner['erid']): ?>
(<?= htmlspecialchars($banner['erid']) ?>)
<?php endif; ?>
<span style="float: right; color: #e74c3c; font-weight: bold;">
<?= number_format($banner['views_count']) ?> показов
</span>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<!-- Таблица с данными -->
<div class="table-container">
<?php if (!empty($stats)): ?>
<table>
<thead>
<tr>
<th>ID</th>
<th>Название</th>
<th>ERID</th>
<th>Ссылка</th>
<th>ID пространства</th>
<th>Устройства</th>
<th>Статус</th>
<th>Показы</th>
<th>Уникальные</th>
<th>Первый показ</th>
<th>Последний показ</th>
<th>CTR</th>
</tr>
</thead>
<tbody>
<?php
$days_diff = max(1, (strtotime($end_date) - strtotime($start_date)) / (60 * 60 * 24) + 1);
foreach ($stats as $row):
$avg_per_day = $row['views_count'] / $days_diff;
$ctr = $row['views_count'] > 0 ? ($row['unique_views'] / $row['views_count'] * 100) : 0;
?>
<tr>
<td><?= $row['item_id'] ?></td>
<td><strong><?= htmlspecialchars($row['itemname']) ?></strong></td>
<td class="erid-column"><?= htmlspecialchars($row['erid'] ?: '—') ?></td>
<td style="max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
<?= htmlspecialchars($row['adv_link']) ?>
</td>
<td><?= $row['space_id'] ?></td>
<td>
<?php if ($row['desktop']): ?><span class="device-icon" title="Desktop">🖥️</span><?php endif; ?>
<?php if ($row['phone']): ?><span class="device-icon" title="Phone">📱</span><?php endif; ?>
<?php if ($row['tablet']): ?><span class="device-icon" title="Tablet">📟</span><?php endif; ?>
<?php if ($row['android']): ?><span class="device-icon" title="Android">🤖</span><?php endif; ?>
</td>
<td class="<?= $row['adv_active'] ? 'status-active' : 'status-inactive' ?>">
<?= $row['adv_active'] ? 'Активен' : 'Неактивен' ?>
</td>
<td><span style="color: #e74c3c; font-weight: bold;"><?= number_format($row['views_count']) ?></span></td>
<td><?= number_format($row['unique_views']) ?></td>
<td><?= $row['first_view'] ? date('d.m.Y H:i', strtotime($row['first_view'])) : '—' ?></td>
<td><?= $row['last_view'] ? date('d.m.Y H:i', strtotime($row['last_view'])) : '—' ?></td>
<td><?= number_format($ctr, 1) ?>%</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<div class="no-data">
<h3>📭 Нет данных за выбранный период</h3>
<p>Попробуйте изменить параметры фильтрации</p>
</div>
<?php endif; ?>
</div>
</div>
<script>
// Экспорт в CSV
function exportToCSV() {
const params = new URLSearchParams(window.location.search);
params.set('export', 'csv');
window.location.href = 'report_export.php?' + params.toString();
}
// Сброс фильтров
function resetFilters() {
document.getElementById('start_date').value = '<?= date('Y-m-01') ?>';
document.getElementById('end_date').value = '<?= date('Y-m-d') ?>';
document.getElementById('adv_id').value = '';
document.getElementById('space_id').value = '';
}
// Выход из системы
function logout() {
if (confirm('Вы уверены, что хотите выйти?')) {
window.location.href = '?logout=1';
}
}
// Автоматическое ограничение дат
document.getElementById('start_date').addEventListener('change', function() {
const endDate = document.getElementById('end_date');
if (this.value > endDate.value) {
endDate.value = this.value;
}
});
document.getElementById('end_date').addEventListener('change', function() {
const startDate = document.getElementById('start_date');
if (this.value < startDate.value) {
startDate.value = this.value;
}
});
</script>
</body>
</html>
<?php
// Обработка выхода
if (isset($_GET['logout'])) {
session_destroy();
header('Location: report.php');
exit;
}
?>