[默认值, 最小, 最大];最小为 null 的是字符串。 */ public const KEYS = [ 'simulation_enabled' => ['0', 0, 1], 'simulation_max_concurrency' => ['2', 1, 10], 'simulation_rate_per_minute' => ['30', 1, 120], 'simulation_max_members_per_task' => ['20', 1, 200], 'simulation_max_members_total' => ['500', 1, 5000], 'simulation_max_amount' => ['10000', 1, 1000000], 'simulation_initial_balance' => ['0', 0, 1000000], 'simulation_wager_bypass' => ['1', 0, 1], 'simulation_default_domain' => ['sim.local', null, null], // 一键启动的默认任务:注册 → 登录 → 充值 → 下注 → 结算 → 提现 → 退出 'simulation_task_members' => ['5', 1, 200], 'simulation_task_recharge' => ['1000', 1, 1000000], 'simulation_task_bet_category' => ['sport', null, null], 'simulation_task_bet_amount' => ['100', 1, 1000000], 'simulation_task_bet_count' => ['3', 1, 50], 'simulation_task_withdraw' => ['200', 0, 1000000], 'simulation_task_interval' => ['2', 0, 3600], // 持续运行:在日期区间内一轮接一轮地自动启动上面的默认任务 'simulation_schedule_enabled' => ['0', 0, 1], 'simulation_schedule_start' => ['', null, null], 'simulation_schedule_end' => ['', null, null], 'simulation_schedule_gap_minutes' => ['10', 0, 1440], ]; public static function all(): array { $stored = DB::table('config')->whereIn('field', array_keys(self::KEYS))->pluck('val', 'field'); $out = []; foreach (self::KEYS as $key => [$default]) $out[$key] = (string) ($stored[$key] ?? $default); return $out; } public static function get(string $key): string { return self::all()[$key]; } public static function save(array $input): array { return DB::transaction(function () use ($input) { $before = self::all(); foreach (self::KEYS as $key => [, $min, $max]) { if (!array_key_exists($key, $input)) continue; $value = trim((string) $input[$key]); if ($key === 'simulation_task_bet_category') { if (!in_array($value, ['sport', 'lottery'], true)) throw new Exception('默认任务的下注分类只能是体育或彩票', HttpStatus::CUSTOM_ERROR); } elseif (in_array($key, ['simulation_schedule_start', 'simulation_schedule_end'], true)) { if ($value !== '' && (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $value) || date('Y-m-d', (int) strtotime($value)) !== $value)) { throw new Exception('持续运行的日期格式应为 YYYY-MM-DD', HttpStatus::CUSTOM_ERROR); } } elseif ($min === null) { if ($value === '' || strlen($value) > 100) throw new Exception('域名格式不正确', HttpStatus::CUSTOM_ERROR); } elseif (!is_numeric($value) || $value < $min || $value > $max) { throw new Exception("{$key} 应在 {$min} 到 {$max} 之间", HttpStatus::CUSTOM_ERROR); } $exists = DB::table('config')->where('field', $key)->exists(); if ($exists) DB::table('config')->where('field', $key)->update(['val' => $value]); else DB::table('config')->insert(['field' => $key, 'val' => $value]); } $after = self::all(); if ($after['simulation_schedule_enabled'] === '1') { [$start, $end] = [$after['simulation_schedule_start'], $after['simulation_schedule_end']]; if ($start === '' || $end === '' || $start > $end) throw new Exception('开启持续运行前,请填写正确的开始和结束日期', HttpStatus::CUSTOM_ERROR); if ((strtotime($end) - strtotime($start)) / 86400 > 92) throw new Exception('持续运行的日期区间不能超过 92 天', HttpStatus::CUSTOM_ERROR); } return ['before' => $before, 'after' => $after]; }); } public static function enabled(): bool { return self::get('simulation_enabled') === '1'; } public static function assertEnabled(): void { if (!MemberAudience::enabled()) throw new Exception('用户类型字段尚未迁移,无法使用模拟管理', HttpStatus::CUSTOM_ERROR); if (!self::enabled()) throw new Exception('模拟功能未开启,请先在“模拟设置”中开启', HttpStatus::CUSTOM_ERROR); } /** 写操作的目标必须是后台模拟会员,返回该会员。 */ public static function member($memberId): object { $user = DB::table('users')->where('member_id', (string) $memberId)->where('user_type', MemberAudience::TYPE_SIMULATED)->first(); if (!$user) throw new Exception('只能操作模拟会员', HttpStatus::CUSTOM_ERROR); return $user; } public static function assertAmount($amount): string { if (!is_numeric($amount) || $amount <= 0) throw new Exception('金额格式不正确', HttpStatus::CUSTOM_ERROR); if ($amount > (float) self::get('simulation_max_amount')) throw new Exception('金额超过模拟设置的单次上限', HttpStatus::CUSTOM_ERROR); return number_format((float) $amount, 2, '.', ''); } }