| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106 |
- <?php
- namespace App\Services;
- use App\Services\Sharing\EffectiveBet;
- use Carbon\Carbon;
- use Illuminate\Support\Facades\DB;
- use Illuminate\Support\Facades\Log;
- use InvalidArgumentException;
- use Throwable;
- /**
- * Keeps unpaid rebates (New York day) and backflow (Shanghai month) rows in step with share_bet_facts,
- * so admin lists show every game category. Paid rows are never touched; payout recomputes on its own.
- */
- class RewardBaseSyncService
- {
- private const CURSOR = 'reward_bases_cursor';
- public function run(?string $from = null, ?string $to = null): array
- {
- foreach ([$from, $to] as $date) {
- if ($date !== null && (!preg_match('/^\d{4}-\d{2}-\d{2}$/D', $date) || Carbon::parse($date)->toDateString() !== $date)) {
- throw new InvalidArgumentException('Dates must use Y-m-d');
- }
- }
- if (($from === null) !== ($to === null) || ($from !== null && $from > $to)) throw new InvalidArgumentException('Invalid date range');
- $result = ['rebates' => 0, 'backflow' => 0, 'failed' => 0];
- $startedAt = now()->setTimezone('Asia/Shanghai')->format('Y-m-d H:i:s');
- $query = DB::table('share_bet_facts')->select(['id', 'user_id', 'bet_at', 'bet_date']);
- if ($from !== null) {
- // A New York day spans two Shanghai dates.
- $query->whereBetween('bet_date', [$from, Carbon::parse($to)->addDay()->toDateString()]);
- } else {
- $cursor = DB::table('share_system_state')->where('key', self::CURSOR)->value('value');
- $since = $cursor ? EffectiveBet::time(json_decode($cursor, true, 512, JSON_THROW_ON_ERROR)['updated_at'])->subMinutes(5)
- : now()->subDays(2);
- $query->where('updated_at', '>=', $since->setTimezone('Asia/Shanghai')->format('Y-m-d H:i:s'));
- }
- $days = $months = [];
- $query->orderBy('id')->chunkById(500, function ($facts) use (&$days, &$months, $from, $to) {
- foreach ($facts as $fact) {
- $day = Carbon::parse($fact->bet_at, 'Asia/Shanghai')->setTimezone('America/New_York')->toDateString();
- if ($from === null || ($day >= $from && $day <= $to)) $days[$fact->user_id . '|' . $day] = [$fact->user_id, $day];
- $months[$fact->user_id . '|' . substr((string) $fact->bet_date, 0, 7)] = [$fact->user_id, substr((string) $fact->bet_date, 0, 7)];
- }
- });
- $users = [];
- foreach (['rebate' => $days, 'backflow' => $months] as $type => $keys) {
- foreach ($keys as [$userId, $period]) {
- try {
- $user = $users[$userId] ??= DB::table('users')->where('id', $userId)->first(['id', 'member_id', 'level', 'username', 'first_name']);
- if (!$user) continue;
- if ($type === 'rebate' ? $this->rebate($user, $period) : $this->backflow($user, $period)) {
- ++$result[$type === 'rebate' ? 'rebates' : 'backflow'];
- }
- } catch (Throwable $e) {
- ++$result['failed'];
- Log::error('reward_base_sync_failed', ['type' => $type, 'user_id' => $userId, 'period' => $period, 'exception' => $e]);
- }
- }
- }
- if ($from === null && $result['failed'] === 0) {
- DB::table('share_system_state')->updateOrInsert(['key' => self::CURSOR],
- ['value' => json_encode(['updated_at' => $startedAt]), 'updated_at' => $startedAt, 'created_at' => $startedAt]);
- }
- return $result;
- }
- private function rebate(object $user, string $date): bool
- {
- return DB::transaction(function () use ($user, $date) {
- $row = DB::table('rebates')->where('member_id', $user->member_id)->where('date', $date)->lockForUpdate()->first();
- if ($row && (int) $row->status !== 0) return false;
- $reward = RewardBasisService::rebateReward(RewardBasisService::rebateBasis($user, $date), (int) ($user->level ?? 0));
- if (!$reward['has_facts'] && !$row) return false;
- $values = ['betting_amount' => $reward['betting_amount'], 'effective_betting_amount' => $reward['effective_amount'],
- 'profit' => $reward['profit'], 'amount' => $reward['amount'], 'need_flow' => $reward['need_flow'],
- 'details' => json_encode($reward['details'], JSON_UNESCAPED_UNICODE),
- 'username' => (string) ($user->username ?? ''), 'first_name' => (string) ($user->first_name ?? ''),
- 'updated_at' => now()];
- if ($row) {
- // Rows without facts are legacy PC28 days; their stored basis stays the payout basis.
- if (!$reward['has_facts']) return false;
- return DB::table('rebates')->where('id', $row->id)->where('status', 0)->update($values) > 0;
- }
- DB::table('rebates')->insert($values + ['member_id' => $user->member_id, 'date' => $date, 'status' => 0, 'created_at' => now()]);
- return true;
- }, 3);
- }
- private function backflow(object $user, string $month): bool
- {
- return DB::transaction(function () use ($user, $month) {
- $row = DB::table('backflow')->where('member_id', $user->member_id)->where('date', $month)->lockForUpdate()->first();
- if ($row && (int) $row->status !== 0) return false;
- $reward = RewardBasisService::backflowReward(RewardBasisService::backflowBasis($user, $month));
- if (!$reward['has_facts'] && !$row) return false;
- $values = ['loss_amount' => $reward['loss_amount'], 'amount' => $reward['amount'], 'need_flow' => $reward['need_flow'],
- 'details' => json_encode($reward['details'], JSON_UNESCAPED_UNICODE), 'updated_at' => now()];
- if ($row) return DB::table('backflow')->where('id', $row->id)->where('status', 0)->update($values) > 0;
- DB::table('backflow')->insert($values + ['member_id' => $user->member_id, 'date' => $month, 'status' => 0, 'created_at' => now()]);
- return true;
- }, 3);
- }
- }
|