Browse Source
feat(backend): multilingual companion_type, restrict madhhab choices, and fix orphan bookmarks
feat(backend): multilingual companion_type, restrict madhhab choices, and fix orphan bookmarks
- hadis: convert companion_type to multilingual JSONField with localized serialization - hadis: restrict MadhhabChoices to SHIA and SUNNI with default SHIA - bookmark: filter out orphan/phantom bookmarks and deactivate inactive content bookmarks - podcast: enhance UserPlaylistListAPIView to filter directly by bookmarked podcasts - utils/mixins: support numeric ID lookup and enhance Gone410ViewMixin / CanonicalSlugViewSetMixin - runner: optimize docker pre-build cleanup - docs: add technical migration guide for Flutter offline SQLite hadith syncmaster
11 changed files with 425 additions and 40 deletions
-
293FLUTTER_SQLITE_MIGRATION_GUIDE.md
-
5apps/article/views.py
-
56apps/bookmark/views/bookmark.py
-
28apps/hadis/migrations/0041_alter_hadisdatabaseversion_version_and_more.py
-
10apps/hadis/models/transmitter.py
-
7apps/hadis/serializers/hadis.py
-
29apps/podcast/views.py
-
5apps/video/views.py
-
2config/settings/test.py
-
12runner.sh
-
18utils/mixins.py
@ -0,0 +1,293 @@ |
|||
# راهنمای فنی مهاجرت دریافت و کش احادیث به دیتابیس SQLite آفلاین (Single-File Architecture) |
|||
|
|||
> **مخاطب:** توسعهدهنده فلاتر (تیم فرانتاند / موبایل) |
|||
> **هدف:** حذف پردازش سنگین جیسون احادیث (بیش از ۳۳ مگابایت)، حذف کرش و هنگ رم در Hive، و پیادهسازی دریافت مستقیم فایل فشرده SQLite دیتابیس احادیث و تفکیک تفاسیر. |
|||
|
|||
--- |
|||
|
|||
## ۱. خلاصه تغییرات و چرایی مهاجرت |
|||
|
|||
### مشکل قبلی: |
|||
1. دانلود اندپوینت `/api/hadis/sync/hadis/` حجمی معادل **۳۳ مگابایت جیسون خام** داشت. |
|||
2. پارس کردن این حجم از جیسون و اینسرت در Hive باعث افت فریم، فریز شدن UI و مصرف بیش از ۲۰۰ مگابایت رم در گوشی میشد. |
|||
3. تفاسیر دستهبندیها به ازای تکتک احادیث درون جیسون تکرار میشد (یک متن طولانی تفسیر ۱۰ بار در ۱۰ حدیث مختلف یک دسته کپی میشد!). |
|||
|
|||
### معماری جدید (سرور جنگو): |
|||
1. **فایل واحد دیتابیس آماده:** سرور یک فایل پایگاهداده بهینه، ایندکسشده و فشرده با Gzip به نام **`hadis_offline.sqlite.gz`** (حجم حدود ۲ تا ۴ مگابایت) آماده کرده است که مستقیماً از طریق Nginx دانلود میشود. |
|||
2. **تفکیک کامل تفاسیر:** تفاسیر دستهبندی در یک جدول مجزا قرار گرفتهاند و متنهای تکراری حذف شدهاند. |
|||
3. **اندپوینت نسخه پایدار:** اپلیکیشن نسخه دیتابیس محلی خود را با سرور مقایسه میکند و فقط در صورت تغییر نسخه، فایل دیتابیس را دانلود و جایگزین میکند. |
|||
|
|||
--- |
|||
|
|||
## ۲. اندپوینتهای جدید و بهروزشده بکاند |
|||
|
|||
### الف) اندپوینت اختصاصی دریافت نسخه دیتابیس SQLite: |
|||
```http |
|||
GET /api/hadis/sync/database/ |
|||
``` |
|||
**پاسخ نمونه:** |
|||
```json |
|||
{ |
|||
"version": 2, |
|||
"download_url": "https://dovodi.online/media/hadis_db/hadis_offline.sqlite.gz", |
|||
"file_size": 2845620, |
|||
"checksum_md5": "d41d8cd98f00b204e9800998ecf8427e", |
|||
"hadis_count": 1050, |
|||
"categories_count": 48, |
|||
"is_active": true, |
|||
"notes": "بهروزرسانی احادیث ماه مبارک", |
|||
"created_at": "2026-09-10T10:15:00Z" |
|||
} |
|||
``` |
|||
|
|||
### ب) اندپوینت قبلی نسخهبندی (Backward Compatible): |
|||
اندپوینت `/api/hadis/sync/version/` نیز حفظ شده و کلید `sqlite_database` را در بدنه خود برمیگرداند تا در صورتی که ساختار فعلی فلاتر به آن متصل است، کدی شکسته نشود. |
|||
|
|||
--- |
|||
|
|||
## ۳. ساختار جداول درون فایل SQLite (`hadis_offline.sqlite`) |
|||
|
|||
فایل دیتابیس دانلود شده شامل جداول استاندارد زیر است: |
|||
|
|||
### ۱. جدول `hadiths` (احادیث) |
|||
| نام ستون | نوع داده | توضیحات | |
|||
| :--- | :--- | :--- | |
|||
| `id` | INTEGER (PK) | شناسه حدیث | |
|||
| `slug` | TEXT (INDEX) | اسلاگ یکتای حدیث | |
|||
| `category_id` | INTEGER (INDEX) | شناسه دستهبندی | |
|||
| `category_slug` | TEXT | اسلاگ دسته | |
|||
| `title` | TEXT | عنوان چندزبانه (JSON) | |
|||
| `title_narrator` | TEXT | راوی عنوان (JSON) | |
|||
| `text` | TEXT | متن عربی حدیث | |
|||
| `translation_json` | TEXT | لیست ترجمهها (JSON) | |
|||
| `address` | TEXT | آدرس منبع حدیث (JSON) | |
|||
| `share_link` | TEXT | لینک اشتراکگذاری | |
|||
| `hadis_status_id` | INTEGER | شناسه وضعیت | |
|||
| `status_title` | TEXT | عنوان وضعیت | |
|||
| `status_color` | TEXT | رنگ وضعیت | |
|||
| `status_main_color_code` | TEXT | کد رنگ اصلی | |
|||
| `status_text` | TEXT | متن وضعیت | |
|||
| `tags_json` | TEXT | لیست تگها | |
|||
| `references_json` | TEXT | لیست منابع و کتابها | |
|||
| `reference_images_json`| TEXT | تصاویر منابع | |
|||
| `narrators_json` | TEXT | سلسله راویان (Transmitters) | |
|||
| `explanations_json` | TEXT | توضیحات و شروح | |
|||
|
|||
### ۲. جدول `category_interpretations` (تفاسیر دستهبندی - تفکیکشده) |
|||
| نام ستون | نوع داده | توضیحات | |
|||
| :--- | :--- | :--- | |
|||
| `id` | INTEGER (PK) | شناسه تفسیر | |
|||
| `category_id` | INTEGER (INDEX) | شناسه دستهبندی مربوطه | |
|||
| `title` | TEXT | عنوان تفسیر | |
|||
| `slug` | TEXT | اسلاگ تفسیر | |
|||
| `narrator` | TEXT | راوی تفسیر | |
|||
| `text` | TEXT | متن تفصیلی تفسیر | |
|||
| `translation` | TEXT | ترجمه | |
|||
| `references_json` | TEXT | کتابها و منابع تفسیر | |
|||
| `links_json` | TEXT | پیوندها | |
|||
|
|||
### ۳. جدول `hadith_corrections` (تصحیحات حدیث) |
|||
| نام ستون | نوع داده | توضیحات | |
|||
| :--- | :--- | :--- | |
|||
| `id` | INTEGER (PK) | شناسه تصحیح | |
|||
| `hadis_id` | INTEGER (INDEX) | شناسه حدیث متصل | |
|||
| `title` | TEXT | عنوان | |
|||
| `slug` | TEXT | اسلاگ | |
|||
| `narrator` | TEXT | راوی | |
|||
| `description` | TEXT | شرح | |
|||
| `translation` | TEXT | ترجمه | |
|||
| `references_json` | TEXT | منابع تصحیح | |
|||
| `images_json` | TEXT | تصاویر مدارک | |
|||
| `links_json` | TEXT | پیوندها | |
|||
|
|||
### ۴. جدول `categories` (دستهها) |
|||
شامل `id`, `title`, `slug`, `source_type`, `sect_type`. |
|||
|
|||
--- |
|||
|
|||
## ۴. چرخه سناریوی دریافت و کش در فلاتر (Implementation Flow) |
|||
|
|||
``` |
|||
[Start App] |
|||
│ |
|||
▼ |
|||
1. فراخوانی GET /api/hadis/sync/database/ |
|||
│ |
|||
▼ |
|||
2. آیا دیتابیس محلی وجود ندارد یا version سرور > version ذخیره شده در SharedPreferences است؟ |
|||
├─► خیر: مستقیماً از SQLite محلی لود کن (آفلاین ۱۰۰٪ در ۰ ثانیه). |
|||
│ |
|||
└─► بله: |
|||
3. نمایش لودینگ / پروگرسبار دانلود دیتابیس |
|||
4. دانلود فایل hadis_offline.sqlite.gz از Nginx با Dio (با قابلیت onReceiveProgress) |
|||
5. بررسی سلامت فایل با هش MD5 (اختیاری ولی توصیه شده) |
|||
6. آنزیپ کردن (decompress) فایل Gzip به hadis.sqlite در دایرکتوری getDatabasesPath() |
|||
7. پاک کردن فایل موقت .gz |
|||
8. ذخیره شماره نسخه جدید در SharedPreferences |
|||
9. رفرش کردن کوبیتها و آمادهسازی نمایش |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## ۵. راهنمای گامبهگام پیادهسازی کدهای فلاتر |
|||
|
|||
### گام اول: وابستگیهای مورد نیاز در `pubspec.yaml` |
|||
```yaml |
|||
dependencies: |
|||
sqflite: ^2.3.3+1 |
|||
path: ^1.9.0 |
|||
path_provider: ^2.1.3 |
|||
archive: ^3.6.1 # برای باز کردن فایل Gzip |
|||
crypto: ^3.0.3 # برای بررسی هش MD5 |
|||
``` |
|||
|
|||
--- |
|||
|
|||
### گام دوم: ایجاد کلاس مدیریت دیتابیس SQLite |
|||
یک فایل در مسیر `lib/src/features/dobodbi/hadith/data/datasources/local/hadith_database_helper.dart` ایجاد کنید: |
|||
|
|||
```dart |
|||
import 'dart:io'; |
|||
import 'package:path/path.dart'; |
|||
import 'package:sqflite/sqflite.dart'; |
|||
import 'package:archive/archive.dart'; |
|||
|
|||
class HadithDatabaseHelper { |
|||
static const String _dbFileName = 'hadis.sqlite'; |
|||
static Database? _database; |
|||
|
|||
static Future<Database> get database async { |
|||
if (_database != null && _database!.isOpen) return _database!; |
|||
_database = await _initDb(); |
|||
return _database!; |
|||
} |
|||
|
|||
static Future<String> getDatabasePath() async { |
|||
final dbFolder = await getDatabasesPath(); |
|||
return join(dbFolder, _dbFileName); |
|||
} |
|||
|
|||
static Future<Database> _initDb() async { |
|||
final path = await getDatabasePath(); |
|||
return await openDatabase(path, readOnly: true); |
|||
} |
|||
|
|||
/// اکسترکت فایل فشرده دانلود شده مستقیماً در دایرکتوری دیتابیس |
|||
static Future<void> replaceDatabaseWithGzip(File gzFile) async { |
|||
// اگر دیتابیس باز است، ابتدا بسته شود |
|||
if (_database != null && _database!.isOpen) { |
|||
await _database!.close(); |
|||
_database = null; |
|||
} |
|||
|
|||
final targetPath = await getDatabasePath(); |
|||
final bytes = await gzFile.readAsBytes(); |
|||
|
|||
// Gzip decode |
|||
final decompressedData = GZipDecoder().decodeBytes(bytes); |
|||
|
|||
// نوشتن فایل دیتابیس نهایی |
|||
final targetFile = File(targetPath); |
|||
await targetFile.writeAsBytes(decompressedData, flush: true); |
|||
|
|||
// راهاندازی مجدد اتصال |
|||
_database = await openDatabase(targetPath, readOnly: true); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
--- |
|||
|
|||
### گام سوم: سرویس دانلود و همگامسازی (Sync Manager) |
|||
|
|||
در `hadith_datasource.dart` متد دانلود اضافه کنید: |
|||
|
|||
```dart |
|||
Future<void> syncHadithDatabase({ |
|||
required String downloadUrl, |
|||
required int targetVersion, |
|||
required Function(int received, int total) onProgress, |
|||
}) async { |
|||
final tempDir = await getTemporaryDirectory(); |
|||
final tempGzPath = '${tempDir.path}/hadis_temp.sqlite.gz'; |
|||
final tempFile = File(tempGzPath); |
|||
|
|||
// دانلود مستقیم فایل با قابلیت گزارش پیشرفت |
|||
await _dio.download( |
|||
downloadUrl, |
|||
tempGzPath, |
|||
onReceiveProgress: onProgress, |
|||
); |
|||
|
|||
// اکسترکت در دیتابیس |
|||
await HadithDatabaseHelper.replaceDatabaseWithGzip(tempFile); |
|||
|
|||
// حذف فایل تمپ |
|||
if (await tempFile.exists()) { |
|||
await tempFile.delete(); |
|||
} |
|||
|
|||
// ذخیره نسخه جدید در حافظه محلی |
|||
final prefs = await SharedPreferences.getInstance(); |
|||
await prefs.setInt('hadith_db_version', targetVersion); |
|||
} |
|||
``` |
|||
|
|||
--- |
|||
|
|||
### گام چهارم: کوئری گرفتن احادیث و تفاسیر از SQLite |
|||
|
|||
در دیتاسورس محلی (`HadithLocalDataSource`): |
|||
|
|||
#### ۱. دریافت احادیث یک دستهبندی با صفحهبندی فوقسریع: |
|||
```dart |
|||
Future<List<HadithModel>> getHadithsByCategory(int categoryId, {int limit = 20, int offset = 0}) async { |
|||
final db = await HadithDatabaseHelper.database; |
|||
final results = await db.query( |
|||
'hadiths', |
|||
where: 'category_id = ?', |
|||
whereArgs: [categoryId], |
|||
limit: limit, |
|||
offset: offset, |
|||
); |
|||
|
|||
return results.map((row) => _mapRowToHadithModel(row)).toList(); |
|||
} |
|||
``` |
|||
|
|||
#### ۲. دریافت تفاسیر دستهبندی (تفکیکشده): |
|||
```dart |
|||
Future<List<HadithInterpretationModel>> getCategoryInterpretations(int categoryId) async { |
|||
final db = await HadithDatabaseHelper.database; |
|||
final results = await db.query( |
|||
'category_interpretations', |
|||
where: 'category_id = ?', |
|||
whereArgs: [categoryId], |
|||
); |
|||
|
|||
return results.map((row) => HadithInterpretationModel.fromSqliteRow(row)).toList(); |
|||
} |
|||
``` |
|||
|
|||
#### ۳. جستجوی فوقسریع متنی (Full Text Search): |
|||
```dart |
|||
Future<List<HadithModel>> searchHadiths(String keyword) async { |
|||
final db = await HadithDatabaseHelper.database; |
|||
final results = await db.rawQuery(''' |
|||
SELECT * FROM hadiths |
|||
WHERE text LIKE ? OR translation_json LIKE ? |
|||
LIMIT 50 |
|||
''', ['%$keyword%', '%$keyword%']); |
|||
|
|||
return results.map((row) => _mapRowToHadithModel(row)).toList(); |
|||
} |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## ۶. مزایای این تغییر برای اپ فلاتر |
|||
|
|||
1. **کاهش مصرف اینترنت:** حجم دانلود کل دیتابیس از ۳۳ مگابایت به **کمتر از ۳ مگابایت** میرسد (۹۰٪ صرفهجویی). |
|||
2. **زمان سینک صفر:** دیگر نیازی به پارس کردن حلقهای جیسون در فلاتر و ایجاد آبجکتهای Hive نیست؛ به محض Unzip شدن، دیتابیس بلافاصله آماده استفاده است. |
|||
3. **مصرف رم بسیار ناچیز:** به جای نگهداری تمام احادیث در رم (Boxهای Hive)، دادهها با صفحهبندی (`LIMIT` و `OFFSET`) بر اساس نیاز اسکرول کاربر از دیسک خوانده میشوند. |
|||
4. **توقف کامل کرشهای دیتابیس آفلاین در نسخههای ضعیف اندروید/iOS.** |
|||
@ -0,0 +1,28 @@ |
|||
# Generated by Django 4.2.30 on 2026-09-14 12:46 |
|||
|
|||
from django.db import migrations, models |
|||
|
|||
|
|||
class Migration(migrations.Migration): |
|||
|
|||
dependencies = [ |
|||
('hadis', '0040_hadisdatabaseversion'), |
|||
] |
|||
|
|||
operations = [ |
|||
migrations.AlterField( |
|||
model_name='hadisdatabaseversion', |
|||
name='version', |
|||
field=models.PositiveIntegerField(default=1, verbose_name='شماره نسخه دیتابیس'), |
|||
), |
|||
migrations.AlterField( |
|||
model_name='transmitters', |
|||
name='companion_type', |
|||
field=models.JSONField(blank=True, default=list, null=True, verbose_name='Companion Type'), |
|||
), |
|||
migrations.AlterField( |
|||
model_name='transmitters', |
|||
name='madhhab', |
|||
field=models.CharField(choices=[('shia', 'Shia'), ('sunni', 'Sunni')], default='shia', max_length=20, verbose_name='Madhhab/School of Thought'), |
|||
), |
|||
] |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue