| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116 |
- <?php
- namespace App\Services\Simulation;
- use App\Constants\HttpStatus;
- use App\Support\MemberAudience;
- use Exception;
- use Illuminate\Support\Facades\DB;
- /**
- * 自动任务:模板(simulation_tasks)和运行(simulation_task_runs)。
- * 一次运行按步骤顺序执行,每一步对每个模拟会员做 count 次;实际执行在 SimulationRunner。
- */
- final class SimulationTaskService
- {
- public const STEP_TYPES = ['register', 'login', 'recharge', 'bet', 'settle', 'withdraw', 'logout'];
- /** 每个会员只做一次的步骤,count 不生效。 */
- public const ONCE = ['register', 'login', 'logout', 'settle'];
- public static function normalizeSteps($steps): array
- {
- if (!is_array($steps) || $steps === []) throw new Exception('请至少添加一个任务步骤', HttpStatus::CUSTOM_ERROR);
- if (count($steps) > 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;
- }
- }
|