| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156 |
- <?php
- namespace App\Services;
- use App\Services\Sharing\EffectiveBet;
- use Carbon\Carbon;
- use Illuminate\Support\Facades\DB;
- use Illuminate\Support\Facades\Schema;
- use Illuminate\Validation\ValidationException;
- /** Promotion eligibility is independent of commission accrual and never uses raw stake as effective stake. */
- class PromotionFacts
- {
- public static function now(): string { return now('Asia/Shanghai')->format('Y-m-d H:i:s'); }
- public function policy(): array
- {
- $row = DB::table('share_policies')->where('effective_date', '<=', substr(self::now(), 0, 10))->orderByDesc('effective_date')->first();
- if (!$row || $row->valid_member_min_deposit === null || $row->valid_member_min_effective_bet === null) {
- throw ValidationException::withMessages(['policy' => '请先配置已生效的有效会员充值和有效投注门槛']);
- }
- return ['id' => $row->id, 'effective_date' => $row->effective_date,
- 'min_deposit' => (string)$row->valid_member_min_deposit, 'min_effective_bet' => (string)$row->valid_member_min_effective_bet];
- }
- public function children(int $userId): array
- {
- return DB::table('share_members as m')->join('users as u', 'u.id', '=', 'm.user_id')
- ->where('m.parent_user_id', $userId)->where('u.from', '<>', 2)->where('m.bound_at', '<=', self::now())
- ->get(['u.id', 'u.member_id', 'u.created_at', 'm.bound_at'])->map(fn ($r) => (array)$r)->all();
- }
- public function deposits(array $members, string $from, ?string $until = null, int $cursor = 0): array
- {
- $totals = [];
- if (!$members) return $totals;
- $q = DB::table('balance_logs')->whereIn('member_id', $members)->where('type', 1)->where('amount', '>', 0)
- ->whereIn('change_type', ['充值', '三方充值'])->where('created_at', '>=', $from)->where('created_at', '<=', self::now())->where('id', '>', $cursor);
- if ($until !== null) $q->where('created_at', '<', $until);
- foreach ($q->orderBy('id')->cursor() as $row) {
- $day = substr($row->created_at, 0, 10);
- $totals[$day] = bcadd($totals[$day] ?? '0', (string)$row->amount, 10);
- }
- return $totals;
- }
- public function cursors(): array
- {
- $result = [];
- foreach (['share_bet_facts', 'lhc_order_item', 'sport_order', 'sport_game_order', 'third_game_orders', 'bets', 'jisu_game_order'] as $table) {
- if (Schema::hasTable($table)) $result[$table] = (int)DB::table($table)->max('id');
- }
- return $result;
- }
- public function betting(array $ids, string $from, ?string $until = null, array $cursors = []): array
- {
- $totals = ['effective' => [], 'profit' => '0'];
- if (!$ids) return $totals;
- $currency = (string)config('third_game.currency', 'CNY');
- $q = DB::table('share_bet_facts')->whereIn('user_id', $ids)->where('bet_at', '>=', $from)
- ->where('bet_at', '<=', self::now())->where('currency', $currency)->where('settled', 1)->where('voided', 0);
- if ($until !== null) $q->where('bet_at', '<', $until);
- foreach ($q->cursor() as $row) {
- if (!$this->afterCursor($row, $cursors)) continue;
- $this->addBet($totals, $row->bet_at, (string)$row->bet_amount, (string)$row->net_profit);
- }
- // The legacy Telegram games are not imported into sharing facts. Read only their own sources.
- $members = DB::table('users')->whereIn('id', $ids)->pluck('member_id')->all();
- foreach (['bets', 'jisu_game_order'] as $table) {
- if (!Schema::hasTable($table)) continue;
- $q = DB::table($table)->whereIn('member_id', $members)->where('created_at', '>=', $from)->where('created_at', '<=', self::now())
- ->where('id', '>', $cursors[$table] ?? 0)->whereIn('status', $table === 'bets' ? [2] : [1, 2]);
- if ($until !== null) $q->where('created_at', '<', $until);
- foreach ($q->cursor() as $row) {
- $profit = bcsub((string)($table === 'bets' ? $row->profit : $row->win_amount), (string)$row->amount, 10);
- $this->addBet($totals, $row->created_at, (string)$row->amount, $profit);
- }
- }
- return $totals;
- }
- private function afterCursor(object $fact, array $cursors): bool
- {
- if (!$cursors) return true;
- if ($fact->source === 'third_game_orders' && isset($cursors['third_game_orders'])) {
- $id = DB::table('third_game_orders')->where('order_key', $fact->source_id)->value('id');
- return $id !== null && (int)$id > $cursors['third_game_orders'];
- }
- if (isset($cursors[$fact->source])) return ctype_digit((string)$fact->source_id) && (int)$fact->source_id > $cursors[$fact->source];
- return (int)$fact->id > ($cursors['share_bet_facts'] ?? 0);
- }
- private function addBet(array &$totals, string $at, string $stake, string $profit): void
- {
- $day = substr($at, 0, 10);
- $totals['effective'][$day] = bcadd($totals['effective'][$day] ?? '0', EffectiveBet::amount($stake, $profit), 10);
- $totals['profit'] = bcadd($totals['profit'], $profit, 10);
- }
- public function qualified(array $child, array $policy, ?string $before = null): bool
- {
- $deposits = $this->deposits([(string)$child['member_id']], '1970-01-01 00:00:00', $before);
- $bet = $this->betting([(int)$child['id']], '1970-01-01 00:00:00', $before);
- return bccomp($this->total($deposits), $policy['min_deposit'], 10) >= 0
- && bccomp($this->total($bet['effective']), $policy['min_effective_bet'], 10) >= 0;
- }
- public function total(array $amounts): string
- {
- $total = '0'; foreach ($amounts as $amount) $total = bcadd($total, (string)$amount, 10); return $total;
- }
- public function logins(string $memberId, string $from): array
- {
- $days = [];
- foreach (DB::table('user_login')->where('user_id', $memberId)->where('status', 1)->where('created_at', '>=', $from)
- ->where('created_at', '<=', self::now())->cursor() as $row) $days[substr($row->created_at, 0, 10)] = true;
- $userId = DB::table('users')->where('member_id', $memberId)->value('id');
- foreach (['promotion_presence', 'reporting_activity'] as $table) {
- if (!Schema::hasTable($table)) continue;
- 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;
- }
- return $days;
- }
- public function online(int $userId, string $from): array
- {
- $last = []; $intervals = []; $observations = [];
- foreach (['reporting_activity', 'promotion_presence'] as $table) {
- if (!Schema::hasTable($table)) continue;
- foreach (DB::table($table)->where('user_id', $userId)->where('observed_at', '>=', $from)->where('observed_at', '<=', self::now())->cursor() as $row) $observations[] = $row;
- }
- usort($observations, fn ($a, $b) => strcmp($a->observed_at, $b->observed_at));
- foreach ($observations as $row) {
- $at = Carbon::parse($row->observed_at, 'Asia/Shanghai')->timestamp;
- $key = $row->session_hash;
- if (isset($last[$key]) && $at > $last[$key] && $at - $last[$key] <= 300) {
- $start = $last[$key];
- while ($start < $at) {
- $day = Carbon::createFromTimestamp($start, 'Asia/Shanghai')->toDateString();
- $end = min($at, Carbon::parse($day, 'Asia/Shanghai')->addDay()->timestamp);
- $intervals[$day][] = [$start, $end]; $start = $end;
- }
- }
- $last[$key] = $at;
- }
- $result = [];
- foreach ($intervals as $day => $list) {
- usort($list, fn ($a, $b) => $a[0] <=> $b[0]); $end = 0; $seconds = 0;
- foreach ($list as [$a, $b]) { if ($b > max($a, $end)) $seconds += $b - max($a, $end); $end = max($end, $b); }
- $result[$day] = $seconds;
- }
- return $result;
- }
- }
|