Simulation.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. <?php
  2. namespace App\Http\Controllers\admin;
  3. use App\Constants\HttpStatus;
  4. use App\Http\Controllers\Controller;
  5. use App\Services\MemberDailyReportService;
  6. use App\Services\OperationAuditService;
  7. use App\Services\PaymentOrderService;
  8. use App\Services\Reporting\ReportSync;
  9. use App\Services\Sharing\BetFactSynchronizer;
  10. use App\Services\Simulation\SimulationBetService;
  11. use App\Services\Simulation\SimulationFundService;
  12. use App\Services\Simulation\SimulationMemberService;
  13. use App\Services\Simulation\SimulationResetService;
  14. use App\Services\Simulation\SimulationSettings;
  15. use App\Services\Simulation\SimulationTaskService;
  16. use App\Support\MemberAudience;
  17. use Illuminate\Http\JsonResponse;
  18. use Illuminate\Support\Facades\DB;
  19. use Illuminate\Validation\ValidationException;
  20. /**
  21. * 模拟管理。只有这里能看到后台模拟会员(users.user_type = 2)的数据:
  22. * 列表和报表直接复用正式接口的代码,只是放在 MemberAudience::simulated() 里执行,所以行结构和统计口径与正式页面完全一致。
  23. */
  24. class Simulation extends Controller
  25. {
  26. /** 在“仅模拟会员”口径下调用正式控制器的方法。 */
  27. private function mirror(string $controller, string $method): JsonResponse
  28. {
  29. return MemberAudience::simulated(fn () => app()->call([app($controller), $method]));
  30. }
  31. private function run(callable $work): JsonResponse
  32. {
  33. try {
  34. return $this->success($work());
  35. } catch (ValidationException $e) {
  36. return $this->error(HttpStatus::CUSTOM_ERROR, $e->validator->errors()->first());
  37. } catch (\Throwable $e) {
  38. return $this->error(HttpStatus::CUSTOM_ERROR, $e->getMessage());
  39. }
  40. }
  41. private function audit(string $type, $id, string $action, array $after = []): void
  42. {
  43. OperationAuditService::record('simulation_' . $type, $id === null ? null : (int) $id, $action, [], $after);
  44. }
  45. private function page($query, string $order = 'id'): array
  46. {
  47. $page = max(1, (int) request()->input('page', 1));
  48. $limit = min(200, max(1, (int) request()->input('limit', 15)));
  49. return ['total' => (clone $query)->count(), 'data' => $query->orderByDesc($order)->forPage($page, $limit)->get()];
  50. }
  51. // ---------- 概览 ----------
  52. public function overview()
  53. {
  54. return $this->run(function () {
  55. $start = request()->input('start_date') ?: date('Y-m-d');
  56. $end = date('Y-m-d', strtotime((request()->input('end_date') ?: date('Y-m-d')) . ' +1 day'));
  57. $members = MemberAudience::simulatedMembers('member_id');
  58. $logs = fn (array $types) => (string) DB::table('balance_logs')->whereIn('member_id', $members)->whereIn('change_type', $types)
  59. ->where('created_at', '>=', $start)->where('created_at', '<', $end)->sum('amount');
  60. $like = fn (string $suffix) => (string) DB::table('balance_logs')->whereIn('member_id', $members)->where('change_type', 'like', '%' . $suffix)
  61. ->where('created_at', '>=', $start)->where('created_at', '<', $end)->sum('amount');
  62. $bet = bcmul($like('投注'), '-1', 2);
  63. $payout = bcadd(bcadd($like('中奖'), $like('退款'), 2), $like('退分'), 2);
  64. $wallet = DB::table('wallets')->whereIn('member_id', $members)->selectRaw('COALESCE(SUM(available_balance),0) a, COALESCE(SUM(frozen_balance),0) f')->first();
  65. $runs = DB::table('simulation_task_runs')->whereIn('status', ['queued', 'running', 'paused'])->orderByDesc('id')->limit(10)->get();
  66. return [
  67. 'enabled' => SimulationSettings::enabled(), 'migrated' => MemberAudience::enabled(), 'start_date' => $start,
  68. 'schedule' => ['enabled' => SimulationSettings::get('simulation_schedule_enabled') === '1', 'start' => SimulationSettings::get('simulation_schedule_start'),
  69. 'end' => SimulationSettings::get('simulation_schedule_end'), 'gap_minutes' => (int) SimulationSettings::get('simulation_schedule_gap_minutes')],
  70. 'members' => ['total' => DB::table('users')->where('user_type', MemberAudience::TYPE_SIMULATED)->count(),
  71. 'new' => DB::table('users')->where('user_type', MemberAudience::TYPE_SIMULATED)->where('created_at', '>=', $start)->where('created_at', '<', $end)->count(),
  72. 'online' => SimulationMemberService::onlineUserIds()->count()],
  73. 'funds' => ['recharge' => bcadd($logs(['充值', '三方充值']), '0', 2), 'manual_credit' => bcadd($logs(['人工充值']), '0', 2),
  74. 'manual_debit' => bcmul($logs(['人工扣款']), '-1', 2), // 提现被拒绝时退款记为正数的“三方提现”,一并计入才是净提现
  75. 'withdraw' => bcmul($logs(['提现', '人工提现', '三方提现']), '-1', 2),
  76. 'wallet_available' => bcadd((string) $wallet->a, '0', 2), 'wallet_frozen' => bcadd((string) $wallet->f, '0', 2)],
  77. 'bets' => ['bet_amount' => $bet, 'payout' => $payout, 'win_loss' => bcsub($bet, $payout, 2),
  78. 'unsettled' => DB::table('lhc_order')->whereIn('member_id', $members)->where('lottery_status', 0)->count()
  79. + SimulationBetService::sportQuery()->where('sport_order.settlement_status', 0)->where('sport_order.status', 1)->count(),
  80. // 第三方游戏的输赢不记“投注/中奖”流水(与真实三方游戏一致),单独按注单统计
  81. 'third' => (array) DB::table('third_game_orders')->whereIn('user_id', MemberAudience::simulatedMembers('id'))
  82. ->where('bet_time', '>=', $start)->where('bet_time', '<', $end)
  83. ->selectRaw('COUNT(*) AS orders, COALESCE(SUM(bet_amount),0) AS bet_amount, COALESCE(SUM(settled_amount),0) AS member_win_loss')->first()],
  84. 'runs' => $runs->map(fn ($run) => SimulationTaskService::present($run))->all(),
  85. ];
  86. });
  87. }
  88. // ---------- 会员 ----------
  89. public function members()
  90. {
  91. $response = $this->mirror(User::class, 'index');
  92. $body = $response->getData(true);
  93. if (($body['code'] ?? -1) !== 0 || empty($body['data']['data'])) return $response;
  94. $online = array_flip(SimulationMemberService::onlineUserIds()->map(fn ($id) => (string) $id)->all());
  95. foreach ($body['data']['data'] as &$row) $row['sim_online'] = isset($online[(string) ($row['user_id'] ?? '')]);
  96. return response()->json($body);
  97. }
  98. public function createMembers()
  99. {
  100. return $this->run(function () {
  101. $params = request()->validate(['count' => ['required', 'integer', 'min:1'], 'domain' => ['nullable', 'string', 'max:100'],
  102. 'initial_balance' => ['nullable', 'numeric', 'min:0']]);
  103. $created = SimulationMemberService::create((int) $params['count'], $params);
  104. $this->audit('member', null, 'create', ['count' => count($created), 'member_ids' => array_column($created, 'member_id')]);
  105. return ['created' => $created];
  106. });
  107. }
  108. public function memberDetail()
  109. {
  110. return $this->run(fn () => SimulationMemberService::detail(request()->validate(['member_id' => ['required', 'string']])['member_id']));
  111. }
  112. public function memberBanned()
  113. {
  114. return $this->run(function () {
  115. $params = request()->validate(['member_id' => ['required', 'string'], 'is_banned' => ['required', 'integer', 'in:0,1']]);
  116. SimulationMemberService::setBanned($params['member_id'], (int) $params['is_banned']);
  117. $this->audit('member', null, 'banned', $params);
  118. return [];
  119. });
  120. }
  121. public function memberLogin()
  122. {
  123. return $this->run(function () {
  124. $params = request()->validate(['member_id' => ['required', 'string'], 'platform' => ['nullable', 'string', 'max:16'], 'domain' => ['nullable', 'string', 'max:100']]);
  125. return SimulationMemberService::login($params['member_id'], $params['platform'] ?? 'H5', $params['domain'] ?? null);
  126. });
  127. }
  128. public function memberLogout()
  129. {
  130. return $this->run(fn () => ['closed' => SimulationMemberService::logout(request()->validate(['member_id' => ['required', 'string']])['member_id'])]);
  131. }
  132. public function loginLogs() { return $this->mirror(User::class, 'loginLog'); }
  133. // ---------- 资金 ----------
  134. private function orders(array $types)
  135. {
  136. $query = DB::table('payment_orders')->join('users', 'users.member_id', '=', 'payment_orders.member_id')
  137. ->where('users.user_type', MemberAudience::TYPE_SIMULATED)->whereIn('payment_orders.type', $types)
  138. ->select('payment_orders.id', 'payment_orders.order_no', 'payment_orders.member_id', 'users.account', 'users.first_name', 'payment_orders.type',
  139. 'payment_orders.amount', 'payment_orders.frozen_amount', 'payment_orders.channel', 'payment_orders.status', 'payment_orders.remark', 'payment_orders.created_at');
  140. if (request()->filled('member_id')) $query->where('payment_orders.member_id', (string) request()->input('member_id'));
  141. if (request()->filled('account')) $query->where('users.account', 'like', '%' . trim((string) request()->input('account')) . '%');
  142. if (request()->filled('status')) $query->where('payment_orders.status', (int) request()->input('status'));
  143. if (request()->filled('start_time')) $query->where('payment_orders.created_at', '>=', request()->input('start_time') . ' 00:00:00');
  144. if (request()->filled('end_time')) $query->where('payment_orders.created_at', '<=', request()->input('end_time') . ' 23:59:59');
  145. return $query;
  146. }
  147. public function recharges()
  148. {
  149. return $this->run(fn () => $this->page($this->orders([PaymentOrderService::TYPE_PAY, PaymentOrderService::TYPE_SELF_PAY]), 'payment_orders.id'));
  150. }
  151. public function recharge()
  152. {
  153. return $this->run(function () {
  154. $params = request()->validate(['member_id' => ['required', 'string'], 'amount' => ['required', 'numeric', 'min:0.01'],
  155. 'channel' => ['nullable', 'in:company,third'], 'result' => ['nullable', 'in:success,pending,fail']]);
  156. $order = SimulationFundService::recharge($params['member_id'], $params['amount'], $params['channel'] ?? 'third', $params['result'] ?? 'success');
  157. $this->audit('fund', $order->id, 'recharge', $params);
  158. return $order;
  159. });
  160. }
  161. public function withdrawals()
  162. {
  163. return $this->run(fn () => $this->page($this->orders([PaymentOrderService::TYPE_SELF_PAYOUT]), 'payment_orders.id'));
  164. }
  165. public function withdraw()
  166. {
  167. return $this->run(function () {
  168. $params = request()->validate(['member_id' => ['required', 'string'], 'amount' => ['required', 'numeric', 'min:0.01']]);
  169. $order = SimulationFundService::withdrawApply($params['member_id'], $params['amount']);
  170. $this->audit('fund', $order->id, 'withdraw_apply', $params);
  171. return $order;
  172. });
  173. }
  174. public function withdrawAudit()
  175. {
  176. return $this->run(function () {
  177. $params = request()->validate(['id' => ['required', 'integer', 'min:1'], 'approve' => ['required', 'integer', 'in:0,1'], 'remark' => ['nullable', 'string', 'max:120']]);
  178. $order = SimulationFundService::withdrawAudit((int) $params['id'], (int) $params['approve'] === 1, (string) ($params['remark'] ?? ''));
  179. $this->audit('fund', $order->id, (int) $params['approve'] === 1 ? 'withdraw_approve' : 'withdraw_reject', $params);
  180. return $order;
  181. });
  182. }
  183. public function adjust()
  184. {
  185. return $this->run(function () {
  186. $params = request()->validate(['member_id' => ['required', 'string'], 'amount' => ['required', 'numeric', 'min:0.01'],
  187. 'direction' => ['required', 'in:credit,debit'], 'remark' => ['required', 'string', 'max:120']]);
  188. $log = SimulationFundService::adjust($params['member_id'], $params['amount'], $params['direction'], $params['remark']);
  189. $this->audit('fund', $log->id, 'adjust_' . $params['direction'], $params);
  190. return $log;
  191. });
  192. }
  193. public function ledger() { return $this->mirror(Balance::class, 'log'); }
  194. public function freezes() { return $this->mirror(WalletFreeze::class, 'index'); }
  195. public function freeze() { return $this->applyFreeze(false); }
  196. public function unfreeze() { return $this->applyFreeze(true); }
  197. private function applyFreeze(bool $unfreeze)
  198. {
  199. return $this->run(function () use ($unfreeze) {
  200. $params = request()->validate(['member_id' => ['required', 'string'], 'amount' => ['required', 'numeric', 'min:0.01'], 'reason' => ['required', 'string', 'max:120']]);
  201. $record = SimulationFundService::freeze($params['member_id'], $params['amount'], $params['reason'], (int) (request()->user->id ?? 0), $unfreeze);
  202. $this->audit('fund', $record['id'] ?? null, $unfreeze ? 'unfreeze' : 'freeze', $params);
  203. return $record;
  204. });
  205. }
  206. // ---------- 注单 ----------
  207. public function betOptions() { return $this->run(fn () => SimulationBetService::options(request()->input('game') ?: null)); }
  208. public function lotteryBets() { return $this->mirror(LhcOrder::class, 'list'); }
  209. public function lotteryBet()
  210. {
  211. return $this->run(function () {
  212. // PC28 按 keywords 下注;其他彩种按玩法编号 item_id 下注
  213. $params = request()->validate(['member_id' => ['required', 'string'], 'game' => ['nullable', 'string', 'max:16'], 'keywords' => ['nullable', 'string', 'max:64'],
  214. 'item_id' => ['nullable', 'integer', 'min:1'], 'amount' => ['required', 'numeric', 'min:0.01']]);
  215. $order = SimulationBetService::lottery($params['member_id'], (string) ($params['keywords'] ?? ''), $params['amount'], $params['game'] ?? null,
  216. isset($params['item_id']) ? (int) $params['item_id'] : null);
  217. $this->audit('bet', $order->id, 'lottery_bet', $params);
  218. return $order;
  219. });
  220. }
  221. public function sportBets()
  222. {
  223. return $this->run(function () {
  224. $query = SimulationBetService::sportQuery()->select('sport_order.id', 'sport_order.order_id', 'sport_order.issue', 'users.member_id', 'users.account',
  225. 'users.first_name', 'sport_order.odd_name', 'sport_order.odd_value', 'sport_order.odd', 'sport_order.amount', 'sport_order.win_amount',
  226. 'sport_order.return_amount', 'sport_order.profit_and_loss', 'sport_order.is_win', 'sport_order.status', 'sport_order.settlement_status',
  227. 'sport_order.create_time', 'sport_order.settlement_time');
  228. if (request()->filled('member_id')) $query->where('users.member_id', (string) request()->input('member_id'));
  229. if (request()->filled('account')) $query->where('users.account', 'like', '%' . trim((string) request()->input('account')) . '%');
  230. if (request()->filled('settlement_status')) $query->where('sport_order.settlement_status', (int) request()->input('settlement_status'));
  231. if (request()->filled('start_time')) $query->where('sport_order.create_time', '>=', strtotime(request()->input('start_time') . ' 00:00:00'));
  232. if (request()->filled('end_time')) $query->where('sport_order.create_time', '<=', strtotime(request()->input('end_time') . ' 23:59:59'));
  233. return $this->page($query, 'sport_order.id');
  234. });
  235. }
  236. public function sportBet()
  237. {
  238. return $this->run(function () {
  239. $params = request()->validate(['member_id' => ['required', 'string'], 'amount' => ['required', 'numeric', 'min:0.01'], 'odd' => ['required', 'numeric'],
  240. 'title' => ['nullable', 'string', 'max:100'], 'selection' => ['nullable', 'string', 'max:100']]);
  241. $order = SimulationBetService::sport($params['member_id'], $params['amount'], $params['odd'], (string) ($params['title'] ?? ''), (string) ($params['selection'] ?? ''));
  242. $this->audit('bet', $order->id, 'sport_bet', $params);
  243. return $order;
  244. });
  245. }
  246. public function sportSettle()
  247. {
  248. return $this->run(function () {
  249. $params = request()->validate(['id' => ['required', 'integer', 'min:1'], 'result' => ['required', 'in:win,lose,draw,half']]);
  250. $order = SimulationBetService::settleSport((int) $params['id'], $params['result']);
  251. $this->audit('bet', $order->id, 'sport_settle', $params);
  252. return $order;
  253. });
  254. }
  255. public function sportCancel()
  256. {
  257. return $this->run(function () {
  258. $params = request()->validate(['id' => ['required', 'integer', 'min:1'], 'remark' => ['nullable', 'string', 'max:120']]);
  259. $order = SimulationBetService::cancelSport((int) $params['id'], (string) ($params['remark'] ?? ''));
  260. $this->audit('bet', $order->id, 'sport_cancel', $params);
  261. return $order;
  262. });
  263. }
  264. public function thirdBets() { return $this->mirror(ThirdGameOrder::class, 'index'); }
  265. /** 第三方游戏模拟注单:只在本地生成,不向三方平台发请求。 */
  266. public function thirdBet()
  267. {
  268. return $this->run(function () {
  269. $params = request()->validate(['member_id' => ['required', 'string'], 'amount' => ['required', 'numeric', 'min:0.01'],
  270. 'game_type' => ['nullable', 'integer', 'min:1', 'max:7'], 'result' => ['nullable', 'in:win,lose']]);
  271. $order = SimulationBetService::third($params['member_id'], $params['amount'], isset($params['game_type']) ? (int) $params['game_type'] : null, $params['result'] ?? null);
  272. $this->audit('bet', $order->id, 'third_bet', $params);
  273. return $order;
  274. });
  275. }
  276. // ---------- 自动任务 ----------
  277. public function templates()
  278. {
  279. return $this->run(function () {
  280. $page = $this->page(DB::table('simulation_tasks'));
  281. $page['data'] = $page['data']->map(fn ($row) => ['steps' => json_decode($row->steps, true)] + (array) $row)->all();
  282. return $page;
  283. });
  284. }
  285. public function saveTemplate()
  286. {
  287. return $this->run(function () {
  288. $id = SimulationTaskService::saveTemplate(request()->all(), request()->user->id ?? null);
  289. $this->audit('task', $id, 'template_save', ['name' => request()->input('name')]);
  290. return ['id' => $id];
  291. });
  292. }
  293. public function deleteTemplate()
  294. {
  295. return $this->run(function () {
  296. $id = (int) request()->validate(['id' => ['required', 'integer', 'min:1']])['id'];
  297. DB::table('simulation_tasks')->where('id', $id)->delete();
  298. $this->audit('task', $id, 'template_delete');
  299. return [];
  300. });
  301. }
  302. public function runs()
  303. {
  304. return $this->run(function () {
  305. $query = DB::table('simulation_task_runs');
  306. if (request()->filled('status')) $query->where('status', (string) request()->input('status'));
  307. if (request()->filled('name')) $query->where('name', 'like', '%' . trim((string) request()->input('name')) . '%');
  308. $page = $this->page($query);
  309. $page['data'] = $page['data']->map(fn ($run) => SimulationTaskService::present($run))->all();
  310. return $page;
  311. });
  312. }
  313. public function startRun()
  314. {
  315. return $this->run(function () {
  316. $id = SimulationTaskService::start(request()->all(), request()->user->id ?? null);
  317. $this->audit('task', $id, 'run_start', ['task_id' => request()->input('task_id'), 'name' => request()->input('name')]);
  318. return ['id' => $id];
  319. });
  320. }
  321. /** 一键启动:按模拟设置里的默认任务参数跑一遍完整流程。 */
  322. public function quickStart()
  323. {
  324. return $this->run(function () {
  325. $id = SimulationTaskService::quickStart(request()->user->id ?? null);
  326. $this->audit('task', $id, 'run_quick_start');
  327. return ['id' => $id];
  328. });
  329. }
  330. public function runStatus()
  331. {
  332. return $this->run(function () {
  333. $params = request()->validate(['id' => ['required', 'integer', 'min:1'], 'action' => ['required', 'in:pause,resume,stop']]);
  334. $run = SimulationTaskService::setStatus((int) $params['id'], $params['action']);
  335. $this->audit('task', $run->id, 'run_' . $params['action']);
  336. return SimulationTaskService::present($run);
  337. });
  338. }
  339. public function events()
  340. {
  341. return $this->run(function () {
  342. $query = DB::table('simulation_task_events');
  343. foreach (['run_id', 'status', 'action', 'member_id'] as $field) if (request()->filled($field)) $query->where($field, request()->input($field));
  344. return $this->page($query);
  345. });
  346. }
  347. // ---------- 报表:与正式报表同一套代码,只统计模拟会员 ----------
  348. public function reportUsers() { return $this->mirror(MemberDailyReport::class, 'index'); }
  349. public function reportFunds() { return $this->mirror(Funds::class, 'summary'); }
  350. public function reportHome() { return $this->mirror(Home::class, 'index'); }
  351. public function reportGames() { return $this->mirror(Reporting::class, 'summary'); }
  352. public function reportFundFlow() { return $this->mirror(Reporting::class, 'funds'); }
  353. // 报表页面里的筛选项、明细、注单与事件弹窗,同样只看模拟会员
  354. public function reportOptions() { return $this->mirror(Reporting::class, 'options'); }
  355. public function reportDetails() { return $this->mirror(Reporting::class, 'details'); }
  356. public function reportOrders() { return $this->mirror(Reporting::class, 'orders'); }
  357. public function reportEvents() { return $this->mirror(Reporting::class, 'events'); }
  358. public function reportRegistrations() { return $this->mirror(User::class, 'registerTrend'); }
  359. public function reportDomains() { return $this->mirror(User::class, 'registerDomain'); }
  360. /** 立即刷新报表依赖的中间数据(平时由定时任务每 1~10 分钟刷新一次)。 */
  361. public function reportRefresh()
  362. {
  363. return $this->run(function () {
  364. $date = request()->validate(['date' => ['nullable', 'date_format:Y-m-d']])['date'] ?? date('Y-m-d');
  365. $facts = app(BetFactSynchronizer::class)->sync($date, $date, true);
  366. $reporting = app(ReportSync::class)->sync();
  367. $daily = app(MemberDailyReportService::class)->refresh($date);
  368. return ['date' => $date, 'facts' => $facts, 'reporting' => $reporting, 'member_daily' => $daily];
  369. });
  370. }
  371. // ---------- 操作日志 ----------
  372. public function audits()
  373. {
  374. return $this->run(function () {
  375. $query = DB::table('operation_audits')->where('resource_type', 'like', 'simulation\_%');
  376. if (request()->filled('resource_type')) $query->where('resource_type', 'simulation_' . request()->input('resource_type'));
  377. if (request()->filled('operator')) $query->where('operator_name', 'like', '%' . trim((string) request()->input('operator')) . '%');
  378. if (request()->filled('start_time')) $query->where('created_at', '>=', request()->input('start_time') . ' 00:00:00');
  379. if (request()->filled('end_time')) $query->where('created_at', '<=', request()->input('end_time') . ' 23:59:59');
  380. $page = $this->page($query);
  381. $page['data'] = $page['data']->map(fn ($row) => ['changes' => json_decode((string) $row->changes, true)] + (array) $row)->all();
  382. return $page;
  383. });
  384. }
  385. // ---------- 设置与重置 ----------
  386. public function settings() { return $this->run(fn () => SimulationSettings::all()); }
  387. public function saveSettings()
  388. {
  389. return $this->run(function () {
  390. $change = SimulationSettings::save(request()->all());
  391. OperationAuditService::record('simulation_settings', null, 'save', $change['before'], $change['after']);
  392. return $change['after'];
  393. });
  394. }
  395. public function resetPreview() { return $this->run(fn () => SimulationResetService::preview()); }
  396. public function reset()
  397. {
  398. return $this->run(function () {
  399. $params = request()->validate(['confirm_token' => ['required', 'string'], 'confirm_text' => ['required', 'string']]);
  400. $deleted = SimulationResetService::reset($params['confirm_token'], $params['confirm_text']);
  401. $this->audit('reset', null, 'reset', $deleted);
  402. return ['deleted' => $deleted];
  403. });
  404. }
  405. }