fix mobile and names
This commit is contained in:
1 parent
e3a6d1f9d4
commit
7e5c83bea2
12 files changed
+82792
-6
No files matched your search
@@ -0,0 +1,642 @@
|
||||
# افزودن پیشوند «آقای/خانم» به نام مشتریان
|
||||
|
||||
## چرا؟
|
||||
|
||||
فیلد `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` اجرا میشود:
|
||||
|
||||
```bash
|
||||
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` حالا **صریحاً** این فایل را کپی میکند (خط ۶۳):
|
||||
|
||||
```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:
|
||||
|
||||
```bash
|
||||
# الف) کپی دستی داخل کانتینر در حال اجرا (تا بازسازی بعدی کانتینر پایدار است)
|
||||
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
|
||||
```
|
||||
|
||||
برای تأیید اینکه ایمیج ساختهشده واقعاً فایل را دارد:
|
||||
|
||||
```bash
|
||||
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` بدهی.
|
||||
|
||||
### اگر وسط کار قطع شد
|
||||
|
||||
```bash
|
||||
# الف) ادامهٔ خودکار: فقط همان دستور را دوباره بزن.
|
||||
# خودش آخرین 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`.
|
||||
|
||||
### بررسی نتیجه
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
cd /mnt/d/projects/khatibi
|
||||
.venv/Scripts/python.exe manage.py fix_customer_names --dry-run --samples 5
|
||||
```
|
||||
|
||||
از Windows (PowerShell / CMD):
|
||||
|
||||
```bat
|
||||
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
|
||||
```
|
||||
|
||||
### ۲) اجرای واقعی (فقط روی مقصد دلخواه)
|
||||
|
||||
```bash
|
||||
.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`)،
|
||||
ولی برای اطمینان:
|
||||
|
||||
```bash
|
||||
docker compose run --rm migrate # یا: python manage.py migrate
|
||||
```
|
||||
|
||||
اگر جدول job نباشد، دستور **پیش از هر تغییری** خطا میدهد و هیچ نامی را
|
||||
تغییر نمیدهد:
|
||||
|
||||
```
|
||||
CommandError: جدول «admin_panel_customernamefixjob» وجود ندارد.
|
||||
ابتدا migration را اعمال کن: python manage.py migrate
|
||||
```
|
||||
|
||||
### ترتیب دقیق اجرا
|
||||
|
||||
خلاصهٔ اجرایی و کامل این ترتیب در بخش
|
||||
**«سریعترین مسیر روی Production»** بالای همین سند آمده؛ این همان نسخهٔ
|
||||
جزئیتر با تأکید بر فایل داده:
|
||||
|
||||
```bash
|
||||
# ۰) فایل داده داخل کانتینر — تأیید وجود و هش
|
||||
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`).
|
||||
|
||||
```bash
|
||||
# داخل کانتینر (لینوکس) — نتیجه در S3 ذخیره و اعتبارسنجی میشود
|
||||
docker compose exec web python manage.py sqlite_db_backup
|
||||
```
|
||||
|
||||
اگر S3 در دسترس نیست یا روی ویندوز/WSL کار میکنی، همین بکاپ WAL-safe را
|
||||
مستقیم بگیر (همهجا کار میکند و به env نیاز ندارد):
|
||||
|
||||
```bash
|
||||
# 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 شامل سه چیز است که **در یک تراکنش اتمیک** ثبت میشوند:
|
||||
|
||||
1. تغییرات `name_family` مشتریان واجد شرایط همان batch،
|
||||
2. `last_processed_pk` (checkpoint)،
|
||||
3. `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 تازه). اجرای همزمان مجاز نیست.
|
||||
```
|
||||
|
||||
### دستورهای دقیق ادامه / شروع مجدد
|
||||
|
||||
```bash
|
||||
# ادامهٔ خودکار آخرین 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» نمیکند. برای برگشت، بکاپِ قبل از اجرا
|
||||
را بازیابی کن:
|
||||
|
||||
```bash
|
||||
# ۱) توقف سرویسهایی که مینویسند
|
||||
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، پیش از/پس از اجرا)
|
||||
|
||||
```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 'خانم %';
|
||||
```
|
||||
|
||||
## تستها
|
||||
|
||||
```bash
|
||||
.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 …` استفاده کن.
|
||||
|
||||
|
||||
## چکلیست سریع (کپی/پیست)
|
||||
|
||||
```bash
|
||||
# ── توسعه (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شده |
|
||||
|
||||
|
||||
Reference in new issue
Block a user