RewardBasisService.php 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. <?php
  2. namespace App\Services;
  3. use App\Services\Sharing\EffectiveBet;
  4. use Carbon\Carbon;
  5. use Illuminate\Support\Facades\DB;
  6. /**
  7. * 笔笔返/回水的计算口径:按分类汇总 share_bet_facts,再套用 RewardRuleService 的档位。
  8. * 笔笔返按纽约自然日,回水按北京时间自然月,与原有 rebates.date / backflow.date 一致。
  9. */
  10. class RewardBasisService
  11. {
  12. public static function user(string $memberId): ?object
  13. {
  14. return DB::table('users')->where('member_id', $memberId)->first(['id', 'member_id', 'level', 'username', 'first_name']);
  15. }
  16. /** Category totals for one New York day: bet (non-void), effective and profit (settled only). */
  17. public static function rebateBasis(object $user, string $nyDate): array
  18. {
  19. $start = Carbon::parse($nyDate . ' 00:00:00', 'America/New_York')->setTimezone('Asia/Shanghai');
  20. $end = $start->copy()->addDay();
  21. return self::sum(DB::table('share_bet_facts')->where('user_id', $user->id)
  22. ->whereBetween('bet_date', [$start->toDateString(), $end->toDateString()])
  23. ->where('bet_at', '>=', $start->format('Y-m-d H:i:s'))->where('bet_at', '<', $end->format('Y-m-d H:i:s')));
  24. }
  25. /** Category totals for one Asia/Shanghai month (Y-m). */
  26. public static function backflowBasis(object $user, string $month): array
  27. {
  28. $start = Carbon::parse($month . '-01', 'Asia/Shanghai');
  29. return self::sum(DB::table('share_bet_facts')->where('user_id', $user->id)
  30. ->whereBetween('bet_date', [$start->toDateString(), $start->copy()->endOfMonth()->toDateString()]));
  31. }
  32. /** Highest tier of the member's level whose threshold the category's effective bet reaches. */
  33. public static function rebateReward(array $basis, int $level, ?array $tiers = null): array
  34. {
  35. $tiers ??= RewardRuleService::tiers('rebate');
  36. $result = self::empty($basis);
  37. foreach ($basis as $category => $row) {
  38. $detail = self::detail($category, $row) + ['level' => $level];
  39. if (bccomp($row['effective_amount'], '0', 2) > 0) {
  40. foreach ($tiers[$category] ?? [] as $tier) {
  41. if ($tier['level'] !== $level || bccomp($row['effective_amount'], $tier['min_effective_amount'], 2) < 0) continue;
  42. $amount = bcmul($row['effective_amount'], bcdiv($tier['percentage'], '100', 8), 2);
  43. $detail = self::matched($detail, $tier, $amount) + ['min_effective_amount' => $tier['min_effective_amount'],
  44. 'percentage' => $tier['percentage']];
  45. break;
  46. }
  47. }
  48. $result = self::add($result, $detail);
  49. }
  50. return $result;
  51. }
  52. /** Highest tier whose loss threshold the category's monthly net loss reaches; the reward is a fixed amount. */
  53. public static function backflowReward(array $basis, ?array $tiers = null): array
  54. {
  55. $tiers ??= RewardRuleService::tiers('backflow');
  56. $result = self::empty($basis) + ['loss_amount' => '0.00'];
  57. foreach ($basis as $category => $row) {
  58. $loss = bccomp($row['profit'], '0', 2) < 0 ? bcsub('0', $row['profit'], 2) : '0.00';
  59. $detail = self::detail($category, $row) + ['loss_amount' => $loss];
  60. if (bccomp($loss, '0', 2) > 0) {
  61. $result['loss_amount'] = bcadd($result['loss_amount'], $loss, 2);
  62. foreach ($tiers[$category] ?? [] as $tier) {
  63. if (bccomp($loss, $tier['min_loss_amount'], 2) < 0) continue;
  64. $detail = self::matched($detail, $tier, $tier['reward_amount']) + ['min_loss_amount' => $tier['min_loss_amount']];
  65. break;
  66. }
  67. }
  68. $result = self::add($result, $detail);
  69. }
  70. return $result;
  71. }
  72. /** Fresh rebate result for a stored row; legacy PC28 days without any facts keep their stored effective bet. */
  73. public static function rebate(object $reward): array
  74. {
  75. $user = self::user((string) $reward->member_id);
  76. $basis = $user ? self::rebateBasis($user, (string) $reward->date) : [];
  77. if ($basis === [] && bccomp((string) $reward->effective_betting_amount, '0', 2) > 0) {
  78. $basis = ['self:lottery' => ['bet_amount' => self::money($reward->betting_amount),
  79. 'effective_amount' => self::money($reward->effective_betting_amount), 'profit' => self::money($reward->profit ?? 0)]];
  80. }
  81. return self::rebateReward($basis, (int) ($user->level ?? 0));
  82. }
  83. public static function backflow(object $reward): array
  84. {
  85. $user = self::user((string) $reward->member_id);
  86. return self::backflowReward($user ? self::backflowBasis($user, (string) $reward->date) : []);
  87. }
  88. private static function sum($query): array
  89. {
  90. $currency = strtoupper(trim((string) config('third_game.currency', 'CNY')));
  91. $rows = $query->where('voided', 0)->where('currency', $currency)->groupBy('category')
  92. ->selectRaw('category, SUM(bet_amount) AS bet_amount, SUM(effective_amount) AS effective_amount, SUM(net_profit) AS profit')
  93. ->orderBy('category')->get();
  94. $basis = [];
  95. foreach ($rows as $row) {
  96. $basis[$row->category] = ['bet_amount' => self::money($row->bet_amount),
  97. 'effective_amount' => self::money($row->effective_amount), 'profit' => self::money($row->profit)];
  98. }
  99. return $basis;
  100. }
  101. private static function empty(array $basis): array
  102. {
  103. return ['betting_amount' => '0.00', 'effective_amount' => '0.00', 'profit' => '0.00',
  104. 'amount' => '0.00', 'need_flow' => '0.00', 'details' => [], 'has_facts' => $basis !== []];
  105. }
  106. private static function detail(string $category, array $row): array
  107. {
  108. return ['category' => $category, 'label' => RewardRuleService::categories()[$category] ?? $category,
  109. 'bet_amount' => $row['bet_amount'], 'effective_amount' => $row['effective_amount'], 'profit' => $row['profit'],
  110. 'rule_id' => null, 'turnover_multiple' => '0.00', 'amount' => '0.00', 'need_flow' => '0.00'];
  111. }
  112. private static function matched(array $detail, array $tier, string $amount): array
  113. {
  114. return array_merge($detail, ['rule_id' => $tier['id'], 'turnover_multiple' => $tier['turnover_multiple'],
  115. 'amount' => $amount, 'need_flow' => bcmul($amount, $tier['turnover_multiple'], 2)]);
  116. }
  117. private static function add(array $result, array $detail): array
  118. {
  119. $result['betting_amount'] = bcadd($result['betting_amount'], $detail['bet_amount'], 2);
  120. $result['effective_amount'] = bcadd($result['effective_amount'], $detail['effective_amount'], 2);
  121. $result['profit'] = bcadd($result['profit'], $detail['profit'], 2);
  122. $result['amount'] = bcadd($result['amount'], $detail['amount'], 2);
  123. $result['need_flow'] = bcadd($result['need_flow'], $detail['need_flow'], 2);
  124. $result['details'][] = $detail;
  125. return $result;
  126. }
  127. /** SUM() is an exact string on MySQL but may be a float on SQLite. */
  128. private static function money($value): string
  129. {
  130. if ($value === null || $value === '') return '0.00';
  131. if (is_float($value) || is_int($value)) $value = number_format((float) $value, 10, '.', '');
  132. return bcadd(EffectiveBet::decimal($value), '0', 2);
  133. }
  134. }