|
|
@@ -17,7 +17,12 @@ use Illuminate\Http\JsonResponse;
|
|
|
use App\Models\User as UserModel;
|
|
|
use App\Models\UserSession;
|
|
|
use App\Models\UserLogin;
|
|
|
+use App\Models\ThirdGameRecycle;
|
|
|
+use App\Models\Wallet;
|
|
|
+use App\Services\BalanceLogService;
|
|
|
+use App\Services\ThirdGameBalanceService;
|
|
|
use Illuminate\Support\Facades\Cache;
|
|
|
+use Illuminate\Support\Str;
|
|
|
|
|
|
class User extends Controller
|
|
|
{
|
|
|
@@ -126,6 +131,298 @@ class User extends Controller
|
|
|
return $this->success($result);
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 查询指定用户在 ag、pg 等三方游戏平台的余额明细。
|
|
|
+ */
|
|
|
+ public function thirdGameDetail(ThirdGameBalanceService $service): JsonResponse
|
|
|
+ {
|
|
|
+ try {
|
|
|
+ $params = request()->validate([
|
|
|
+ 'member_id' => ['required', 'string', 'min:1'],
|
|
|
+ ]);
|
|
|
+ if (!UserModel::where('member_id', $params['member_id'])->exists()) {
|
|
|
+ throw new Exception('用户不存在', HttpStatus::CUSTOM_ERROR);
|
|
|
+ }
|
|
|
+
|
|
|
+ $result = $service->detail((string) $params['member_id']);
|
|
|
+ if (!$result['ok']) {
|
|
|
+ throw new Exception($result['msg'], HttpStatus::CUSTOM_ERROR);
|
|
|
+ }
|
|
|
+ unset($result['ok']);
|
|
|
+ return $this->success($result);
|
|
|
+ } catch (ValidationException $e) {
|
|
|
+ return $this->error(HttpStatus::CUSTOM_ERROR, $e->validator->errors()->first());
|
|
|
+ } catch (Exception $e) {
|
|
|
+ return $this->error(HttpStatus::CUSTOM_ERROR, $e->getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 一键回收指定用户的全部三方游戏余额并入账主钱包。
|
|
|
+ */
|
|
|
+ public function recycleThirdGameBalance(ThirdGameBalanceService $service): JsonResponse
|
|
|
+ {
|
|
|
+ $lock = null;
|
|
|
+ $lockAcquired = false;
|
|
|
+ try {
|
|
|
+ $params = request()->validate([
|
|
|
+ 'member_id' => ['required', 'string', 'min:1'],
|
|
|
+ ]);
|
|
|
+ $memberId = (string) $params['member_id'];
|
|
|
+ if (!UserModel::where('member_id', $memberId)->exists()) {
|
|
|
+ throw new Exception('用户不存在', HttpStatus::CUSTOM_ERROR);
|
|
|
+ }
|
|
|
+ if (!Wallet::where('member_id', $memberId)->exists()) {
|
|
|
+ throw new Exception('用户钱包不存在', HttpStatus::CUSTOM_ERROR);
|
|
|
+ }
|
|
|
+
|
|
|
+ $lock = Cache::lock('admin_third_game_recycle:' . $memberId, 120);
|
|
|
+ $lockAcquired = $lock->get();
|
|
|
+ if (!$lockAcquired) {
|
|
|
+ throw new Exception('该用户正在回收三方余额,请勿重复操作', HttpStatus::CUSTOM_ERROR);
|
|
|
+ }
|
|
|
+
|
|
|
+ $pendingCredit = ThirdGameRecycle::where('member_id', $memberId)
|
|
|
+ ->where('status', ThirdGameRecycle::STATUS_PROVIDER_SUCCEEDED)
|
|
|
+ ->orderBy('id')
|
|
|
+ ->first();
|
|
|
+ if ($pendingCredit) {
|
|
|
+ try {
|
|
|
+ $credited = $this->creditThirdGameRecycle($pendingCredit->id);
|
|
|
+ } catch (Exception $e) {
|
|
|
+ throw new Exception(
|
|
|
+ '三方余额已回收但钱包补入账失败,流水号:' . $pendingCredit->operation_id,
|
|
|
+ HttpStatus::CUSTOM_ERROR
|
|
|
+ );
|
|
|
+ }
|
|
|
+ $credited['recovered_pending_operation'] = true;
|
|
|
+ return $this->success($credited);
|
|
|
+ }
|
|
|
+
|
|
|
+ $unresolved = ThirdGameRecycle::where('member_id', $memberId)
|
|
|
+ ->whereIn('status', [
|
|
|
+ ThirdGameRecycle::STATUS_PENDING,
|
|
|
+ ThirdGameRecycle::STATUS_UNCERTAIN,
|
|
|
+ ])
|
|
|
+ ->orderByDesc('id')
|
|
|
+ ->first();
|
|
|
+ if ($unresolved) {
|
|
|
+ throw new Exception(
|
|
|
+ '存在待核对的三方回收流水:' . $unresolved->operation_id . ',请先人工核对三方账单',
|
|
|
+ HttpStatus::CUSTOM_ERROR
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ $operation = ThirdGameRecycle::create([
|
|
|
+ 'operation_id' => (string) Str::uuid(),
|
|
|
+ 'member_id' => $memberId,
|
|
|
+ 'player_id' => $service->playerId($memberId),
|
|
|
+ 'status' => ThirdGameRecycle::STATUS_PENDING,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $recycled = $service->recycle($memberId);
|
|
|
+ if (!$recycled['ok']) {
|
|
|
+ $operation->status = !empty($recycled['uncertain'])
|
|
|
+ ? ThirdGameRecycle::STATUS_UNCERTAIN
|
|
|
+ : ThirdGameRecycle::STATUS_FAILED;
|
|
|
+ $operation->error = $recycled['msg'];
|
|
|
+ $operation->save();
|
|
|
+ throw new Exception(
|
|
|
+ $recycled['msg'] . ',流水号:' . $operation->operation_id,
|
|
|
+ HttpStatus::CUSTOM_ERROR
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ $operation->status = ThirdGameRecycle::STATUS_PROVIDER_SUCCEEDED;
|
|
|
+ $operation->game_balance = $recycled['game_balance'];
|
|
|
+ $operation->wallet_balance = $recycled['wallet_balance'];
|
|
|
+ $operation->save();
|
|
|
+
|
|
|
+ try {
|
|
|
+ $credited = $this->creditThirdGameRecycle($operation->id);
|
|
|
+ } catch (Exception $e) {
|
|
|
+ throw new Exception(
|
|
|
+ '三方余额已回收但钱包入账失败,请重试回收以补入账,流水号:' . $operation->operation_id,
|
|
|
+ HttpStatus::CUSTOM_ERROR
|
|
|
+ );
|
|
|
+ }
|
|
|
+ return $this->success($credited);
|
|
|
+ } catch (ValidationException $e) {
|
|
|
+ return $this->error(HttpStatus::CUSTOM_ERROR, $e->validator->errors()->first());
|
|
|
+ } catch (Exception $e) {
|
|
|
+ return $this->error(HttpStatus::CUSTOM_ERROR, $e->getMessage());
|
|
|
+ } finally {
|
|
|
+ if ($lockAcquired && $lock) {
|
|
|
+ $lock->release();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将三方已回收的流水幂等入账;事务失败时保留 provider_succeeded 供下次补偿。
|
|
|
+ */
|
|
|
+ private function creditThirdGameRecycle(int $operationId): array
|
|
|
+ {
|
|
|
+ return DB::transaction(function () use ($operationId) {
|
|
|
+ $operation = ThirdGameRecycle::where('id', $operationId)->lockForUpdate()->firstOrFail();
|
|
|
+ if ($operation->status === ThirdGameRecycle::STATUS_COMPLETED) {
|
|
|
+ $walletBalance = (float) Wallet::where('member_id', $operation->member_id)
|
|
|
+ ->value('available_balance');
|
|
|
+ return $this->recycleResponse($operation, $walletBalance);
|
|
|
+ }
|
|
|
+ if ($operation->status !== ThirdGameRecycle::STATUS_PROVIDER_SUCCEEDED) {
|
|
|
+ throw new Exception('三方回收流水状态不可入账', HttpStatus::CUSTOM_ERROR);
|
|
|
+ }
|
|
|
+
|
|
|
+ $wallet = Wallet::where('member_id', $operation->member_id)->lockForUpdate()->firstOrFail();
|
|
|
+ $before = (string) $wallet->available_balance;
|
|
|
+ $walletGain = (string) $operation->wallet_balance;
|
|
|
+ $after = bcadd($before, $walletGain, 10);
|
|
|
+ if (bccomp($walletGain, '0', 10) > 0) {
|
|
|
+ BalanceLogService::addLog(
|
|
|
+ $operation->member_id,
|
|
|
+ $walletGain,
|
|
|
+ $before,
|
|
|
+ $after,
|
|
|
+ '三方游戏转出',
|
|
|
+ $operation->id,
|
|
|
+ '后台一键回收游戏余额'
|
|
|
+ );
|
|
|
+ $wallet->available_balance = $after;
|
|
|
+ $wallet->save();
|
|
|
+ }
|
|
|
+
|
|
|
+ $operation->status = ThirdGameRecycle::STATUS_COMPLETED;
|
|
|
+ $operation->credited_at = now();
|
|
|
+ $operation->save();
|
|
|
+
|
|
|
+ return $this->recycleResponse($operation, (float) $after);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ private function recycleResponse(ThirdGameRecycle $operation, float $walletBalance): array
|
|
|
+ {
|
|
|
+ return [
|
|
|
+ 'operation_id' => $operation->operation_id,
|
|
|
+ 'member_id' => $operation->member_id,
|
|
|
+ 'recovered_game_balance' => (float) $operation->game_balance,
|
|
|
+ 'recovered_balance' => (float) $operation->wallet_balance,
|
|
|
+ 'wallet_balance' => $walletBalance,
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 查询三方游戏回收流水,用于核对 pending/uncertain 状态。
|
|
|
+ */
|
|
|
+ public function thirdGameRecycleRecords(): JsonResponse
|
|
|
+ {
|
|
|
+ try {
|
|
|
+ $params = request()->validate([
|
|
|
+ 'page' => ['nullable', 'integer', 'min:1'],
|
|
|
+ 'limit' => ['nullable', 'integer', 'min:1', 'max:100'],
|
|
|
+ 'member_id' => ['nullable', 'string', 'min:1'],
|
|
|
+ 'status' => ['nullable', 'string', 'in:pending,provider_succeeded,completed,failed,uncertain'],
|
|
|
+ ]);
|
|
|
+ $page = (int) ($params['page'] ?? 1);
|
|
|
+ $limit = (int) ($params['limit'] ?? 15);
|
|
|
+ $query = ThirdGameRecycle::query()
|
|
|
+ ->when(!empty($params['member_id']), function ($query) use ($params) {
|
|
|
+ $query->where('member_id', $params['member_id']);
|
|
|
+ })
|
|
|
+ ->when(!empty($params['status']), function ($query) use ($params) {
|
|
|
+ $query->where('status', $params['status']);
|
|
|
+ });
|
|
|
+
|
|
|
+ $total = (clone $query)->count();
|
|
|
+ $list = $query->orderByDesc('id')->forPage($page, $limit)->get();
|
|
|
+ return $this->success(['total' => $total, 'data' => $list]);
|
|
|
+ } catch (ValidationException $e) {
|
|
|
+ return $this->error(HttpStatus::CUSTOM_ERROR, $e->validator->errors()->first());
|
|
|
+ } catch (Exception $e) {
|
|
|
+ return $this->error(HttpStatus::CUSTOM_ERROR, $e->getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 人工核对待确认流水:确认未回收,或按三方实际金额补入账。
|
|
|
+ */
|
|
|
+ public function resolveThirdGameRecycle(ThirdGameBalanceService $service): JsonResponse
|
|
|
+ {
|
|
|
+ $lock = null;
|
|
|
+ $lockAcquired = false;
|
|
|
+ try {
|
|
|
+ $params = request()->validate([
|
|
|
+ 'operation_id' => ['required', 'uuid'],
|
|
|
+ 'action' => ['required', 'string', 'in:failed,credit'],
|
|
|
+ 'game_balance' => ['nullable', 'required_if:action,credit', 'numeric', 'min:0'],
|
|
|
+ 'remark' => ['nullable', 'string', 'max:500'],
|
|
|
+ ]);
|
|
|
+ $operation = ThirdGameRecycle::where('operation_id', $params['operation_id'])->first();
|
|
|
+ if (!$operation) {
|
|
|
+ throw new Exception('三方回收流水不存在', HttpStatus::CUSTOM_ERROR);
|
|
|
+ }
|
|
|
+
|
|
|
+ $lock = Cache::lock('admin_third_game_recycle:' . $operation->member_id, 120);
|
|
|
+ $lockAcquired = $lock->get();
|
|
|
+ if (!$lockAcquired) {
|
|
|
+ throw new Exception('该用户正在处理三方回收,请稍后再试', HttpStatus::CUSTOM_ERROR);
|
|
|
+ }
|
|
|
+
|
|
|
+ $operation = DB::transaction(function () use ($operation, $params, $service) {
|
|
|
+ $operation = ThirdGameRecycle::where('id', $operation->id)->lockForUpdate()->firstOrFail();
|
|
|
+ if (!in_array($operation->status, [
|
|
|
+ ThirdGameRecycle::STATUS_PENDING,
|
|
|
+ ThirdGameRecycle::STATUS_UNCERTAIN,
|
|
|
+ ], true)) {
|
|
|
+ throw new Exception('当前流水状态无需人工处理', HttpStatus::CUSTOM_ERROR);
|
|
|
+ }
|
|
|
+
|
|
|
+ $remark = trim((string) ($params['remark'] ?? ''));
|
|
|
+ if ($params['action'] === 'failed') {
|
|
|
+ $operation->status = ThirdGameRecycle::STATUS_FAILED;
|
|
|
+ $operation->resolution_remark = '后台人工核对:确认三方未回收'
|
|
|
+ . ($remark !== '' ? ';' . $remark : '');
|
|
|
+ } else {
|
|
|
+ $gameBalance = (float) $params['game_balance'];
|
|
|
+ $operation->status = ThirdGameRecycle::STATUS_PROVIDER_SUCCEEDED;
|
|
|
+ $operation->game_balance = $gameBalance;
|
|
|
+ $operation->wallet_balance = $service->toWalletBalance($gameBalance);
|
|
|
+ $operation->resolution_remark = '后台人工核对:确认三方已回收'
|
|
|
+ . ($remark !== '' ? ';' . $remark : '');
|
|
|
+ }
|
|
|
+ $operation->save();
|
|
|
+ return $operation;
|
|
|
+ });
|
|
|
+
|
|
|
+ if ($params['action'] === 'failed') {
|
|
|
+ return $this->success([
|
|
|
+ 'operation_id' => $operation->operation_id,
|
|
|
+ 'member_id' => $operation->member_id,
|
|
|
+ 'status' => $operation->status,
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ $credited = $this->creditThirdGameRecycle($operation->id);
|
|
|
+ } catch (Exception $e) {
|
|
|
+ throw new Exception(
|
|
|
+ '核对金额已保存但钱包入账失败,请再次提交回收操作补入账,流水号:' . $operation->operation_id,
|
|
|
+ HttpStatus::CUSTOM_ERROR
|
|
|
+ );
|
|
|
+ }
|
|
|
+ $credited['resolved_manually'] = true;
|
|
|
+ return $this->success($credited);
|
|
|
+ } catch (ValidationException $e) {
|
|
|
+ return $this->error(HttpStatus::CUSTOM_ERROR, $e->validator->errors()->first());
|
|
|
+ } catch (Exception $e) {
|
|
|
+ return $this->error(HttpStatus::CUSTOM_ERROR, $e->getMessage());
|
|
|
+ } finally {
|
|
|
+ if ($lockAcquired && $lock) {
|
|
|
+ $lock->release();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
public function merge(): JsonResponse
|
|
|
{
|
|
|
DB::beginTransaction();
|