SimulationMemberService.php 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. <?php
  2. namespace App\Services\Simulation;
  3. use App\Constants\HttpStatus;
  4. use App\Models\User;
  5. use App\Models\UserLogin;
  6. use App\Models\Wallet;
  7. use App\Support\MemberAudience;
  8. use Exception;
  9. use Illuminate\Support\Facades\DB;
  10. use Illuminate\Support\Facades\Schema;
  11. /** 后台模拟会员:users.user_type = 2、from = 2、没有代理和上级,不能登录前台。 */
  12. final class SimulationMemberService
  13. {
  14. /** @return list<array{id:int,member_id:string,account:string}> */
  15. public static function create(int $count, array $options = []): array
  16. {
  17. SimulationSettings::assertEnabled();
  18. $settings = SimulationSettings::all();
  19. if ($count < 1 || $count > (int) $settings['simulation_max_members_per_task']) {
  20. throw new Exception('一次最多创建 ' . $settings['simulation_max_members_per_task'] . ' 个模拟会员', HttpStatus::CUSTOM_ERROR);
  21. }
  22. $existing = DB::table('users')->where('user_type', MemberAudience::TYPE_SIMULATED)->count();
  23. if ($existing + $count > (int) $settings['simulation_max_members_total']) {
  24. throw new Exception('模拟会员总数将超过上限 ' . $settings['simulation_max_members_total'], HttpStatus::CUSTOM_ERROR);
  25. }
  26. $domain = trim((string) ($options['domain'] ?? '')) ?: $settings['simulation_default_domain'];
  27. $initial = $options['initial_balance'] ?? $settings['simulation_initial_balance'];
  28. $created = [];
  29. for ($i = 0; $i < $count; $i++) {
  30. $user = DB::transaction(function () use ($domain) {
  31. $sn = self::serial();
  32. $user = (new User())->forceFill([
  33. 'member_id' => $sn, 'user_id' => $sn, 'account' => 'sim_' . $sn, 'first_name' => '模拟_' . substr($sn, -4),
  34. 'username' => '', 'from' => '2', 'user_type' => MemberAudience::TYPE_SIMULATED,
  35. 'register_ip' => '127.0.0.1', 'register_domain' => $domain, 'last_active_time' => time(),
  36. ]);
  37. $user->save();
  38. (new Wallet())->forceFill(['user_id' => $user->id, 'member_id' => $sn, 'coin' => 'USDT', 'net' => 'TRC20', 'available_balance' => 0])->save();
  39. return $user;
  40. });
  41. MemberAudience::remember($user->member_id);
  42. if (is_numeric($initial) && $initial > 0) SimulationFundService::adjust($user->member_id, $initial, 'credit', '模拟会员初始余额');
  43. $created[] = ['id' => (int) $user->id, 'member_id' => (string) $user->member_id, 'account' => (string) $user->account];
  44. }
  45. return $created;
  46. }
  47. /** 8 位 1-9 数字,与前台会员编号同形;纯数字是因为 wallets.member_id 是 bigint。 */
  48. private static function serial(): string
  49. {
  50. for ($try = 0; $try < 50; $try++) {
  51. $sn = '';
  52. for ($i = 0; $i < 8; $i++) $sn .= (string) random_int(1, 9);
  53. if (!DB::table('users')->where('member_id', $sn)->orWhere('user_id', $sn)->exists()) return $sn;
  54. }
  55. throw new Exception('会员编号生成失败,请重试', HttpStatus::CUSTOM_ERROR);
  56. }
  57. public static function setBanned($memberId, int $banned): void
  58. {
  59. $user = SimulationSettings::member($memberId);
  60. DB::table('users')->where('id', $user->id)->update(['is_banned' => $banned ? 1 : 0]);
  61. }
  62. /** 只写登录日志,不建会话、不签发 token:模拟会员没有可登录前台的凭证。 */
  63. public static function login($memberId, string $platform = 'H5', ?string $domain = null): array
  64. {
  65. SimulationSettings::assertEnabled();
  66. $user = SimulationSettings::member($memberId);
  67. self::logout($memberId);
  68. $row = ['user_id' => (string) $user->user_id, 'login_account' => (string) $user->account, 'login_ip' => '127.0.0.1',
  69. 'login_domain' => $domain ?: SimulationSettings::get('simulation_default_domain'), 'platform' => $platform, 'status' => UserLogin::STATUS_SUCCESS,
  70. 'created_at' => now(), 'updated_at' => now()];
  71. $row = array_filter($row, fn ($column) => Schema::hasColumn('user_login', $column), ARRAY_FILTER_USE_KEY);
  72. $id = DB::table('user_login')->insertGetId($row);
  73. $update = ['last_active_time' => time()];
  74. if (Schema::hasColumn('users', 'last_login_time')) $update += ['last_login_time' => time(), 'login_count' => DB::raw('COALESCE(login_count, 0) + 1')];
  75. DB::table('users')->where('id', $user->id)->update($update);
  76. return ['id' => $id, 'member_id' => (string) $user->member_id];
  77. }
  78. public static function logout($memberId): int
  79. {
  80. $user = SimulationSettings::member($memberId);
  81. if (!Schema::hasColumn('user_login', 'logout_time')) return 0;
  82. $open = DB::table('user_login')->where('user_id', (string) $user->user_id)->where('status', UserLogin::STATUS_SUCCESS)->whereNull('logout_time')->get(['id', 'created_at']);
  83. foreach ($open as $row) {
  84. DB::table('user_login')->where('id', $row->id)->update(['logout_time' => now(),
  85. 'online_duration' => max(0, now()->getTimestamp() - strtotime((string) $row->created_at)), 'updated_at' => now()]);
  86. }
  87. return $open->count();
  88. }
  89. /** 当前“在线”的模拟会员:有未登出的成功登录记录。 */
  90. public static function onlineUserIds()
  91. {
  92. $query = DB::table('user_login')->where('status', UserLogin::STATUS_SUCCESS)->whereIn('user_id', MemberAudience::simulatedMembers('user_id'));
  93. if (Schema::hasColumn('user_login', 'logout_time')) $query->whereNull('logout_time');
  94. return $query->distinct()->pluck('user_id');
  95. }
  96. public static function detail($memberId): array
  97. {
  98. $user = SimulationSettings::member($memberId);
  99. $wallet = DB::table('wallets')->where('member_id', $user->member_id)->first();
  100. return [
  101. 'user' => ['id' => $user->id, 'member_id' => $user->member_id, 'account' => $user->account, 'first_name' => $user->first_name,
  102. 'is_banned' => (int) ($user->is_banned ?? 0), 'register_domain' => $user->register_domain ?? '', 'created_at' => $user->created_at],
  103. 'wallet' => $wallet ? ['available_balance' => (string) $wallet->available_balance, 'frozen_balance' => (string) $wallet->frozen_balance,
  104. 'wager_required' => (string) ($wallet->wager_required ?? 0), 'wager_completed' => (string) ($wallet->wager_completed ?? 0)] : null,
  105. 'logs' => DB::table('balance_logs')->where('member_id', $user->member_id)->orderByDesc('id')->limit(20)->get(),
  106. ];
  107. }
  108. }