فصل ۸: تست، استقرار و پروژه‌ی پایانی

پروژه‌ی پایانی: سامانه‌ی ثبت سفارش فرش

همه‌چیز کنار هم

یک کارخانه‌ی فرش ماشینی در کاشان می‌خواهد سفارش فروشگاه‌های سراسر کشور را آنلاین بگیرد. نیازها: فروشگاه‌ها با موبایل و OTP وارد شوند، طرح‌ها را با قیمت و موجودی ببینند، سفارش چندقلمی ثبت کنند و وضعیتش را پیگیری کنند؛ کارمندان فروش در ادمین سفارش‌ها را تأیید کنند؛ اپ اندرویدی بازاریاب‌ها از API استفاده کند؛ و مدیر هر ماه گزارش فروش ماه‌های شمسی را ببیند. این درس نقشه‌ی اجرای پروژه است؛ هر گام به درس‌های قبلی ارجاع دارد.

گام ۱: ساختار و مدل‌ها

اپمدل‌هادرس مرجع
accountsUser (موبایل)، OTP در Redis۵ و ۳۰
catalogCarpet، CarpetImage۶ و ۲۴
ordersCustomer، Order، OrderItem۷، ۸ و ۱۰
apiSerializerها و ViewSetها۳۱ و ۳۲
reportsبدون مدل؛ فقط کوئری و Viewهمین درس
# apps/orders/models.py (بخش کلیدی)
class Order(models.Model):
    ...
    objects = OrderQuerySet.as_manager()

    def recalculate_total(self):
        agg = self.items.aggregate(s=Sum(F("quantity") * F("unit_price"), default=0))
        self.total = agg["s"] - self.discount
        self.save(update_fields=["total"])

گام ۲: ثبت سفارش

فرم سربرگ + inline formset اقلام (درس ۲۳)، و ذخیره از طریق سرویس place_order با atomic و select_for_update (درس ۱۵). قیمت واحد همیشه از سرور خوانده می‌شود. بعد از commit، پیامک تأیید با Celery (درس ۳۴).

گام ۳: ادمین کارمندان

OrderAdmin با inline اقلام، action «تأیید» که فقط گروه «فروش» با مجوز approve_order می‌بیند، ستون مبلغ تومانی و تاریخ شمسی، و فیلتر سفارشی «این ماه شمسی» (درس‌های ۲۶ تا ۲۹).

گام ۴: API اپ بازاریاب‌ها

endpointهای /api/v1/carpets/ (فقط‌خواندنی برای بازاریاب) و /api/v1/orders/ (فقط سفارش‌های خودش)، با JWT و throttle جدا برای درخواست OTP.

گام ۵: گزارش ماهانه‌ی شمسی

TruncMonth ماه میلادی می‌دهد؛ پس بازه‌ی هر ماه شمسی را با jdatetime می‌سازیم و با یک کوئری aggregate برای هر ماه جمع می‌زنیم:

# apps/reports/services.py
import datetime

import jdatetime
from django.db.models import Count, Sum
from django.utils import timezone

from apps.orders.models import Order

MONTHS = ["فروردین", "اردیبهشت", "خرداد", "تیر", "مرداد", "شهریور",
          "مهر", "آبان", "آذر", "دی", "بهمن", "اسفند"]


def jalali_month_range(jy, jm):
    start = jdatetime.date(jy, jm, 1).togregorian()
    ny, nm = (jy + 1, 1) if jm == 12 else (jy, jm + 1)
    end = jdatetime.date(ny, nm, 1).togregorian()
    tz = timezone.get_current_timezone()
    return (timezone.make_aware(datetime.datetime.combine(start, datetime.time.min), tz),
            timezone.make_aware(datetime.datetime.combine(end, datetime.time.min), tz))


def yearly_report(jy):
    rows = []
    paid = Order.objects.filter(status__in=["paid", "shipped"])
    for jm in range(1, 13):
        start, end = jalali_month_range(jy, jm)
        agg = paid.filter(created_at__gte=start, created_at__lt=end).aggregate(
            n=Count("id"), total=Sum("total", default=0))
        rows.append({"month": MONTHS[jm - 1], **agg})
    return rows

View گزارش با PermissionRequiredMixin و cache_page یک‌ساعته، و قالب با فیلتر toman (درس ۲۰). برای خروجی اکسل می‌توانید همین rows را با openpyxl بنویسید.

گام ۶: تست و استقرار

دست‌کم این تست‌ها: دسترسی نداشتن کاربر به سفارش دیگران، rollback در کمبود موجودی، تعداد کوئری فهرست سفارش‌ها و اعتبارسنجی کد ملی. سپس استقرار طبق درس‌های ۳۷ و ۳۸ و اجرای check --deploy بدون هشدار.

نکته‌هایی که کمتر کسی می‌داند

  • ماه‌های اسفند ۲۹ یا ۳۰ روزه‌اند؛ با گرفتن «اول ماه بعد» به‌عنوان مرز انتهایی (مثل کد بالا) هرگز لازم نیست طول ماه را حساب کنید.
  • دوازده کوئری aggregate برای گزارش سالانه کاملاً قابل‌قبول است؛ اگر ماهانه صدها گزارش می‌خواهید، به‌جای آن ستونی با کد ماه شمسی (مثلاً ۱۴۰۴۰۸) هنگام ذخیره‌ی سفارش بسازید و روی آن GROUP BY کنید.
  • Sum(F("quantity") * F("unit_price")) ضرب را در SQL انجام می‌دهد؛ برای جلوگیری از سرریز روی جمع‌های بزرگ، هر دو ستون را BigInteger نگه دارید.
  • پیش از راه‌اندازی، داده‌ی آزمایشی واقع‌گرایانه (چند هزار سفارش با factory) بسازید و صفحات را با debug-toolbar بسنجید؛ مشکلات N+1 با ده رکورد دیده نمی‌شوند.
  • تاریخچه‌ی تغییر وضعیت سفارش را در یک مدل جدا (OrderEvent با کاربر و زمان) ثبت کنید؛ اولین سؤال مدیر بعد از هر مشکل «چه کسی و کی؟» است.

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