SuperballActivityService.php 34 KB

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