Files
khatibi/recalc_last_bazdid.md
2026-10-11 19:04:20 +03:30

139 lines
6.8 KiB
Markdown

# بازمحاسبهٔ «آخرین بازدید» مشتریان
## چرا؟
فیلد `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` است، نه این ستون. در صورت شک، همان کوئری‌های بخش
«بررسی صحت داده» ملاک هستند.
```