# بازمحاسبهٔ «آخرین بازدید» مشتریان ## چرا؟ فیلد `Customer.last_bazdid_dt` یعنی «آخرین بازدیدی که برای مشتری انجام شده» و صفحهٔ `/panel/entezar/` بر پایهٔ آن مشتریانِ **معوق** (بیش از N ماه بازدید نشده) را لیست می‌کند. قاعدهٔ درست (منبع حقیقت): این مقدار باید برابر **بزرگ‌ترین `finishDateTime` میان اقدام‌های `actionType='bazdid'` و `status='finish'` همان مشتری** باشد — دقیقاً همان چیزی که ویوهای `customerNewAction` و `customerEditAction` می‌نویسند: ```python if newAction.status == 'finish' and newAction.actionType == 'bazdid': customer_obj.last_bazdid_dt = newAction.finishDateTime customer_obj.save() ``` پس اگر رکوردی از راهی **غیر از این ویوها** وارد شود (اسکریپت، ایمپورت، ویرایش دستی دیتابیس، اجرای دستی `gt_esh` و…)، این فیلد عقب می‌ماند و مشتریِ واقعاً بازدیدشده، اشتباهاً «معوق» شمرده می‌شود. این دستور آن واگرایی (drift) را از روی جدول `Action` ترمیم می‌کند. > دستور **idempotent** است؛ اجرای دوباره‌اش هیچ تغییری ایجاد نمی‌کند. ## دستور نام دستور منیج‌منت: `recalc_last_bazdid` مسیر فایل: `admin_panel/management/commands/recalc_last_bazdid.py` ### ۱) اول «شبیه‌سازی» — بدون هیچ تغییری فقط گزارش می‌دهد چه چیزی تغییر می‌کند. **قبل از هر اجرای واقعی این را بزنید.** از WSL: ```bash .venv/Scripts/python.exe manage.py recalc_last_bazdid --dry-run ``` از Windows: ```powershell .venv\Scripts\python.exe manage.py recalc_last_bazdid --dry-run ``` ### ۲) اجرای واقعی ```bash .venv/Scripts/python.exe manage.py recalc_last_bazdid ``` ## گزینه‌ها | گزینه | پیش‌فرض | کار | |---|---|---| | `--dry-run` | خاموش | فقط گزارش؛ **هیچ رکوردی ذخیره نمی‌شود**. اول این را بزنید. | | `--clear-orphans` | خاموش | مشتریانی که مقدار دارند ولی **هیچ اقدام بازدیدِ تمام‌شده‌ای** پشتشان نیست را هم `NULL` می‌کند. به‌طور پیش‌فرض دست‌نخورده می‌مانند. فقط اگر مطمئنید این ستون آلوده است. | | `--batch-size` | `500` | اندازهٔ دستهٔ خواندن مشتریان در هر گام. | ## دقیقاً چه می‌کند؟ برای هر مشتری، `MAX(finishDateTime)` اقدام‌های بازدیدِ تمام‌شده محاسبه می‌شود، سپس: | وضعیت مشتری | اقدام دستور | |---|---| | بازدید دارد و مقدار فعلی **برابر** MAX است | دست نمی‌زند (`از قبل درست`) | | بازدید دارد و مقدار فعلی **کوچک‌تر یا NULL** است | به MAX **به‌روز** می‌کند — از لیست معوق‌ها خارج می‌شود (`جدیدتر شد`) | | بازدید دارد و مقدار فعلی **بزرگ‌تر از MAX** است | به MAX **عقب** می‌برد (`عقب‌تر شد`) | | بازدید ندارد و مقدار `NULL` است | کاری ندارد (طبیعی) | | بازدید ندارد ولی مقداری دارد | **دست نمی‌زند** — مگر `--clear-orphans` بدهید (`خالی شد`) | نوشتن با `Customer.objects.filter(pk=…).update(last_bazdid_dt=…)` داخل یک `transaction.atomic()` انجام می‌شود؛ یعنی **فقط همین یک ستون** لمس می‌شود و هیچ `save()` کاملی که فیلدهای دیگر (`auto_now` و…) را تغییر دهد اجرا نمی‌شود. ## نمونهٔ خروجی ``` بازمحاسبهٔ آخرین بازدید مشتریان آغاز شد. گزارش بازمحاسبهٔ آخرین بازدید: بررسی‌شده (کل مشتریان): 10,426 دارای بازدیدِ تمام‌شده: 9,151 از قبل درست: 9,084 نیازمند اصلاح: 67 → جدیدتر شد (خروج از معوق): 67 → عقب‌تر شد: 0 بدون اقدام ولی دارای مقدار (دست‌نخورده): 0 NULL و بدون اقدام (طبیعی): 1,275 انجام شد: 67 رکورد به‌روزرسانی شد. ``` ## اجرا روی production **اول از دیتابیس بکاپ بگیرید**، سپس: ```bash python manage.py recalc_last_bazdid --dry-run python manage.py recalc_last_bazdid ``` دستور تراکنشی است و در صورت خطا، هیچ تغییری کامیت نمی‌شود. ## بررسی صحت داده (SQL، پیش از اجرا) اگر می‌خواهید بدون دستور، وضعیت را بسنجید: ```sql -- مشتریانی که مقدارشان با آخرین بازدید واقعی اختلاف دارد (تعداد «نیازمند اصلاح») SELECT COUNT(*) FROM admin_panel_customer cu JOIN (SELECT customer_id, MAX(finishDateTime) mx FROM admin_panel_action WHERE actionType='bazdid' AND status='finish' GROUP BY customer_id) a ON a.customer_id = cu.id WHERE cu.last_bazdid_dt IS NULL OR cu.last_bazdid_dt <> a.mx; -- مشتریانی که بازدید تمام‌شده دارند ولی مقدارشان NULL است (بدترین حالت) SELECT COUNT(*) FROM admin_panel_customer cu WHERE cu.last_bazdid_dt IS NULL AND EXISTS (SELECT 1 FROM admin_panel_action a WHERE a.customer_id=cu.id AND a.actionType='bazdid' AND a.status='finish'); -- تعداد سالم (مقدار دارد) SELECT COUNT(*) FROM admin_panel_customer WHERE last_bazdid_dt IS NOT NULL; ``` ## نکته‌های مهم - مقادیر `NULL` مشتریانی که **هرگز** بازدید تمام‌شده نداشته‌اند، **طبیعی** است و دستور آن‌ها را پر نمی‌کند (اگر پر کند، یعنی داده ساختن — که غلط است). - دستور **هیچ فاکتور، سند، اقدام یا مشتری نمی‌سازد و حذف نمی‌کند**؛ فقط یک ستون را هم‌راستا می‌کند. - صفحهٔ `/panel/entezar/` یک کش در `CacheTable(key='9mahbazdid')` نگه می‌دارد؛ عددِ روی صفحه ممکن است یک اجرا عقب باشد. هر بار باز شدن صفحه آن را به‌روز می‌کند. - منبع حقیقت همیشه جدول `Action` است، نه این ستون. در صورت شک، همان کوئری‌های بخش «بررسی صحت داده» ملاک هستند. ```