| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173 |
- <?php
- namespace App\Services;
- use App\Constants\HttpStatus;
- use App\Models\User;
- use App\Models\Wallet;
- use App\Models\WalletFreezeRecord as Record;
- use Carbon\Carbon;
- use Exception;
- use Illuminate\Database\QueryException;
- use Illuminate\Support\Facades\DB;
- /**
- * 后台人工冻结 / 解冻。
- * 冻结:可用余额 → 冻结余额,并减少打码量;解冻:冻结余额 → 可用余额,并增加打码量。
- * 解冻金额不超过该会员未解冻的人工冻结金额,提现冻结中的资金不能被人工解冻。
- */
- class WalletFreezeService
- {
- public static function freeze(string $requestId, int $adminId, string $memberId, string $amount, string $wagerAmount, string $reason): array
- {
- return self::apply(true, $requestId, $adminId, $memberId, $amount, $wagerAmount, $reason);
- }
- public static function unfreeze(string $requestId, int $adminId, string $memberId, string $amount, string $wagerAmount, string $reason): array
- {
- return self::apply(false, $requestId, $adminId, $memberId, $amount, $wagerAmount, $reason);
- }
- /** 会员尚未解冻的人工冻结金额。 */
- public static function manualFrozen(string $memberId): string
- {
- $frozen = (string)Record::where('member_id', $memberId)->where('type', Record::TYPE_MANUAL_FREEZE)->sum('amount');
- $unfrozen = (string)Record::where('member_id', $memberId)->where('type', Record::TYPE_MANUAL_UNFREEZE)->sum('amount');
- $left = bcsub(bcadd($frozen, '0', 10), bcadd($unfrozen, '0', 10), 10);
- return bccomp($left, '0', 10) > 0 ? $left : '0';
- }
- private static function apply(bool $isFreeze, string $requestId, int $adminId, string $memberId, string $amount, string $wagerAmount, string $reason): array
- {
- $type = $isFreeze ? Record::TYPE_MANUAL_FREEZE : Record::TYPE_MANUAL_UNFREEZE;
- $amount = bcadd($amount, '0', 10);
- $wagerAmount = bcadd($wagerAmount === '' ? '0' : $wagerAmount, '0', 10);
- if (bccomp($amount, '0', 10) <= 0) throw new Exception('金额必须大于0', HttpStatus::CUSTOM_ERROR);
- if (bccomp($wagerAmount, '0', 10) < 0) throw new Exception('打码量不能小于0', HttpStatus::CUSTOM_ERROR);
- $replay = fn () => self::replay($requestId, $type, $memberId, $amount, $wagerAmount);
- if ($existing = $replay()) return $existing;
- try {
- $record = DB::transaction(function () use ($isFreeze, $type, $requestId, $adminId, $memberId, $amount, $wagerAmount, $reason) {
- $wallet = Wallet::where('member_id', $memberId)->lockForUpdate()->first();
- if (!$wallet) throw new Exception('用户钱包不存在', HttpStatus::CUSTOM_ERROR);
- $user = User::where('member_id', $memberId)->first(['id']);
- if (!$user) throw new Exception('用户不存在', HttpStatus::CUSTOM_ERROR);
- $beforeAvailable = bcadd((string)$wallet->available_balance, '0', 10);
- $beforeFrozen = bcadd((string)$wallet->frozen_balance, '0', 10);
- if ($isFreeze) {
- if (bccomp($beforeAvailable, $amount, 10) < 0) throw new Exception('可用余额不足', HttpStatus::CUSTOM_ERROR);
- $afterAvailable = bcsub($beforeAvailable, $amount, 10);
- $afterFrozen = bcadd($beforeFrozen, $amount, 10);
- } else {
- $limit = self::manualFrozen($memberId);
- if (bccomp($beforeFrozen, $limit, 10) < 0) $limit = $beforeFrozen;
- if (bccomp($limit, $amount, 10) < 0) throw new Exception('可解冻金额不足,最多可解冻:' . bcadd($limit, '0', 2), HttpStatus::CUSTOM_ERROR);
- $afterAvailable = bcadd($beforeAvailable, $amount, 10);
- $afterFrozen = bcsub($beforeFrozen, $amount, 10);
- }
- $wallet->available_balance = $afterAvailable;
- $wallet->frozen_balance = $afterFrozen;
- if (!$wallet->save()) throw new Exception('钱包更新失败', HttpStatus::CUSTOM_ERROR);
- $record = Record::create([
- 'user_id' => $user->id, 'member_id' => $memberId, 'type' => $type, 'amount' => $amount,
- 'wager_amount' => $wagerAmount, 'reason' => mb_substr($reason, 0, 255),
- 'status' => $isFreeze ? Record::STATUS_FROZEN : Record::STATUS_DONE, 'request_id' => $requestId,
- 'admin_id' => $adminId, 'before_available' => $beforeAvailable, 'after_available' => $afterAvailable,
- 'before_frozen' => $beforeFrozen, 'after_frozen' => $afterFrozen,
- ]);
- BalanceLogService::addLog($memberId, $isFreeze ? bcsub('0', $amount, 10) : $amount, $beforeAvailable, $afterAvailable,
- $isFreeze ? '人工冻结' : '人工解冻', $record->id, $reason);
- WageringService::addRequirement($wallet, $isFreeze ? bcsub('0', $wagerAmount, 10) : $wagerAmount,
- $isFreeze ? 'freeze' : 'unfreeze', $record->id, $reason, $adminId);
- return $record;
- }, 3);
- } catch (QueryException $e) {
- // 同一 request_id 并发提交:唯一索引冲突后返回已成功的那一笔
- if ($existing = $replay()) return $existing;
- throw $e;
- }
- return self::format($record);
- }
- private static function replay(string $requestId, string $type, string $memberId, string $amount, string $wagerAmount): ?array
- {
- $existing = Record::where('request_id', $requestId)->first();
- if (!$existing) return null;
- if ($existing->type !== $type || (string)$existing->member_id !== $memberId
- || bccomp((string)$existing->amount, $amount, 10) !== 0 || bccomp((string)$existing->wager_amount, $wagerAmount, 10) !== 0) {
- throw new Exception('request_id 已被其他冻结操作使用', HttpStatus::CUSTOM_ERROR);
- }
- return self::format($existing);
- }
- /** 冻结记录列表(后台)。 */
- public static function list(array $params): array
- {
- $page = max(1, (int)($params['page'] ?? 1));
- $limit = min(200, max(1, (int)($params['limit'] ?? 20)));
- $query = Record::query()->with('member');
- if (!empty($params['member_id'])) $query->where('member_id', (string)$params['member_id']);
- if (!empty($params['account'])) {
- $query->whereIn('member_id', User::select('member_id')->where('account', 'like', '%' . trim((string)$params['account']) . '%'));
- }
- if (!empty($params['type'])) $query->where('type', (string)$params['type']);
- if (!empty($params['status'])) $query->where('status', (string)$params['status']);
- if (!empty($params['start_date']) && !empty($params['end_date'])) {
- $query->whereBetween('created_at', [
- Carbon::parse($params['start_date'], 'Asia/Shanghai')->startOfDay(),
- Carbon::parse($params['end_date'], 'Asia/Shanghai')->endOfDay(),
- ]);
- }
- $total = (clone $query)->count('id');
- $rows = $query->orderByDesc('id')->forPage($page, $limit)->get();
- $admins = DB::table('admin')->whereIn('id', $rows->pluck('admin_id')->filter()->unique()->all() ?: [0])->pluck('username', 'id');
- return [
- 'total' => $total,
- 'data' => $rows->map(fn (Record $row) => self::format($row, $admins->all()))->all(),
- 'options' => [
- 'types' => self::options(Record::TYPE_LABELS),
- 'statuses' => self::options(Record::STATUS_LABELS),
- ],
- ];
- }
- public static function format(Record $row, array $admins = []): array
- {
- $member = $row->relationLoaded('member') ? $row->member : null;
- return [
- 'id' => (int)$row->id,
- 'member_id' => (string)$row->member_id,
- 'account' => (string)($member->account ?? ''),
- 'username' => (string)($member->username ?? ''),
- 'first_name' => (string)($member->first_name ?? ''),
- 'type' => (string)$row->type,
- 'type_text' => Record::TYPE_LABELS[$row->type] ?? $row->type,
- 'amount' => bcadd((string)$row->amount, '0', 2),
- 'wager_amount' => bcadd((string)$row->wager_amount, '0', 2),
- 'reason' => (string)$row->reason,
- 'status' => (string)$row->status,
- 'status_text' => Record::STATUS_LABELS[$row->status] ?? $row->status,
- 'related_type' => $row->related_type,
- 'related_id' => $row->related_id === null ? null : (int)$row->related_id,
- 'admin_id' => $row->admin_id === null ? null : (int)$row->admin_id,
- 'admin_name' => $row->admin_id === null ? '' : (string)($admins[$row->admin_id] ?? ''),
- 'before_available' => bcadd((string)$row->before_available, '0', 2),
- 'after_available' => bcadd((string)$row->after_available, '0', 2),
- 'before_frozen' => bcadd((string)$row->before_frozen, '0', 2),
- 'after_frozen' => bcadd((string)$row->after_frozen, '0', 2),
- 'finished_at' => $row->finished_at ? (string)$row->finished_at : null,
- 'created_at' => (string)$row->created_at,
- ];
- }
- private static function options(array $labels): array
- {
- $result = [];
- foreach ($labels as $value => $label) $result[] = ['label' => $label, 'value' => $value];
- return $result;
- }
- }
|