fix sms
This commit is contained in:
1 parent
1dea3509a7
commit
e3a6d1f9d4
42 files changed
+33086
-15779
No files matched your search
@@ -0,0 +1,156 @@
|
||||
"""بازمحاسبهٔ ``Customer.last_bazdid_dt`` از روی اقدامهای «بازدیدِ تمامشده».
|
||||
|
||||
فیلد ``last_bazdid_dt`` «آخرین بازدید» مشتری است و صفحهٔ ``/panel/entezar/`` بر پایهٔ
|
||||
آن مشتریانِ معوق (بیش از N ماه بازدید نشده) را لیست میکند. این مقدار باید برابر
|
||||
«بزرگترین ``finishDateTime`` میان اقدامهای ``actionType='bazdid'`` و ``status='finish'``
|
||||
هر مشتری» باشد — همان چیزی که ویوهای ``customerNewAction``/``customerEditAction``
|
||||
مینویسند.
|
||||
|
||||
اگر رکوردی از راهی غیر از این ویوها وارد شود (اسکریپت، ایمپورت، ویرایش دستی دیتابیس)،
|
||||
این فیلد عقب میماند و مشتریِ واقعاً بازدیدشده اشتباهاً «معوق» شمرده میشود. این
|
||||
دستور آن واگرایی (drift) را از منبع حقیقت (جدول Action) ترمیم میکند.
|
||||
|
||||
python manage.py recalc_last_bazdid --dry-run # فقط گزارش، بدون هیچ تغییری
|
||||
python manage.py recalc_last_bazdid # اعمال اصلاحات
|
||||
|
||||
مشخصات:
|
||||
* idempotent — اجرای دوباره پس از یکبار اجرا هیچ تغییری ایجاد نمیکند.
|
||||
* پیشفرض محافظهکارانه است: فقط مقدار را **بهروز** میکند و هیچوقت روی مقادیری که
|
||||
اقدامِ پشتیبان ندارند دست نمیگذارد (مگر با ``--clear-orphans``).
|
||||
* روی دیتابیس production هم قابل اجرا است.
|
||||
"""
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db import transaction
|
||||
from django.db.models import Max
|
||||
|
||||
from ...models import Action, Customer
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = (
|
||||
"بازمحاسبهٔ Customer.last_bazdid_dt از MAX(finishDateTime) اقدامهای "
|
||||
"بازدیدِ تمامشده (ترمیم واگرایی داده؛ idempotent)."
|
||||
)
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"--dry-run", action="store_true",
|
||||
help="فقط گزارش کن که چه چیزی تغییر میکند؛ هیچ رکوردی ذخیره نمیشود.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--clear-orphans", action="store_true",
|
||||
help="مقدارهایی که هیچ اقدامِ بازدیدِ تمامشدهای پشتشان نیست را هم NULL کن "
|
||||
"(پیشفرض: دستنخورده بمانند). فقط اگر مطمئنی این ستون آلوده است.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--batch-size", type=int, default=500,
|
||||
help="اندازهٔ دستهٔ خواندن مشتریان در هر گام (پیشفرض ۵۰۰).",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
dry_run = options["dry_run"]
|
||||
clear_orphans = options["clear_orphans"]
|
||||
batch_size = max(1, options["batch_size"])
|
||||
|
||||
prefix = "[شبیهسازی] " if dry_run else ""
|
||||
self.stdout.write(self.style.MIGRATE_HEADING(
|
||||
f"{prefix}بازمحاسبهٔ آخرین بازدید مشتریان آغاز شد."
|
||||
))
|
||||
|
||||
# منبع حقیقت: بزرگترین زمانِ پایانِ بازدید در اقدامهای تمامشدهٔ هر مشتری.
|
||||
# (Max مقادیر NULL را نادیده میگیرد؛ مشتریِ بدون بازدید بهکل در نقشه نمیآید.)
|
||||
latest_rows = (
|
||||
Action.objects
|
||||
.filter(actionType="bazdid", status="finish")
|
||||
.exclude(customer_id__isnull=True)
|
||||
.values("customer_id")
|
||||
.annotate(mx=Max("finishDateTime"))
|
||||
)
|
||||
latest = {
|
||||
row["customer_id"]: row["mx"]
|
||||
for row in latest_rows.iterator(chunk_size=batch_size)
|
||||
if row["mx"] is not None
|
||||
}
|
||||
|
||||
stats = {
|
||||
"scanned": 0, # کل مشتریان بررسیشده
|
||||
"with_visit": 0, # مشتریان دارای بازدیدِ تمامشده
|
||||
"updated": 0, # مقدار اصلاح/بهروز شد
|
||||
"moved_forward": 0, # جدیدتر شد ⇒ از لیست معوقها خارج میشود
|
||||
"moved_backward": 0, # عقبتر شد
|
||||
"set_null": 0, # با --clear-orphans خالی شد
|
||||
"already_ok": 0, # از قبل درست بود
|
||||
"kept_orphan": 0, # مقدار داشت ولی اقدامِ پشتیبان نداشت (دستنخورده ماند)
|
||||
"null_and_no_action": 0, # NULL و بدون اقدام (طبیعی؛ کاری لازم نیست)
|
||||
}
|
||||
|
||||
pending = [] # (customer_id, target_value) برای اعمال گروهی
|
||||
|
||||
qs = Customer.objects.only("id", "last_bazdid_dt").order_by("id")
|
||||
for customer in qs.iterator(chunk_size=batch_size):
|
||||
stats["scanned"] += 1
|
||||
mx = latest.get(customer.id)
|
||||
current = customer.last_bazdid_dt
|
||||
|
||||
if mx is not None:
|
||||
stats["with_visit"] += 1
|
||||
if current == mx:
|
||||
stats["already_ok"] += 1
|
||||
continue
|
||||
if current is None or current < mx:
|
||||
stats["moved_forward"] += 1
|
||||
else:
|
||||
stats["moved_backward"] += 1
|
||||
stats["updated"] += 1
|
||||
pending.append((customer.id, mx))
|
||||
else:
|
||||
# هیچ بازدیدِ تمامشدهای پشتوانهٔ این مقدار نیست.
|
||||
if current is None:
|
||||
stats["null_and_no_action"] += 1
|
||||
elif clear_orphans:
|
||||
stats["set_null"] += 1
|
||||
pending.append((customer.id, None))
|
||||
else:
|
||||
stats["kept_orphan"] += 1
|
||||
|
||||
if not dry_run and pending:
|
||||
with transaction.atomic():
|
||||
for customer_id, target in pending:
|
||||
Customer.objects.filter(pk=customer_id).update(
|
||||
last_bazdid_dt=target
|
||||
)
|
||||
|
||||
self._report(stats, dry_run, clear_orphans)
|
||||
|
||||
def _report(self, stats, dry_run, clear_orphans):
|
||||
style = self.style
|
||||
label = "[شبیهسازی] " if dry_run else ""
|
||||
self.stdout.write(style.MIGRATE_LABEL(f"\n{label}گزارش بازمحاسبهٔ آخرین بازدید:"))
|
||||
self.stdout.write(f" بررسیشده (کل مشتریان): {stats['scanned']:,}")
|
||||
self.stdout.write(f" دارای بازدیدِ تمامشده: {stats['with_visit']:,}")
|
||||
self.stdout.write(f" از قبل درست: {stats['already_ok']:,}")
|
||||
self.stdout.write(f" نیازمند اصلاح: {stats['updated']:,}")
|
||||
self.stdout.write(f" → جدیدتر شد (خروج از معوق): {stats['moved_forward']:,}")
|
||||
self.stdout.write(f" → عقبتر شد: {stats['moved_backward']:,}")
|
||||
if clear_orphans:
|
||||
self.stdout.write(f" → خالی شد (بدون اقدام): {stats['set_null']:,}")
|
||||
else:
|
||||
self.stdout.write(
|
||||
f" بدون اقدام ولی دارای مقدار (دستنخورده): {stats['kept_orphan']:,}"
|
||||
)
|
||||
self.stdout.write(
|
||||
f" NULL و بدون اقدام (طبیعی): {stats['null_and_no_action']:,}"
|
||||
)
|
||||
|
||||
if dry_run:
|
||||
self.stdout.write(style.WARNING(
|
||||
"\n[حالت شبیهسازی] هیچ رکوردی ذخیره نشد. برای اعمال، بدون --dry-run اجرا کن."
|
||||
))
|
||||
elif stats["updated"]:
|
||||
self.stdout.write(style.SUCCESS(
|
||||
f"\nانجام شد: {stats['updated']:,} رکورد بهروزرسانی شد."
|
||||
))
|
||||
else:
|
||||
self.stdout.write(style.SUCCESS(
|
||||
"\nانجام شد: هیچ اصلاحی لازم نبود (داده از قبل درست است)."
|
||||
))
|
||||
Reference in new issue
Block a user