RewardGrantService.php 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. <?php
  2. namespace App\Services;
  3. use App\Constants\HttpStatus;
  4. use App\Models\Backflow;
  5. use App\Models\Rebate;
  6. use App\Models\Wallet;
  7. use App\Models\WalletBonus;
  8. use App\Support\BackflowPeriod;
  9. use Carbon\Carbon;
  10. use Illuminate\Support\Facades\DB;
  11. use RuntimeException;
  12. /** Shared transaction for manual and automatic payouts. Never trusts a previously loaded reward row. */
  13. class RewardGrantService
  14. {
  15. public static function grant(string $type, int $id, string $operator, bool $skipPaid = false, ?string $scheduledCutoff = null): bool
  16. {
  17. if (!in_array($type, RewardGrantSettingsService::TYPES, true)) {
  18. throw new RuntimeException('奖励类型错误', HttpStatus::CUSTOM_ERROR);
  19. }
  20. return DB::transaction(function () use ($type, $id, $operator, $skipPaid, $scheduledCutoff) {
  21. $model = $type === 'rebate' ? Rebate::class : Backflow::class;
  22. $reward = $model::whereKey($id)->lockForUpdate()->first();
  23. if (!$reward) throw new RuntimeException('数据不存在', HttpStatus::CUSTOM_ERROR);
  24. // status 2 = 周期内提前发放过,仍可继续发放新增差额。
  25. if (!in_array((int) $reward->status, $model::OPEN_STATUSES, true)) {
  26. if ($skipPaid) return false;
  27. throw new RuntimeException('奖励已发放,无需再次发放', HttpStatus::CUSTOM_ERROR);
  28. }
  29. $period = (string) $reward->date;
  30. $dailyFrom = null;
  31. if ($type === 'rebate') {
  32. $today = Carbon::now('America/New_York')->toDateString();
  33. $closed = $period < $today;
  34. if ($period > $today || ($scheduledCutoff !== null && !$closed)) {
  35. throw new RuntimeException('未到发放时间', HttpStatus::CUSTOM_ERROR);
  36. }
  37. if ($scheduledCutoff !== null && $period >= $scheduledCutoff) return false;
  38. } else {
  39. $today = Carbon::now(BackflowPeriod::TIMEZONE)->toDateString();
  40. $dailyFrom = BackflowPeriod::dailyFrom();
  41. [$start, $end] = BackflowPeriod::bounds($period, $dailyFrom);
  42. $closed = $today >= $end;
  43. if ($start > $today) throw new RuntimeException('未到发放时间', HttpStatus::CUSTOM_ERROR);
  44. if ($scheduledCutoff !== null && (!$closed || $end > $scheduledCutoff)) return false;
  45. }
  46. $backflowDetails = '';
  47. if ($type === 'rebate') {
  48. $result = RewardBasisService::rebate($reward);
  49. $basis = $result['effective_amount'];
  50. $reward->betting_amount = $result['betting_amount'];
  51. $reward->effective_betting_amount = $basis;
  52. $reward->rebate_ratio = bccomp($basis, '0', 2) > 0 ? bcdiv($result['amount'], $basis, 6) : '0';
  53. $reward->audited_by = $operator;
  54. $changeType = '笔笔返';
  55. $remark = "日期:{$reward->date};有效投注:{$basis}";
  56. foreach ($result['details'] as $detail) {
  57. if ($detail['rule_id'] === null) continue;
  58. $remark .= ";{$detail['label']}:{$detail['effective_amount']}×{$detail['percentage']}%";
  59. }
  60. } else {
  61. $result = RewardBasisService::backflow($reward, $dailyFrom);
  62. if ($closed && $result['has_unsettled'] && BackflowPeriod::waitsForSettlement($period, $dailyFrom)) {
  63. if ($scheduledCutoff !== null) return false;
  64. throw new RuntimeException('该期仍有未结算注单,请待结算后发放', HttpStatus::CUSTOM_ERROR);
  65. }
  66. $reward->loss_amount = $result['loss_amount'];
  67. $reward->backflow_ratio = '0';
  68. $changeType = '回水';
  69. $remark = "日期:{$reward->date};亏损:{$result['loss_amount']}";
  70. foreach ($result['details'] as $detail) {
  71. if ($detail['rule_id'] === null) continue;
  72. $backflowDetails .= ";{$detail['label']}亏损{$detail['loss_amount']}奖励{$detail['amount']}";
  73. }
  74. }
  75. $amount = $result['amount'];
  76. $needFlow = $result['need_flow'];
  77. $reward->details = $result['details'];
  78. $reward->need_flow = $needFlow;
  79. // 每次只发新增差额,金额回落不追回;未实际入账的打码量也不能记为已发放。
  80. $pay = self::positive(bcsub($amount, (string) $reward->granted_amount, 2));
  81. $payFlow = bccomp($pay, '0', 2) > 0 ? self::positive(bcsub($needFlow, (string) $reward->granted_need_flow, 2)) : '0.00';
  82. if (!$closed && bccomp($pay, '0', 2) <= 0) {
  83. throw new RuntimeException("暂无可发放金额(按当前规则应发 {$amount},已发放 " . bcadd((string) $reward->granted_amount, '0', 2) . ')', HttpStatus::CUSTOM_ERROR);
  84. }
  85. $paymentRemark = ";本次发放:{$pay};累计发放:" . bcadd((string) $reward->granted_amount, $pay, 2);
  86. if ($type === 'backflow' && mb_strlen($remark . $backflowDetails . $paymentRemark) > 255) {
  87. $backflowDetails = ';档位明细见回水记录';
  88. }
  89. $remark .= $backflowDetails . $paymentRemark;
  90. $credit = null;
  91. if (bccomp($pay, '0', 2) > 0) {
  92. $wallet = Wallet::where('member_id', $reward->member_id)->lockForUpdate()->first();
  93. if (!$wallet) throw new RuntimeException('会员钱包不存在', HttpStatus::CUSTOM_ERROR);
  94. $before = (string) $wallet->available_balance;
  95. $wallet->available_balance = bcadd($before, $pay, 10);
  96. if (!$wallet->save()) throw new RuntimeException('余额更新失败', HttpStatus::CUSTOM_ERROR);
  97. $credit = BalanceLogService::addLog($reward->member_id, $pay, $before, $wallet->available_balance,
  98. $changeType, $reward->id, $remark);
  99. if (bccomp($payFlow, '0', 2) > 0) {
  100. WalletBonus::addData($reward->member_id, $type === 'rebate' ? 4 : 5, $pay, 0, $reward->id, $payFlow);
  101. // 规则配置了打码倍数时计入提现打码量
  102. WageringService::addRequirement($wallet, $payFlow, 'activity:' . $changeType, $reward->id, $remark);
  103. }
  104. }
  105. $reward->amount = $amount;
  106. $reward->granted_amount = bcadd((string) $reward->granted_amount, $pay, 2);
  107. $reward->granted_need_flow = bcadd((string) $reward->granted_need_flow, $payFlow, 2);
  108. $reward->status = $closed ? 1 : 2;
  109. $reward->grant_time = Carbon::now()->timestamp;
  110. if (!$reward->save()) throw new RuntimeException('发放失败', HttpStatus::CUSTOM_ERROR);
  111. if ($credit) RewardPayoutMessageService::send($credit, (string) $reward->date);
  112. return true;
  113. }, 3);
  114. }
  115. private static function positive(string $value): string
  116. {
  117. return bccomp($value, '0', 2) > 0 ? $value : '0.00';
  118. }
  119. }