""" 用户积分服务 Bug #43 fix: replace non-atomic points deduction with a SELECT FOR UPDATE inside an atomic transaction to eliminate the race condition that allowed the balance to go negative. """ from django.db import transaction from django.db.models import F from apps.users.models import User, PointsRecord class InsufficientPointsError(Exception): """积分不足异常""" def deduct_points(user_id, amount, record_type, description=''): """ 原子性地扣减用户积分,并写入流水记录。 Bug #43 fix: the original code was: user.points -= amount # read-modify-write – not atomic user.save() # concurrent calls can all pass the balance # check and drive points negative The fix uses SELECT FOR UPDATE inside an atomic block so that concurrent deductions are serialised at the database level, and an extra guard (points__gte=amount) prevents the update from proceeding when the balance is insufficient. """ with transaction.atomic(): # Lock the row so no other transaction can read stale data updated_rows = User.objects.filter( id=user_id, points__gte=amount, # guard: only deduct when balance is sufficient ).update(points=F('points') - amount) if updated_rows == 0: # Either the user doesn't exist or balance was insufficient user = User.objects.filter(id=user_id).first() if user is None: raise ValueError(f'用户 {user_id} 不存在') raise InsufficientPointsError( f'积分不足: 当前余额 {user.points},需要 {amount}' ) PointsRecord.objects.create( user_id=user_id, amount=-amount, type=record_type, description=description, ) def add_points(user_id, amount, record_type, description=''): """ 原子性地增加用户积分,并写入流水记录。 """ with transaction.atomic(): User.objects.filter(id=user_id).update(points=F('points') + amount) PointsRecord.objects.create( user_id=user_id, amount=amount, type=record_type, description=description, )