Fix race condition and expired lock validation for showcase kit cart
- Add select_for_update() with transaction.atomic() to prevent race condition when multiple users try to add the same showcase kit simultaneously - Validate cart lock expiration when loading cart from Redis - showcase kits with expired locks are now filtered out instead of being displayed 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -227,10 +227,25 @@ def pos_terminal(request):
|
||||
# Проверяем что товар существует
|
||||
Product.objects.get(id=item['id'])
|
||||
cart_data[cart_key] = item
|
||||
elif item['type'] in ('kit', 'showcase_kit'):
|
||||
# Проверяем что комплект существует
|
||||
elif item['type'] == 'kit':
|
||||
# Обычный комплект - только проверяем существование
|
||||
ProductKit.objects.get(id=item['id'])
|
||||
cart_data[cart_key] = item
|
||||
elif item['type'] == 'showcase_kit':
|
||||
# Витринный комплект - проверяем существование И актуальность блокировки
|
||||
kit = ProductKit.objects.get(id=item['id'])
|
||||
|
||||
# Проверяем, что блокировка всё ещё активна для этого пользователя
|
||||
has_valid_lock = Reservation.objects.filter(
|
||||
product_kit=kit,
|
||||
locked_by_user=request.user,
|
||||
cart_lock_expires_at__gt=timezone.now(),
|
||||
status='reserved'
|
||||
).exists()
|
||||
|
||||
if has_valid_lock:
|
||||
cart_data[cart_key] = item
|
||||
# Если блокировка истекла - не добавляем в корзину (товар доступен другим)
|
||||
except (Product.DoesNotExist, ProductKit.DoesNotExist):
|
||||
# Товар или комплект удален - пропускаем
|
||||
continue
|
||||
@@ -468,43 +483,45 @@ def add_showcase_kit_to_cart(request, kit_id):
|
||||
status='active'
|
||||
)
|
||||
|
||||
# Проверяем существующие блокировки
|
||||
existing_locks = Reservation.objects.filter(
|
||||
product_kit=kit,
|
||||
cart_lock_expires_at__gt=timezone.now(),
|
||||
status='reserved'
|
||||
).exclude(
|
||||
locked_by_user=request.user
|
||||
).select_related('locked_by_user')
|
||||
# Атомарная проверка и создание блокировки (предотвращает race condition)
|
||||
with transaction.atomic():
|
||||
# Блокируем строки резервов для этого комплекта на уровне БД
|
||||
reservations = Reservation.objects.select_for_update().filter(
|
||||
product_kit=kit,
|
||||
status='reserved'
|
||||
)
|
||||
|
||||
if existing_locks.exists():
|
||||
lock = existing_locks.first()
|
||||
time_left = (lock.cart_lock_expires_at - timezone.now()).total_seconds() / 60
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': f'Этот букет уже в корзине кассира "{lock.locked_by_user.username}". '
|
||||
f'Блокировка истечет через {int(time_left)} мин.'
|
||||
}, status=409) # 409 Conflict
|
||||
# Проверяем существующие блокировки другими пользователями
|
||||
existing_lock = reservations.filter(
|
||||
cart_lock_expires_at__gt=timezone.now()
|
||||
).exclude(
|
||||
locked_by_user=request.user
|
||||
).select_related('locked_by_user').first()
|
||||
|
||||
# Создаем или продлеваем блокировку для текущего пользователя
|
||||
lock_expires_at = timezone.now() + timedelta(minutes=30)
|
||||
session_id = request.session.session_key or ''
|
||||
if existing_lock:
|
||||
time_left = (existing_lock.cart_lock_expires_at - timezone.now()).total_seconds() / 60
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': f'Этот букет уже в корзине кассира "{existing_lock.locked_by_user.username}". '
|
||||
f'Блокировка истечет через {int(time_left)} мин.'
|
||||
}, status=409) # 409 Conflict
|
||||
|
||||
# Обновляем все резервы этого комплекта
|
||||
updated_count = Reservation.objects.filter(
|
||||
product_kit=kit,
|
||||
status='reserved'
|
||||
).update(
|
||||
cart_lock_expires_at=lock_expires_at,
|
||||
locked_by_user=request.user,
|
||||
cart_session_id=session_id
|
||||
)
|
||||
# Создаем или продлеваем блокировку для текущего пользователя
|
||||
lock_expires_at = timezone.now() + timedelta(minutes=30)
|
||||
session_id = request.session.session_key or ''
|
||||
|
||||
if updated_count == 0:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'У комплекта нет активных резервов. Возможно, он уже продан.'
|
||||
}, status=400)
|
||||
# Обновляем все резервы этого комплекта (теперь атомарно!)
|
||||
updated_count = reservations.update(
|
||||
cart_lock_expires_at=lock_expires_at,
|
||||
locked_by_user=request.user,
|
||||
cart_session_id=session_id
|
||||
)
|
||||
|
||||
if updated_count == 0:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'У комплекта нет активных резервов. Возможно, он уже продан.'
|
||||
}, status=400)
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
|
||||
Reference in New Issue
Block a user