Улучшение системы работы с фото: добавлена команда очистки битых записей и оптимизация обработки изображений

This commit is contained in:
2026-01-06 09:25:37 +03:00
parent 0f19542ac9
commit 288716deba
14 changed files with 535 additions and 122 deletions

View File

@@ -0,0 +1,179 @@
"""
Management команда для удаления записей фотографий, файлы которых не существуют на диске.
Проверяет ProductPhoto, ProductKitPhoto, ProductCategoryPhoto.
Использование:
# Для конкретного тенанта
python manage.py cleanup_missing_photos --schema=anatol --dry-run
python manage.py cleanup_missing_photos --schema=anatol
# Для всех тенантов
python manage.py cleanup_missing_photos --all-tenants --dry-run
python manage.py cleanup_missing_photos --all-tenants
"""
from django.core.management.base import BaseCommand
from django.core.files.storage import default_storage
from django_tenants.utils import schema_context, get_tenant_model
from products.models import ProductPhoto, ProductKitPhoto, ProductCategoryPhoto
class Command(BaseCommand):
help = 'Удаляет записи фотографий, файлы которых не существуют на диске'
def add_arguments(self, parser):
parser.add_argument(
'--dry-run',
action='store_true',
help='Показать что будет удалено, но не удалять',
)
parser.add_argument(
'--schema',
type=str,
help='Schema name (subdomain) тенанта для обработки',
)
parser.add_argument(
'--all-tenants',
action='store_true',
help='Обработать все тенанты (кроме public)',
)
def handle(self, *args, **options):
dry_run = options['dry_run']
schema_name = options.get('schema')
all_tenants = options.get('all_tenants')
if not schema_name and not all_tenants:
self.stdout.write(
self.style.ERROR(
'\n❌ Ошибка: укажите либо --schema=<имя>, либо --all-tenants\n'
'Примеры:\n'
' python manage.py cleanup_missing_photos --schema=anatol --dry-run\n'
' python manage.py cleanup_missing_photos --all-tenants\n'
)
)
return
# Получаем список тенантов для обработки
Tenant = get_tenant_model()
if all_tenants:
tenants = Tenant.objects.exclude(schema_name='public')
else:
try:
tenants = [Tenant.objects.get(schema_name=schema_name)]
except Tenant.DoesNotExist:
self.stdout.write(
self.style.ERROR(f'\n❌ Тенант с schema "{schema_name}" не найден\n')
)
return
# Обрабатываем каждый тенант
for tenant in tenants:
self._process_tenant(tenant, dry_run)
def _process_tenant(self, tenant, dry_run):
"""Обработка одного тенанта"""
self.stdout.write('\n' + '=' * 70)
self.stdout.write(
self.style.SUCCESS(f'Тенант: {tenant.name} (schema: {tenant.schema_name})')
)
self.stdout.write('=' * 70)
if dry_run:
self.stdout.write(self.style.WARNING('РЕЖИМ ПРОВЕРКИ (записи не будут удалены)\n'))
else:
self.stdout.write(self.style.WARNING('УДАЛЕНИЕ БИТЫХ ФОТОГРАФИЙ\n'))
with schema_context(tenant.schema_name):
# Счетчики
total_checked = 0
total_missing = 0
total_deleted = 0
# Проверяем ProductPhoto
self.stdout.write('\n1. Проверка фотографий товаров (ProductPhoto)...')
product_photos = ProductPhoto.objects.select_related('product').all()
for photo in product_photos:
total_checked += 1
# Проверяем существование файла
if not photo.image or not default_storage.exists(photo.image.name):
total_missing += 1
product_name = photo.product.name if photo.product else 'N/A'
file_path = photo.image.name if photo.image else 'N/A'
self.stdout.write(
self.style.ERROR(
f' ❌ Товар: {product_name} (ID: {photo.product_id}) - '
f'Файл не найден: {file_path}'
)
)
if not dry_run:
photo.delete()
total_deleted += 1
self.stdout.write(self.style.SUCCESS(f' ✓ Запись удалена'))
# Проверяем ProductKitPhoto
self.stdout.write('\n2. Проверка фотографий комплектов (ProductKitPhoto)...')
kit_photos = ProductKitPhoto.objects.select_related('kit').all()
for photo in kit_photos:
total_checked += 1
if not photo.image or not default_storage.exists(photo.image.name):
total_missing += 1
kit_name = photo.kit.name if photo.kit else 'N/A'
file_path = photo.image.name if photo.image else 'N/A'
self.stdout.write(
self.style.ERROR(
f' ❌ Комплект: {kit_name} (ID: {photo.kit_id}) - '
f'Файл не найден: {file_path}'
)
)
if not dry_run:
photo.delete()
total_deleted += 1
self.stdout.write(self.style.SUCCESS(f' ✓ Запись удалена'))
# Проверяем ProductCategoryPhoto
self.stdout.write('\n3. Проверка фотографий категорий (ProductCategoryPhoto)...')
category_photos = ProductCategoryPhoto.objects.select_related('category').all()
for photo in category_photos:
total_checked += 1
if not photo.image or not default_storage.exists(photo.image.name):
total_missing += 1
category_name = photo.category.name if photo.category else 'N/A'
file_path = photo.image.name if photo.image else 'N/A'
self.stdout.write(
self.style.ERROR(
f' ❌ Категория: {category_name} (ID: {photo.category_id}) - '
f'Файл не найден: {file_path}'
)
)
if not dry_run:
photo.delete()
total_deleted += 1
self.stdout.write(self.style.SUCCESS(f' ✓ Запись удалена'))
# Итоговая статистика для тенанта
self.stdout.write('\n' + '-' * 70)
self.stdout.write(self.style.SUCCESS(f'Всего проверено записей: {total_checked}'))
self.stdout.write(self.style.WARNING(f'Найдено битых записей: {total_missing}'))
if dry_run:
self.stdout.write(
self.style.WARNING(
f'\n⚠ РЕЖИМ ПРОВЕРКИ: записи НЕ удалены'
)
)
else:
self.stdout.write(self.style.SUCCESS(f'Удалено записей: {total_deleted}'))
self.stdout.write(self.style.SUCCESS('\n✓ Очистка завершена'))