where('status', 'running')->count(); if ($slots > 0) { $queued = DB::table('simulation_task_runs')->where('status', 'queued')->orderBy('id')->limit($slots)->pluck('id'); foreach ($queued as $id) { DB::table('simulation_task_runs')->where('id', $id)->where('status', 'queued') ->update(['status' => 'running', 'started_at' => now(), 'next_run_at' => now(), 'updated_at' => now()]); } } $budget = max(1, intdiv((int) $settings['simulation_rate_per_minute'], 30)); $done = 0; $due = DB::table('simulation_task_runs')->where('status', 'running')->where('next_run_at', '<=', now())->orderBy('next_run_at')->pluck('id'); foreach ($due as $id) { for ($i = 0; $i < $budget; $i++) { [$acted, $continue] = self::advance((int) $id); $done += $acted ? 1 : 0; if (!$continue) break; } } return $done; } /** @return array{0:bool,1:bool} [是否执行了动作, 本轮是否可以继续推进这个运行] */ private static function advance(int $id): array { return DB::transaction(function () use ($id) { $run = DB::table('simulation_task_runs')->where('id', $id)->lockForUpdate()->first(); if (!$run || $run->status !== 'running' || (string) $run->next_run_at > now()->toDateTimeString()) return [false, false]; $steps = json_decode($run->steps, true); $members = json_decode($run->member_ids, true) ?: []; $cursor = json_decode((string) $run->cursor, true) + ['step' => 0, 'member' => 0, 'seq' => 1, 'waited' => 0, 'streak' => 0]; if ($cursor['step'] >= count($steps)) { DB::table('simulation_task_runs')->where('id', $id)->update(['status' => 'completed', 'finished_at' => now(), 'updated_at' => now()]); return [false, false]; } $step = $steps[$cursor['step']]; $memberId = $step['type'] === 'register' ? '' : (string) ($members[$cursor['member']] ?? ''); $event = ['run_id' => $id, 'member_id' => $memberId, 'step_index' => $cursor['step'], 'seq' => $cursor['seq'], 'action' => $step['type'], 'status' => 'ok', 'ref_type' => null, 'ref_id' => null, 'amount' => $step['amount'] ?? null, 'message' => '', 'created_at' => now()]; try { if ($step['type'] !== 'register' && $memberId === '') throw new \RuntimeException('没有可用的模拟会员(注册步骤可能失败)'); $result = DB::transaction(fn () => self::perform($step, $memberId)); if ($result === null) { // 彩票尚未开奖,稍后再看 if (++$cursor['waited'] <= self::MAX_WAITS) { DB::table('simulation_task_runs')->where('id', $id)->update(['cursor' => json_encode($cursor), 'next_run_at' => now()->addSeconds(self::WAIT_SECONDS), 'updated_at' => now()]); return [false, false]; } throw new \RuntimeException('等待开奖结算超时'); } $event = array_merge($event, $result); if ($step['type'] === 'register') { $members[] = $event['member_id']; $event['seq'] = $cursor['member'] + 1; // 注册没有会员,用序号保证事件唯一 } $cursor['streak'] = 0; } catch (Throwable $e) { $event['status'] = 'failed'; $event['message'] = mb_substr($e->getMessage(), 0, 500); if ($step['type'] === 'register') $event['seq'] = $cursor['member'] + 1; $cursor['streak']++; } DB::table('simulation_task_events')->insertOrIgnore($event); $cursor['waited'] = 0; if (++$cursor['seq'] > $step['count']) { $cursor['seq'] = 1; if (++$cursor['member'] >= (int) $run->member_count) { $cursor['member'] = 0; $cursor['step']++; } } $update = ['cursor' => json_encode($cursor), 'member_ids' => json_encode($members), 'updated_at' => now(), 'next_run_at' => now()->addSeconds((int) $step['interval']), ($event['status'] === 'ok' ? 'completed' : 'failed') => DB::raw(($event['status'] === 'ok' ? 'completed' : 'failed') . ' + 1')]; if ($event['status'] === 'failed') $update['last_error'] = $event['message']; $finished = $cursor['step'] >= count($steps); if ($cursor['streak'] >= self::MAX_FAILURE_STREAK) $update += ['status' => 'failed', 'finished_at' => now()]; elseif ($finished) $update += ['status' => 'completed', 'finished_at' => now()]; DB::table('simulation_task_runs')->where('id', $id)->update($update); return [true, !isset($update['status']) && (int) $step['interval'] === 0]; }); } /** 执行一个动作。返回要写进事件的字段;返回 null 表示还要等(彩票未开奖)。 */ private static function perform(array $step, string $memberId): ?array { switch ($step['type']) { case 'register': $created = SimulationMemberService::create(1)[0]; return ['member_id' => $created['member_id'], 'ref_type' => 'users', 'ref_id' => (string) $created['id'], 'message' => '注册 ' . $created['account']]; case 'login': $login = SimulationMemberService::login($memberId, $step['platform'] ?? 'H5'); return ['ref_type' => 'user_login', 'ref_id' => (string) $login['id'], 'message' => '登录成功']; case 'logout': return ['message' => '退出 ' . SimulationMemberService::logout($memberId) . ' 个会话']; case 'recharge': $order = SimulationFundService::recharge($memberId, $step['amount'], 'third', 'success'); return ['ref_type' => 'payment_orders', 'ref_id' => (string) $order->id, 'message' => '充值成功']; case 'withdraw': $order = SimulationFundService::withdrawApply($memberId, $step['amount']); SimulationFundService::withdrawAudit((int) $order->id, true); return ['ref_type' => 'payment_orders', 'ref_id' => (string) $order->id, 'message' => '提现成功']; case 'bet': if (($step['category'] ?? 'lottery') === 'sport') { $order = SimulationBetService::sport($memberId, $step['amount'], number_format(mt_rand(150, 250) / 100, 2, '.', '')); return ['ref_type' => 'sport_order', 'ref_id' => (string) $order->id, 'message' => '体育下注 赔率 ' . (float) $order->odd]; } $keywords = ['大', '小', '单', '双'][mt_rand(0, 3)]; $order = SimulationBetService::lottery($memberId, $keywords, $step['amount']); return ['ref_type' => 'lhc_order', 'ref_id' => (string) $order->id, 'message' => "彩票下注 {$order->issue} 期 {$keywords}"]; case 'settle': $pending = LhcOrder::where('member_id', $memberId)->where('bet_source', LhcOrder::SOURCE_SIMULATION) ->where('lottery_status', LhcOrder::STATUS_STAY)->count(); $user = SimulationSettings::member($memberId); $sport = SimulationBetService::sportQuery()->where('sport_order.user_id', (string) $user->user_id) ->where('sport_order.settlement_status', 0)->where('sport_order.status', 1)->pluck('sport_order.id'); foreach ($sport as $orderId) { $mode = $step['settle_mode'] ?? 'random'; SimulationBetService::settleSport((int) $orderId, $mode === 'random' ? ['win', 'lose', 'draw', 'half'][mt_rand(0, 3)] : $mode); } if ($pending > 0) return null; return ['amount' => null, 'message' => '体育结算 ' . count($sport) . ' 单,彩票已全部开奖']; } throw new \RuntimeException('未知步骤:' . $step['type']); } }