همهچیز کنار هم
یک کارخانهی فرش ماشینی در کاشان میخواهد سفارش فروشگاههای سراسر کشور را آنلاین بگیرد. نیازها: فروشگاهها با موبایل و OTP وارد شوند، طرحها را با قیمت و موجودی ببینند، سفارش چندقلمی ثبت کنند و وضعیتش را پیگیری کنند؛ کارمندان فروش در ادمین سفارشها را تأیید کنند؛ اپ اندرویدی بازاریابها از API استفاده کند؛ و مدیر هر ماه گزارش فروش ماههای شمسی را ببیند. این درس نقشهی اجرای پروژه است؛ هر گام به درسهای قبلی ارجاع دارد.
گام ۱: ساختار و مدلها
| اپ | مدلها | درس مرجع |
|---|---|---|
| accounts | User (موبایل)، OTP در Redis | ۵ و ۳۰ |
| catalog | Carpet، CarpetImage | ۶ و ۲۴ |
| orders | Customer، Order، OrderItem | ۷، ۸ و ۱۰ |
| api | Serializerها و 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 با کاربر و زمان) ثبت کنید؛ اولین سؤال مدیر بعد از هر مشکل «چه کسی و کی؟» است.