| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427 |
- <?php
- namespace App\Http\Controllers\admin;
- use App\Constants\HttpStatus;
- use App\Http\Controllers\Controller;
- use App\Services\MemberDailyReportService;
- use App\Services\OperationAuditService;
- use App\Services\PaymentOrderService;
- use App\Services\Reporting\ReportSync;
- use App\Services\Sharing\BetFactSynchronizer;
- use App\Services\Simulation\SimulationBetService;
- use App\Services\Simulation\SimulationFundService;
- use App\Services\Simulation\SimulationMemberService;
- use App\Services\Simulation\SimulationResetService;
- use App\Services\Simulation\SimulationSettings;
- use App\Services\Simulation\SimulationTaskService;
- use App\Support\MemberAudience;
- use Illuminate\Http\JsonResponse;
- use Illuminate\Support\Facades\DB;
- use Illuminate\Validation\ValidationException;
- /**
- * 模拟管理。只有这里能看到后台模拟会员(users.user_type = 2)的数据:
- * 列表和报表直接复用正式接口的代码,只是放在 MemberAudience::simulated() 里执行,所以行结构和统计口径与正式页面完全一致。
- */
- class Simulation extends Controller
- {
- /** 在“仅模拟会员”口径下调用正式控制器的方法。 */
- private function mirror(string $controller, string $method): JsonResponse
- {
- return MemberAudience::simulated(fn () => 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];
- });
- }
- }
|