| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157 |
- <?php
- namespace App\Services;
- use App\Constants\HttpStatus;
- use App\Models\User;
- use App\Models\Wallet;
- use App\Models\WalletFreezeRecord as Record;
- use Exception;
- use Illuminate\Database\Eloquent\Model;
- use Illuminate\Support\Facades\DB;
- /**
- * 提现资金冻结:
- * - hold:申请时校验打码量,可用余额转入冻结余额,创建提现单并重置打码周期。
- * - release:审核拒绝 / 三方失败 / 撤回,冻结余额退回可用余额。
- * - settle:提现成功,扣除冻结余额。
- * frozen_amount = 0 的旧单(上线前申请,已直接扣可用余额)保持原逻辑:拒绝退回可用余额,成功不再扣款。
- */
- class WithdrawFundsService
- {
- public const WITHDRAWS = 'withdraws';
- public const PAYMENT_ORDERS = 'payment_orders';
- /** @var array<int,bool> */
- private static array $enabled = [];
- public static function enabled(): bool
- {
- if (!WageringService::enabled()) return false;
- $key = spl_object_id(DB::connection()->getPdo());
- if (!array_key_exists($key, self::$enabled)) {
- $schema = DB::connection()->getSchemaBuilder();
- self::$enabled[$key] = $schema->hasTable('wallet_freeze_records')
- && $schema->hasColumn('withdraws', 'frozen_amount') && $schema->hasColumn('payment_orders', 'frozen_amount');
- }
- return self::$enabled[$key];
- }
- public static function flushSchemaCache(): void
- {
- self::$enabled = [];
- }
- /**
- * @param callable(string $beforeAvailable, string $afterAvailable): Model $createOrder 创建提现单
- * @return array{0: Model, 1: Wallet}
- */
- public static function hold(string $memberId, $amount, string $changeType, string $remark, string $relatedType, callable $createOrder, ?string $logAmount = null): array
- {
- // 先在锁外重算当前打码量(耗时查询不占用钱包行锁);锁内只用缓存值判断。
- // 之后才结算的注单只会让缓存偏低,不会让未完成打码量的会员提前提现。
- $unlocked = WageringService::enabled() ? Wallet::where('member_id', $memberId)->first() : null;
- if ($unlocked && bccomp(WageringService::remaining($unlocked->wager_required, $unlocked->wager_completed), '0', 10) > 0) {
- WageringService::refresh($unlocked);
- }
- return DB::transaction(function () use ($memberId, $amount, $changeType, $remark, $relatedType, $createOrder, $logAmount) {
- $wallet = Wallet::where('member_id', $memberId)->lockForUpdate()->first();
- if (!$wallet) throw new Exception('钱包不存在', HttpStatus::CUSTOM_ERROR);
- $amount = bcadd((string)$amount, '0', 10);
- if (bccomp($amount, '0', 10) <= 0) throw new Exception('提现金额错误', HttpStatus::CUSTOM_ERROR);
- $beforeAvailable = bcadd((string)$wallet->available_balance, '0', 10);
- if (bccomp($beforeAvailable, $amount, 10) < 0) throw new Exception('您的钱包余额不足!', HttpStatus::CUSTOM_ERROR);
- WageringService::assertWithdrawable($wallet, false);
- $enabled = self::enabled();
- $beforeFrozen = bcadd((string)$wallet->frozen_balance, '0', 10);
- $afterAvailable = bcsub($beforeAvailable, $amount, 10);
- $afterFrozen = $enabled ? bcadd($beforeFrozen, $amount, 10) : $beforeFrozen;
- $wallet->available_balance = $afterAvailable;
- $wallet->frozen_balance = $afterFrozen;
- if (!$wallet->save()) throw new Exception('钱包更新失败!', HttpStatus::CUSTOM_ERROR);
- $order = $createOrder($beforeAvailable, $afterAvailable);
- if ($enabled) {
- $order->frozen_amount = $amount;
- $order->save();
- }
- BalanceLogService::addLog($memberId, $logAmount ?? bcsub('0', $amount, 10), $beforeAvailable, $afterAvailable, $changeType, $order->id, $remark);
- if ($enabled) {
- Record::create([
- 'user_id' => User::where('member_id', $memberId)->value('id'), 'member_id' => $memberId,
- 'type' => Record::TYPE_WITHDRAW_HOLD, 'amount' => $amount, 'wager_amount' => '0',
- 'reason' => $changeType . '申请冻结', 'status' => Record::STATUS_HOLDING,
- 'related_type' => $relatedType, 'related_id' => $order->id,
- 'before_available' => $beforeAvailable, 'after_available' => $afterAvailable,
- 'before_frozen' => $beforeFrozen, 'after_frozen' => $afterFrozen,
- ]);
- }
- WageringService::resetCycle($wallet, 'reset', $relatedType . ':' . $order->id);
- return [$order, $wallet];
- }, 3);
- }
- /**
- * 退回提现资金到可用余额(拒绝 / 失败 / 撤回)。调用方负责先把提现单改为终态,避免重复退回。
- * @param string $legacyAmount 旧单(frozen_amount=0)应退回的金额
- * @return array{before: string, after: string, amount: string}|null 已处理过返回 null
- */
- public static function release(string $relatedType, Model $order, $legacyAmount, string $changeType, string $remark): ?array
- {
- return DB::transaction(function () use ($relatedType, $order, $legacyAmount, $changeType, $remark) {
- $wallet = Wallet::where('member_id', $order->member_id)->lockForUpdate()->first();
- if (!$wallet) throw new Exception('钱包不存在', HttpStatus::CUSTOM_ERROR);
- $frozenAmount = self::frozenAmount($order);
- $record = null;
- if (bccomp($frozenAmount, '0', 10) > 0) {
- $record = self::holdRecord($relatedType, (int)$order->id);
- if ($record && $record->status !== Record::STATUS_HOLDING) return null;
- $amount = $frozenAmount;
- $beforeFrozen = bcadd((string)$wallet->frozen_balance, '0', 10);
- if (bccomp($beforeFrozen, $amount, 10) < 0) throw new Exception('冻结余额不足,无法退回提现', HttpStatus::CUSTOM_ERROR);
- $wallet->frozen_balance = bcsub($beforeFrozen, $amount, 10);
- } else {
- $amount = bcadd((string)$legacyAmount, '0', 10);
- }
- $before = bcadd((string)$wallet->available_balance, '0', 10);
- $after = bcadd($before, $amount, 10);
- $wallet->available_balance = $after;
- if (!$wallet->save()) throw new Exception('钱包更新失败', HttpStatus::CUSTOM_ERROR);
- BalanceLogService::addLog($order->member_id, $amount, $before, $after, $changeType, $order->id, $remark);
- $record?->update(['status' => Record::STATUS_RELEASED, 'finished_at' => PromotionFacts::now(),
- 'after_available' => $after, 'after_frozen' => $wallet->frozen_balance]);
- return ['before' => $before, 'after' => $after, 'amount' => $amount];
- }, 3);
- }
- /** 提现成功:扣除冻结余额。旧单无需处理。 */
- public static function settle(string $relatedType, Model $order): void
- {
- $frozenAmount = self::frozenAmount($order);
- if (bccomp($frozenAmount, '0', 10) <= 0) return;
- DB::transaction(function () use ($relatedType, $order, $frozenAmount) {
- $wallet = Wallet::where('member_id', $order->member_id)->lockForUpdate()->first();
- if (!$wallet) throw new Exception('钱包不存在', HttpStatus::CUSTOM_ERROR);
- $record = self::holdRecord($relatedType, (int)$order->id);
- if ($record && $record->status !== Record::STATUS_HOLDING) return;
- $beforeFrozen = bcadd((string)$wallet->frozen_balance, '0', 10);
- if (bccomp($beforeFrozen, $frozenAmount, 10) < 0) throw new Exception('冻结余额不足,无法完成提现', HttpStatus::CUSTOM_ERROR);
- $wallet->frozen_balance = bcsub($beforeFrozen, $frozenAmount, 10);
- if (!$wallet->save()) throw new Exception('钱包更新失败', HttpStatus::CUSTOM_ERROR);
- $record?->update(['status' => Record::STATUS_SETTLED, 'finished_at' => PromotionFacts::now(), 'after_frozen' => $wallet->frozen_balance]);
- }, 3);
- }
- private static function frozenAmount(Model $order): string
- {
- return bcadd((string)($order->getAttribute('frozen_amount') ?? '0'), '0', 10);
- }
- private static function holdRecord(string $relatedType, int $id): ?Record
- {
- if (!self::enabled()) return null;
- return Record::where('related_type', $relatedType)->where('related_id', $id)
- ->where('type', Record::TYPE_WITHDRAW_HOLD)->lockForUpdate()->first();
- }
- }
|