PromotionFacts.php 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. <?php
  2. namespace App\Services;
  3. use App\Services\Sharing\EffectiveBet;
  4. use Carbon\Carbon;
  5. use Illuminate\Support\Facades\DB;
  6. use Illuminate\Support\Facades\Schema;
  7. use Illuminate\Validation\ValidationException;
  8. /** Promotion eligibility is independent of commission accrual and never uses raw stake as effective stake. */
  9. class PromotionFacts
  10. {
  11. public static function now(): string { return now('Asia/Shanghai')->format('Y-m-d H:i:s'); }
  12. public function policy(): array
  13. {
  14. $row = DB::table('share_policies')->where('effective_date', '<=', substr(self::now(), 0, 10))->orderByDesc('effective_date')->first();
  15. if (!$row || $row->valid_member_min_deposit === null || $row->valid_member_min_effective_bet === null) {
  16. throw ValidationException::withMessages(['policy' => '请先配置已生效的有效会员充值和有效投注门槛']);
  17. }
  18. return ['id' => $row->id, 'effective_date' => $row->effective_date,
  19. 'min_deposit' => (string)$row->valid_member_min_deposit, 'min_effective_bet' => (string)$row->valid_member_min_effective_bet];
  20. }
  21. public function children(int $userId): array
  22. {
  23. return DB::table('share_members as m')->join('users as u', 'u.id', '=', 'm.user_id')
  24. ->where('m.parent_user_id', $userId)->where('u.from', '<>', 2)->where('m.bound_at', '<=', self::now())
  25. ->get(['u.id', 'u.member_id', 'u.created_at', 'm.bound_at'])->map(fn ($r) => (array)$r)->all();
  26. }
  27. public function deposits(array $members, string $from, ?string $until = null, int $cursor = 0): array
  28. {
  29. $totals = [];
  30. if (!$members) return $totals;
  31. $q = DB::table('balance_logs')->whereIn('member_id', $members)->where('type', 1)->where('amount', '>', 0)
  32. ->whereIn('change_type', ['充值', '三方充值'])->where('created_at', '>=', $from)->where('created_at', '<=', self::now())->where('id', '>', $cursor);
  33. if ($until !== null) $q->where('created_at', '<', $until);
  34. foreach ($q->orderBy('id')->cursor() as $row) {
  35. $day = substr($row->created_at, 0, 10);
  36. $totals[$day] = bcadd($totals[$day] ?? '0', (string)$row->amount, 10);
  37. }
  38. return $totals;
  39. }
  40. public function cursors(): array
  41. {
  42. $result = [];
  43. foreach (['share_bet_facts', 'lhc_order_item', 'sport_order', 'sport_game_order', 'third_game_orders', 'bets', 'jisu_game_order'] as $table) {
  44. if (Schema::hasTable($table)) $result[$table] = (int)DB::table($table)->max('id');
  45. }
  46. return $result;
  47. }
  48. public function betting(array $ids, string $from, ?string $until = null, array $cursors = []): array
  49. {
  50. $totals = ['effective' => [], 'profit' => '0'];
  51. if (!$ids) return $totals;
  52. $currency = (string)config('third_game.currency', 'CNY');
  53. $q = DB::table('share_bet_facts')->whereIn('user_id', $ids)->where('bet_at', '>=', $from)
  54. ->where('bet_at', '<=', self::now())->where('currency', $currency)->where('settled', 1)->where('voided', 0);
  55. if ($until !== null) $q->where('bet_at', '<', $until);
  56. foreach ($q->cursor() as $row) {
  57. if (!$this->afterCursor($row, $cursors)) continue;
  58. $this->addBet($totals, $row->bet_at, (string)$row->bet_amount, (string)$row->net_profit);
  59. }
  60. // The legacy Telegram games are not imported into sharing facts. Read only their own sources.
  61. $members = DB::table('users')->whereIn('id', $ids)->pluck('member_id')->all();
  62. foreach (['bets', 'jisu_game_order'] as $table) {
  63. if (!Schema::hasTable($table)) continue;
  64. $q = DB::table($table)->whereIn('member_id', $members)->where('created_at', '>=', $from)->where('created_at', '<=', self::now())
  65. ->where('id', '>', $cursors[$table] ?? 0)->whereIn('status', $table === 'bets' ? [2] : [1, 2]);
  66. if ($until !== null) $q->where('created_at', '<', $until);
  67. foreach ($q->cursor() as $row) {
  68. $profit = bcsub((string)($table === 'bets' ? $row->profit : $row->win_amount), (string)$row->amount, 10);
  69. $this->addBet($totals, $row->created_at, (string)$row->amount, $profit);
  70. }
  71. }
  72. return $totals;
  73. }
  74. private function afterCursor(object $fact, array $cursors): bool
  75. {
  76. if (!$cursors) return true;
  77. if ($fact->source === 'third_game_orders' && isset($cursors['third_game_orders'])) {
  78. $id = DB::table('third_game_orders')->where('order_key', $fact->source_id)->value('id');
  79. return $id !== null && (int)$id > $cursors['third_game_orders'];
  80. }
  81. if (isset($cursors[$fact->source])) return ctype_digit((string)$fact->source_id) && (int)$fact->source_id > $cursors[$fact->source];
  82. return (int)$fact->id > ($cursors['share_bet_facts'] ?? 0);
  83. }
  84. private function addBet(array &$totals, string $at, string $stake, string $profit): void
  85. {
  86. $day = substr($at, 0, 10);
  87. $totals['effective'][$day] = bcadd($totals['effective'][$day] ?? '0', EffectiveBet::amount($stake, $profit), 10);
  88. $totals['profit'] = bcadd($totals['profit'], $profit, 10);
  89. }
  90. public function qualified(array $child, array $policy, ?string $before = null): bool
  91. {
  92. $deposits = $this->deposits([(string)$child['member_id']], '1970-01-01 00:00:00', $before);
  93. $bet = $this->betting([(int)$child['id']], '1970-01-01 00:00:00', $before);
  94. return bccomp($this->total($deposits), $policy['min_deposit'], 10) >= 0
  95. && bccomp($this->total($bet['effective']), $policy['min_effective_bet'], 10) >= 0;
  96. }
  97. public function total(array $amounts): string
  98. {
  99. $total = '0'; foreach ($amounts as $amount) $total = bcadd($total, (string)$amount, 10); return $total;
  100. }
  101. public function logins(string $memberId, string $from): array
  102. {
  103. $days = [];
  104. foreach (DB::table('user_login')->where('user_id', $memberId)->where('status', 1)->where('created_at', '>=', $from)
  105. ->where('created_at', '<=', self::now())->cursor() as $row) $days[substr($row->created_at, 0, 10)] = true;
  106. $userId = DB::table('users')->where('member_id', $memberId)->value('id');
  107. foreach (['promotion_presence', 'reporting_activity'] as $table) {
  108. if (!Schema::hasTable($table)) continue;
  109. foreach (DB::table($table)->where('user_id', $userId)->where('observed_at', '>=', $from)->where('observed_at', '<=', self::now())->cursor() as $row) $days[substr($row->observed_at, 0, 10)] = true;
  110. }
  111. return $days;
  112. }
  113. public function online(int $userId, string $from): array
  114. {
  115. $last = []; $intervals = []; $observations = [];
  116. foreach (['reporting_activity', 'promotion_presence'] as $table) {
  117. if (!Schema::hasTable($table)) continue;
  118. foreach (DB::table($table)->where('user_id', $userId)->where('observed_at', '>=', $from)->where('observed_at', '<=', self::now())->cursor() as $row) $observations[] = $row;
  119. }
  120. usort($observations, fn ($a, $b) => strcmp($a->observed_at, $b->observed_at));
  121. foreach ($observations as $row) {
  122. $at = Carbon::parse($row->observed_at, 'Asia/Shanghai')->timestamp;
  123. $key = $row->session_hash;
  124. if (isset($last[$key]) && $at > $last[$key] && $at - $last[$key] <= 300) {
  125. $start = $last[$key];
  126. while ($start < $at) {
  127. $day = Carbon::createFromTimestamp($start, 'Asia/Shanghai')->toDateString();
  128. $end = min($at, Carbon::parse($day, 'Asia/Shanghai')->addDay()->timestamp);
  129. $intervals[$day][] = [$start, $end]; $start = $end;
  130. }
  131. }
  132. $last[$key] = $at;
  133. }
  134. $result = [];
  135. foreach ($intervals as $day => $list) {
  136. usort($list, fn ($a, $b) => $a[0] <=> $b[0]); $end = 0; $seconds = 0;
  137. foreach ($list as [$a, $b]) { if ($b > max($a, $end)) $seconds += $b - max($a, $end); $end = max($end, $b); }
  138. $result[$day] = $seconds;
  139. }
  140. return $result;
  141. }
  142. }