app()->call([app($controller), $method])); } private function run(callable $work): JsonResponse { try { return $this->success($work()); } catch (ValidationException $e) { return $this->error(HttpStatus::CUSTOM_ERROR, $e->validator->errors()->first()); } catch (\Throwable $e) { return $this->error(HttpStatus::CUSTOM_ERROR, $e->getMessage()); } } private function audit(string $type, $id, string $action, array $after = []): void { OperationAuditService::record('simulation_' . $type, $id === null ? null : (int) $id, $action, [], $after); } private function page($query, string $order = 'id'): array { $page = max(1, (int) request()->input('page', 1)); $limit = min(200, max(1, (int) request()->input('limit', 15))); return ['total' => (clone $query)->count(), 'data' => $query->orderByDesc($order)->forPage($page, $limit)->get()]; } // ---------- 概览 ---------- public function overview() { return $this->run(function () { $start = request()->input('start_date') ?: date('Y-m-d'); $end = date('Y-m-d', strtotime((request()->input('end_date') ?: date('Y-m-d')) . ' +1 day')); $members = MemberAudience::simulatedMembers('member_id'); $logs = fn (array $types) => (string) DB::table('balance_logs')->whereIn('member_id', $members)->whereIn('change_type', $types) ->where('created_at', '>=', $start)->where('created_at', '<', $end)->sum('amount'); $like = fn (string $suffix) => (string) DB::table('balance_logs')->whereIn('member_id', $members)->where('change_type', 'like', '%' . $suffix) ->where('created_at', '>=', $start)->where('created_at', '<', $end)->sum('amount'); $bet = bcmul($like('投注'), '-1', 2); $payout = bcadd(bcadd($like('中奖'), $like('退款'), 2), $like('退分'), 2); $wallet = DB::table('wallets')->whereIn('member_id', $members)->selectRaw('COALESCE(SUM(available_balance),0) a, COALESCE(SUM(frozen_balance),0) f')->first(); $runs = DB::table('simulation_task_runs')->whereIn('status', ['queued', 'running', 'paused'])->orderByDesc('id')->limit(10)->get(); return [ 'enabled' => SimulationSettings::enabled(), 'migrated' => MemberAudience::enabled(), 'start_date' => $start, 'members' => ['total' => DB::table('users')->where('user_type', MemberAudience::TYPE_SIMULATED)->count(), 'new' => DB::table('users')->where('user_type', MemberAudience::TYPE_SIMULATED)->where('created_at', '>=', $start)->where('created_at', '<', $end)->count(), 'online' => SimulationMemberService::onlineUserIds()->count()], 'funds' => ['recharge' => bcadd($logs(['充值', '三方充值']), '0', 2), 'manual_credit' => bcadd($logs(['人工充值']), '0', 2), 'manual_debit' => bcmul($logs(['人工扣款']), '-1', 2), // 提现被拒绝时退款记为正数的“三方提现”,一并计入才是净提现 'withdraw' => bcmul($logs(['提现', '人工提现', '三方提现']), '-1', 2), 'wallet_available' => bcadd((string) $wallet->a, '0', 2), 'wallet_frozen' => bcadd((string) $wallet->f, '0', 2)], 'bets' => ['bet_amount' => $bet, 'payout' => $payout, 'win_loss' => bcsub($bet, $payout, 2), 'unsettled' => DB::table('lhc_order')->whereIn('member_id', $members)->where('lottery_status', 0)->count() + SimulationBetService::sportQuery()->where('sport_order.settlement_status', 0)->where('sport_order.status', 1)->count()], 'runs' => $runs->map(fn ($run) => SimulationTaskService::present($run))->all(), ]; }); } // ---------- 会员 ---------- public function members() { $response = $this->mirror(User::class, 'index'); $body = $response->getData(true); if (($body['code'] ?? -1) !== 0 || empty($body['data']['data'])) return $response; $online = array_flip(SimulationMemberService::onlineUserIds()->map(fn ($id) => (string) $id)->all()); foreach ($body['data']['data'] as &$row) $row['sim_online'] = isset($online[(string) ($row['user_id'] ?? '')]); return response()->json($body); } public function createMembers() { return $this->run(function () { $params = request()->validate(['count' => ['required', 'integer', 'min:1'], 'domain' => ['nullable', 'string', 'max:100'], 'initial_balance' => ['nullable', 'numeric', 'min:0']]); $created = SimulationMemberService::create((int) $params['count'], $params); $this->audit('member', null, 'create', ['count' => count($created), 'member_ids' => array_column($created, 'member_id')]); return ['created' => $created]; }); } public function memberDetail() { return $this->run(fn () => SimulationMemberService::detail(request()->validate(['member_id' => ['required', 'string']])['member_id'])); } public function memberBanned() { return $this->run(function () { $params = request()->validate(['member_id' => ['required', 'string'], 'is_banned' => ['required', 'integer', 'in:0,1']]); SimulationMemberService::setBanned($params['member_id'], (int) $params['is_banned']); $this->audit('member', null, 'banned', $params); return []; }); } public function memberLogin() { return $this->run(function () { $params = request()->validate(['member_id' => ['required', 'string'], 'platform' => ['nullable', 'string', 'max:16'], 'domain' => ['nullable', 'string', 'max:100']]); return SimulationMemberService::login($params['member_id'], $params['platform'] ?? 'H5', $params['domain'] ?? null); }); } public function memberLogout() { return $this->run(fn () => ['closed' => SimulationMemberService::logout(request()->validate(['member_id' => ['required', 'string']])['member_id'])]); } public function loginLogs() { return $this->mirror(User::class, 'loginLog'); } // ---------- 资金 ---------- private function orders(array $types) { $query = DB::table('payment_orders')->join('users', 'users.member_id', '=', 'payment_orders.member_id') ->where('users.user_type', MemberAudience::TYPE_SIMULATED)->whereIn('payment_orders.type', $types) ->select('payment_orders.id', 'payment_orders.order_no', 'payment_orders.member_id', 'users.account', 'users.first_name', 'payment_orders.type', 'payment_orders.amount', 'payment_orders.frozen_amount', 'payment_orders.channel', 'payment_orders.status', 'payment_orders.remark', 'payment_orders.created_at'); if (request()->filled('member_id')) $query->where('payment_orders.member_id', (string) request()->input('member_id')); if (request()->filled('account')) $query->where('users.account', 'like', '%' . trim((string) request()->input('account')) . '%'); if (request()->filled('status')) $query->where('payment_orders.status', (int) request()->input('status')); if (request()->filled('start_time')) $query->where('payment_orders.created_at', '>=', request()->input('start_time') . ' 00:00:00'); if (request()->filled('end_time')) $query->where('payment_orders.created_at', '<=', request()->input('end_time') . ' 23:59:59'); return $query; } public function recharges() { return $this->run(fn () => $this->page($this->orders([PaymentOrderService::TYPE_PAY, PaymentOrderService::TYPE_SELF_PAY]), 'payment_orders.id')); } public function recharge() { return $this->run(function () { $params = request()->validate(['member_id' => ['required', 'string'], 'amount' => ['required', 'numeric', 'min:0.01'], 'channel' => ['nullable', 'in:company,third'], 'result' => ['nullable', 'in:success,pending,fail']]); $order = SimulationFundService::recharge($params['member_id'], $params['amount'], $params['channel'] ?? 'third', $params['result'] ?? 'success'); $this->audit('fund', $order->id, 'recharge', $params); return $order; }); } public function withdrawals() { return $this->run(fn () => $this->page($this->orders([PaymentOrderService::TYPE_SELF_PAYOUT]), 'payment_orders.id')); } public function withdraw() { return $this->run(function () { $params = request()->validate(['member_id' => ['required', 'string'], 'amount' => ['required', 'numeric', 'min:0.01']]); $order = SimulationFundService::withdrawApply($params['member_id'], $params['amount']); $this->audit('fund', $order->id, 'withdraw_apply', $params); return $order; }); } public function withdrawAudit() { return $this->run(function () { $params = request()->validate(['id' => ['required', 'integer', 'min:1'], 'approve' => ['required', 'integer', 'in:0,1'], 'remark' => ['nullable', 'string', 'max:120']]); $order = SimulationFundService::withdrawAudit((int) $params['id'], (int) $params['approve'] === 1, (string) ($params['remark'] ?? '')); $this->audit('fund', $order->id, (int) $params['approve'] === 1 ? 'withdraw_approve' : 'withdraw_reject', $params); return $order; }); } public function adjust() { return $this->run(function () { $params = request()->validate(['member_id' => ['required', 'string'], 'amount' => ['required', 'numeric', 'min:0.01'], 'direction' => ['required', 'in:credit,debit'], 'remark' => ['required', 'string', 'max:120']]); $log = SimulationFundService::adjust($params['member_id'], $params['amount'], $params['direction'], $params['remark']); $this->audit('fund', $log->id, 'adjust_' . $params['direction'], $params); return $log; }); } public function ledger() { return $this->mirror(Balance::class, 'log'); } public function freezes() { return $this->mirror(WalletFreeze::class, 'index'); } public function freeze() { return $this->applyFreeze(false); } public function unfreeze() { return $this->applyFreeze(true); } private function applyFreeze(bool $unfreeze) { return $this->run(function () use ($unfreeze) { $params = request()->validate(['member_id' => ['required', 'string'], 'amount' => ['required', 'numeric', 'min:0.01'], 'reason' => ['required', 'string', 'max:120']]); $record = SimulationFundService::freeze($params['member_id'], $params['amount'], $params['reason'], (int) (request()->user->id ?? 0), $unfreeze); $this->audit('fund', $record['id'] ?? null, $unfreeze ? 'unfreeze' : 'freeze', $params); return $record; }); } // ---------- 注单 ---------- public function betOptions() { return $this->run(fn () => SimulationBetService::options()); } public function lotteryBets() { return $this->mirror(LhcOrder::class, 'list'); } public function lotteryBet() { return $this->run(function () { $params = request()->validate(['member_id' => ['required', 'string'], 'keywords' => ['required', 'string', 'max:32'], 'amount' => ['required', 'numeric', 'min:0.01']]); $order = SimulationBetService::lottery($params['member_id'], $params['keywords'], $params['amount']); $this->audit('bet', $order->id, 'lottery_bet', $params); return $order; }); } public function sportBets() { return $this->run(function () { $query = SimulationBetService::sportQuery()->select('sport_order.id', 'sport_order.order_id', 'sport_order.issue', 'users.member_id', 'users.account', 'users.first_name', 'sport_order.odd_name', 'sport_order.odd_value', 'sport_order.odd', 'sport_order.amount', 'sport_order.win_amount', 'sport_order.return_amount', 'sport_order.profit_and_loss', 'sport_order.is_win', 'sport_order.status', 'sport_order.settlement_status', 'sport_order.create_time', 'sport_order.settlement_time'); if (request()->filled('member_id')) $query->where('users.member_id', (string) request()->input('member_id')); if (request()->filled('account')) $query->where('users.account', 'like', '%' . trim((string) request()->input('account')) . '%'); if (request()->filled('settlement_status')) $query->where('sport_order.settlement_status', (int) request()->input('settlement_status')); if (request()->filled('start_time')) $query->where('sport_order.create_time', '>=', strtotime(request()->input('start_time') . ' 00:00:00')); if (request()->filled('end_time')) $query->where('sport_order.create_time', '<=', strtotime(request()->input('end_time') . ' 23:59:59')); return $this->page($query, 'sport_order.id'); }); } public function sportBet() { return $this->run(function () { $params = request()->validate(['member_id' => ['required', 'string'], 'amount' => ['required', 'numeric', 'min:0.01'], 'odd' => ['required', 'numeric'], 'title' => ['nullable', 'string', 'max:100'], 'selection' => ['nullable', 'string', 'max:100']]); $order = SimulationBetService::sport($params['member_id'], $params['amount'], $params['odd'], (string) ($params['title'] ?? ''), (string) ($params['selection'] ?? '')); $this->audit('bet', $order->id, 'sport_bet', $params); return $order; }); } public function sportSettle() { return $this->run(function () { $params = request()->validate(['id' => ['required', 'integer', 'min:1'], 'result' => ['required', 'in:win,lose,draw,half']]); $order = SimulationBetService::settleSport((int) $params['id'], $params['result']); $this->audit('bet', $order->id, 'sport_settle', $params); return $order; }); } public function sportCancel() { return $this->run(function () { $params = request()->validate(['id' => ['required', 'integer', 'min:1'], 'remark' => ['nullable', 'string', 'max:120']]); $order = SimulationBetService::cancelSport((int) $params['id'], (string) ($params['remark'] ?? '')); $this->audit('bet', $order->id, 'sport_cancel', $params); return $order; }); } public function thirdBets() { return $this->mirror(ThirdGameOrder::class, 'index'); } // ---------- 自动任务 ---------- public function templates() { return $this->run(function () { $page = $this->page(DB::table('simulation_tasks')); $page['data'] = $page['data']->map(fn ($row) => ['steps' => json_decode($row->steps, true)] + (array) $row)->all(); return $page; }); } public function saveTemplate() { return $this->run(function () { $id = SimulationTaskService::saveTemplate(request()->all(), request()->user->id ?? null); $this->audit('task', $id, 'template_save', ['name' => request()->input('name')]); return ['id' => $id]; }); } public function deleteTemplate() { return $this->run(function () { $id = (int) request()->validate(['id' => ['required', 'integer', 'min:1']])['id']; DB::table('simulation_tasks')->where('id', $id)->delete(); $this->audit('task', $id, 'template_delete'); return []; }); } public function runs() { return $this->run(function () { $query = DB::table('simulation_task_runs'); if (request()->filled('status')) $query->where('status', (string) request()->input('status')); if (request()->filled('name')) $query->where('name', 'like', '%' . trim((string) request()->input('name')) . '%'); $page = $this->page($query); $page['data'] = $page['data']->map(fn ($run) => SimulationTaskService::present($run))->all(); return $page; }); } public function startRun() { return $this->run(function () { $id = SimulationTaskService::start(request()->all(), request()->user->id ?? null); $this->audit('task', $id, 'run_start', ['task_id' => request()->input('task_id'), 'name' => request()->input('name')]); return ['id' => $id]; }); } public function runStatus() { return $this->run(function () { $params = request()->validate(['id' => ['required', 'integer', 'min:1'], 'action' => ['required', 'in:pause,resume,stop']]); $run = SimulationTaskService::setStatus((int) $params['id'], $params['action']); $this->audit('task', $run->id, 'run_' . $params['action']); return SimulationTaskService::present($run); }); } public function events() { return $this->run(function () { $query = DB::table('simulation_task_events'); foreach (['run_id', 'status', 'action', 'member_id'] as $field) if (request()->filled($field)) $query->where($field, request()->input($field)); return $this->page($query); }); } // ---------- 报表:与正式报表同一套代码,只统计模拟会员 ---------- public function reportUsers() { return $this->mirror(MemberDailyReport::class, 'index'); } public function reportFunds() { return $this->mirror(Funds::class, 'summary'); } public function reportHome() { return $this->mirror(Home::class, 'index'); } public function reportGames() { return $this->mirror(Reporting::class, 'summary'); } public function reportFundFlow() { return $this->mirror(Reporting::class, 'funds'); } public function reportRegistrations() { return $this->mirror(User::class, 'registerTrend'); } public function reportDomains() { return $this->mirror(User::class, 'registerDomain'); } /** 立即刷新报表依赖的中间数据(平时由定时任务每 1~10 分钟刷新一次)。 */ public function reportRefresh() { return $this->run(function () { $date = request()->validate(['date' => ['nullable', 'date_format:Y-m-d']])['date'] ?? date('Y-m-d'); $facts = app(BetFactSynchronizer::class)->sync($date, $date, true); $reporting = app(ReportSync::class)->sync(); $daily = app(MemberDailyReportService::class)->refresh($date); return ['date' => $date, 'facts' => $facts, 'reporting' => $reporting, 'member_daily' => $daily]; }); } // ---------- 操作日志 ---------- public function audits() { return $this->run(function () { $query = DB::table('operation_audits')->where('resource_type', 'like', 'simulation\_%'); if (request()->filled('resource_type')) $query->where('resource_type', 'simulation_' . request()->input('resource_type')); if (request()->filled('operator')) $query->where('operator_name', 'like', '%' . trim((string) request()->input('operator')) . '%'); if (request()->filled('start_time')) $query->where('created_at', '>=', request()->input('start_time') . ' 00:00:00'); if (request()->filled('end_time')) $query->where('created_at', '<=', request()->input('end_time') . ' 23:59:59'); $page = $this->page($query); $page['data'] = $page['data']->map(fn ($row) => ['changes' => json_decode((string) $row->changes, true)] + (array) $row)->all(); return $page; }); } // ---------- 设置与重置 ---------- public function settings() { return $this->run(fn () => SimulationSettings::all()); } public function saveSettings() { return $this->run(function () { $change = SimulationSettings::save(request()->all()); OperationAuditService::record('simulation_settings', null, 'save', $change['before'], $change['after']); return $change['after']; }); } public function resetPreview() { return $this->run(fn () => SimulationResetService::preview()); } public function reset() { return $this->run(function () { $params = request()->validate(['confirm_token' => ['required', 'string'], 'confirm_text' => ['required', 'string']]); $deleted = SimulationResetService::reset($params['confirm_token'], $params['confirm_text']); $this->audit('reset', null, 'reset', $deleted); return ['deleted' => $deleted]; }); } }