35 KiB
افزودن پیشوند «آقای/خانم» به نام مشتریان
چرا؟
فیلد Customer.name_family (همان «نام» مشتری) سالها بدون پیشوند وارد شده:
محمد یاوری، مریم احمدی. برای پیامک و گزارشها بهتر است نام با پیشوند
محترمانه بیاید: آقای محمد یاوری، خانم مریم احمدی.
اما این کار فقط وقتی مجاز است که جنسیتِ اولین کلمهٔ نام با اطمینان قابل
تعیین باشد. فایل iranianNamesDataset.json (حدود ۲۰ هزار نام، M/F) همین
مرجع است.
قاعدهٔ بنیادین: اگر کوچکترین ابهامی در تشخیص جنسیت باشد، هیچ تغییری اعمال نمیشود. حدس جنسیت، تطبیق تقریبی (fuzzy) و انتخاب تصادفی ممنوع است.
مسیرهای اصلی:
| جزء | مسیر |
|---|---|
| منطق تصمیم (بدون Django) | admin_panel/name_fix.py |
| دستور اجرا | admin_panel/management/commands/fix_customer_names.py |
| مدل وضعیت job | CustomerNameFixJob در admin_panel/models/system.py |
| مهاجرت | admin_panel/migrations/0072_customernamefixjob.py |
| تستها | tests/test_customer_name_fix.py |
| فایل داده | iranianNamesDataset.json در ریشهٔ پروژه (/app/iranianNamesDataset.json در کانتینر) |
سریعترین مسیر روی Production (خلاصهٔ اجرایی)
روی VPS، python میزبان وجود ندارد؛ همهچیز داخل کانتینر web اجرا میشود:
cd /path/to/khatibi
# ۱) تأیید وجود فایل داده داخل ایمیج (پیشنیاز قطعی این دستور)
docker compose exec web ls -l /app/iranianNamesDataset.json
docker compose exec web sha256sum /app/iranianNamesDataset.json
# ۲) اعمال migration (فقط این دستور به جدول CustomerNameFixJob نیاز دارد)
docker compose run --rm migrate
# ۳) بکاپ دیتابیس
docker compose exec web python -c "
import sqlite3, pathlib, datetime
src = pathlib.Path('/app/sqlite/db.sqlite3')
dst = src.parent / f'db_backup_{datetime.datetime.now():%Y%m%d_%H%M%S}.sqlite3'
s = sqlite3.connect(f'file:{src}?mode=ro', uri=True, timeout=30); d = sqlite3.connect(dst)
s.backup(d, pages=250, sleep=0.05); d.close(); s.close(); print('backup:', dst)"
# ۴) شبیهسازی روی دیتابیس واقعی — هیچ نوشتنی ندارد
docker compose exec web python manage.py fix_customer_names --dry-run --batch-size 200 --samples 5
# ۵) اجرای واقعی (هر دو فلگ لازم است)
docker compose exec web python manage.py fix_customer_names --apply --confirm-production --batch-size 200
چرا این ترتیب؟
| مرحله | چرا |
|---|---|
| ۱ | دستور مسیر پیشفرض را از settings.BASE_DIR میسازد. اگر فایل داخل ایمیج نباشد، دستور با «فایل داده پیدا نشد» رد میشود. |
| ۲ | fix_customer_names پیش از هر نوشتن، وجود جدول admin_panel_customernamefixjob را چک میکند و بدون آن خطا میدهد. (روی این پروژه entrypoint.sh و سرویس migrate هم خودشان اجرا میکنند، ولی صریح زدن ضرری ندارد.) |
| ۳ | دستور Undo ندارد؛ برگشت فقط با بکاپ ممکن است. |
| ۴ | پیشفرض بدون نوشتن است؛ اعداد (خصوصاً تعداد قابلاصلاح و نامهای مبهم) را میبینی. |
| ۵ | نوشتن نیازمند تأیید صریح اپراتور است، نه حدس از DEBUG یا نام محیط. |
✅ فایل JSON داخل ایمیج کپی میشود (تضمینشده)
Dockerfile حالا صریحاً این فایل را کپی میکند (خط ۶۳):
COPY --chown=django:django iranianNamesDataset.json /app/iranianNamesDataset.json
یعنی مسیر پیشفرض دستور (settings.BASE_DIR = /app) همیشه داخل ایمیج
موجود است و به COPY . . یا محتوای .dockerignore وابسته نیست.
عواقب این انتخاب:
- اگر فایل در build context نباشد، build شکست میخورد (fail-fast) — این رفتار مطلوب است: خطا همانجا در CI دیده میشود، نه بعداً روی سرور با «فایل داده پیدا نشد».
- روی سرورهای میزبان (VPS/CI) باید فایل در build context حاضر باشد. اگر
Coolify/CI ایمیج را از گیت میسازد، فایل باید commit شده باشد (الان
git statusآن را??نشان میدهد، یعنی هنوز track نشده).
برای تست سریع بدون commit و بدون rebuild:
# الف) کپی دستی داخل کانتینر در حال اجرا (تا بازسازی بعدی کانتینر پایدار است)
docker compose cp iranianNamesDataset.json web:/app/iranianNamesDataset.json
# ب) bind-mount از میزبان (فایل هرجا باشد، با هر rebuild هم میماند)
# docker-compose.override.yml
# services:
# web:
# volumes:
# - ./iranianNamesDataset.json:/app/iranianNamesDataset.json:ro
docker compose exec web python manage.py fix_customer_names --dry-run \
--json-path /app/iranianNamesDataset.json
برای تأیید اینکه ایمیج ساختهشده واقعاً فایل را دارد:
docker compose exec web ls -l /app/iranianNamesDataset.json
docker compose exec web sha256sum /app/iranianNamesDataset.json # باید با hash گزارش دستور یکی باشد
چون jobها با SHA-256 فایل شناسایی میشوند، فایل باید بایتبهبایت همان چیزی باشد که dry-run دید. اگر فایل را عوض کردی، ادامهٔ job قدیمی رد میشود و باید
--new-jobبدهی.
اگر وسط کار قطع شد
# الف) ادامهٔ خودکار: فقط همان دستور را دوباره بزن.
# خودش آخرین job ناتمام با همان hash و همان نسخهٔ الگوریتم را پیدا و ادامه میدهد.
docker compose exec web python manage.py fix_customer_names --apply --confirm-production --batch-size 200
# ب) ادامهٔ صریح یک job مشخص
docker compose exec web python manage.py fix_customer_names --apply --confirm-production --resume 3
# ج) اگر شمارهٔ job را نمیدانی:
docker compose exec web python manage.py shell -c "
from admin_panel.models import CustomerNameFixJob as J
print(list(J.objects.values('id','status','last_processed_pk','initial_max_pk','stats')))"
اگر job قبلی
completedباشد، اجرای دوباره خطا میدهد و خودکار job جدید نمیسازد (تا داده دوباره دست نخورد). برای اجرای عمدی از صفر:--new-job.
بررسی نتیجه
docker compose exec web python manage.py shell -c "
from admin_panel.models import Customer
print('آقای:', Customer.objects.filter(name_family__startswith='آقای ').count())
print('خانم:', Customer.objects.filter(name_family__startswith='خانم ').count())"
فقط فیلد name_family مشتریان واجد شرایط. هیچ فیلدی به مدل مشتری اضافه
نشده، هیچ جنسیتی ذخیره نمیشود، و تلفن/آدرس/یادداشت/وضعیت حساب
دستنخورده میمانند. جدول PhoneNumber هم لمس نمیشود.
الگوریتم تصمیم (به همین ترتیب)
| # | بررسی | نتیجه |
|---|---|---|
| ۱ | نام خالی / فقط فاصله / NULL |
بدون تغییر |
| ۲ | اولین کلمه آقا، آقای یا خانم |
بدون تغییر (پیشوند تکرار نمیشود) |
| ۳ | نام تککلمهای | بدون تغییر (ممکن است فقط فامیل باشد) |
| ۴ | اولین کلمه در فایل پیدا نشود | بدون تغییر |
| ۵ | جنسیت اولین کلمه مبهم/نامعتبر باشد | بدون تغییر |
| ۶ | وگرنه | افزودن قطعی پیشوند |
جستوجو فقط روی اولین کلمه انجام میشود؛ از فامیل و کلمات بعدی برای حدس استفاده نمیشود.
نمونههای رفتاری
| مقدار فعلی | نتیجه |
|---|---|
محمد یاوری |
آقای محمد یاوری |
مریم احمدی |
خانم مریم احمدی |
یاوری |
بدون تغییر — تککلمهای |
محمد |
بدون تغییر — تککلمهای |
آقا محمد یاوری |
بدون تغییر |
آقای محمد یاوری |
بدون تغییر |
خانم مریم احمدی |
بدون تغییر |
نام ناشناخته احمدی |
بدون تغییر — ناشناخته |
آرش محمدی |
بدون تغییر — جنسیت متناقض در فایل |
محمد یاوری |
آقای محمد یاوری (فاصلههای سر/ته فقط در این حالت حذف میشوند) |
نام ناشناخته احمدی |
بدون تغییر، با حفظ فاصلههای اصلی |
نکته: حذف فاصلههای سر/ته بهتنهایی هیچوقت باعث تغییر نمیشود؛ فقط وقتی
تصمیم قطعی به افزودن پیشوند گرفته شد، خروجی از نام strip()شده ساخته میشود.
فاصلههای داخلی دستنخورده میمانند (محمد یاوری → آقای محمد یاوری).
نرمالسازی (فقط برای کلید جستوجو)
NFC + حذف فاصلهٔ سر/ته + یکسانسازی ي→ی و ك→ک. مقدارِ ذخیرهشدهٔ مشتری
حرفبهحرف همان مقدار اصلی است (علي رضایی → آقای علي رضایی).
برخورد با نامهای تکراری و متناقض
یک نام فقط وقتی قابل استفاده است که همهٔ رکوردهای همان کلید نرمالشده
جنسیت معتبر (M/F) و یکسان داشته باشند:
| وضعیت در فایل | تصمیم |
|---|---|
چند رکورد، همه M یا همه F |
قابل استفاده |
همان نام هم M هم F |
مبهم ⇒ بدون تغییر |
جنسیت ناشناخته (D، MF، FF، خالی) |
غیرقابل استفاده ⇒ بدون تغییر |
رکورد ناقص/Names غیر رشته/نام خالی |
رد میشود؛ کل آن نام کنار گذاشته میشود |
| یکیشدن دو کلید با جنسیت متناقض پس از نرمالسازی | مبهم ⇒ بدون تغییر |
«اولین رکورد» یا «پرتکرارترین جنسیت» بدون دلیل معتبر انتخاب نمیشود؛ تناقض یعنی نام کنار گذاشته میشود.
وضعیت فایل دادهٔ فعلی پروژه:
SHA-256: f5953a216a0b71e59e20249f5bfa6a380a2c3179c6b15e4f0e60616b94948114
رکورد کل: 19881 | کلید قابلاستفاده: 19870 | کلید غیرقابلاستفاده: 5
جنسیت نامعتبر: 5 (فروزان/فروزن/فروزنده = D، گوهربانو = FF، مهروز = MF)
نام تکراری: 6، همه همجنسیت (بدون تناقض)
گزینههای دستور
| گزینه | پیشفرض | کار |
|---|---|---|
--json-path |
iranianNamesDataset.json در ریشهٔ پروژه |
مسیر فایل داده |
--batch-size |
200 |
تعداد مشتری در هر batch |
--dry-run |
خاموش (اما رفتار پیشفرض همین است) | گزارش بدون هیچ نوشتنی؛ همراه --apply ناسازگار است |
--apply |
خاموش | نوشتن واقعی؛ بدون آن اجرا فقط گزارش است |
--confirm-production |
خاموش | تأیید صریح اپراتور؛ همراه --apply الزامی |
--resume <JOB_ID> |
— | ادامهٔ یک job مشخص |
--new-job |
خاموش | شروع عمدی job جدید، حتی اگر job تکمیلشدهای برای همین فایل باشد |
--samples |
5 |
تعداد نمونهٔ «قبل/بعد» در گزارش |
پیشفرض دستور بدون نوشتن است. هیچکدام از اینها تغییر نمیدهند: نام مشتری، رکورد job، ساختار دیتابیس، فایل JSON. حتی migration هم خودکار اجرا نمیشود.
اجرای محلی (توسعه)
۱) اول همیشه شبیهسازی
از WSL:
cd /mnt/d/projects/khatibi
.venv/Scripts/python.exe manage.py fix_customer_names --dry-run --samples 5
از Windows (PowerShell / CMD):
cd D:\projects\khatibi
.venv\Scripts\python.exe manage.py fix_customer_names --dry-run --samples 5
خروجی نمونه (دادهٔ واقعی همین پروژه، ۱۰٬۴۲۶ مشتری):
مقصد دیتابیس: alias=default engine=django.db.backends.sqlite3
NAME: D:\Projects\khatibi\data9\kt.sqlite3
(فقط نام/مسیر دیتابیس چاپ میشود؛ credential چاپ نمیشود.)
اعتبارسنجی فایل داده
مسیر: D:\Projects\khatibi\iranianNamesDataset.json
SHA-256: f5953a21…
رکورد کل: 19881 | کلید قابلاستفاده: 19870 | کلید غیرقابلاستفاده: 5
ردشده → malformed: 0 | نام خالی: 0 | جنسیت نامعتبر: 5
نام تکراری: 6 (همجنسیت: 6 | متناقض: 0)
گزارش dry-run — هیچ تغییری نوشته نشد
مشتریان بررسیشده: 10426
قابل اصلاح / اصلاحشده: 7303
بدون تغییر: 3123
تککلمهای: 1576 | دارای پیشوند: 89 | خالی/نامعتبر: 0
ناشناخته در JSON: 1457 | جنسیت مبهم/نامعتبر: 1
batchهای تکمیلشده: 53
نمونهٔ تغییرات (قبل → بعد):
«مجتبی سالاری» → «آقای مجتبی سالاری»
«مرتضی محمد باقری » → «آقای مرتضی محمد باقری»
«محمد فصاحت » → «آقای محمد فصاحت»
برای اجرای واقعی: --apply --confirm-production
۲) اجرای واقعی (فقط روی مقصد دلخواه)
.venv/Scripts/python.exe manage.py fix_customer_names --apply --confirm-production
بدون --confirm-production:
CommandError: برای نوشتن واقعی، هم --apply و هم --confirm-production لازم است.
اجرا روی سرور Production
پیشنیاز: migration باید اعمال شده باشد. روی این پروژه migration سرویس وب
خودکار اجرا میشود (entrypoint.sh و سرویس migrate در docker-compose.yml)،
ولی برای اطمینان:
docker compose run --rm migrate # یا: python manage.py migrate
اگر جدول job نباشد، دستور پیش از هر تغییری خطا میدهد و هیچ نامی را تغییر نمیدهد:
CommandError: جدول «admin_panel_customernamefixjob» وجود ندارد.
ابتدا migration را اعمال کن: python manage.py migrate
ترتیب دقیق اجرا
خلاصهٔ اجرایی و کامل این ترتیب در بخش «سریعترین مسیر روی Production» بالای همین سند آمده؛ این همان نسخهٔ جزئیتر با تأکید بر فایل داده:
# ۰) فایل داده داخل کانتینر — تأیید وجود و هش
docker compose exec web ls -l /app/iranianNamesDataset.json
docker compose exec web sha256sum /app/iranianNamesDataset.json
# ۱) اعمال migration (لازم؛ جدول CustomerNameFixJob را میسازد)
docker compose run --rm migrate
# ۲) بکاپ دیتابیس (بخش «بکاپ پیش از اجرای واقعی» را ببین)
docker compose exec web python manage.py sqlite_db_backup # نیازمند env های S3
# اگر S3 تنظیم نیست، اسنیپت WAL-safe بخش «سریعترین مسیر» را بزن.
# ۳) شبیهسازی روی دیتابیس واقعی — هیچ نوشتنی ندارد
docker compose exec web python manage.py fix_customer_names --dry-run --batch-size 200 --samples 5
# ۴) اگر اعداد منطقی بود: اجرای واقعی
docker compose exec web python manage.py fix_customer_names --apply --confirm-production --batch-size 200
# ۵) بررسی خلاصه و وضعیت job
docker compose exec web python manage.py shell -c "from admin_panel.models import CustomerNameFixJob as J; print(list(J.objects.values('id','status','source_hash','last_processed_pk','initial_max_pk','stats')))"
چرا
docker compose exec webو نهpython manage.py؟ روی VPS پایتون پروژه فقط داخل کانتینر است و دیتابیس هم در ولوم/app/sqliteهمان کانتینر قرار دارد. اجرایpython manage.pyروی میزبان یا به دیتابیس اشتباه وصل میشود یا اصلاً وجود ندارد.
اطمینان از انتخاب دیتابیس درست
دستور از تنظیمات فعال Django (DJANGO_SETTINGS_MODULE) استفاده میکند و هیچ
مسیر SQLite ای هاردکد نشده است. پیش از هر کاری، alias + engine + مسیر دیتابیس
را چاپ میکند (بدون credential):
مقصد دیتابیس: alias=default engine=django.db.backends.sqlite3
NAME: /app/sqlite/db.sqlite3
توجه: ماژول تنظیمات production فعال است.
- روی سرور،
DJANGO_SETTINGS_MODULE=khatibi.production_settingsوNAME=/app/sqlite/db.sqlite3(ولومkhatibi-sqlite-dbدر کانتینر) است. - در توسعه،
khatibi.settingsوdata9/kt.sqlite3. - اگر مسیر چاپشده همان چیزی نیست که انتظار داری، همانجا
Ctrl+Cبزن؛ هیچ دادهای تا آن لحظه نوشته نشده است. - تشخیص production بر پایهٔ نام ماژول تنظیمات است، نه
DEBUG. (در این پروژهproduction_settings.pyمقدارDEBUG=Trueدارد؛ پس اتکا بهDEBUGغلط بود و عمداً از آن استفاده نشده.)
بکاپ پیش از اجرای واقعی
دستور آمادهٔ همین پروژه روی سرور (لینوکس/کانتینر): sqlite_db_backup — از متد
بومی SQLite (Connection.backup) با اتصال mode=ro استفاده میکند و بنابراین با
حالت WAL سازگار است. دو شرط دارد: فقط روی لینوکس اجرا میشود (fcntl) و
متغیرهای S3 را لازم دارد (S3_ACCESS_KEY, S3_SECRET_KEY, S3_ENDPOINT,
S3_BUCKET).
# داخل کانتینر (لینوکس) — نتیجه در S3 ذخیره و اعتبارسنجی میشود
docker compose exec web python manage.py sqlite_db_backup
اگر S3 در دسترس نیست یا روی ویندوز/WSL کار میکنی، همین بکاپ WAL-safe را مستقیم بگیر (همهجا کار میکند و به env نیاز ندارد):
# WSL
.venv/Scripts/python.exe - <<'PY'
import sqlite3, pathlib, datetime
src = pathlib.Path('data9/kt.sqlite3')
dst = src.parent / f"db_backup_{datetime.datetime.now():%Y%m%d_%H%M%S}.sqlite3"
s = sqlite3.connect(f"file:{src}?mode=ro", uri=True, timeout=30)
d = sqlite3.connect(dst)
s.backup(d, pages=250, sleep=0.05) # کپی سطح C، سازگار با WAL
d.close(); s.close()
print("backup:", dst, dst.stat().st_size, "bytes")
PY
کپی سادهٔ فایل SQLite در حین اجرای برنامه بکاپ معتبر نیست. دیتابیس در حالت
WALاجرا میشود (entrypoint.sh)؛ اگر فقطdb.sqlite3راcpکنی، تراکنشهای موجود درdb.sqlite3-walاز دست میروند و ممکن است فایل خراب بازیابی شود. اگر دستی بکاپ میگیری، باید هر سه فایل (db.sqlite3،-wal،-shm) در لحظهٔ بدون نوشتن کپی شوند — و بهترین راه همانsqlite3.Connection.backupبالا است.
نکات مکمل:
- فضای دیسک: قبل از بکاپ حداقل ~۱.۵ برابر حجم دیتابیس خالی باشد (دیتابیس فعلی ≈۷۹ مگابایت).
- وضعیت WAL:
PRAGMA journal_mode;بایدwalبرگرداند. - دسترسی فایل: کاربر اجراکنندهٔ کانتینر باید روی
/app/sqliteو روی فایل JSON دسترسی خواندن/نوشتن داشته باشد. - فایل
iranianNamesDataset.jsonهرگز تغییر داده، بازنویسی یا حذف نمیشود؛ دستور فقط آن را میخواند.
ادامهٔ job پس از crash / قطع برق
هر batch شامل سه چیز است که در یک تراکنش اتمیک ثبت میشوند:
- تغییرات
name_familyمشتریان واجد شرایط همان batch، last_processed_pk(checkpoint)،heartbeat_atو آمار تجمیعی.
پس یا کل batch ثبت شده یا هیچکدام. دامنهٔ job در لحظهٔ آغاز با بزرگترین
primary key مشتریان قفل میشود (initial_max_pk)؛ مشتریان جدیدتر وارد دامنه
نمیشوند.
گزینههای ادامه
| وضعیت | رفتار پیشفرض | راهحل |
|---|---|---|
| job ناتمام با همان فایل و همان نسخهٔ الگوریتم | خودکار ادامه مییابد | فقط همان دستور --apply --confirm-production را دوباره بزن |
| میخواهی صریحاً همان job را ادامه دهی | — | --apply --confirm-production --resume <JOB_ID> |
job قبلی completed است |
خطا؛ خودکار job جدید نمیسازد | --apply --confirm-production --new-job |
| فایل عوض شده (hash متفاوت) | توقف، بدون هیچ تغییری | --new-job با فایل جدید |
| نسخهٔ الگوریتم عوض شده | توقف | --new-job |
پیام خطاهای مربوطه:
CommandError: hash فایل ورودی با job#3 نمیخواند؛ ادامه رد شد و هیچ تغییری اعمال نمیشود.
CommandError: job#3 قبلاً تکمیل شده است؛ برای اجرای دوباره --new-job بده.
CommandError: job#3 همین حالا در حال اجراست (heartbeat تازه). اجرای همزمان مجاز نیست.
دستورهای دقیق ادامه / شروع مجدد
# ادامهٔ خودکار آخرین job ناتمامِ همین فایل (سادهترین راه)
python manage.py fix_customer_names --apply --confirm-production
# ادامهٔ صریح یک job مشخص
python manage.py fix_customer_names --apply --confirm-production --resume 3
# شروع عمدی job جدید (پس از تکمیل job قبلی یا با فایل جدید)
python manage.py fix_customer_names --apply --confirm-production --new-job
# شروع job جدید با فایل دیگر و حجم batch کوچکتر (ساعات پرترافیک)
python manage.py fix_customer_names --apply --confirm-production --new-job \
--json-path /app/data/iranianNamesDataset_v2.json --batch-size 50
اجرای دوبارهٔ بیخطر: با --new-job روی دادهٔ اصلاحشده، changed = 0 میشود،
چون نامهای دارای پیشوند دیگر رد میشوند.
قفل و اجرای همزمان
- جلوگیری از اجرای همزمان با heartbeat انجام میشود، نه
select_for_update(). (روی SQLite قفل ردیفی واقعی وجود ندارد؛ اتکا بهselect_for_updateگمراهکننده بود.) - هر batch مقدار
heartbeat_atرا بهروز میکند. jobی کهstatus=runningدارد و heartbeat آن تازه است (<LOCK_TTL = 15 دقیقه)، اجرای دوم را مسدود میکند. - اگر سرور وسط کار خاموش شود، heartbeat کهنه میماند و اجرای بعدی قفل را
بهصورت کنترلشده آزاد میکند و بهعنوان
failedعلامت میزند:
job#1 ناتمام رها شده بود و اکنون بهعنوان failed آزاد شد.
پس از آن، همان دستور از last_processed_pk ادامه میدهد. قفل، دائمی نمیشود.
SQLite، قفل دیتابیس و بار روی سایت
- batchهای کوچک (
--batch-size 200) با تراکنشهای کوتاه؛ هیچ تراکنش واحدی روی ۱۰٬۴۲۶ مشتری ساخته نمیشود. - خطای
database is locked: بازآزمایی حداکثر4بار با backoff نمایی (۰.۵s، ۱s، ۲s) بههمراه jitter. retry نامحدود وجود ندارد. - خطاهای غیرقابلبازیابی فوراً بالا میآیند؛ batch جاری rollback میشود،
checkpoint جلو نمیرود و job با
failedثبت میشود. PRAGMA busy_timeout=30000موجود (درadmin_panel/signals.py) دستنخورده است.journal_mode=WALوsynchronous=FULLهم تغییر داده نشدهاند.- محدودیت باقیمانده: SQLite در هر لحظه فقط یک نویسنده دارد. بهتر است دستور را در ساعات کمترافیک اجرا کنی؛ اگر web/worker همزمان بنویسند، ممکن است چند batch با backoff کندتر شوند (نه خطا). سایت متوقف نمیشود.
مدل وضعیت job
CustomerNameFixJob — فقط وضعیت و checkpoint. هیچ اطلاعاتی از جنسیت یا هویت
مشتری در آن ذخیره نمیشود.
| فیلد | معنا |
|---|---|
source_hash |
SHA-256 فایل ورودی؛ مبنای تشخیص «همان فایل» |
algorithm_version |
نسخهٔ الگوریتم تصمیم (ALGORITHM_VERSION) |
status |
running / paused / completed / failed |
batch_size |
اندازهٔ batch هنگام آغاز |
initial_max_pk |
سقف دامنه (بیشترین pk مشتریان در آغاز) |
last_processed_pk |
checkpoint؛ آخرین pk پردازششده |
stats |
آمار تجمیعی (JSON) |
note |
توضیح وضعیت/خطا |
started_at / updated_at / finished_at / heartbeat_at |
زمانها |
شرط خواندن هر batch: last_processed_pk < pk <= initial_max_pk، مرتب بر اساس
pk. هیچوقت از offset یا شمارهٔ ردیف استفاده نمیشود (offset با افزودن/حذف
رکورد جابهجا میشود و باعث پرش/تکرار میشود).
بکاپ/بازیابی خودکار نیست
دستور بکاپ نمیگیرد و تغییرات را «undo» نمیکند. برای برگشت، بکاپِ قبل از اجرا را بازیابی کن:
# ۱) توقف سرویسهایی که مینویسند
docker compose stop web worker scheduler delivery
# ۲) جایگزینی فایل دیتابیس در ولوم، با پاک کردن WAL/SHM قدیمی
docker run --rm \
-v khatibi-sqlite-db:/data \
-v "$(pwd)":/backup \
alpine sh -c "cp /backup/db_backup_YYYYmmdd_HHMMSS.sqlite3 /data/db.sqlite3 \
&& rm -f /data/db.sqlite3-wal /data/db.sqlite3-shm"
# ۳) بالا آوردن دوباره (سرویس migrate اول اجرا میشود)
docker compose start migrate web worker scheduler delivery
اگر از sqlite_db_backup استفاده کردهای، فایل بکاپ در S3 است؛ اول آن را با
aws s3 cp/کلاینت S3 دانلود کن و بعد مراحل بالا را اجرا کن.
خطاهای رایج و اقدام درست
| خطا | معنا | اقدام |
|---|---|---|
فایل داده پیدا نشد: … |
مسیر --json-path غلط |
مسیر را با ls تأیید کن |
JSON نامعتبر است: … |
فایل خراب است | هیچ تغییری اعمال نشد؛ فایل درست را بگذار |
کلید «RECORDS» در فایل JSON وجود ندارد |
ساختار فایل عوض شده | فایل را اصلاح کن — هیچ نامی تغییر نمیکند |
جدول … وجود ندارد |
migration اعمال نشده | python manage.py migrate |
hash فایل ورودی … نمیخواند |
فایل عوض شده | یا فایل قبلی را برگردان، یا --new-job |
همین حالا در حال اجراست |
یک اجرای دیگر فعال است | صبر کن یا --resume همان job را بده |
database is locked (پس از ۴ تلاش) |
فشار نوشتن همزمان | دوباره اجرا کن؛ از checkpoint ادامه میدهد |
برای نوشتن واقعی، هم --apply و هم --confirm-production لازم است |
تأیید ناقص | هر دو گزینه را بده |
بررسی صحت داده (SQL، پیش از/پس از اجرا)
-- وضعیت jobها
SELECT id, status, source_hash, last_processed_pk, initial_max_pk,
started_at, finished_at
FROM admin_panel_customernamefixjob ORDER BY id;
-- مشتریانی که پیشوند ندارند ولی نامشان چندکلمهای است (کاندید بالقوه)
SELECT COUNT(*) FROM admin_panel_customer
WHERE TRIM(name_family) LIKE '% %'
AND name_family NOT LIKE 'آقا %'
AND name_family NOT LIKE 'آقای %'
AND name_family NOT LIKE 'خانم %';
-- تعدادی که پیشوند گرفتهاند (نتیجهٔ اجرای واقعی)
SELECT COUNT(*) FROM admin_panel_customer WHERE name_family LIKE 'آقای %';
SELECT COUNT(*) FROM admin_panel_customer WHERE name_family LIKE 'خانم %';
تستها
.venv/Scripts/python.exe manage.py test tests.test_customer_name_fix # ۳۰ تست
.venv/Scripts/python.exe manage.py test tests # کل مجموعه
.venv/Scripts/python.exe manage.py makemigrations --check --dry-run # باید «No changes detected» بدهد
تستها فقط روی دیتابیس تستی Django (SQLite درونحافظه) و با فایل JSON ساختگیِ
موقت اجرا میشوند؛ نه به data9/kt.sqlite3 دست میزنند و نه به فایل دادهٔ واقعی.
نکتهها و محدودیتها
- نام تککلمهای هیچوقت تغییر نمیکند — حتی اگر در فایل با جنسیت قطعی باشد؛ چون ممکن است فقط فامیل باشد.
- حرف اضافه/کم کردن ممنوع است: نه fuzzy matching، نه حذف حروف، نه حدس از فامیل، نه انتخاب تصادفی جنسیت.
- نامهای دارای پیشوند دوباره پیشوند نمیگیرند (idempotent).
- دستور
save()کامل صدا نمیزند؛ رویCustomerسیگنال و منطق اجباری درsave()وجود ندارد، پسbulk_update(['name_family'])امن است و فقط همان یک ستون را مینویسد (فیلدهایauto_now_addمثلcreate_dateلمس نمیشوند). - منطق روی
name_familyاجرا میشود، نهname— این نام واقعی فیلد در این پروژه است. - ۵ نام موجود در فایل جنسیت نامعتبر دارند (
D/FF/MF) و عمداً کنار گذاشته میشوند؛ مشتریانی با آن نامها بدون پیشوند میمانند. - نامهایی که در فایل نیستند (۱۴۵۷ مورد در دادهٔ فعلی) پیشوند نمیگیرند. اگر
بعداً فایل کاملتری آمد، با
--json-pathفایل جدید و--new-jobمیتوان دور تازهای اجرا کرد. - برای تغییر قواعد تصمیم یا نرمالسازی،
ALGORITHM_VERSIONدرadmin_panel/name_fix.pyرا بالا ببر تا jobهای قدیمی ناخواسته ادامه داده نشوند. - کنسول ویندوز: متن فارسی در خروجی ممکن است
charmapخطا بدهد؛ مثل بقیهٔ سرویسهای پروژه ازpython -X utf8 manage.py …استفاده کن.
چکلیست سریع (کپی/پیست)
# ── توسعه (WSL) ──────────────────────────────────────────────
.venv/Scripts/python.exe manage.py test tests.test_customer_name_fix
.venv/Scripts/python.exe manage.py fix_customer_names --dry-run --samples 5
# ── Production ───────────────────────────────────────────────
docker compose run --rm migrate # ۱) مهاجرت
docker compose exec web python manage.py sqlite_db_backup # ۲) بکاپ
docker compose exec web python manage.py fix_customer_names --dry-run --samples 10
docker compose exec web python manage.py fix_customer_names --apply --confirm-production
docker compose exec web python manage.py fix_customer_names --apply --confirm-production --resume <JOB_ID>
گزارش نهایی یک اجرای واقعی:
گزارش اجرای واقعی job#1 (completed)
مشتریان بررسیشده: 10426
قابل اصلاح / اصلاحشده: 7303
بدون تغییر: 3123
تککلمهای: 1576 | دارای پیشوند: 89 | خالی/نامعتبر: 0
ناشناخته در JSON: 1457 | جنسیت مبهم/نامعتبر: 1
batchهای تکمیلشده: 53
شناسهٔ job: 1 | شروع: … | پایان: …
نمونهٔ تغییرات (قبل → بعد):
«مجتبی سالاری» → «آقای مجتبی سالاری»
تعریف کلیدهای آماری
| کلید | معنا |
|---|---|
scanned |
کل مشتریان بررسیشده در دامنهٔ job |
changed |
نامهایی که پیشوند گرفتند (یا در dry-run، میگیرند) |
unchanged |
scanned - changed |
single_word |
تککلمهای ⇒ دستنخورده |
has_prefix |
از قبل آقا/آقای/خانم ⇒ دستنخورده |
empty_invalid |
خالی/فقط فاصله ⇒ دستنخورده |
unknown_name |
اولین کلمه در فایل نیست ⇒ دستنخورده |
ambiguous_gender |
جنسیت متناقض/نامعتبر ⇒ دستنخورده |
batches |
تعداد batchهای commitشده |