SuperballActivityService.php 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681
  1. <?php
  2. namespace App\Services;
  3. use App\Facade\TableName;
  4. use App\Game\Services\OuroGameService;
  5. use App\Http\helper\NumConfig;
  6. use Carbon\Carbon;
  7. use Illuminate\Support\Facades\DB;
  8. use Illuminate\Support\Facades\Redis;
  9. use App\Services\VipService;
  10. /**
  11. * Superball Activity: recharge + turnover task, balls, lucky number, prize pool.
  12. * All amounts in internal units (NUM_VALUE) where needed for DB/score.
  13. */
  14. class SuperballActivityService
  15. {
  16. public const TIER_MAX = 'A';
  17. public const MULTIPLIER_MIN = 1.0;
  18. public const MULTIPLIER_MAX = 3.0;
  19. public const MULTIPLIER_STEP = 0.5;
  20. public const LUCKY_REWARD_PER_BALL = 10; // display unit per matching ball
  21. /**
  22. * Get full activity info: yesterday data, today data, tiers, user task, multiplier.
  23. */
  24. public function getInfo(int $userId): array
  25. {
  26. $today = Carbon::today()->format('Y-m-d');
  27. $yesterday = Carbon::yesterday()->format('Y-m-d');
  28. $tiers = $this->getTierConfig();
  29. $yesterdayDaily = $this->getOrCreateDaily($yesterday);
  30. $todayDaily = $this->getOrCreateDaily($today);
  31. $userTask = $this->getUserTask($userId, $today);
  32. $multiplierRow = $this->getUserMultiplier($userId);
  33. // VIP 等级与每日免费球数(赠送球数 = VIP 等级)
  34. $vipLevel = $this->getUserVipLevel($userId);
  35. $vipFreeBalls = $vipLevel > 0 ? $vipLevel : 0;
  36. $rechargeToday = $this->getUserRechargeForDate($userId, $today);
  37. $turnoverToday = $this->getUserTotalBetForDate($userId, $today);
  38. $turnoverProgress = (int) $turnoverToday;
  39. $tierConfig = $userTask ? $this->getTierConfigByTier($userTask->tier) : null;
  40. $rechargeRequired = $tierConfig ? (int) $tierConfig->recharge_required : 0;
  41. $turnoverRequired = $tierConfig ? (int) $tierConfig->turnover_required : 0;
  42. $rechargeDisplay = $rechargeToday;
  43. $turnoverDisplay = $turnoverProgress / NumConfig::NUM_VALUE;
  44. $taskCompleted = $tierConfig && $rechargeDisplay >= $rechargeRequired && $turnoverDisplay >= $turnoverRequired;
  45. $canUpgrade = $userTask && $userTask->tier !== self::TIER_MAX && $taskCompleted;
  46. $canClaim = $taskCompleted && $userTask && (int) $userTask->status === 0;
  47. $yesterdayBalls = $this->getUserBalls($userId, $yesterday);
  48. $yesterdayLucky = (int) ($yesterdayDaily->lucky_number ?? 0);
  49. $yesterdayPrizeLog = $this->getUserPrizeLog($userId, $yesterday);
  50. $yesterdayBasePerBall = 0;
  51. if ($yesterdayDaily->total_balls > 0 && $yesterdayDaily->pool_amount > 0) {
  52. $yesterdayBasePerBall = (int) ($yesterdayDaily->pool_amount / $yesterdayDaily->total_balls);
  53. }
  54. $yesterdayMyPrize = $yesterdayPrizeLog ? (int) $yesterdayPrizeLog->total_amount : 0;
  55. $yesterdayMultiplier = $yesterdayPrizeLog ? (float) $yesterdayPrizeLog->multiplier : 1.0;
  56. $canClaimYesterday = false;
  57. $yesterdayPendingPrize = 0;
  58. if (!$yesterdayPrizeLog && count($yesterdayBalls) > 0) {
  59. $canClaimYesterday = true;
  60. $yesterdayPendingPrize = $this->calculateYesterdayPrizeForUser($userId, $yesterday);
  61. }
  62. $todayBasePerBall = 0;
  63. if ($todayDaily->total_balls > 0 && $todayDaily->pool_amount > 0) {
  64. $todayBasePerBall = (int) ($todayDaily->pool_amount / $todayDaily->total_balls);
  65. }
  66. $todayBalls = $this->getUserBalls($userId, $today);
  67. $todayMyBallsList = array_map(function ($b) {
  68. return ['ball_index' => (int) $b->ball_index, 'number' => (int) $b->number];
  69. }, $todayBalls);
  70. $todayNumberCounts = [];
  71. foreach ($todayBalls as $b) {
  72. $n = (int) $b->number;
  73. $todayNumberCounts[$n] = ($todayNumberCounts[$n] ?? 0) + 1;
  74. }
  75. // 0 点到 1 点前,前端展示的今日整体数据全部置为 0(不影响实际统计与任务进度)
  76. $hour = (int) Carbon::now()->format('G'); // 0-23
  77. if ($hour < 1) {
  78. $todayDisplayPoolAmount = 0;
  79. $todayDisplayCompleted = 0;
  80. $todayDisplayTotalBalls = 0;
  81. $todayDisplayBasePerBall = 0;
  82. $todayDisplayMyBalls = [];
  83. $todayDisplayNumberCounts = [];
  84. } else {
  85. $todayDisplayPoolAmount = (int) $todayDaily->pool_amount;
  86. $todayDisplayCompleted = (int) ($todayDaily->completed_count ?? 0);
  87. $todayDisplayTotalBalls = (int) ($todayDaily->total_balls ?? 0);
  88. $todayDisplayBasePerBall = $todayBasePerBall;
  89. $todayDisplayMyBalls = $todayMyBallsList;
  90. $todayDisplayNumberCounts = $todayNumberCounts;
  91. }
  92. $last7Lucky = $this->getLast7DaysLuckyNumbersPrivate();
  93. $data = [
  94. 'yesterday' => [
  95. 'pool_amount' => (int) $yesterdayDaily->pool_amount,
  96. 'pool_amount_display' => (int) $yesterdayDaily->pool_amount / NumConfig::NUM_VALUE,
  97. 'base_reward_per_ball' => $yesterdayBasePerBall,
  98. 'base_reward_per_ball_display' => $yesterdayBasePerBall / NumConfig::NUM_VALUE,
  99. 'lucky_number' => $yesterdayLucky,
  100. 'completed_count' => (int) ($yesterdayDaily->completed_count ?? 0),
  101. 'total_balls' => (int) ($yesterdayDaily->total_balls ?? 0),
  102. 'my_balls' => $yesterdayBalls,
  103. 'my_balls_with_lucky' => array_map(function ($b) use ($yesterdayLucky) {
  104. return ['number' => (int) $b->number, 'is_lucky' => (int) $b->number === $yesterdayLucky];
  105. }, $yesterdayBalls),
  106. 'my_prize' => $yesterdayMyPrize,
  107. 'my_prize_display' => $yesterdayMyPrize / NumConfig::NUM_VALUE,
  108. 'my_multiplier' => $yesterdayMultiplier,
  109. 'can_claim_yesterday' => $canClaimYesterday,
  110. 'pending_prize' => $yesterdayPendingPrize,
  111. 'pending_prize_display' => $yesterdayPendingPrize / NumConfig::NUM_VALUE,
  112. ],
  113. 'today' => [
  114. 'pool_amount' => $todayDisplayPoolAmount,
  115. 'pool_amount_display' => $todayDisplayPoolAmount / NumConfig::NUM_VALUE,
  116. 'completed_count' => $todayDisplayCompleted,
  117. 'total_balls' => $todayDisplayTotalBalls,
  118. 'base_reward_per_ball' => $todayDisplayBasePerBall,
  119. 'base_reward_per_ball_display' => $todayDisplayBasePerBall / NumConfig::NUM_VALUE,
  120. 'my_balls' => $todayDisplayMyBalls,
  121. 'number_counts' => $todayDisplayNumberCounts,
  122. ],
  123. 'tiers' => $tiers,
  124. 'user_task' => $userTask ? [
  125. 'tier' => $userTask->tier,
  126. 'recharge_required' => $rechargeRequired,
  127. 'turnover_required' => $turnoverRequired,
  128. 'recharge_progress' => $rechargeDisplay,
  129. 'turnover_progress' => $turnoverDisplay,
  130. 'task_completed' => $taskCompleted,
  131. 'status' => (int) $userTask->status,
  132. 'can_claim' => $canClaim,
  133. 'can_upgrade' => $canUpgrade,
  134. 'ball_count' => $tierConfig ? (int) $tierConfig->ball_count : 0,
  135. ] : null,
  136. 'multiplier' => [
  137. 'value' => (float) ($multiplierRow->multiplier ?? 1.0),
  138. 'consecutive_days' => (int) ($multiplierRow->consecutive_days ?? 0),
  139. 'min' => self::MULTIPLIER_MIN,
  140. 'max' => self::MULTIPLIER_MAX,
  141. 'step' => self::MULTIPLIER_STEP,
  142. ],
  143. 'vip' => [
  144. 'level' => $vipLevel,
  145. 'daily_free_balls' => $vipFreeBalls,
  146. ],
  147. 'lucky_reward_per_ball' => self::LUCKY_REWARD_PER_BALL,
  148. 'lucky_numbers_7_days' => $last7Lucky,
  149. ];
  150. return $data;
  151. }
  152. /**
  153. * 获取用户 VIP 等级(用于每日赠送球数计算)
  154. */
  155. protected function getUserVipLevel(int $userId): int
  156. {
  157. if ($userId <= 0) {
  158. return 0;
  159. }
  160. // 从 YN_VIPAccount 读取累计充值金额,交由 VipService 计算 VIP 等级
  161. $userRecharge = (int) DB::table('QPAccountsDB.dbo.YN_VIPAccount')
  162. ->where('UserID', $userId)
  163. ->value('Recharge');
  164. return (int) VipService::calculateVipLevel($userId, $userRecharge);
  165. }
  166. /**
  167. * Select task tier for today (with optional confirm). Creates or updates user task.
  168. * @return array success: ['success' => true] | failure: ['success' => false, 'message' => ['key', 'fallback']]
  169. */
  170. public function selectTier(int $userId, string $tier): array
  171. {
  172. $today = Carbon::today()->format('Y-m-d');
  173. $config = $this->getTierConfigByTier($tier);
  174. if (!$config) {
  175. return ['success' => false, 'message' => ['web.superball.activity_not_found', 'Activity not found']];
  176. }
  177. $existing = $this->getUserTask($userId, $today);
  178. if ($existing) {
  179. return ['success' => false, 'message' => ['web.superball.tier_already_selected', 'Already selected tier for today']];
  180. }
  181. DB::connection('write')->table(TableName::agent() . 'superball_user_task')->insert([
  182. 'user_id' => $userId,
  183. 'task_date' => $today,
  184. 'tier' => $tier,
  185. 'total_bet_snapshot' => 0,
  186. 'status' => 0,
  187. 'created_at' => now()->format('Y-m-d H:i:s'),
  188. 'updated_at' => now()->format('Y-m-d H:i:s'),
  189. ]);
  190. return ['success' => true];
  191. }
  192. /**
  193. * Upgrade to higher tier (keep progress). Only allowed when task completed and not A.
  194. * @return array success: ['success' => true] | failure: ['success' => false, 'message' => [...]]
  195. */
  196. public function upgradeTier(int $userId, string $newTier): array
  197. {
  198. $today = Carbon::today()->format('Y-m-d');
  199. $task = $this->getUserTask($userId, $today);
  200. if (!$task) {
  201. return ['success' => false, 'message' => ['web.superball.no_task_today', 'No task for today']];
  202. }
  203. if ((int) $task->status === 1) {
  204. return ['success' => false, 'message' => ['web.superball.already_claimed', 'Already claimed']];
  205. }
  206. $tierOrder = ['E' => 1, 'D' => 2, 'C' => 3, 'B' => 4, 'A' => 5];
  207. $currentOrder = $tierOrder[$task->tier] ?? 0;
  208. $newOrder = $tierOrder[$newTier] ?? 0;
  209. if ($newOrder <= $currentOrder) {
  210. return ['success' => false, 'message' => ['web.superball.cannot_downgrade', 'Can only upgrade to higher tier']];
  211. }
  212. $rechargeToday = $this->getUserRechargeForDate($userId, $today);
  213. $turnoverToday = $this->getUserTotalBetForDate($userId, $today);
  214. $newConfig = $this->getTierConfigByTier($task->tier);
  215. $rechargeOk = $rechargeToday >= (int) $newConfig->recharge_required;
  216. $turnoverOk = ($turnoverToday / NumConfig::NUM_VALUE) >= (int) $newConfig->turnover_required;
  217. // var_dump($rechargeToday,$turnoverToday,$newConfig);
  218. if (!$rechargeOk || !$turnoverOk) {
  219. return ['success' => false, 'message' => ['web.superball.task_not_completed', 'Task not completed']];
  220. }
  221. DB::connection('write')->table(TableName::agent() . 'superball_user_task')
  222. ->where('user_id', $userId)
  223. ->where('task_date', $today)
  224. ->update(['tier' => $newTier, 'updated_at' => now()->format('Y-m-d H:i:s')]);
  225. return ['success' => true,'user_id' => $userId, 'new_tier' => $newTier,'task_date'=>$today];
  226. }
  227. /**
  228. * Claim reward: grant balls, update daily total_balls/completed_count, update multiplier, mark task claimed.
  229. * @return array success: ['ball_count' => n, 'message' => ...] | failure: ['success' => false, 'message' => [...]]
  230. */
  231. public function claimReward(int $userId): array
  232. {
  233. $today = Carbon::today()->format('Y-m-d');
  234. $task = $this->getUserTask($userId, $today);
  235. if (!$task) {
  236. return ['success' => false, 'message' => ['web.superball.no_task_today', 'No task for today']];
  237. }
  238. if ((int) $task->status === 1) {
  239. return ['success' => false, 'message' => ['web.superball.already_claimed', 'Already claimed']];
  240. }
  241. $tierConfig = $this->getTierConfigByTier($task->tier);
  242. if (!$tierConfig) {
  243. return ['success' => false, 'message' => ['web.superball.activity_not_found', 'Activity not found']];
  244. }
  245. $rechargeToday = $this->getUserRechargeForDate($userId, $today);
  246. $turnoverToday = $this->getUserTotalBetForDate($userId, $today);
  247. $rechargeOk = ($rechargeToday) >= (int) $tierConfig->recharge_required;
  248. $turnoverOk = ($turnoverToday / NumConfig::NUM_VALUE) >= (int) $tierConfig->turnover_required;
  249. if (!$rechargeOk || !$turnoverOk) {
  250. return ['success' => false, 'message' => ['web.superball.task_not_completed', 'Task not completed']];
  251. }
  252. // 基础任务球数 + 每日 VIP 免费球数(赠送球 = VIP 等级)
  253. $baseBallCount = (int) $tierConfig->ball_count;
  254. $vipLevel = $this->getUserVipLevel($userId);
  255. $vipFreeBalls = $vipLevel > 0 ? $vipLevel : 0;
  256. $ballCount = $baseBallCount + $vipFreeBalls;
  257. DB::connection('write')->transaction(function () use ($userId, $today, $task, $ballCount) {
  258. DB::connection('write')->table(TableName::agent() . 'superball_user_task')
  259. ->where('user_id', $userId)
  260. ->where('task_date', $today)
  261. ->update(['status' => 1, 'updated_at' => now()->format('Y-m-d H:i:s')]);
  262. $daily = DB::connection('write')->table(TableName::agent() . 'superball_daily')
  263. ->where('pool_date', $today)
  264. ->lockForUpdate()
  265. ->first();
  266. if ($daily) {
  267. DB::connection('write')->table(TableName::agent() . 'superball_daily')
  268. ->where('pool_date', $today)
  269. ->update([
  270. 'total_balls' => (int) $daily->total_balls + $ballCount,
  271. 'completed_count' => (int) $daily->completed_count + 1,
  272. 'updated_at' => now()->format('Y-m-d H:i:s'),
  273. ]);
  274. }
  275. $this->updateUserMultiplier($userId, $today);
  276. });
  277. return [
  278. 'ball_count' => $ballCount,
  279. 'base_ball_count' => $baseBallCount,
  280. 'vip_free_balls' => $vipFreeBalls,
  281. 'message' => 'Claim success, please select numbers for your balls',
  282. ];
  283. }
  284. /**
  285. * Submit numbers for all balls (0-9 per ball). Must have claimed and not yet submitted.
  286. * @return array success: ['success' => true] | failure: ['success' => false, 'message' => [...]]
  287. */
  288. public function submitNumbers(int $userId, array $numbers): array
  289. {
  290. $today = Carbon::today()->format('Y-m-d');
  291. $task = $this->getUserTask($userId, $today);
  292. if (!$task || (int) $task->status !== 1) {
  293. return ['success' => false, 'message' => ['web.superball.no_claimed_task', 'No claimed task for today']];
  294. }
  295. $tierConfig = $this->getTierConfigByTier($task->tier);
  296. if (!$tierConfig) {
  297. return ['success' => false, 'message' => ['web.superball.activity_not_found', 'Activity not found']];
  298. }
  299. // 与领取时保持一致:基础任务球数 + 每日 VIP 免费球数
  300. $baseBallCount = (int) $tierConfig->ball_count;
  301. $vipLevel = $this->getUserVipLevel($userId);
  302. $vipFreeBalls = $vipLevel > 0 ? $vipLevel : 0;
  303. $ballCount = $baseBallCount + $vipFreeBalls;
  304. if (count($numbers) !== $ballCount) {
  305. return ['success' => false, 'message' => ['web.superball.number_count_mismatch', 'Number count mismatch']];
  306. }
  307. $existing = DB::connection('write')->table(TableName::agent() . 'superball_user_balls')
  308. ->where('user_id', $userId)
  309. ->where('ball_date', $today)
  310. ->count();
  311. if ($existing > 0) {
  312. return ['success' => false, 'message' => ['web.superball.numbers_already_submitted', 'Numbers already submitted']];
  313. }
  314. DB::connection('write')->transaction(function () use ($userId, $today, $numbers) {
  315. // 写入用户今日所有球号
  316. foreach ($numbers as $index => $num) {
  317. $n = (int) $num;
  318. if ($n < 0 || $n > 9) {
  319. throw new \RuntimeException('Number must be 0-9');
  320. }
  321. DB::connection('write')->table(TableName::agent() . 'superball_user_balls')->insert([
  322. 'user_id' => $userId,
  323. 'ball_date' => $today,
  324. 'ball_index' => $index + 1,
  325. 'number' => $n,
  326. 'created_at' => now()->format('Y-m-d H:i:s'),
  327. ]);
  328. }
  329. // 若用户选择的号码等于当日 lucky_number,则把 superball_daily.lucky_count 累加
  330. $daily = DB::connection('write')
  331. ->table(TableName::agent() . 'superball_daily')
  332. ->where('pool_date', $today)
  333. ->lockForUpdate()
  334. ->first();
  335. if ($daily) {
  336. $luckyNumber = (int) $daily->lucky_number;
  337. $matched = 0;
  338. foreach ($numbers as $num) {
  339. if ((int) $num === $luckyNumber) {
  340. $matched++;
  341. }
  342. }
  343. if ($matched > 0) {
  344. DB::connection('write')
  345. ->table(TableName::agent() . 'superball_daily')
  346. ->where('pool_date', $today)
  347. ->update([
  348. 'lucky_count' => (int) $daily->lucky_count + $matched,
  349. 'updated_at' => now()->format('Y-m-d H:i:s'),
  350. ]);
  351. }
  352. }
  353. });
  354. return ['success' => true];
  355. }
  356. /**
  357. * Get user's balls for a date (for number selection page or display).
  358. */
  359. public function getMyBalls(int $userId, string $date): array
  360. {
  361. $rows = DB::table(TableName::agent() . 'superball_user_balls')
  362. ->where('user_id', $userId)
  363. ->where('ball_date', $date)
  364. ->orderBy('ball_index')
  365. ->get();
  366. return array_map(function ($r) {
  367. return ['ball_index' => (int) $r->ball_index, 'number' => (int) $r->number];
  368. }, $rows->all());
  369. }
  370. /**
  371. * User claims yesterday's reward (next-day claim, no auto distribution).
  372. * Formula: base = pool / total_balls * ball_count, lucky = 10 * matched_balls (display), total = base * multiplier + lucky.
  373. * @return array success: data with total_amount etc. | failure: ['success' => false, 'message' => [...]]
  374. */
  375. public function claimYesterdayReward(int $userId): array
  376. {
  377. $yesterday = Carbon::yesterday()->format('Y-m-d');
  378. $existing = $this->getUserPrizeLog($userId, $yesterday);
  379. if ($existing) {
  380. return ['success' => false, 'message' => ['web.superball.yesterday_already_claimed', 'Yesterday reward already claimed']];
  381. }
  382. $balls = $this->getUserBalls($userId, $yesterday);
  383. if (count($balls) === 0) {
  384. return ['success' => false, 'message' => ['web.superball.no_balls_yesterday', 'No balls for yesterday, nothing to claim']];
  385. }
  386. $daily = DB::table(TableName::agent() . 'superball_daily')->where('pool_date', $yesterday)->first();
  387. if (!$daily || (int) $daily->total_balls <= 0) {
  388. return ['success' => false, 'message' => ['web.superball.pool_not_ready', 'Yesterday pool not ready']];
  389. }
  390. $basePerBall = (int) ($daily->pool_amount / $daily->total_balls);
  391. $luckyNumber = (int) $daily->lucky_number;
  392. $multiplierRow = $this->getUserMultiplier($userId);
  393. $multiplier = (float) ($multiplierRow->multiplier ?? 1.0);
  394. // 选号可重复:每个球单独比对幸运号,中奖球数 = 号码等于幸运号的球个数(同一号码可多球)
  395. $matched = 0;
  396. foreach ($balls as $b) {
  397. if ((int) $b->number === $luckyNumber) {
  398. $matched++;
  399. }
  400. }
  401. $baseAmount = $basePerBall * count($balls);
  402. $luckyAmountDisplay = self::LUCKY_REWARD_PER_BALL * $matched;
  403. $luckyAmountInternal = $luckyAmountDisplay * NumConfig::NUM_VALUE;
  404. $totalAmount = (int) round($baseAmount * $multiplier) + $luckyAmountInternal;
  405. DB::connection('write')->table(TableName::agent() . 'superball_prize_log')->insert([
  406. 'user_id' => $userId,
  407. 'settle_date' => $yesterday,
  408. 'base_amount' => $baseAmount,
  409. 'lucky_amount' => $luckyAmountInternal,
  410. 'multiplier' => $multiplier,
  411. 'total_amount' => $totalAmount,
  412. 'created_at' => now()->format('Y-m-d H:i:s'),
  413. ]);
  414. OuroGameService::AddScore($userId, $totalAmount, 90);
  415. return [
  416. 'total_amount' => $totalAmount,
  417. 'total_amount_display' => $totalAmount / NumConfig::NUM_VALUE,
  418. 'base_amount' => $baseAmount,
  419. 'lucky_amount' => $luckyAmountInternal,
  420. 'multiplier' => $multiplier,
  421. ];
  422. }
  423. /**
  424. * Calculate yesterday prize for user (for display only, no claim).
  425. */
  426. public function calculateYesterdayPrizeForUser(int $userId, string $yesterday): int
  427. {
  428. $daily = DB::table(TableName::agent() . 'superball_daily')->where('pool_date', $yesterday)->first();
  429. if (!$daily || (int) $daily->total_balls <= 0) {
  430. return 0;
  431. }
  432. $balls = $this->getUserBalls($userId, $yesterday);
  433. if (count($balls) === 0) {
  434. return 0;
  435. }
  436. $basePerBall = (int) ($daily->pool_amount / $daily->total_balls);
  437. $luckyNumber = (int) $daily->lucky_number;
  438. $multiplierRow = $this->getUserMultiplier($userId);
  439. $multiplier = (float) ($multiplierRow->multiplier ?? 1.0);
  440. // 选号可重复:按球逐个比对幸运号,中奖球数 = 号码等于幸运号的球个数
  441. $matched = 0;
  442. foreach ($balls as $b) {
  443. if ((int) $b->number === $luckyNumber) {
  444. $matched++;
  445. }
  446. }
  447. $baseAmount = $basePerBall * count($balls);
  448. $luckyAmountInternal = self::LUCKY_REWARD_PER_BALL * $matched * NumConfig::NUM_VALUE;
  449. return (int) round($baseAmount * $multiplier) + $luckyAmountInternal;
  450. }
  451. /**
  452. * Get last 7 days lucky numbers (for display).
  453. */
  454. public function getLast7DaysLuckyNumbers(): array
  455. {
  456. return $this->getLast7DaysLuckyNumbersPrivate();
  457. }
  458. /**
  459. * Ensure daily row and lucky number for date (idempotent).
  460. */
  461. public function getOrCreateDaily(string $date): \stdClass
  462. {
  463. $row = DB::table(TableName::agent() . 'superball_daily')->where('pool_date', $date)->first();
  464. if ($row) {
  465. return $row;
  466. }
  467. $lucky = mt_rand(0, 9);
  468. DB::connection('write')->table(TableName::agent() . 'superball_daily')->insert([
  469. 'pool_date' => $date,
  470. 'pool_amount' => 0,
  471. 'total_balls' => 0,
  472. 'lucky_number' => $lucky,
  473. 'completed_count' => 0,
  474. 'lucky_count' => 0,
  475. 'created_at' => now()->format('Y-m-d H:i:s'),
  476. 'updated_at' => now()->format('Y-m-d H:i:s'),
  477. ]);
  478. return DB::table(TableName::agent() . 'superball_daily')->where('pool_date', $date)->first();
  479. }
  480. /**
  481. * Update pool amount for a date (call from job that aggregates daily turnover).
  482. */
  483. public function updatePoolAmount(string $date, int $poolAmountInternal): void
  484. {
  485. DB::connection('write')->table(TableName::agent() . 'superball_daily')
  486. ->where('pool_date', $date)
  487. ->update(['pool_amount' => $poolAmountInternal, 'updated_at' => now()->format('Y-m-d H:i:s')]);
  488. }
  489. // --- private helpers ---
  490. private function getTierConfig(): array
  491. {
  492. $rows = DB::table(TableName::agent() . 'superball_tier_config')
  493. ->orderBy('sort_index')
  494. ->get();
  495. return array_map(function ($r) {
  496. return [
  497. 'tier' => $r->tier,
  498. 'recharge_required' => (int) $r->recharge_required,
  499. 'turnover_required' => (int) $r->turnover_required,
  500. 'ball_count' => (int) $r->ball_count,
  501. ];
  502. }, $rows->all());
  503. }
  504. private function getTierConfigByTier(string $tier): ?\stdClass
  505. {
  506. return DB::table(TableName::agent() . 'superball_tier_config')->where('tier', $tier)->first();
  507. }
  508. private function getUserTask(int $userId, string $date): ?\stdClass
  509. {
  510. return DB::table(TableName::agent() . 'superball_user_task')
  511. ->where('user_id', $userId)
  512. ->where('task_date', $date)
  513. ->first();
  514. }
  515. private function getUserRechargeForDate(int $userId, string $date): int
  516. {
  517. $dateId = str_replace('-', '', $date);
  518. $row = DB::table(TableName::QPRecordDB() . 'RecordUserDataStatisticsNew')
  519. ->where('UserID', $userId)
  520. ->where('DateID', $dateId)
  521. ->first();
  522. return $row ? (int) $row->Recharge : 0;
  523. }
  524. /** 当日流水:从按日统计表取当天 TotalBet(内部单位) */
  525. private function getUserTotalBetForDate(int $userId, string $date): int
  526. {
  527. $dateId = str_replace('-', '', $date);
  528. $row = DB::table(TableName::QPRecordDB() . 'RecordUserDataStatisticsNew')
  529. ->where('UserID', $userId)
  530. ->where('DateID', $dateId)
  531. ->first();
  532. return $row && isset($row->TotalBet) ? (int) $row->TotalBet : 0;
  533. }
  534. private function getUserTotalBet(int $userId): int
  535. {
  536. $row = DB::table(TableName::QPRecordDB() . 'RecordUserTotalStatistics')
  537. ->where('UserID', $userId)
  538. ->first();
  539. return $row ? (int) $row->TotalBet : 0;
  540. }
  541. private function getUserBalls(int $userId, string $date): array
  542. {
  543. return DB::table(TableName::agent() . 'superball_user_balls')
  544. ->where('user_id', $userId)
  545. ->where('ball_date', $date)
  546. ->orderBy('ball_index')
  547. ->get()
  548. ->all();
  549. }
  550. private function getUserMultiplier(int $userId): ?\stdClass
  551. {
  552. return DB::table(TableName::agent() . 'superball_user_multiplier')->where('user_id', $userId)->first();
  553. }
  554. private function getUserPrizeLog(int $userId, string $date): ?\stdClass
  555. {
  556. return DB::table(TableName::agent() . 'superball_prize_log')
  557. ->where('user_id', $userId)
  558. ->where('settle_date', $date)
  559. ->first();
  560. }
  561. private function getLast7DaysLuckyNumbersPrivate(): array
  562. {
  563. $dates = [];
  564. for ($i = 0; $i < 7; $i++) {
  565. $dates[] = Carbon::today()->subDays($i)->format('Y-m-d');
  566. }
  567. $rows = DB::table(TableName::agent() . 'superball_daily')
  568. ->whereIn('pool_date', $dates)
  569. ->orderBy('pool_date', 'desc')
  570. ->get();
  571. $map = [];
  572. foreach ($rows as $r) {
  573. $map[$r->pool_date] = (int) $r->lucky_number;
  574. }
  575. return array_map(function ($d) use ($map) {
  576. return ['date' => $d, 'lucky_number' => $map[$d] ?? null];
  577. }, $dates);
  578. }
  579. private function updateUserMultiplier(int $userId, string $taskDate): void
  580. {
  581. $row = DB::connection('write')->table(TableName::agent() . 'superball_user_multiplier')
  582. ->where('user_id', $userId)
  583. ->lockForUpdate()
  584. ->first();
  585. $multiplier = self::MULTIPLIER_MIN;
  586. $consecutive = 1;
  587. if ($row) {
  588. $last = $row->last_task_date ? (string) $row->last_task_date : null;
  589. $prev = Carbon::parse($taskDate)->subDay()->format('Y-m-d');
  590. if ($last === $prev) {
  591. $consecutive = (int) $row->consecutive_days + 1;
  592. $multiplier = min(self::MULTIPLIER_MAX, (float) $row->multiplier + self::MULTIPLIER_STEP);
  593. } elseif ($last !== null && $last !== $taskDate) {
  594. $daysDiff = (int) Carbon::parse($taskDate)->diffInDays(Carbon::parse($last));
  595. if ($daysDiff > 1) {
  596. $multiplier = max(self::MULTIPLIER_MIN, (float) $row->multiplier - self::MULTIPLIER_STEP);
  597. $consecutive = 1;
  598. } else {
  599. $multiplier = (float) $row->multiplier;
  600. $consecutive = (int) $row->consecutive_days + 1;
  601. }
  602. }
  603. }
  604. DB::connection('write')->table(TableName::agent() . 'superball_user_multiplier')->updateOrInsert(
  605. ['user_id' => $userId],
  606. [
  607. 'consecutive_days' => $consecutive,
  608. 'last_task_date' => $taskDate,
  609. 'multiplier' => $multiplier,
  610. 'updated_at' => now()->format('Y-m-d H:i:s'),
  611. ]
  612. );
  613. }
  614. }