SimulationResetService.php 5.0 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. <?php
  2. namespace App\Services\Simulation;
  3. use App\Constants\HttpStatus;
  4. use App\Support\MemberAudience;
  5. use Exception;
  6. use Illuminate\Support\Facades\Cache;
  7. use Illuminate\Support\Facades\DB;
  8. use Illuminate\Support\Facades\Schema;
  9. /**
  10. * 重置:删除全部后台模拟会员以及他们在业务表、报表投影表里的数据。不可恢复。
  11. * 每条删除都以“键列 IN (SELECT … FROM users WHERE user_type = 2)”为条件,不拼接内存里的编号,不清空整表。
  12. */
  13. final class SimulationResetService
  14. {
  15. public const CONFIRM_TEXT = '重置模拟数据';
  16. private const TOKEN_KEY = 'simulation_reset_token';
  17. /** 表 => [表里的会员列, 对应的 users 列] */
  18. private const TABLES = [
  19. 'balance_logs' => ['member_id', 'member_id'], 'payment_orders' => ['member_id', 'member_id'], 'withdraws' => ['member_id', 'member_id'],
  20. 'recharges' => ['member_id', 'member_id'], 'wallet_freeze_records' => ['member_id', 'member_id'], 'wager_records' => ['member_id', 'member_id'],
  21. 'lhc_order_item' => ['member_id', 'member_id'], 'lhc_order' => ['member_id', 'member_id'], 'sport_game_order' => ['member_id', 'member_id'],
  22. 'jisu_game_order' => ['member_id', 'member_id'], 'bets' => ['member_id', 'member_id'], 'backflow' => ['member_id', 'member_id'],
  23. 'rebates' => ['member_id', 'member_id'], 'activity_user' => ['member_id', 'member_id'],
  24. 'sport_order' => ['user_id', 'user_id'], 'user_login' => ['user_id', 'user_id'], 'user_session' => ['user_id', 'user_id'], 'yuebao_item' => ['user_id', 'user_id'],
  25. 'share_bet_facts' => ['user_id', 'id'], 'third_game_orders' => ['user_id', 'id'], 'reporting_orders' => ['user_id', 'id'],
  26. 'reporting_events' => ['user_id', 'id'], 'reporting_contexts' => ['user_id', 'id'], 'reporting_activity' => ['user_id', 'id'],
  27. 'member_daily_reports' => ['user_id', 'id'],
  28. 'wallets' => ['member_id', 'member_id'],
  29. ];
  30. private static function scoped(string $table)
  31. {
  32. [$column, $userColumn] = self::TABLES[$table];
  33. return DB::table($table)->whereIn($column, MemberAudience::simulatedMembers($userColumn));
  34. }
  35. private static function tables(): array
  36. {
  37. return array_values(array_filter(array_keys(self::TABLES), fn ($table) => Schema::hasTable($table) && Schema::hasColumn($table, self::TABLES[$table][0])));
  38. }
  39. private static function assertSafe(): void
  40. {
  41. if (!MemberAudience::enabled()) throw new Exception('用户类型字段尚未迁移', HttpStatus::CUSTOM_ERROR);
  42. if (DB::table('simulation_task_runs')->whereIn('status', ['queued', 'running', 'paused'])->exists()) {
  43. throw new Exception('还有未结束的自动任务,请先停止', HttpStatus::CUSTOM_ERROR);
  44. }
  45. // 模拟会员一定是 from = 2 且没有代理;出现别的情况说明 user_type 被误标,拒绝批量删除
  46. $odd = DB::table('users')->where('user_type', MemberAudience::TYPE_SIMULATED)
  47. ->where(fn ($q) => $q->where('from', '<>', '2')->orWhereNull('from')->orWhereNotNull('agent_id'))->count();
  48. if ($odd > 0) throw new Exception("有 {$odd} 个标记为模拟的会员不符合模拟会员特征(来源或代理异常),已中止,请人工核查", HttpStatus::CUSTOM_ERROR);
  49. }
  50. /** 预览将删除的行数,并发放一次性确认口令(10 分钟有效)。 */
  51. public static function preview(): array
  52. {
  53. self::assertSafe();
  54. $counts = ['users' => DB::table('users')->where('user_type', MemberAudience::TYPE_SIMULATED)->count()];
  55. foreach (self::tables() as $table) $counts[$table] = self::scoped($table)->count();
  56. $counts['simulation_task_runs'] = DB::table('simulation_task_runs')->count();
  57. $counts['simulation_task_events'] = DB::table('simulation_task_events')->count();
  58. $token = bin2hex(random_bytes(16));
  59. Cache::put(self::TOKEN_KEY, $token, 600);
  60. return ['counts' => $counts, 'confirm_token' => $token, 'confirm_text' => self::CONFIRM_TEXT];
  61. }
  62. public static function reset(string $token, string $text): array
  63. {
  64. if ($text !== self::CONFIRM_TEXT) throw new Exception('确认短语不正确', HttpStatus::CUSTOM_ERROR);
  65. $expected = Cache::pull(self::TOKEN_KEY);
  66. if (!$expected || !hash_equals((string) $expected, $token)) throw new Exception('确认口令已失效,请重新预览', HttpStatus::CUSTOM_ERROR);
  67. self::assertSafe();
  68. $deleted = [];
  69. DB::transaction(function () use (&$deleted) {
  70. foreach (self::tables() as $table) $deleted[$table] = self::scoped($table)->delete();
  71. $deleted['simulation_task_events'] = DB::table('simulation_task_events')->delete();
  72. $deleted['simulation_task_runs'] = DB::table('simulation_task_runs')->delete();
  73. $deleted['users'] = DB::table('users')->where('user_type', MemberAudience::TYPE_SIMULATED)->where('from', '2')->whereNull('agent_id')->delete();
  74. });
  75. return $deleted;
  76. }
  77. }