70 lines
2.3 KiB
Python
70 lines
2.3 KiB
Python
"""
|
||
用户积分服务
|
||
|
||
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,
|
||
)
|