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:
2025-11-15 11:57:11 +03:00
parent 3426cdd091
commit 0bdb42401a
3 changed files with 176 additions and 35 deletions

View File

@@ -165,14 +165,55 @@ class BaseProductEntity(models.Model):
super().delete()
def save(self, *args, **kwargs):
"""Автогенерация slug из name если не задан"""
if not self.slug or self.slug.strip() == '':
# Используем централизованный сервис для генерации slug
from ..services.slug_service import SlugService
self.slug = SlugService.generate_unique_slug(
self.name,
self.__class__,
self.pk
)
"""
Автогенерация slug из name если не задан.
Использует transaction.atomic() и retry логику для обработки race condition.
"""
from django.db import transaction, IntegrityError
from ..services.slug_service import SlugService
super().save(*args, **kwargs)
# Генерируем базовый slug
if not self.slug or self.slug.strip() == '':
transliterated_name = __import__('unidecode', fromlist=['unidecode']).unidecode(self.name)
base_slug = __import__('django.utils.text', fromlist=['slugify']).slugify(transliterated_name)
else:
base_slug = self.slug
# Пытаемся сохранить с retry при IntegrityError
max_retries = 5
for attempt in range(max_retries):
try:
with transaction.atomic():
# Попытка 1: используем обычный способ генерации
if not self.slug or self.slug.strip() == '':
if attempt == 0:
# Первая попытка - используем обычный generate_unique_slug
self.slug = SlugService.generate_unique_slug(
self.name,
self.__class__,
self.pk
)
else:
# Retry попытки - используем get_next_available_slug с суффиксом
try:
self.slug = SlugService.get_next_available_slug(
base_slug,
self.__class__,
self.pk,
start_counter=attempt
)
except ValueError:
# Если все попытки с суффиксом исчерпаны, добавляем timestamp
import time
self.slug = f"{base_slug}-{int(time.time() % 10000)}"
# Основное сохранение
super().save(*args, **kwargs)
return # Успешно сохранили, выходим
except IntegrityError as e:
# Если это последняя попытка, выбрасываем исключение
if attempt == max_retries - 1:
raise
# Иначе пытаемся снова со следующим slug'ом
continue