feat: 引入事务悲观锁(lockForUpdate)与余额二次校验,防范高并发资产越权与透支漏洞
This commit is contained in:
@@ -65,21 +65,30 @@ class UserCurrencyService
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($user, $currency, $amount, $source, $remark, $roomId, $field) {
|
||||
// 原子性更新用户属性(用 increment/decrement 防并发竞态)
|
||||
// 原子性更新用户属性(用 lockForUpdate 悲观锁锁定对应的数据库行)
|
||||
$lockedUser = User::query()
|
||||
->whereKey($user->id)
|
||||
->lockForUpdate()
|
||||
->firstOrFail();
|
||||
|
||||
if ($amount > 0) {
|
||||
$user->increment($field, $amount);
|
||||
$lockedUser->increment($field, $amount);
|
||||
} else {
|
||||
// 扣除时不让余额低于 0
|
||||
$user->decrement($field, min(abs($amount), $user->$field ?? 0));
|
||||
// 扣除时不让余额低于 0,基于锁定的最新数据计算
|
||||
$currentBalance = (int) $lockedUser->$field;
|
||||
$deductAmount = min(abs($amount), $currentBalance);
|
||||
if ($deductAmount > 0) {
|
||||
$lockedUser->decrement($field, $deductAmount);
|
||||
}
|
||||
}
|
||||
|
||||
// 重新读取最新余额(避免缓存脏数据)
|
||||
$balanceAfter = (int) $user->fresh()->$field;
|
||||
$balanceAfter = (int) $lockedUser->fresh()->$field;
|
||||
|
||||
// 写入流水记录(快照当前用户名,排行 JOIN 时再取最新名)
|
||||
UserCurrencyLog::create([
|
||||
'user_id' => $user->id,
|
||||
'username' => $user->username,
|
||||
'user_id' => $lockedUser->id,
|
||||
'username' => $lockedUser->username,
|
||||
'currency' => $currency,
|
||||
'amount' => $amount,
|
||||
'balance_after' => $balanceAfter,
|
||||
@@ -87,6 +96,9 @@ class UserCurrencyService
|
||||
'remark' => $remark,
|
||||
'room_id' => $roomId,
|
||||
]);
|
||||
|
||||
// 同步修改内存中 $user 的实例属性,避免后续逻辑拿到过期的数据
|
||||
$user->setAttribute($field, $balanceAfter);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user