SuperballActivityService.php 29 KB

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