fix: Обработка race condition при дублировании slug товаров
Реализована трёхуровневая защита от IntegrityError при создании товаров с одинаковым названием: 1. SlugService улучшен: - Добавлен метод _get_base_queryset() для работы с мягким удалением - Проверка всех объектов включая удалённые (all_objects) - Новый метод get_next_available_slug() для retry обработки - Максимум 100 попыток поиска уникального slug 2. BaseProductEntity.save() защищена: - transaction.atomic() для атомарности операции - Retry логика с 5 попытками при IntegrityError - При конфликте добавляется суффикс (-1, -2, -3...) - Fallback на timestamp если суффиксы исчерпаны 3. Views обрабатывают IntegrityError: - ProductCreateView.form_valid() перехватывает ошибку - ProductUpdateView.form_valid() перехватывает ошибку - Пользователю показывается дружелюбное сообщение об ошибке - Нет 500 ошибок - вместо этого form_invalid() с сообщением Эффект: - До: User создаёт товар "Роза красная" 2 раза → IntegrityError → 500 ошибка - После: User создаёт товар "Роза красная" 2 раза → slug автоматически становится "roza-krasnaya-1" Протестировано на Django shell и синтаксис проверен. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -113,20 +113,41 @@ class ProductCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView)
|
||||
return reverse_lazy('products:product-list')
|
||||
|
||||
def form_valid(self, form):
|
||||
response = super().form_valid(form)
|
||||
from django.db import IntegrityError
|
||||
|
||||
# Handle photo uploads
|
||||
photo_errors = handle_photos(self.request, self.object, ProductPhoto, 'product')
|
||||
if photo_errors:
|
||||
for error in photo_errors:
|
||||
# Если это предупреждение о лимите фото - warning, иначе - error
|
||||
if 'Загружено' in error and 'обработано только' in error:
|
||||
messages.warning(self.request, error)
|
||||
else:
|
||||
messages.error(self.request, error)
|
||||
try:
|
||||
response = super().form_valid(form)
|
||||
|
||||
messages.success(self.request, f'Товар "{form.instance.name}" успешно создан!')
|
||||
return response
|
||||
# Handle photo uploads
|
||||
photo_errors = handle_photos(self.request, self.object, ProductPhoto, 'product')
|
||||
if photo_errors:
|
||||
for error in photo_errors:
|
||||
# Если это предупреждение о лимите фото - warning, иначе - error
|
||||
if 'Загружено' in error and 'обработано только' in error:
|
||||
messages.warning(self.request, error)
|
||||
else:
|
||||
messages.error(self.request, error)
|
||||
|
||||
messages.success(self.request, f'Товар "{form.instance.name}" успешно создан!')
|
||||
return response
|
||||
|
||||
except IntegrityError as e:
|
||||
# Обработка ошибки дублирования slug'а или других unique constraints
|
||||
error_msg = str(e).lower()
|
||||
if 'slug' in error_msg or 'duplicate key' in error_msg:
|
||||
messages.error(
|
||||
self.request,
|
||||
f'Ошибка: товар с названием "{form.instance.name}" уже существует. '
|
||||
'Пожалуйста, используйте другое название.'
|
||||
)
|
||||
else:
|
||||
messages.error(
|
||||
self.request,
|
||||
'Ошибка при сохранении товара. Пожалуйста, проверьте введённые данные.'
|
||||
)
|
||||
|
||||
# Перенаправляем обратно на форму с сохранённой информацией
|
||||
return self.form_invalid(form)
|
||||
|
||||
|
||||
class ProductDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
||||
@@ -164,20 +185,41 @@ class ProductUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView)
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
response = super().form_valid(form)
|
||||
from django.db import IntegrityError
|
||||
|
||||
# Handle photo uploads
|
||||
photo_errors = handle_photos(self.request, self.object, ProductPhoto, 'product')
|
||||
if photo_errors:
|
||||
for error in photo_errors:
|
||||
# Если это предупреждение о лимите фото - warning, иначе - error
|
||||
if 'Загружено' in error and 'обработано только' in error:
|
||||
messages.warning(self.request, error)
|
||||
else:
|
||||
messages.error(self.request, error)
|
||||
try:
|
||||
response = super().form_valid(form)
|
||||
|
||||
messages.success(self.request, f'Товар "{form.instance.name}" успешно обновлен!')
|
||||
return response
|
||||
# Handle photo uploads
|
||||
photo_errors = handle_photos(self.request, self.object, ProductPhoto, 'product')
|
||||
if photo_errors:
|
||||
for error in photo_errors:
|
||||
# Если это предупреждение о лимите фото - warning, иначе - error
|
||||
if 'Загружено' in error and 'обработано только' in error:
|
||||
messages.warning(self.request, error)
|
||||
else:
|
||||
messages.error(self.request, error)
|
||||
|
||||
messages.success(self.request, f'Товар "{form.instance.name}" успешно обновлен!')
|
||||
return response
|
||||
|
||||
except IntegrityError as e:
|
||||
# Обработка ошибки дублирования slug'а или других unique constraints
|
||||
error_msg = str(e).lower()
|
||||
if 'slug' in error_msg or 'duplicate key' in error_msg:
|
||||
messages.error(
|
||||
self.request,
|
||||
f'Ошибка: товар с названием "{form.instance.name}" уже существует. '
|
||||
'Пожалуйста, используйте другое название.'
|
||||
)
|
||||
else:
|
||||
messages.error(
|
||||
self.request,
|
||||
'Ошибка при сохранении товара. Пожалуйста, проверьте введённые данные.'
|
||||
)
|
||||
|
||||
# Перенаправляем обратно на форму с сохранённой информацией
|
||||
return self.form_invalid(form)
|
||||
|
||||
|
||||
class ProductDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
|
||||
|
||||
Reference in New Issue
Block a user