format('Y-m-d'); $yesterday = Carbon::yesterday()->format('Y-m-d'); $tiers = $this->getTierConfig(); $yesterdayDaily = $this->getOrCreateDaily($yesterday); $todayDaily = $this->getOrCreateDaily($today); $userTask = $this->getUserTask($userId, $today); $multiplierRow = $this->getUserMultiplier($userId); $rechargeToday = $this->getUserRechargeForDate($userId, $today); $turnoverToday = $this->getUserTotalBetForDate($userId, $today); $turnoverProgress = (int) $turnoverToday; $tierConfig = $userTask ? $this->getTierConfigByTier($userTask->tier) : null; $rechargeRequired = $tierConfig ? (int) $tierConfig->recharge_required : 0; $turnoverRequired = $tierConfig ? (int) $tierConfig->turnover_required : 0; $rechargeDisplay = $rechargeToday; $turnoverDisplay = $turnoverProgress / NumConfig::NUM_VALUE; $taskCompleted = $tierConfig && $rechargeDisplay >= $rechargeRequired && $turnoverDisplay >= $turnoverRequired; $canUpgrade = $userTask && $userTask->tier !== self::TIER_MAX && $taskCompleted; $canClaim = $taskCompleted && $userTask && (int) $userTask->status === 0; $yesterdayBalls = $this->getUserBalls($userId, $yesterday); $yesterdayLucky = (int) ($yesterdayDaily->lucky_number ?? 0); $yesterdayPrizeLog = $this->getUserPrizeLog($userId, $yesterday); $yesterdayBasePerBall = 0; if ($yesterdayDaily->total_balls > 0 && $yesterdayDaily->pool_amount > 0) { $yesterdayBasePerBall = (int) ($yesterdayDaily->pool_amount / $yesterdayDaily->total_balls); } $yesterdayMyPrize = $yesterdayPrizeLog ? (int) $yesterdayPrizeLog->total_amount : 0; $yesterdayMultiplier = $yesterdayPrizeLog ? (float) $yesterdayPrizeLog->multiplier : 1.0; $canClaimYesterday = false; $yesterdayPendingPrize = 0; if (!$yesterdayPrizeLog && count($yesterdayBalls) > 0) { $canClaimYesterday = true; $yesterdayPendingPrize = $this->calculateYesterdayPrizeForUser($userId, $yesterday); } $todayBasePerBall = 0; if ($todayDaily->total_balls > 0 && $todayDaily->pool_amount > 0) { $todayBasePerBall = (int) ($todayDaily->pool_amount / $todayDaily->total_balls); } $todayBalls = $this->getUserBalls($userId, $today); $todayMyBallsList = array_map(function ($b) { return ['ball_index' => (int) $b->ball_index, 'number' => (int) $b->number]; }, $todayBalls); $todayNumberCounts = []; foreach ($todayBalls as $b) { $n = (int) $b->number; $todayNumberCounts[$n] = ($todayNumberCounts[$n] ?? 0) + 1; } // 0 点到 1 点前,前端展示的今日整体数据全部置为 0(不影响实际统计与任务进度) $hour = (int) Carbon::now()->format('G'); // 0-23 if ($hour < 1) { $todayDisplayPoolAmount = 0; $todayDisplayCompleted = 0; $todayDisplayTotalBalls = 0; $todayDisplayBasePerBall = 0; $todayDisplayMyBalls = []; $todayDisplayNumberCounts = []; } else { $todayDisplayPoolAmount = (int) $todayDaily->pool_amount; $todayDisplayCompleted = (int) ($todayDaily->completed_count ?? 0); $todayDisplayTotalBalls = (int) ($todayDaily->total_balls ?? 0); $todayDisplayBasePerBall = $todayBasePerBall; $todayDisplayMyBalls = $todayMyBallsList; $todayDisplayNumberCounts = $todayNumberCounts; } $last7Lucky = $this->getLast7DaysLuckyNumbersPrivate(); return [ 'yesterday' => [ 'pool_amount' => (int) $yesterdayDaily->pool_amount, 'pool_amount_display' => (int) $yesterdayDaily->pool_amount / NumConfig::NUM_VALUE, 'base_reward_per_ball' => $yesterdayBasePerBall, 'base_reward_per_ball_display' => $yesterdayBasePerBall / NumConfig::NUM_VALUE, 'lucky_number' => $yesterdayLucky, 'completed_count' => (int) ($yesterdayDaily->completed_count ?? 0), 'total_balls' => (int) ($yesterdayDaily->total_balls ?? 0), 'my_balls' => $yesterdayBalls, 'my_balls_with_lucky' => array_map(function ($b) use ($yesterdayLucky) { return ['number' => (int) $b->number, 'is_lucky' => (int) $b->number === $yesterdayLucky]; }, $yesterdayBalls), 'my_prize' => $yesterdayMyPrize, 'my_prize_display' => $yesterdayMyPrize / NumConfig::NUM_VALUE, 'my_multiplier' => $yesterdayMultiplier, 'can_claim_yesterday' => $canClaimYesterday, 'pending_prize' => $yesterdayPendingPrize, 'pending_prize_display' => $yesterdayPendingPrize / NumConfig::NUM_VALUE, ], 'today' => [ 'pool_amount' => $todayDisplayPoolAmount, 'pool_amount_display' => $todayDisplayPoolAmount / NumConfig::NUM_VALUE, 'completed_count' => $todayDisplayCompleted, 'total_balls' => $todayDisplayTotalBalls, 'base_reward_per_ball' => $todayDisplayBasePerBall, 'base_reward_per_ball_display' => $todayDisplayBasePerBall / NumConfig::NUM_VALUE, 'my_balls' => $todayDisplayMyBalls, 'number_counts' => $todayDisplayNumberCounts, ], 'tiers' => $tiers, 'user_task' => $userTask ? [ 'tier' => $userTask->tier, 'recharge_required' => $rechargeRequired, 'turnover_required' => $turnoverRequired, 'recharge_progress' => $rechargeDisplay, 'turnover_progress' => $turnoverDisplay, 'task_completed' => $taskCompleted, 'status' => (int) $userTask->status, 'can_claim' => $canClaim, 'can_upgrade' => $canUpgrade, 'ball_count' => $tierConfig ? (int) $tierConfig->ball_count : 0, ] : null, 'multiplier' => [ 'value' => (float) ($multiplierRow->multiplier ?? 1.0), 'consecutive_days' => (int) ($multiplierRow->consecutive_days ?? 0), 'min' => self::MULTIPLIER_MIN, 'max' => self::MULTIPLIER_MAX, 'step' => self::MULTIPLIER_STEP, ], 'lucky_reward_per_ball' => self::LUCKY_REWARD_PER_BALL, 'lucky_numbers_7_days' => $last7Lucky, ]; } /** * Select task tier for today (with optional confirm). Creates or updates user task. * @return array success: ['success' => true] | failure: ['success' => false, 'message' => ['key', 'fallback']] */ public function selectTier(int $userId, string $tier): array { $today = Carbon::today()->format('Y-m-d'); $config = $this->getTierConfigByTier($tier); if (!$config) { return ['success' => false, 'message' => ['web.superball.activity_not_found', 'Activity not found']]; } $existing = $this->getUserTask($userId, $today); if ($existing) { return ['success' => false, 'message' => ['web.superball.tier_already_selected', 'Already selected tier for today']]; } DB::connection('write')->table(TableName::agent() . 'superball_user_task')->insert([ 'user_id' => $userId, 'task_date' => $today, 'tier' => $tier, 'total_bet_snapshot' => 0, 'status' => 0, 'created_at' => now()->format('Y-m-d H:i:s'), 'updated_at' => now()->format('Y-m-d H:i:s'), ]); return ['success' => true]; } /** * Upgrade to higher tier (keep progress). Only allowed when task completed and not A. * @return array success: ['success' => true] | failure: ['success' => false, 'message' => [...]] */ public function upgradeTier(int $userId, string $newTier): array { $today = Carbon::today()->format('Y-m-d'); $task = $this->getUserTask($userId, $today); if (!$task) { return ['success' => false, 'message' => ['web.superball.no_task_today', 'No task for today']]; } if ((int) $task->status === 1) { return ['success' => false, 'message' => ['web.superball.already_claimed', 'Already claimed']]; } $tierOrder = ['E' => 1, 'D' => 2, 'C' => 3, 'B' => 4, 'A' => 5]; $currentOrder = $tierOrder[$task->tier] ?? 0; $newOrder = $tierOrder[$newTier] ?? 0; if ($newOrder <= $currentOrder) { return ['success' => false, 'message' => ['web.superball.cannot_downgrade', 'Can only upgrade to higher tier']]; } $rechargeToday = $this->getUserRechargeForDate($userId, $today); $turnoverToday = $this->getUserTotalBetForDate($userId, $today); $newConfig = $this->getTierConfigByTier($task->tier); $rechargeOk = $rechargeToday >= (int) $newConfig->recharge_required; $turnoverOk = ($turnoverToday / NumConfig::NUM_VALUE) >= (int) $newConfig->turnover_required; // var_dump($rechargeToday,$turnoverToday,$newConfig); if (!$rechargeOk || !$turnoverOk) { return ['success' => false, 'message' => ['web.superball.task_not_completed', 'Task not completed']]; } DB::connection('write')->table(TableName::agent() . 'superball_user_task') ->where('user_id', $userId) ->where('task_date', $today) ->update(['tier' => $newTier, 'updated_at' => now()->format('Y-m-d H:i:s')]); return ['success' => true,'user_id' => $userId, 'new_tier' => $newTier,'task_date'=>$today]; } /** * Claim reward: grant balls, update daily total_balls/completed_count, update multiplier, mark task claimed. * @return array success: ['ball_count' => n, 'message' => ...] | failure: ['success' => false, 'message' => [...]] */ public function claimReward(int $userId): array { $today = Carbon::today()->format('Y-m-d'); $task = $this->getUserTask($userId, $today); if (!$task) { return ['success' => false, 'message' => ['web.superball.no_task_today', 'No task for today']]; } if ((int) $task->status === 1) { return ['success' => false, 'message' => ['web.superball.already_claimed', 'Already claimed']]; } $tierConfig = $this->getTierConfigByTier($task->tier); if (!$tierConfig) { return ['success' => false, 'message' => ['web.superball.activity_not_found', 'Activity not found']]; } $rechargeToday = $this->getUserRechargeForDate($userId, $today); $turnoverToday = $this->getUserTotalBetForDate($userId, $today); $rechargeOk = ($rechargeToday) >= (int) $tierConfig->recharge_required; $turnoverOk = ($turnoverToday / NumConfig::NUM_VALUE) >= (int) $tierConfig->turnover_required; if (!$rechargeOk || !$turnoverOk) { return ['success' => false, 'message' => ['web.superball.task_not_completed', 'Task not completed']]; } $ballCount = (int) $tierConfig->ball_count; DB::connection('write')->transaction(function () use ($userId, $today, $task, $ballCount) { DB::connection('write')->table(TableName::agent() . 'superball_user_task') ->where('user_id', $userId) ->where('task_date', $today) ->update(['status' => 1, 'updated_at' => now()->format('Y-m-d H:i:s')]); $daily = DB::connection('write')->table(TableName::agent() . 'superball_daily') ->where('pool_date', $today) ->lockForUpdate() ->first(); if ($daily) { DB::connection('write')->table(TableName::agent() . 'superball_daily') ->where('pool_date', $today) ->update([ 'total_balls' => (int) $daily->total_balls + $ballCount, 'completed_count' => (int) $daily->completed_count + 1, 'updated_at' => now()->format('Y-m-d H:i:s'), ]); } $this->updateUserMultiplier($userId, $today); }); return [ 'ball_count' => $ballCount, 'message' => 'Claim success, please select numbers for your balls', ]; } /** * Submit numbers for all balls (0-9 per ball). Must have claimed and not yet submitted. * @return array success: ['success' => true] | failure: ['success' => false, 'message' => [...]] */ public function submitNumbers(int $userId, array $numbers): array { $today = Carbon::today()->format('Y-m-d'); $task = $this->getUserTask($userId, $today); if (!$task || (int) $task->status !== 1) { return ['success' => false, 'message' => ['web.superball.no_claimed_task', 'No claimed task for today']]; } $tierConfig = $this->getTierConfigByTier($task->tier); if (!$tierConfig) { return ['success' => false, 'message' => ['web.superball.activity_not_found', 'Activity not found']]; } $ballCount = (int) $tierConfig->ball_count; if (count($numbers) !== $ballCount) { return ['success' => false, 'message' => ['web.superball.number_count_mismatch', 'Number count mismatch']]; } $existing = DB::connection('write')->table(TableName::agent() . 'superball_user_balls') ->where('user_id', $userId) ->where('ball_date', $today) ->count(); if ($existing > 0) { return ['success' => false, 'message' => ['web.superball.numbers_already_submitted', 'Numbers already submitted']]; } DB::connection('write')->transaction(function () use ($userId, $today, $numbers) { // 写入用户今日所有球号 foreach ($numbers as $index => $num) { $n = (int) $num; if ($n < 0 || $n > 9) { throw new \RuntimeException('Number must be 0-9'); } DB::connection('write')->table(TableName::agent() . 'superball_user_balls')->insert([ 'user_id' => $userId, 'ball_date' => $today, 'ball_index' => $index + 1, 'number' => $n, 'created_at' => now()->format('Y-m-d H:i:s'), ]); } // 若用户选择的号码等于当日 lucky_number,则把 superball_daily.lucky_count 累加 $daily = DB::connection('write') ->table(TableName::agent() . 'superball_daily') ->where('pool_date', $today) ->lockForUpdate() ->first(); if ($daily) { $luckyNumber = (int) $daily->lucky_number; $matched = 0; foreach ($numbers as $num) { if ((int) $num === $luckyNumber) { $matched++; } } if ($matched > 0) { DB::connection('write') ->table(TableName::agent() . 'superball_daily') ->where('pool_date', $today) ->update([ 'lucky_count' => (int) $daily->lucky_count + $matched, 'updated_at' => now()->format('Y-m-d H:i:s'), ]); } } }); return ['success' => true]; } /** * Get user's balls for a date (for number selection page or display). */ public function getMyBalls(int $userId, string $date): array { $rows = DB::table(TableName::agent() . 'superball_user_balls') ->where('user_id', $userId) ->where('ball_date', $date) ->orderBy('ball_index') ->get(); return array_map(function ($r) { return ['ball_index' => (int) $r->ball_index, 'number' => (int) $r->number]; }, $rows->all()); } /** * User claims yesterday's reward (next-day claim, no auto distribution). * Formula: base = pool / total_balls * ball_count, lucky = 10 * matched_balls (display), total = base * multiplier + lucky. * @return array success: data with total_amount etc. | failure: ['success' => false, 'message' => [...]] */ public function claimYesterdayReward(int $userId): array { $yesterday = Carbon::yesterday()->format('Y-m-d'); $existing = $this->getUserPrizeLog($userId, $yesterday); if ($existing) { return ['success' => false, 'message' => ['web.superball.yesterday_already_claimed', 'Yesterday reward already claimed']]; } $balls = $this->getUserBalls($userId, $yesterday); if (count($balls) === 0) { return ['success' => false, 'message' => ['web.superball.no_balls_yesterday', 'No balls for yesterday, nothing to claim']]; } $daily = DB::table(TableName::agent() . 'superball_daily')->where('pool_date', $yesterday)->first(); if (!$daily || (int) $daily->total_balls <= 0) { return ['success' => false, 'message' => ['web.superball.pool_not_ready', 'Yesterday pool not ready']]; } $basePerBall = (int) ($daily->pool_amount / $daily->total_balls); $luckyNumber = (int) $daily->lucky_number; $multiplierRow = $this->getUserMultiplier($userId); $multiplier = (float) ($multiplierRow->multiplier ?? 1.0); // 选号可重复:每个球单独比对幸运号,中奖球数 = 号码等于幸运号的球个数(同一号码可多球) $matched = 0; foreach ($balls as $b) { if ((int) $b->number === $luckyNumber) { $matched++; } } $baseAmount = $basePerBall * count($balls); $luckyAmountDisplay = self::LUCKY_REWARD_PER_BALL * $matched; $luckyAmountInternal = $luckyAmountDisplay * NumConfig::NUM_VALUE; $totalAmount = (int) round($baseAmount * $multiplier) + $luckyAmountInternal; DB::connection('write')->table(TableName::agent() . 'superball_prize_log')->insert([ 'user_id' => $userId, 'settle_date' => $yesterday, 'base_amount' => $baseAmount, 'lucky_amount' => $luckyAmountInternal, 'multiplier' => $multiplier, 'total_amount' => $totalAmount, 'created_at' => now()->format('Y-m-d H:i:s'), ]); OuroGameService::AddScore($userId, $totalAmount, 90); return [ 'total_amount' => $totalAmount, 'total_amount_display' => $totalAmount / NumConfig::NUM_VALUE, 'base_amount' => $baseAmount, 'lucky_amount' => $luckyAmountInternal, 'multiplier' => $multiplier, ]; } /** * Calculate yesterday prize for user (for display only, no claim). */ public function calculateYesterdayPrizeForUser(int $userId, string $yesterday): int { $daily = DB::table(TableName::agent() . 'superball_daily')->where('pool_date', $yesterday)->first(); if (!$daily || (int) $daily->total_balls <= 0) { return 0; } $balls = $this->getUserBalls($userId, $yesterday); if (count($balls) === 0) { return 0; } $basePerBall = (int) ($daily->pool_amount / $daily->total_balls); $luckyNumber = (int) $daily->lucky_number; $multiplierRow = $this->getUserMultiplier($userId); $multiplier = (float) ($multiplierRow->multiplier ?? 1.0); // 选号可重复:按球逐个比对幸运号,中奖球数 = 号码等于幸运号的球个数 $matched = 0; foreach ($balls as $b) { if ((int) $b->number === $luckyNumber) { $matched++; } } $baseAmount = $basePerBall * count($balls); $luckyAmountInternal = self::LUCKY_REWARD_PER_BALL * $matched * NumConfig::NUM_VALUE; return (int) round($baseAmount * $multiplier) + $luckyAmountInternal; } /** * Get last 7 days lucky numbers (for display). */ public function getLast7DaysLuckyNumbers(): array { return $this->getLast7DaysLuckyNumbersPrivate(); } /** * Ensure daily row and lucky number for date (idempotent). */ public function getOrCreateDaily(string $date): \stdClass { $row = DB::table(TableName::agent() . 'superball_daily')->where('pool_date', $date)->first(); if ($row) { return $row; } $lucky = mt_rand(0, 9); DB::connection('write')->table(TableName::agent() . 'superball_daily')->insert([ 'pool_date' => $date, 'pool_amount' => 0, 'total_balls' => 0, 'lucky_number' => $lucky, 'completed_count' => 0, 'lucky_count' => 0, 'created_at' => now()->format('Y-m-d H:i:s'), 'updated_at' => now()->format('Y-m-d H:i:s'), ]); return DB::table(TableName::agent() . 'superball_daily')->where('pool_date', $date)->first(); } /** * Update pool amount for a date (call from job that aggregates daily turnover). */ public function updatePoolAmount(string $date, int $poolAmountInternal): void { DB::connection('write')->table(TableName::agent() . 'superball_daily') ->where('pool_date', $date) ->update(['pool_amount' => $poolAmountInternal, 'updated_at' => now()->format('Y-m-d H:i:s')]); } // --- private helpers --- private function getTierConfig(): array { $rows = DB::table(TableName::agent() . 'superball_tier_config') ->orderBy('sort_index') ->get(); return array_map(function ($r) { return [ 'tier' => $r->tier, 'recharge_required' => (int) $r->recharge_required, 'turnover_required' => (int) $r->turnover_required, 'ball_count' => (int) $r->ball_count, ]; }, $rows->all()); } private function getTierConfigByTier(string $tier): ?\stdClass { return DB::table(TableName::agent() . 'superball_tier_config')->where('tier', $tier)->first(); } private function getUserTask(int $userId, string $date): ?\stdClass { return DB::table(TableName::agent() . 'superball_user_task') ->where('user_id', $userId) ->where('task_date', $date) ->first(); } private function getUserRechargeForDate(int $userId, string $date): int { $dateId = str_replace('-', '', $date); $row = DB::table(TableName::QPRecordDB() . 'RecordUserDataStatisticsNew') ->where('UserID', $userId) ->where('DateID', $dateId) ->first(); return $row ? (int) $row->Recharge : 0; } /** 当日流水:从按日统计表取当天 TotalBet(内部单位) */ private function getUserTotalBetForDate(int $userId, string $date): int { $dateId = str_replace('-', '', $date); $row = DB::table(TableName::QPRecordDB() . 'RecordUserDataStatisticsNew') ->where('UserID', $userId) ->where('DateID', $dateId) ->first(); return $row && isset($row->TotalBet) ? (int) $row->TotalBet : 0; } private function getUserTotalBet(int $userId): int { $row = DB::table(TableName::QPRecordDB() . 'RecordUserTotalStatistics') ->where('UserID', $userId) ->first(); return $row ? (int) $row->TotalBet : 0; } private function getUserBalls(int $userId, string $date): array { return DB::table(TableName::agent() . 'superball_user_balls') ->where('user_id', $userId) ->where('ball_date', $date) ->orderBy('ball_index') ->get() ->all(); } private function getUserMultiplier(int $userId): ?\stdClass { return DB::table(TableName::agent() . 'superball_user_multiplier')->where('user_id', $userId)->first(); } private function getUserPrizeLog(int $userId, string $date): ?\stdClass { return DB::table(TableName::agent() . 'superball_prize_log') ->where('user_id', $userId) ->where('settle_date', $date) ->first(); } private function getLast7DaysLuckyNumbersPrivate(): array { $dates = []; for ($i = 0; $i < 7; $i++) { $dates[] = Carbon::today()->subDays($i)->format('Y-m-d'); } $rows = DB::table(TableName::agent() . 'superball_daily') ->whereIn('pool_date', $dates) ->orderBy('pool_date', 'desc') ->get(); $map = []; foreach ($rows as $r) { $map[$r->pool_date] = (int) $r->lucky_number; } return array_map(function ($d) use ($map) { return ['date' => $d, 'lucky_number' => $map[$d] ?? null]; }, $dates); } private function updateUserMultiplier(int $userId, string $taskDate): void { $row = DB::connection('write')->table(TableName::agent() . 'superball_user_multiplier') ->where('user_id', $userId) ->lockForUpdate() ->first(); $multiplier = self::MULTIPLIER_MIN; $consecutive = 1; if ($row) { $last = $row->last_task_date ? (string) $row->last_task_date : null; $prev = Carbon::parse($taskDate)->subDay()->format('Y-m-d'); if ($last === $prev) { $consecutive = (int) $row->consecutive_days + 1; $multiplier = min(self::MULTIPLIER_MAX, (float) $row->multiplier + self::MULTIPLIER_STEP); } elseif ($last !== null && $last !== $taskDate) { $daysDiff = (int) Carbon::parse($taskDate)->diffInDays(Carbon::parse($last)); if ($daysDiff > 1) { $multiplier = max(self::MULTIPLIER_MIN, (float) $row->multiplier - self::MULTIPLIER_STEP); $consecutive = 1; } else { $multiplier = (float) $row->multiplier; $consecutive = (int) $row->consecutive_days + 1; } } } DB::connection('write')->table(TableName::agent() . 'superball_user_multiplier')->updateOrInsert( ['user_id' => $userId], [ 'consecutive_days' => $consecutive, 'last_task_date' => $taskDate, 'multiplier' => $multiplier, 'updated_at' => now()->format('Y-m-d H:i:s'), ] ); } }