rtc_backend/apps/users/services/points_service.py

70 lines
2.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
用户积分服务
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,
)