feat: 引入事务悲观锁(lockForUpdate)与余额二次校验,防范高并发资产越权与透支漏洞
This commit is contained in:
@@ -17,7 +17,6 @@ use App\Enums\CurrencySource;
|
||||
use App\Models\BankLog;
|
||||
use App\Models\Sysparam;
|
||||
use App\Models\User;
|
||||
use App\Models\UserCurrencyLog;
|
||||
use App\Services\UserCurrencyService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -32,6 +31,7 @@ class BankController extends Controller
|
||||
public function __construct(
|
||||
private readonly UserCurrencyService $currencyService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 查询银行余额及最近20条流水记录
|
||||
*/
|
||||
@@ -105,6 +105,7 @@ class BankController extends Controller
|
||||
$amount = $request->integer('amount');
|
||||
$user = Auth::user();
|
||||
|
||||
// 快速过滤(非锁),降低非法请求穿透到数据库的概率
|
||||
if (($user->jjb ?? 0) < $amount) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
@@ -112,26 +113,49 @@ class BankController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($user, $amount): void {
|
||||
$this->currencyService->change($user, 'gold', -$amount, CurrencySource::BANK_DEPOSIT, "存入银行 {$amount} 金币");
|
||||
try {
|
||||
DB::transaction(function () use ($user, $amount): void {
|
||||
// 1. 强制在数据库层面对用户行数据加写锁(X锁)
|
||||
$lockedUser = User::query()
|
||||
->whereKey($user->id)
|
||||
->lockForUpdate()
|
||||
->firstOrFail();
|
||||
|
||||
$user->increment('bank_jjb', $amount);
|
||||
// 2. 在锁保护下安全校验最新余额
|
||||
if ((int) $lockedUser->jjb < $amount) {
|
||||
throw new \Exception('流通金币不足!当前余额 '.(int) $lockedUser->jjb." 枚,无法存入 {$amount} 枚。");
|
||||
}
|
||||
|
||||
BankLog::create([
|
||||
'user_id' => $user->id,
|
||||
'type' => 'deposit',
|
||||
'amount' => $amount,
|
||||
'balance_after' => $user->fresh()->bank_jjb,
|
||||
// 3. 执行资产扣除,将已加锁的 lockedUser 传给 change 方法
|
||||
$this->currencyService->change($lockedUser, 'gold', -$amount, CurrencySource::BANK_DEPOSIT, "存入银行 {$amount} 金币");
|
||||
|
||||
// 4. 增加银行余额
|
||||
$lockedUser->increment('bank_jjb', $amount);
|
||||
|
||||
// 5. 写入银行流水记录
|
||||
BankLog::create([
|
||||
'user_id' => $lockedUser->id,
|
||||
'type' => 'deposit',
|
||||
'amount' => $amount,
|
||||
'balance_after' => $lockedUser->fresh()->bank_jjb,
|
||||
]);
|
||||
|
||||
// 6. 同步 Auth 内存状态,保障同生命周期内其他地方拿到的是正确数据
|
||||
$user->setAttribute('jjb', $lockedUser->jjb);
|
||||
$user->setAttribute('bank_jjb', $lockedUser->bank_jjb);
|
||||
});
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
});
|
||||
|
||||
$fresh = $user->fresh();
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => "成功存入 {$amount} 枚金币!",
|
||||
'jjb' => $fresh->jjb,
|
||||
'bank_jjb' => $fresh->bank_jjb,
|
||||
'jjb' => $user->jjb,
|
||||
'bank_jjb' => $user->bank_jjb,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -149,6 +173,7 @@ class BankController extends Controller
|
||||
$amount = $request->integer('amount');
|
||||
$user = Auth::user();
|
||||
|
||||
// 快速过滤(非锁)
|
||||
if (($user->bank_jjb ?? 0) < $amount) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
@@ -156,26 +181,49 @@ class BankController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($user, $amount): void {
|
||||
$user->decrement('bank_jjb', $amount);
|
||||
try {
|
||||
DB::transaction(function () use ($user, $amount): void {
|
||||
// 1. 强制在数据库层面对用户行数据加写锁(X锁)
|
||||
$lockedUser = User::query()
|
||||
->whereKey($user->id)
|
||||
->lockForUpdate()
|
||||
->firstOrFail();
|
||||
|
||||
$this->currencyService->change($user, 'gold', $amount, CurrencySource::BANK_WITHDRAW, "取出银行存款 {$amount} 金币");
|
||||
// 2. 校验银行存款是否足够
|
||||
if ((int) $lockedUser->bank_jjb < $amount) {
|
||||
throw new \Exception('银行余额不足!当前存款 '.($lockedUser->bank_jjb ?? 0)." 枚,无法取出 {$amount} 枚。");
|
||||
}
|
||||
|
||||
BankLog::create([
|
||||
'user_id' => $user->id,
|
||||
'type' => 'withdraw',
|
||||
'amount' => $amount,
|
||||
'balance_after' => $user->fresh()->bank_jjb,
|
||||
// 3. 扣除银行存款
|
||||
$lockedUser->decrement('bank_jjb', $amount);
|
||||
|
||||
// 4. 增加流通金币并记录划转日志
|
||||
$this->currencyService->change($lockedUser, 'gold', $amount, CurrencySource::BANK_WITHDRAW, "取出银行存款 {$amount} 金币");
|
||||
|
||||
// 5. 记录银行账户流水
|
||||
BankLog::create([
|
||||
'user_id' => $lockedUser->id,
|
||||
'type' => 'withdraw',
|
||||
'amount' => $amount,
|
||||
'balance_after' => $lockedUser->fresh()->bank_jjb,
|
||||
]);
|
||||
|
||||
// 6. 同步 Auth 内存状态
|
||||
$user->setAttribute('jjb', $lockedUser->jjb);
|
||||
$user->setAttribute('bank_jjb', $lockedUser->bank_jjb);
|
||||
});
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
});
|
||||
|
||||
$fresh = $user->fresh();
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => "成功取出 {$amount} 枚金币!",
|
||||
'jjb' => $fresh->jjb,
|
||||
'bank_jjb' => $fresh->bank_jjb,
|
||||
'jjb' => $user->jjb,
|
||||
'bank_jjb' => $user->bank_jjb,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user