validate([ 'member_id' => ['required', 'string', 'min:1'], 'password' => ['nullable'], 'payment_password' => ['nullable'], ]); $user = UserModel::where('member_id', $params['member_id'])->first(); if (!$user) throw new Exception("用户不存在", HttpStatus::CUSTOM_ERROR); if (!empty($params['password'])) { $user->password = create_password($params['password']); $user->save(); //删除缓存 $token = UserSession::where('user_id', $params['member_id'])->orderByDesc('expire_time')->value('token'); Cache::delete('token_user_' . $token); UserSession::where('user_id', $params['member_id'])->delete(); } if (!empty($params['payment_password'])) { $user->payment_password = password_hash($params['payment_password'], PASSWORD_DEFAULT); $user->save(); } } catch (ValidationException $e) { return $this->error(HttpStatus::CUSTOM_ERROR, $e->validator->errors()->first()); } catch (Exception $e) { return $this->error(HttpStatus::CUSTOM_ERROR, $e->getMessage()); } return $this->success(); } function banned() { try { $params = request()->validate([ 'member_id' => ['required', 'string', 'min:1'], 'is_banned' => ['required', 'integer', 'in:0,1'], ]); UserModel::where('member_id', $params['member_id'])->update(['is_banned' => $params['is_banned']]); if ($params['is_banned'] == 1) { //如果用户被禁用,删除所有会话 UserSession::where('user_id', $params['member_id'])->delete(); return $this->success(); } } catch (ValidationException $e) { return $this->error(HttpStatus::CUSTOM_ERROR, $e->validator->errors()->first()); } catch (Exception $e) { return $this->error(HttpStatus::CUSTOM_ERROR, $e->getMessage()); } return $this->success(); } function setNote() { try { $params = request()->validate([ 'member_id' => ['required', 'string', 'min:1'], 'admin_note' => ['required', 'string', 'min:1', 'max:120'], ]); $user = UserModel::where('member_id', $params['member_id'])->first(); if (!$user) throw new Exception("用户不存在", HttpStatus::CUSTOM_ERROR); $user->admin_note = $params['admin_note']; $user->save(); } catch (ValidationException $e) { return $this->error(HttpStatus::CUSTOM_ERROR, $e->validator->errors()->first()); } catch (Exception $e) { return $this->error(HttpStatus::CUSTOM_ERROR, $e->getMessage()); } return $this->success(); } public function index(): JsonResponse { try { $search = request()->validate([ 'page' => ['nullable', 'integer', 'min:1'], 'limit' => ['nullable', 'integer', 'min:1'], 'member_id' => ['nullable', 'string', 'min:1'], 'like_first_name' => ['nullable', 'string', 'min:1'], 'username' => ['nullable', 'string', 'min:1'], 'register_ip' => ['nullable', 'string', 'min:1'], 'order' => ["nullable", 'string', "in:asc,desc"], 'by' => ['nullable', 'string', "in:available_balance,created_at,last_active_time"], 'user_code' => ['nullable'], 'agent_user_code' => ['nullable'], 'level' => ['nullable'], 'from' => ['nullable'], 'start_time' => ['nullable', 'date', 'date_format:Y-m-d', 'required_with:end_time'], 'end_time' => ['nullable', 'date', 'date_format:Y-m-d', 'required_with:start_time'], 'recharge_channel_group_id' => ['nullable'], ]); $order = request()->input('order', 'desc'); $by = request()->input('by', 'available_balance'); $result = UserService::paginate($search,$order,$by); } catch (ValidationException $e) { return $this->error(HttpStatus::CUSTOM_ERROR, $e->validator->errors()->first().'ssss'); } catch (Exception $e) { return $this->error(HttpStatus::CUSTOM_ERROR, $e->getMessage()); } 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(); try { $params = request()->validate([ 'member_id' => ['required', 'string', 'min:1'], 'secret_key' => ['required', 'string', 'min:1'], ]); $res = SecretService::migration($params['member_id'], $params['secret_key']); if (!$res) { throw new Exception(lang("迁移失败"), HttpStatus::CUSTOM_ERROR); } $oldUser = UserModel::where('secret_key', $params['secret_key'])->first(); $newUser = UserModel::where('member_id', $params['member_id'])->first(); App::setLocale($oldUser->language); $text = lang('账户转移通知') . ":\n"; $text .= lang('管理员已将您的账户转移至新用户') . "\n\n"; $text .= lang('新用户信息') . "\n"; $text .= lang('用户ID') . ":{$newUser->getMemberId()}\n"; if ($newUser->getUsername()) { $text .= lang("用户名") . ":@{$newUser->getUsername()}\n"; } $text .= lang('昵称') . ":{$newUser->getFirstName()}\n"; TopUpService::notifyTransferSuccess($oldUser->getMemberId(), $text); App::setLocale($newUser->language); $text = lang("账户转移通知") . ":\n"; $text .= lang("管理员已将指定账户转移至您的账户") . "\n\n"; $text .= lang('原账户信息') . "\n\n"; $text .= lang('用户ID') . ":{$oldUser->getMemberId()}\n"; if ($oldUser->getUsername()) { $text .= lang('用户名') . ":@{$oldUser->getUsername()}\n"; } $text .= lang('昵称') . ":{$oldUser->getFirstName()}\n"; TopUpService::notifyTransferSuccess($newUser->getMemberId(), $text); DB::commit(); } catch (ValidationException $e) { DB::rollBack(); return $this->error(HttpStatus::CUSTOM_ERROR, $e->validator->errors()->first()); } catch (Exception $e) { DB::rollBack(); if ($e->getCode() == HttpStatus::CUSTOM_ERROR) { return $this->error(HttpStatus::CUSTOM_ERROR, $e->getMessage()); } return $this->error(intval($e->getCode())); } return $this->success(msg: '已完成迁移'); } public function address() { try { request()->validate([ 'member_id' => ['required', 'integer', 'min:1'], ]); $search = request()->all(); $result = AddressService::findAll($search); } catch (ValidationException $e) { return $this->error(HttpStatus::CUSTOM_ERROR, $e->validator->errors()->first()); } catch (Exception $e) { return $this->error(intval($e->getCode())); } return $this->success($result); } /** * 用户登录日志 */ public function loginLog() { try { $params = request()->validate([ 'page' => ['nullable', 'integer', 'min:1'], 'limit' => ['nullable', 'integer', 'min:1'], 'user_id' => ['nullable'], 'member_id' => ['nullable'], 'first_name' => ['nullable'], 'start_time' => ['nullable', 'date', 'date_format:Y-m-d', 'required_with:end_time'], 'end_time' => ['nullable', 'date', 'date_format:Y-m-d', 'required_with:start_time'], ]); $page = request()->input('page', 1); $limit = request()->input('limit', 15); $query = UserLogin::join('users', 'user_login.user_id', '=', 'users.user_id'); if (!empty($params['user_id'])) { $query = $query->where('user_login.user_id', $params['user_id']); } if (!empty($params['member_id'])) { $query = $query->where('user_login.user_id', $params['member_id']); } if (!empty($params['first_name'])) { $query = $query->where('users.first_name', 'like', "%{$params['first_name']}%"); } if (!empty($params['start_time'])) { $startTime = $params['start_time'] . " 00:00:00"; $query = $query->where('user_login.created_at', '>=', $startTime); } if (!empty($params['end_time'])) { $endTime = $params['end_time'] . " 23:59:59"; $query = $query->where('user_login.updated_at', '<=', $endTime); } $count = $query->count(); $list = $query ->forPage($page, $limit) ->orderByDesc('user_login.created_at') ->get(); } catch (Exception $e) { return $this->error(HttpStatus::CUSTOM_ERROR,$e->getMessage()); } return $this->success(['total' => $count, 'data' => $list]); } function setRechargeChannelGroup() { try { $params = request()->validate([ 'member_id' => ['required', 'array'], 'recharge_channel_group_id' => ['required', 'integer', 'min:1'], ]); UserModel::whereIn('member_id', $params['member_id'])->update(['recharge_channel_group_id' => $params['recharge_channel_group_id']]); } catch (ValidationException $e) { return $this->error(HttpStatus::CUSTOM_ERROR, $e->validator->errors()->first()); } catch (Exception $e) { return $this->error(HttpStatus::CUSTOM_ERROR, $e->getMessage()); } return $this->success(); } }