first commit

This commit is contained in:
Maxim
2025-12-11 19:58:49 +03:00
commit 87f3d0a36c
18 changed files with 926 additions and 0 deletions

0
metrics/__init__.py Normal file
View File

10
metrics/admin.py Normal file
View File

@@ -0,0 +1,10 @@
from django.contrib import admin
from .models import ClickEvent
@admin.register(ClickEvent)
class ClickEventAdmin(admin.ModelAdmin):
list_display = ['event_type', 'ip_address', 'created_at']
list_filter = ['event_type', 'created_at']
search_fields = ['ip_address']
readonly_fields = ['created_at']

5
metrics/apps.py Normal file
View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class MetricsConfig(AppConfig):
name = 'metrics'

View File

@@ -0,0 +1,30 @@
# Generated by Django 6.0 on 2025-12-11 15:46
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='ClickEvent',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('event_type', models.CharField(choices=[('bot', 'Переход в бота'), ('channel', 'Переход в TG канал')], max_length=20)),
('ip_address', models.GenericIPAddressField(blank=True, null=True)),
('user_agent', models.TextField(blank=True)),
('referrer', models.URLField(blank=True, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
],
options={
'verbose_name': 'Клик',
'verbose_name_plural': 'Клики',
'ordering': ['-created_at'],
},
),
]

View File

22
metrics/models.py Normal file
View File

@@ -0,0 +1,22 @@
from django.db import models
class ClickEvent(models.Model):
EVENT_TYPES = [
('bot', 'Переход в бота'),
('channel', 'Переход в TG канал'),
]
event_type = models.CharField(max_length=20, choices=EVENT_TYPES)
ip_address = models.GenericIPAddressField(null=True, blank=True)
user_agent = models.TextField(blank=True)
referrer = models.URLField(blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = 'Клик'
verbose_name_plural = 'Клики'
ordering = ['-created_at']
def __str__(self):
return f"{self.get_event_type_display()} - {self.created_at}"

View File

@@ -0,0 +1,306 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Метрики - English Bot</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
h1 {
color: white;
text-align: center;
margin-bottom: 30px;
font-size: 2.5rem;
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: white;
border-radius: 16px;
padding: 24px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
transition: transform 0.3s ease;
}
.stat-card:hover {
transform: translateY(-5px);
}
.stat-card h3 {
color: #666;
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 10px;
}
.stat-card .value {
font-size: 2.5rem;
font-weight: bold;
color: #333;
}
.stat-card.bot .value {
color: #667eea;
}
.stat-card.channel .value {
color: #764ba2;
}
.chart-container {
background: white;
border-radius: 16px;
padding: 24px;
margin-bottom: 30px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
}
.chart-container h2 {
color: #333;
margin-bottom: 20px;
}
.events-table {
background: white;
border-radius: 16px;
padding: 24px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
overflow-x: auto;
}
.events-table h2 {
color: #333;
margin-bottom: 20px;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #eee;
}
th {
background: #f8f9fa;
font-weight: 600;
color: #666;
text-transform: uppercase;
font-size: 0.8rem;
letter-spacing: 1px;
}
td {
color: #333;
}
.badge {
padding: 4px 12px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 500;
}
.badge-bot {
background: #e8eaff;
color: #667eea;
}
.badge-channel {
background: #f3e8ff;
color: #764ba2;
}
.section-title {
display: flex;
align-items: center;
gap: 10px;
}
.section-title::before {
content: '';
width: 4px;
height: 24px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 2px;
}
@media (max-width: 768px) {
h1 {
font-size: 1.8rem;
}
.stat-card .value {
font-size: 2rem;
}
}
</style>
</head>
<body>
<div class="container">
<h1>English Bot - Метрики</h1>
<div class="stats-grid">
<div class="stat-card bot">
<h3>Всего переходов в бота</h3>
<div class="value">{{ total_bot }}</div>
</div>
<div class="stat-card channel">
<h3>Всего переходов в канал</h3>
<div class="value">{{ total_channel }}</div>
</div>
<div class="stat-card bot">
<h3>В бота сегодня</h3>
<div class="value">{{ today_bot }}</div>
</div>
<div class="stat-card channel">
<h3>В канал сегодня</h3>
<div class="value">{{ today_channel }}</div>
</div>
<div class="stat-card bot">
<h3>В бота за неделю</h3>
<div class="value">{{ week_bot }}</div>
</div>
<div class="stat-card channel">
<h3>В канал за неделю</h3>
<div class="value">{{ week_channel }}</div>
</div>
</div>
<div class="chart-container">
<h2 class="section-title">Статистика за 30 дней</h2>
<canvas id="clicksChart" height="100"></canvas>
</div>
<div class="events-table">
<h2 class="section-title">Последние события</h2>
<table>
<thead>
<tr>
<th>Тип</th>
<th>IP адрес</th>
<th>Дата и время</th>
</tr>
</thead>
<tbody>
{% for event in recent_events %}
<tr>
<td>
<span class="badge {% if event.event_type == 'bot' %}badge-bot{% else %}badge-channel{% endif %}">
{{ event.get_event_type_display }}
</span>
</td>
<td>{{ event.ip_address|default:"-" }}</td>
<td>{{ event.created_at|date:"d.m.Y H:i:s" }}</td>
</tr>
{% empty %}
<tr>
<td colspan="3" style="text-align: center; color: #999;">Нет данных</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<script>
const botData = {{ bot_by_day|safe }};
const channelData = {{ channel_by_day|safe }};
// Создаем массив дат за последние 30 дней
const dates = [];
const today = new Date();
for (let i = 29; i >= 0; i--) {
const date = new Date(today);
date.setDate(date.getDate() - i);
dates.push(date.toISOString().split('T')[0]);
}
// Создаем объекты для быстрого поиска
const botMap = {};
botData.forEach(item => botMap[item.date] = item.count);
const channelMap = {};
channelData.forEach(item => channelMap[item.date] = item.count);
// Заполняем данные для графика
const botCounts = dates.map(date => botMap[date] || 0);
const channelCounts = dates.map(date => channelMap[date] || 0);
const labels = dates.map(date => {
const d = new Date(date);
return d.toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' });
});
const ctx = document.getElementById('clicksChart').getContext('2d');
new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [
{
label: 'Переходы в бота',
data: botCounts,
borderColor: '#667eea',
backgroundColor: 'rgba(102, 126, 234, 0.1)',
fill: true,
tension: 0.4
},
{
label: 'Переходы в канал',
data: channelCounts,
borderColor: '#764ba2',
backgroundColor: 'rgba(118, 75, 162, 0.1)',
fill: true,
tension: 0.4
}
]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'top',
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 1
}
}
}
}
});
</script>
</body>
</html>

3
metrics/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

10
metrics/urls.py Normal file
View File

@@ -0,0 +1,10 @@
from django.urls import path
from . import views
app_name = 'metrics'
urlpatterns = [
path('api/track/bot/', views.track_bot_click, name='track_bot'),
path('api/track/channel/', views.track_channel_click, name='track_channel'),
path('', views.dashboard, name='dashboard'),
]

115
metrics/views.py Normal file
View File

@@ -0,0 +1,115 @@
from django.shortcuts import render
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from django.db.models import Count
from django.db.models.functions import TruncDate
from django.utils import timezone
from datetime import timedelta
import json
from .models import ClickEvent
def get_client_ip(request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0].strip()
else:
ip = request.META.get('REMOTE_ADDR')
return ip
@csrf_exempt
@require_http_methods(["POST"])
def track_bot_click(request):
"""Трекинг перехода в бота"""
ClickEvent.objects.create(
event_type='bot',
ip_address=get_client_ip(request),
user_agent=request.META.get('HTTP_USER_AGENT', ''),
referrer=request.META.get('HTTP_REFERER'),
)
return JsonResponse({'status': 'ok', 'event': 'bot'})
@csrf_exempt
@require_http_methods(["POST"])
def track_channel_click(request):
"""Трекинг перехода в TG канал"""
ClickEvent.objects.create(
event_type='channel',
ip_address=get_client_ip(request),
user_agent=request.META.get('HTTP_USER_AGENT', ''),
referrer=request.META.get('HTTP_REFERER'),
)
return JsonResponse({'status': 'ok', 'event': 'channel'})
def dashboard(request):
"""Страница статистики"""
today = timezone.now().date()
week_ago = today - timedelta(days=7)
month_ago = today - timedelta(days=30)
# Общая статистика
total_bot = ClickEvent.objects.filter(event_type='bot').count()
total_channel = ClickEvent.objects.filter(event_type='channel').count()
# Статистика за сегодня
today_bot = ClickEvent.objects.filter(
event_type='bot',
created_at__date=today
).count()
today_channel = ClickEvent.objects.filter(
event_type='channel',
created_at__date=today
).count()
# Статистика за неделю
week_bot = ClickEvent.objects.filter(
event_type='bot',
created_at__date__gte=week_ago
).count()
week_channel = ClickEvent.objects.filter(
event_type='channel',
created_at__date__gte=week_ago
).count()
# Данные для графика (последние 30 дней)
bot_by_day = ClickEvent.objects.filter(
event_type='bot',
created_at__date__gte=month_ago
).annotate(date=TruncDate('created_at')).values('date').annotate(
count=Count('id')
).order_by('date')
channel_by_day = ClickEvent.objects.filter(
event_type='channel',
created_at__date__gte=month_ago
).annotate(date=TruncDate('created_at')).values('date').annotate(
count=Count('id')
).order_by('date')
# Последние события
recent_events = ClickEvent.objects.all()[:20]
context = {
'total_bot': total_bot,
'total_channel': total_channel,
'today_bot': today_bot,
'today_channel': today_channel,
'week_bot': week_bot,
'week_channel': week_channel,
'bot_by_day': json.dumps([
{'date': item['date'].isoformat(), 'count': item['count']}
for item in bot_by_day
]),
'channel_by_day': json.dumps([
{'date': item['date'].isoformat(), 'count': item['count']}
for item in channel_by_day
]),
'recent_events': recent_events,
}
return render(request, 'metrics/dashboard.html', context)