اگر تصور می‌کنید admin پیش‌فرض جنگو برای نمایش آمار کافی است، احتمالاً تا امروز تیم محصول و بازاریابی شما هیچ‌وقت واقعاً از آن استفاده نکرده‌اند؛ چون admin جنگو برای مدیریت داده طراحی شده، نه برای نشان دادن بینش کسب‌وکار در یک نگاه.

چرا پنل سفارشی، یک ضرورت کسب‌وکار است؟

در یکی از پروژه‌های فروشگاهی که چند سال پیش روی آن کار می‌کردم، تیم بازاریابی برای دیدن آمار روزانه، مجبور بود هر روز صبح وارد admin جنگو شود، لیست Interactionها را باز کند، فیلتر بزند و بعد در Excel جمع بزند. این فرآیند، هر روز حدود ۴۰ دقیقه از وقت یک کارشناس را می‌گرفت. بعد از ساخت یک پنل سفارشی با کارت‌های quick view، این زمان به کمتر از ۳۰ ثانیه رسید.

این تجربه نشان می‌دهد که پنل سفارشی، فقط یک تغییر ظاهری نیست؛ یک ابزار بهره‌وری است که ارزش مستقیم ایجاد می‌کند. سه سطح از ارزش در این پنل وجود دارد.

سطح اول، دسترسی سریع به داده. تیم محصول باید در یک نگاه بفهمد امروز چند کاربر آمده، چند نفر آنلاین هستند و کدام صفحه پربازدیدتر بوده است.

سطح دوم، کاهش وابستگی به تیم فنی. بدون پنل سفارشی، هر سؤال آماری به یک درخواست از تیم فنی تبدیل می‌شود. با پنل، پاسخ در چند ثانیه در دسترس است.

سطح سوم، کشف فرصت‌های کسب‌وکار. وقتی داده در دسترس باشد، تصمیم‌گیری سریع‌تر و دقیق‌تر می‌شود.

پنل آمار، یک ویترین از داده نیست؛ یک ابزار تصمیم‌گیری است که ارزشش را با سرعت پاسخ‌دادن به سؤال‌های کسب‌وکار اندازه می‌گیرند.

این اهمیت، در ساختار پروژه‌های آماری مثل اپ جنگو برای ردیابی بازدیدکننده به‌طور مستقیم دیده می‌شود. اگر لایه‌ی نمایش آمار به‌درستی طراحی نشود، تمام داده‌های جمع‌آوری‌شده بی‌استفاده می‌مانند. برای درک عمیق‌تر این موضوع، پیشنهاد می‌کنم ابتدا طراحی مدل Visitor و Visit در جنگو را مطالعه کنید، چون ساختار داده، مستقیماً روی طراحی پنل اثر می‌گذارد.

محدودیت‌های admin پیش‌فرض جنگو

admin پیش‌فرض جنگو، ابزار فوق‌العاده‌ای برای مدیریت داده است. ولی برای نمایش آمار، چهار محدودیت جدی دارد.

محدودیت اول، ساختار جدولی. admin جنگو داده را در جدول‌های پیوسته نشان می‌دهد. این ساختار، برای مدیریت رکوردها مناسب است، ولی برای نمایش شاخص‌های کلیدی، کند و غیرکاربردی است.

محدودیت دوم، عدم نمایش تجمیع. شما نمی‌توانید در admin، یک کارت با عدد «۱۲٬۴۵۶ کاربر آنلاین» ببینید. باید فیلتر بزنید و بعد شمارش کنید.

محدودیت سوم، عملکرد ضعیف در حجم بالا. اگر جدول Interaction شما میلیون‌ها ردیف داشته باشد، هر بار باز کردن admin یک کوئری سنگین می‌زند.

محدودیت چهارم، عدم انعطاف در دسترسی. کنترل دسترسی در admin جنگو بر اساس model permission است. برای یک تیم محصول که فقط باید آمار ببیند، این سطح از دسترسی نامناسب است.

قابلیتadmin پیش‌فرضپنل سفارشی
نمایش کارت‌های شاخصخیربله
تجمیع سریعضعیفقوی
کنترل دسترسی مبتنی بر نقشمحدودکامل
داده‌ی زندهخیربله
کارایی در حجم بالاضعیفقابل بهینه‌سازی

اگر با الگوی تفاوت Middleware و Context Processor کار کرده باشید، می‌دانید که نمایش داده در سطح ارائه، معماری متفاوتی از مدیریت داده در سطح admin دارد.

آناتومی یک کارت quick view

هر کارت quick view، یک واحد کوچک از داده‌ی تجمیع‌شده است. سه عنصر اصلی در هر کارت وجود دارد:

عنصر اول، عنوان. یک برچسب کوتاه که توضیح می‌دهد این کارت چه چیزی را نشان می‌دهد. مثل «کاربران آنلاین»، «بازدیدکنندگان امروز».

عنصر دوم، مقدار اصلی. عدد یا رشته‌ای که شاخص اصلی را نشان می‌دهد. این مقدار باید با فونت بزرگ و واضح نمایش داده شود.

عنصر سوم، زیرنویس. اطلاعات مکمل که به تفسیر مقدار کمک می‌کند. مثل «نسبت به دیروز: ۱۲٪+» یا «آخرین به‌روزرسانی: ۵ دقیقه پیش».

<div class="bg-white rounded-2xl shadow p-5">
  <div class="text-sm text-slate-500">کاربران آنلاین</div>
  <div class="text-3xl font-bold mt-1">{% analytics_online_count %}</div>
  <div class="text-xs text-slate-400 mt-1">آخرین به‌روزرسانی: ۳۰ ثانیه پیش</div>
</div>

این ساختار ساده، چند مزیت دارد.

مزیت اول، خوانایی. کاربر با یک نگاه، مقدار را می‌بیند.

مزیت دوم، قابلیت استفاده‌ی مجدد. همان ساختار را می‌توانید برای ده‌ها کارت مختلف به‌کار بگیرید.

مزیت سوم، پاسخ‌گویی. با یک کلاس CSS مناسب، کارت‌ها در دستگاه‌های مختلف به‌خوبی نمایش داده می‌شوند.

کارت quick view، یک واحد اطلاعاتی متمرکز است، نه یک جدول کوچک. هر کارت باید فقط یک شاخص را نشان دهد.

ساخت اولین پنل با کارت‌های ساده

حالا بیایید یک پنل ساده با چند کارت بسازیم.

# analytics/views.py

from django.contrib.admin.views.decorators import staff_member_required
from django.shortcuts import render


@staff_member_required
def quick_view(request):
    return render(request, "analytics/quick_view.html", {
        "title": "آمار امروز",
    })

و در تمپلیت:

{% extends "analytics/base.html" %}
{% load analytics_tags %}

{% block content %}
<h1 class="text-2xl font-bold mb-6">آمار امروز</h1>

<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">

  <div class="bg-white rounded-2xl shadow p-5">
    <div class="text-sm text-slate-500">کاربران آنلاین</div>
    <div class="text-3xl font-bold mt-1">{% analytics_online_count %}</div>
  </div>

  <div class="bg-white rounded-2xl shadow p-5">
    <div class="text-sm text-slate-500">بازدیدکنندگان امروز</div>
    <div class="text-3xl font-bold mt-1">{% analytics_today_visitors %}</div>
  </div>

  <div class="bg-white rounded-2xl shadow p-5">
    <div class="text-sm text-slate-500">صفحات بازدیدشده</div>
    <div class="text-3xl font-bold mt-1">{% analytics_today_pageviews %}</div>
  </div>

  <div class="bg-white rounded-2xl shadow p-5">
    <div class="text-sm text-slate-500">دقایق حضور</div>
    <div class="text-3xl font-bold mt-1">{% analytics_today_minutes %}</div>
  </div>

</div>
{% endblock %}

این پنل، پنج مزیت نسبت به admin پیش‌فرض دارد.

مزیت اول، سادگی. در یک نگاه، چهار شاخص کلیدی دیده می‌شود.

مزیت دوم، سرعت. بدون فیلتر و جستجو، پاسخ در چند ثانیه به دست می‌آید.

مزیت سوم، زیبایی. ظاهر کارت‌ها، برای تیم محصول خوشایندتر است.

مزیت چهارم، توسعه‌پذیری. هر کارت جدید، فقط چند خط HTML است.

مزیت پنجم، پاسخ‌گویی. کارت‌ها در موبایل و دسکتاپ، به‌درستی چیده می‌شوند.

نکته‌ی مهم: این پنل، از همان tagهایی استفاده می‌کند که در استفاده از تمپلت تگ به‌عنوان متغیر با پارامتر as توضیح داده شده است. اگر می‌خواهید با ساختار کامل tagها آشنا شوید، انتقال منطق از services.py به templatetags راهنمای دقیقی است.

Template tags؛ ستون فقرات داده‌ی پنل

در پنل سفارشی، داده‌ی هر کارت از یک template tag می‌آید. این ساختار، چند مزیت کلیدی دارد.

مزیت اول، جداسازی منطق از نمایش. ویو فقط تمپلیت را render می‌کند، منطق در tag است.

مزیت دوم، قابلیت استفاده‌ی مجدد. یک tag می‌تواند در چند تمپلیت استفاده شود.

مزیت سوم، کش متمرکز. هر tag می‌تواند کش اختصاصی خودش را داشته باشد.

ساختار کامل tagها، در فایل analytics/templatetags/analytics_tags.py قرار می‌گیرد:

from datetime import timedelta, datetime, time

from django import template
from django.core.cache import cache
from django.db.models import Count, Sum
from django.utils import timezone

from analytics.models import Visit, PageView

register = template.Library()


def _humans(qs):
    return qs.exclude(visitor__device_type="bot")


def _today_range():
    today = timezone.localdate()
    tz = timezone.get_current_timezone()
    start = timezone.make_aware(datetime.combine(today, time.min), tz)
    return start, start + timedelta(days=1)


def online_count():
    threshold = timezone.now() - timedelta(seconds=300)
    return _humans(
        Visit.objects.filter(
            is_active=True,
            last_activity__gte=threshold,
        )
    ).count()


def today_unique_visitors():
    start, end = _today_range()
    return _humans(
        Visit.objects.filter(entry_time__gte=start, entry_time__lt=end)
    ).values("visitor").distinct().count()


def today_pageviews():
    start, end = _today_range()
    return _humans(
        PageView.objects.filter(entered_at__gte=start, entered_at__lt=end)
    ).count()


def today_total_minutes():
    start, end = _today_range()
    total = _humans(
        Visit.objects.filter(entry_time__gte=start, entry_time__lt=end)
    ).aggregate(s=Sum("duration_seconds"))["s"] or 0
    return round(total / 60, 1)


@register.simple_tag
def analytics_online_count():
    key = "analytics:online_count"
    value = cache.get(key)
    if value is None:
        value = online_count()
        cache.set(key, value, timeout=30)
    return value


@register.simple_tag
def analytics_today_visitors():
    key = "analytics:today_visitors"
    value = cache.get(key)
    if value is None:
        value = today_unique_visitors()
        cache.set(key, value, timeout=60)
    return value


@register.simple_tag
def analytics_today_pageviews():
    key = "analytics:today_pageviews"
    value = cache.get(key)
    if value is None:
        value = today_pageviews()
        cache.set(key, value, timeout=60)
    return value


@register.simple_tag
def analytics_today_minutes():
    key = "analytics:today_minutes"
    value = cache.get(key)
    if value is None:
        value = today_total_minutes()
        cache.set(key, value, timeout=60)
    return value

نکته‌ی مهم: کش کردن مقادیر کارت‌ها، کلید کارایی پنل است. بدون کش، هر بازدید از پنل چندین کوئری سنگین می‌زند. با کش، تعداد کوئری‌ها به حداقل می‌رسد. اگر با الگوی بهینه‌سازی جنگو برای ترافیک بالا کار کرده باشید، می‌دانید که این نوع کش، در پروژه‌های پربازدید استاندارد است.

کش کردن داده‌ی کارت‌ها

کش در پنل آمار، سه پارامتر کلیدی دارد.

پارامتر اول، مدت زمان کش. چقدر داده‌ی کش معتبر است؟ این مقدار به حساسیت داده بستگی دارد. برای «کاربران آنلاین»، ۳۰ ثانیه کافی است. برای «بازدیدکنندگان امروز»، ۶۰ ثانیه.

پارامتر دوم، کلید کش. کلید باید یکتا باشد و به پارامترهای کوئری وابسته باشد. مثلاً اگر یک کارت بر اساس بازه‌ی زمانی تغییر می‌کند، کلید باید شامل بازه باشد.

پارامتر سوم، invalidate. در شرایط خاص، باید کش را پاک کنید. مثلاً بعد از اجرای کامند پاک‌سازی داده، کش باید invalidate شود. اگر با الگوی کامند مدیریتی پاک‌سازی داده‌های قدیمی کار کرده باشید، می‌دانید که این نوع invalidate، بخشی از انضباط کش است.

from django.core.cache import cache


def invalidate_analytics_cache():
    keys = [
        "analytics:online_count",
        "analytics:today_visitors",
        "analytics:today_pageviews",
        "analytics:today_minutes",
    ]
    for key in keys:
        cache.delete(key)

و در کامند پاک‌سازی:

from analytics.templatetags.analytics_tags import invalidate_analytics_cache


class Command(BaseCommand):
    def handle(self, *args, **options):
        # ... پاک‌سازی
        invalidate_analytics_cache()

اجزای قابل استفاده‌ی مجدد در تمپلیت

در پنل با چندین کارت، تکرار کد HTML اجتناب‌ناپذیر است. راه‌حل، استفاده از اجزای قابل استفاده‌ی مجدد است.

رویکرد اول، partial template. یک فایل تمپلیت جداگانه برای هر کارت، که با {% include %} استفاده می‌شود.

{# analytics/templates/analytics/partials/card.html #}
<div class="bg-white rounded-2xl shadow p-5">
  <div class="text-sm text-slate-500">{{ title }}</div>
  <div class="text-3xl font-bold mt-1">{{ value }}</div>
  {% if subtitle %}
    <div class="text-xs text-slate-400 mt-1">{{ subtitle }}</div>
  {% endif %}
</div>

و در تمپلیت اصلی:

{% include "analytics/partials/card.html" with title="کاربران آنلاین" value=online_count subtitle="آخرین به‌روزرسانی: ۳۰ ثانیه پیش" %}

رویکرد دوم، inclusion_tag. یک tag که خودش تمپلیت را render می‌کند.

# analytics/templatetags/analytics_tags.py

@register.inclusion_tag("analytics/partials/card.html")
def analytics_card(title, value, subtitle=""):
    return {
        "title": title,
        "value": value,
        "subtitle": subtitle,
    }

و در تمپلیت:

{% analytics_card "کاربران آنلاین" online_value "آخرین به‌روزرسانی: ۳۰ ثانیه پیش" %}

رویکرد دوم، تمیزتر و قابل استفاده‌ی مجددتر است. اگر با الگوی راهنمای تعریف تمپلت تگ در جنگو کار کرده باشید، این ساختار برایتان آشناست.

کنترل دسترسی مبتنی بر نقش

پنل آمار، نه فقط برای ادمین‌ها، بلکه برای تیم محصول، بازاریابی و فروش هم مفید است. برای این کار، به کنترل دسترسی مبتنی بر نقش نیاز دارید.

رویکرد اول، Django Groups. از گروه‌های پیش‌فرض جنگو استفاده کنید.

from django.contrib.auth.models import Group, Permission


analytics_viewers, _ = Group.objects.get_or_create(name="Analytics Viewers")


def add_viewer(user):
    user.groups.add(analytics_viewers)

و در ویو:

from django.contrib.auth.decorators import user_passes_test


def is_analytics_viewer(user):
    return user.groups.filter(name="Analytics Viewers").exists() or user.is_staff


@user_passes_test(is_analytics_viewer)
def quick_view(request):
    ...

رویکرد دوم، custom permission. یک permission سفارشی تعریف کنید و بر اساس آن دسترسی بدهید.

class Visit(models.Model):
    class Meta:
        permissions = [
            ("view_analytics", "Can view analytics dashboard"),
        ]

و در ویو:

from django.contrib.auth.decorators import permission_required


@permission_required("analytics.view_analytics")
def quick_view(request):
    ...

نکته‌ی مهم: برای تیم محصول، فقط نمایش کافی است. نباید به آن‌ها دسترسی حذف یا ویرایش داده‌ی خام داده شود.

داده‌ی زنده بدون رفرش صفحه

یکی از بزرگ‌ترین مزیت‌های پنل سفارشی، امکان نمایش داده‌ی زنده است. برای این کار، از AJAX و یک endpoint ساده استفاده می‌کنیم.

# analytics/api.py

from django.http import JsonResponse
from django.views.decorators.http import require_GET
from analytics.templatetags.analytics_tags import (
    online_count,
    today_unique_visitors,
    today_pageviews,
    today_total_minutes,
)


@require_GET
def stats_summary(request):
    if not (request.user.is_authenticated and request.user.is_staff):
        return JsonResponse({"error": "forbidden"}, status=403)

    return JsonResponse({
        "online": online_count(),
        "today_visitors": today_unique_visitors(),
        "today_pageviews": today_pageviews(),
        "today_minutes": today_total_minutes(),
    })

و در تمپلیت:

<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
  <div class="bg-white rounded-2xl shadow p-5">
    <div class="text-sm text-slate-500">کاربران آنلاین</div>
    <div class="text-3xl font-bold mt-1" id="live-online">–</div>
  </div>
  <div class="bg-white rounded-2xl shadow p-5">
    <div class="text-sm text-slate-500">بازدید امروز</div>
    <div class="text-3xl font-bold mt-1" id="live-today">–</div>
  </div>
  <div class="bg-white rounded-2xl shadow p-5">
    <div class="text-sm text-slate-500">صفحات امروز</div>
    <div class="text-3xl font-bold mt-1" id="live-pages">–</div>
  </div>
  <div class="bg-white rounded-2xl shadow p-5">
    <div class="text-sm text-slate-500">دقایق حضور</div>
    <div class="text-3xl font-bold mt-1" id="live-minutes">–</div>
  </div>
</div>

<script>
async function refreshStats() {
  try {
    const r = await fetch("{% url 'analytics:api_summary' %}", {
      credentials: "same-origin",
    });
    if (!r.ok) return;
    const d = await r.json();
    document.getElementById("live-online").textContent = d.online;
    document.getElementById("live-today").textContent = d.today_visitors;
    document.getElementById("live-pages").textContent = d.today_pageviews;
    document.getElementById("live-minutes").textContent = d.today_minutes;
  } catch (e) {}
}

refreshStats();
setInterval(refreshStats, 30000);
</script>

این ساختار، سه مزیت کلیدی دارد.

مزیت اول، داده‌ی زنده. هر ۳۰ ثانیه، مقادیر به‌روزرسانی می‌شوند.

مزیت دوم، سبک بودن. فقط یک درخواست کوچک JSON، نه یک render کامل.

مزیت سوم، عدم نیاز به رفرش. کاربر می‌تواند صفحه را باز نگه دارد و مقادیر به‌روز شوند.

اگر با الگوی ساخت API JSON برای آمار زنده کار کرده باشید، این معماری برایتان آشناست.

کارایی پنل در ترافیک بالا

پنل آمار، به‌خاطر حجم داده‌ای که می‌خواند، می‌تواند به گلوگاه تبدیل شود. چند راهبرد برای بهینه‌سازی:

راهبرد اول، کش روی مقادیر. همان‌طور که قبلاً توضیح دادم، کش مقادیر کارت‌ها، تعداد کوئری‌ها را به حداقل می‌رساند.

راهبرد دوم، جدول‌های تجمیعی. به‌جای محاسبه‌ی هر بار از داده‌ی خام، یک جدول DailyStats داشته باشید که هر شب پر می‌شود.

class DailyStats(models.Model):
    date = models.DateField(unique=True, db_index=True)
    unique_visitors = models.PositiveIntegerField(default=0)
    total_visits = models.PositiveIntegerField(default=0)
    total_pageviews = models.PositiveIntegerField(default=0)
    total_seconds = models.PositiveIntegerField(default=0)
    mobile_count = models.PositiveIntegerField(default=0)
    desktop_count = models.PositiveIntegerField(default=0)

و یک task شبانه که این جدول را پر می‌کند.

راهبرد سوم، محدودیت داده. به‌جای نمایش همه‌ی داده، فقط داده‌ی امروز و دیروز را نمایش دهید. برای بازه‌های طولانی‌تر، از صفحه‌ی گزارش جداگانه استفاده کنید.

راهبرد چهارم، ایندکس‌های هدفمند. مطمئن شوید که فیلدهای entry_time، last_activity و is_active ایندکس دارند.

نکته‌ی مهم: در پروژه‌های بزرگ، پنل آمار می‌تواند روی دیتابیس اصلی فشار بیاورد. راه‌حل، استفاده از یک database router برای جداسازی read و write است. اگر با الگوی بهینه‌سازی جنگو برای ترافیک بالا کار کرده باشید، این معماری برایتان آشناست.

UX و طراحی؛ چرا ظاهر مهم است

پنل آمار، اگر زیبا نباشد، کاربرانش کم می‌شوند. سه اصل UX در طراحی پنل:

اصل اول، سلسله‌مراتب بصری. شاخص‌های مهم‌تر باید بزرگ‌تر و پررنگ‌تر باشند.

اصل دوم، هماهنگی رنگ. از رنگ‌ها برای انتقال معنا استفاده کنید. مثلاً سبز برای رشد، قرمز برای افت.

اصل سوم، پاسخ‌گویی. پنل باید در دسکتاپ، تبلت و موبایل به‌خوبی نمایش داده شود.

جدول زیر، پیشنهاد من برای سلسله‌مراتب کارت‌ها است:

اولویتشاخصموقعیتاندازه
بالاکاربران آنلاینردیف اول، ستون اولبزرگ
بالابازدیدکنندگان امروزردیف اول، ستون دومبزرگ
متوسطصفحات بازدیدشدهردیف دوممتوسط
متوسطدقایق حضورردیف دوممتوسط
پایینپربازدیدترین صفحهردیف سومکوچک

این سلسله‌مراتب، به کاربر کمک می‌کند که در یک نگاه، مهم‌ترین شاخص‌ها را ببیند.

امنیت پنل آمار

پنل آمار، داده‌های حساسی را نمایش می‌دهد. سه لایه‌ی امنیتی را در نظر بگیرید.

لایه‌ی اول، احراز هویت. همه‌ی viewهای پنل باید با @staff_member_required یا @permission_required محافظت شوند.

لایه‌ی دوم، CSRF. همه‌ی formها باید توکن CSRF داشته باشند. endpointهای beacon، استثنا هستند ولی در بخش ساخت API Endpoint برای دریافت Beacon توضیح داده شده که چطور باید محافظت شوند.

لایه‌ی سوم، rate limiting. endpointهای AJAX پنل، باید rate limit داشته باشند تا یک کاربر نتواند سرور را overload کند.

from django_ratelimit.decorators import ratelimit


@require_GET
@ratelimit(key="user", rate="60/m", block=True)
def stats_summary(request):
    ...

تست‌نویسی پنل

سه سطح تست را در نظر بگیرید.

سطح اول، تست دسترسی. بررسی کنید که فقط کاربران مجاز می‌توانند پنل را ببینند.

@pytest.mark.django_db
def test_anonymous_cannot_access_panel(client):
    response = client.get("/panel/stat/")
    assert response.status_code in (302, 403)


@pytest.mark.django_db
def test_staff_can_access_panel(client):
    from django.contrib.auth.models import User

    user = User.objects.create_user(
        "admin", "a@example.com", "pass",
        is_staff=True,
    )
    client.force_login(user)

    response = client.get("/panel/stat/")
    assert response.status_code == 200

سطح دوم، تست کارایی. بررسی کنید که پنل تعداد کوئری مناسبی می‌زند.

from django.test.utils import CaptureQueriesContext
from django.db import connection


@pytest.mark.django_db
def test_panel_query_count(client, django_user_model):
    user = django_user_model.objects.create_user(
        "admin", "a@example.com", "pass",
        is_staff=True,
    )
    client.force_login(user)

    with CaptureQueriesContext(connection) as ctx:
        client.get("/panel/stat/")

    # حداکثر ۲۰ کوئری
    assert len(ctx) <= 20

سطح سوم، تست tagها. بررسی کنید که tagهای آماری درست کار می‌کنند.

def test_online_count_tag_is_cached():
    from django.core.cache import cache
    from analytics.templatetags.analytics_tags import analytics_online_count

    cache.clear()
    first = analytics_online_count()
    second = analytics_online_count()

    assert first == second

این سه سطح تست، به شما اجازه می‌دهند که در طول زمان، تغییرات را با اطمینان اعمال کنید.

anti-patternهای رایج در ساخت پنل آمار

در بازبینی پروژه‌های مختلف، این اشتباهات را زیاد دیده‌ام:

۱. محاسبه‌ی آمار در ویو. اگر منطق آماری در ویو باشد، تکرار آن در چند ویو اجتناب‌ناپذیر است. راه‌حل: استفاده از template tags با کش.

۲. عدم کش. بدون کش، هر بازدید از پنل چندین کوئری سنگین می‌زند. راه‌حل: کش ۳۰ تا ۶۰ ثانیه‌ای.

۳. کوئری‌های سنگین بدون ایندکس. اگر entry_time یا last_activity ایندکس نداشته باشند، کوئری‌ها کند می‌شوند.

۴. عدم کنترل دسترسی. هر کاربری نباید بتواند داده‌های آماری را ببیند. راه‌حل: permission سفارشی.

۵. نمایش داده‌ی خام. پنل آمار نباید لیست رکوردها را نشان دهد. باید شاخص‌های تجمیع‌شده را نشان دهد.

۶. عدم invalidate کش. اگر داده‌ی جدیدی اضافه شد، کش باید invalidate شود. راه‌حل: invalidate در کامندهای پاک‌سازی و در beaconها.

۷. ظاهر ناخوشایند. پنلی که زیبا نباشد، استفاده نمی‌شود. راه‌حل: سرمایه‌گذاری در UX.

۸. عدم پاسخ‌گویی. پنل باید در موبایل هم کار کند. راه‌حل: استفاده از CSS grid با breakpointها.

۹. عدم rate limiting. endpointهای AJAX باید rate limit داشته باشند.

۱۰. عدم تست کارایی. بدون تست کارایی، رگرسیون‌های پنهان در production مشکل‌ساز می‌شوند.

۱۱. اضافه کردن کارت‌های زیاد. پنل با ۲۰ کارت، غیرقابل استفاده است. راه‌حل: فقط ۴ تا ۸ کارت کلیدی.

۱۲. عدم هماهنگی با جدول‌های تجمیعی. در پروژه‌های بزرگ، پنل باید روی جدول‌های تجمیعی کار کند، نه داده‌ی خام. اگر با الگوی ذخیره‌ی PageView و Interaction کار کرده باشید، می‌دانید که این جداسازی، چقدر اهمیت دارد.

پرسش‌های پرتکرار درباره‌ی پنل ادمین سفارشی

آیا باید از admin جنگو استفاده کنم یا پنل سفارشی؟ برای مدیریت داده، admin جنگو. برای نمایش آمار، پنل سفارشی. اگر می‌خواهید هر دو را با هم داشته باشید، می‌توانید یک لینک از admin به پنل سفارشی بگذارید.

چند کارت quick view مناسب است؟ بین ۴ تا ۸ کارت. بیش از این تعداد، پنل را شلوغ می‌کند.

چه مدت کش مناسب است؟ برای داده‌ی زنده مثل «کاربران آنلاین»، ۳۰ ثانیه. برای داده‌ی امروز، ۶۰ ثانیه. برای داده‌ی تاریخی، می‌توانید کش طولانی‌تر داشته باشید.

چطور کش را invalidate کنم؟ در کامندهای پاک‌سازی داده و در beaconهای مهم. یک تابع متمرکز برای invalidate بنویسید و در همه‌ی نقاط مربوطه صدا بزنید.

آیا باید نمودار هم اضافه کنم؟ برای پنل quick view، نمودار ضروری نیست. برای پنل تحلیلی، بله. کتابخانه‌های مثل Chart.js یا Plotly گزینه‌های مناسبی هستند.

چطور با تیم محصول دسترسی به اشتراک بگذارم؟ با Django Groups یا permission سفارشی. به تیم محصول فقط دسترسی view بدهید، نه edit یا delete.

آیا باید پنل را با i18n بین‌المللی کنم؟ اگر تیم چندملیتی دارید، بله. برای تیم فارسی‌زبان، معمولاً نیازی نیست.

چطور با کاربران موبایل که در حرکت هستند، کار کنم؟ پنل باید responsive باشد. از CSS grid با breakpointهای استاندارد استفاده کنید.

آیا باید از HTMX استفاده کنم؟ برای پنل‌های ساده، HTMX انتخاب خوبی است. برای پنل‌های پیچیده، React یا Vue. برای پنل با کارت‌های quick view، HTMX کافی است.

آیا باید داده‌ی خام را در پنل نمایش دهم؟ نه. پنل آمار برای شاخص‌های تجمیع‌شده است. برای داده‌ی خام، از admin یا صفحه‌ی جداگانه استفاده کنید.

چطور با داده‌ی تکراری مقابله کنم؟ با unique constraint در سطح مدل. اگر با الگوی حذف رکوردهای تکراری و یتیم با batch delete کار کرده باشید، می‌دانید که این نوع constraint، بخشی از طراحی idempotent است.

آیا باید پنل را برای اپلیکیشن موبایل بسازم؟ برای پنل آمار، رابط وب کافی است. اگر تیم شما نیاز به اپلیکیشن دارد، می‌توانید یک API برای آن بسازید.

چطور از داده‌ی پنل بکاپ بگیرم؟ داده‌ی پنل، داده‌ی کش است، نه داده‌ی اصلی. از دیتابیس اصلی بکاپ بگیرید.

آیا باید پنل را با CDN سرو کنم؟ برای سایت‌های بین‌المللی، بله. برای سایت‌های داخلی، معمولاً نیازی نیست.

چطور پنل را در برابر خطاهای دیتابیس مقاوم کنم؟ هر tag آماری را در try/except بپیچید و در صورت خطا، یک مقدار پیش‌فرض برگردانید.

@register.simple_tag
def analytics_online_count():
    try:
        key = "analytics:online_count"
        value = cache.get(key)
        if value is None:
            value = online_count()
            cache.set(key, value, timeout=30)
        return value
    except Exception:
        return "—"

نگاهی از منظر مهندس محصول در مقیاس بزرگ

در مقیاس بزرگ، پنل آمار تبدیل به یک محصول داخلی می‌شود که ارزش آن، با تعداد کاربران و سرعت پاسخ‌دهی اندازه‌گیری می‌شود. سه مفهوم بنیادین را باید بازتعریف کنید.

مفهوم اول، جداول تجمیعی چندسطحی. به‌جای محاسبه‌ی هر بار از داده‌ی خام، از چند سطح تجمیع استفاده کنید: دقیقه‌ای، ساعتی، روزانه، ماهانه. هر سطح، یک جدول جداگانه دارد و بر اساس نیاز، از سطح مناسب استفاده می‌شود. این معماری، الگوی استانداردی در سیستم‌های BI (Business Intelligence) است.

مفهوم دوم، کش چندلایه. از ترکیب چند لایه کش استفاده کنید: کش محلی (in-memory)، کش توزیع‌شده (Redis)، و کش دیتابیس (materialized views). این ترکیب، تأخیر پاسخ را به حداقل می‌رساند.

مفهوم سوم، مشاهده‌پذیری. در مقیاس بزرگ، باید بتوانید بفهمید که چرا یک کارت مقدار خاصی را نشان می‌دهد. این کار با ثبت لاگ ساختاریافته و پیوند به کوئری اصلی انجام می‌شود. مفهوم Observability در ویکی‌پدیا توضیح داده شده است.

نکته‌ی آخر: در مقیاس بزرگ، پنل آمار نباید روی دیتابیس اصلی کوئری بزند. باید از یک replica یا یک دیتابیس جداگانه برای تحلیل استفاده کند. اگر با الگوی بهینه‌سازی جنگو برای ترافیک بالا کار کرده باشید، این معماری برایتان آشناست.

پرسشی که در پایان باید پاسخ دهید

قبل از اینکه پنل آمار خود را نهایی کنید، یک پرسش را از خودتان بپرسید: «اگر امروز تیم محصول بخواهد یک شاخص جدید به پنل اضافه کند، چقدر طول می‌کشد؟» اگر پاسخ شما «چند روز» است، یعنی معماری پنل شما به اندازه‌ی کافی ماژولار نیست. اگر پاسخ شما «چند دقیقه» است، یعنی معماری شما به‌درستی طراحی شده است.

ساخت پنل آمار، در نهایت یک تصمیم مهندسی است که به بهره‌وری تیم و کیفیت تصمیم‌گیری کسب‌وکار شما گره خورده است. اگر این تجربه را در پروژه‌ی خودتان داشته‌اید — مثلاً جایی که پنل شما در حجم بالا کند شده یا جایی که یک کارت جدید به‌سختی اضافه شده — برایم جالب است بدانید. مخصوصاً اگر راه‌حل خاصی برای یک سناریوی خاص پیدا کرده‌اید، چون همان راه‌حل‌ها می‌توانند به خواننده‌ی بعدی کمک کنند. تجربه‌ی خودتان را در دیدگاه‌ها بنویسید.