feat: 引入事务悲观锁(lockForUpdate)与余额二次校验,防范高并发资产越权与透支漏洞

This commit is contained in:
pllx
2026-06-30 11:32:52 +08:00
parent d1409d16bb
commit 3563b45038
4 changed files with 132 additions and 42 deletions
+19 -7
View File
@@ -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);
});
}