139 lines
6.8 KiB
Markdown
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` است، نه این ستون. در صورت شک، همان کوئریهای بخش
|
|
«بررسی صحت داده» ملاک هستند.
|
|
```
|