Добавлен асинхронный импорт товаров с параллельной загрузкой фото + исправлен баг со счётчиком SKU
- Реализован импорт Product из CSV/XLSX через Celery с прогресс-баром - Параллельная загрузка фото товаров с внешних URL (масштабируемость до 500+ товаров) - Добавлена модель ProductImportJob для отслеживания статуса импорта - Создан таск download_product_photo_async для загрузки фото в фоне - Интеграция с существующим ImageProcessor (синхронная обработка через use_async=False) - Добавлены view и template для импорта с real-time обновлением через AJAX FIX: Исправлен баг со счётчиком SKU - инкремент только после успешного сохранения - Добавлен SKUCounter.peek_next_value() - возвращает следующий номер БЕЗ инкремента - Добавлен SKUCounter.increment_counter() - инкрементирует счётчик - generate_product_sku() использует peek_next_value() вместо get_next_value() - Добавлен post_save сигнал increment_sku_counter_after_save() для инкремента после создания - Предотвращает пропуски номеров при ошибках валидации (например cost_price NULL) FIX: Исправлена ошибка с is_main в ProductPhoto - ProductPhoto не имеет поля is_main, используется только order - Первое фото (order=0) автоматически считается главным - Удалён параметр is_main из download_product_photo_async и _collect_photo_tasks Изменены файлы: - products/models/base.py - методы для управления счётчиком SKU - products/models/import_job.py - модель для отслеживания импорта - products/services/import_export.py - сервис импорта с поддержкой Celery - products/tasks.py - таски для асинхронного импорта и загрузки фото - products/signals.py - сигнал для инкремента счётчика после сохранения - products/utils/sku_generator.py - использование peek_next_value() - products/views/product_import_views.py - view для импорта - products/templates/products/product_import*.html - UI для импорта - docker/entrypoint.sh - настройка Celery worker (concurrency=4) - requirements.txt - добавлен requests для загрузки фото
This commit is contained in:
45
myproject/products/migrations/0002_productimportjob.py
Normal file
45
myproject/products/migrations/0002_productimportjob.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# Generated by Django 5.0.10 on 2026-01-06 03:40
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ProductImportJob',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('task_id', models.CharField(max_length=255, unique=True, verbose_name='ID задачи Celery')),
|
||||
('file_name', models.CharField(max_length=255, verbose_name='Имя файла')),
|
||||
('file_path', models.CharField(help_text='Временный путь для обработки', max_length=500, verbose_name='Путь к файлу')),
|
||||
('update_existing', models.BooleanField(default=False, verbose_name='Обновлять существующие')),
|
||||
('status', models.CharField(choices=[('pending', 'Ожидает'), ('processing', 'Обрабатывается'), ('completed', 'Завершён'), ('failed', 'Ошибка')], db_index=True, default='pending', max_length=20, verbose_name='Статус')),
|
||||
('total_rows', models.IntegerField(default=0, verbose_name='Всего строк')),
|
||||
('processed_rows', models.IntegerField(default=0, verbose_name='Обработано строк')),
|
||||
('created_count', models.IntegerField(default=0, verbose_name='Создано товаров')),
|
||||
('updated_count', models.IntegerField(default=0, verbose_name='Обновлено товаров')),
|
||||
('skipped_count', models.IntegerField(default=0, verbose_name='Пропущено')),
|
||||
('errors_count', models.IntegerField(default=0, verbose_name='Ошибок')),
|
||||
('errors_json', models.JSONField(blank=True, default=list, verbose_name='Детали ошибок')),
|
||||
('error_message', models.TextField(blank=True, verbose_name='Сообщение об ошибке')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Создано')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Обновлено')),
|
||||
('completed_at', models.DateTimeField(blank=True, null=True, verbose_name='Завершено')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='product_import_jobs', to=settings.AUTH_USER_MODEL, verbose_name='Пользователь')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Задача импорта товаров',
|
||||
'verbose_name_plural': 'Задачи импорта товаров',
|
||||
'ordering': ['-created_at'],
|
||||
'indexes': [models.Index(fields=['task_id'], name='products_pr_task_id_d8dc9f_idx'), models.Index(fields=['status', '-created_at'], name='products_pr_status_326f2c_idx'), models.Index(fields=['user', '-created_at'], name='products_pr_user_id_e32ca9_idx')],
|
||||
},
|
||||
),
|
||||
]
|
||||
Reference in New Issue
Block a user