20) throw new Exception('任务步骤不能超过 20 个', HttpStatus::CUSTOM_ERROR); $out = []; foreach ($steps as $step) { $type = (string) ($step['type'] ?? ''); if (!in_array($type, self::STEP_TYPES, true)) throw new Exception('任务步骤类型不正确:' . $type, HttpStatus::CUSTOM_ERROR); $row = ['type' => $type, 'count' => in_array($type, self::ONCE, true) ? 1 : min(50, max(1, (int) ($step['count'] ?? 1))), 'interval' => min(3600, max(0, (int) ($step['interval'] ?? 2)))]; if (in_array($type, ['recharge', 'bet', 'withdraw'], true)) $row['amount'] = SimulationSettings::assertAmount($step['amount'] ?? 100); if ($type === 'bet') $row['category'] = ($step['category'] ?? 'lottery') === 'sport' ? 'sport' : 'lottery'; if ($type === 'settle') $row['settle_mode'] = in_array($step['settle_mode'] ?? 'random', ['random', 'win', 'lose'], true) ? ($step['settle_mode'] ?? 'random') : 'random'; if (in_array($type, ['register', 'login'], true)) $row['platform'] = substr((string) ($step['platform'] ?? 'H5'), 0, 16); $out[] = $row; } return $out; } public static function saveTemplate(array $input, ?int $adminId): int { $name = trim((string) ($input['name'] ?? '')); if ($name === '' || mb_strlen($name) > 100) throw new Exception('请填写模板名称(不超过 100 字)', HttpStatus::CUSTOM_ERROR); $row = ['name' => $name, 'steps' => json_encode(self::normalizeSteps($input['steps'] ?? null), JSON_UNESCAPED_UNICODE), 'target_mode' => ($input['target_mode'] ?? 'new') === 'existing' ? 'existing' : 'new', 'member_count' => self::memberCount($input['member_count'] ?? 1), 'remark' => mb_substr((string) ($input['remark'] ?? ''), 0, 255), 'updated_at' => now()]; if (!empty($input['id'])) { if (!DB::table('simulation_tasks')->where('id', (int) $input['id'])->update($row)) throw new Exception('模板不存在', HttpStatus::CUSTOM_ERROR); return (int) $input['id']; } return (int) DB::table('simulation_tasks')->insertGetId($row + ['created_by' => $adminId, 'created_at' => now()]); } private static function memberCount($count): int { $count = (int) $count; $max = (int) SimulationSettings::get('simulation_max_members_per_task'); if ($count < 1 || $count > $max) throw new Exception("任务会员数应在 1 到 {$max} 之间", HttpStatus::CUSTOM_ERROR); return $count; } /** 新建一次运行并排队。来自模板(task_id)或直接给步骤;existing 模式要给 member_ids。 */ public static function start(array $input, ?int $adminId): int { SimulationSettings::assertEnabled(); $task = !empty($input['task_id']) ? DB::table('simulation_tasks')->where('id', (int) $input['task_id'])->first() : null; if (!empty($input['task_id']) && !$task) throw new Exception('模板不存在', HttpStatus::CUSTOM_ERROR); $steps = self::normalizeSteps($input['steps'] ?? ($task ? json_decode($task->steps, true) : null)); $memberIds = array_values(array_unique(array_map('strval', (array) ($input['member_ids'] ?? [])))); $registers = in_array('register', array_column($steps, 'type'), true); if ($registers) { if ($steps[0]['type'] !== 'register' || count(array_keys(array_column($steps, 'type'), 'register')) > 1) { throw new Exception('“注册”只能作为任务的第一个步骤,且只能有一个', HttpStatus::CUSTOM_ERROR); } $memberIds = []; $count = self::memberCount($input['member_count'] ?? ($task->member_count ?? 1)); } else { if ($memberIds === []) throw new Exception('任务没有“注册”步骤时,请选择要使用的模拟会员', HttpStatus::CUSTOM_ERROR); $count = self::memberCount(count($memberIds)); $valid = DB::table('users')->whereIn('member_id', $memberIds)->where('user_type', MemberAudience::TYPE_SIMULATED)->count(); if ($valid !== count($memberIds)) throw new Exception('所选会员中包含非模拟会员', HttpStatus::CUSTOM_ERROR); } $total = 0; foreach ($steps as $step) $total += $step['count'] * $count; $name = trim((string) ($input['name'] ?? '')) ?: ($task->name ?? '临时任务'); return (int) DB::table('simulation_task_runs')->insertGetId(['task_id' => $task->id ?? null, 'name' => mb_substr($name, 0, 100), 'steps' => json_encode($steps, JSON_UNESCAPED_UNICODE), 'member_ids' => json_encode($memberIds), 'member_count' => $count, 'status' => 'queued', 'total' => $total, 'cursor' => json_encode(['step' => 0, 'member' => 0, 'seq' => 1, 'waited' => 0]), 'next_run_at' => now(), 'created_by' => $adminId, 'created_at' => now(), 'updated_at' => now()]); } /** 暂停 / 继续 / 停止;下一次调度时生效。 */ public static function setStatus(int $id, string $action): object { $run = DB::table('simulation_task_runs')->where('id', $id)->first(); if (!$run) throw new Exception('运行记录不存在', HttpStatus::CUSTOM_ERROR); $next = match (true) { $action === 'pause' && in_array($run->status, ['queued', 'running'], true) => 'paused', $action === 'resume' && $run->status === 'paused' => 'running', $action === 'stop' && in_array($run->status, ['queued', 'running', 'paused'], true) => 'stopped', default => throw new Exception('当前状态不能执行该操作', HttpStatus::CUSTOM_ERROR), }; $update = ['status' => $next, 'updated_at' => now()]; if ($next === 'running') { SimulationSettings::assertEnabled(); $update['next_run_at'] = now(); } if ($next === 'stopped') $update['finished_at'] = now(); DB::table('simulation_task_runs')->where('id', $id)->where('status', $run->status)->update($update); return DB::table('simulation_task_runs')->where('id', $id)->first(); } public static function present(object $run): array { $row = (array) $run; foreach (['steps', 'member_ids', 'cursor'] as $key) $row[$key] = json_decode((string) ($row[$key] ?? 'null'), true); $row['progress'] = $run->total > 0 ? (int) floor(($run->completed + $run->failed) * 100 / $run->total) : 0; return $row; } }