RewardGrantService.php 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  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 Carbon\Carbon;
  9. use Illuminate\Support\Facades\DB;
  10. use RuntimeException;
  11. /** Shared transaction for manual and automatic payouts. Never trusts a previously loaded reward row. */
  12. class RewardGrantService
  13. {
  14. public static function grant(string $type, int $id, string $operator, bool $skipPaid = false, ?string $scheduledCutoff = null): bool
  15. {
  16. if (!in_array($type, RewardGrantSettingsService::TYPES, true)) {
  17. throw new RuntimeException('奖励类型错误', HttpStatus::CUSTOM_ERROR);
  18. }
  19. return DB::transaction(function () use ($type, $id, $operator, $skipPaid, $scheduledCutoff) {
  20. $model = $type === 'rebate' ? Rebate::class : Backflow::class;
  21. $reward = $model::whereKey($id)->lockForUpdate()->first();
  22. if (!$reward) throw new RuntimeException('数据不存在', HttpStatus::CUSTOM_ERROR);
  23. // 回水 status 2 = 部分发放:当月提前发放过,仍可继续发放差额
  24. $openStatuses = $type === 'backflow' ? [Backflow::STATUS_PENDING, Backflow::STATUS_PARTIAL] : [0];
  25. if (!in_array((int) $reward->status, $openStatuses, true)) {
  26. if ($skipPaid) return false;
  27. throw new RuntimeException('奖励已发放,无需再次发放', HttpStatus::CUSTOM_ERROR);
  28. }
  29. $cutoff = $type === 'rebate'
  30. ? Carbon::now('America/New_York')->format('Y-m-d')
  31. : Carbon::now('Asia/Shanghai')->format('Y-m');
  32. // 回水手动发放不限月份(当月按发放时的亏损结算);笔笔返与自动发放仍须等周期结束。
  33. $manualBackflow = $type === 'backflow' && $scheduledCutoff === null;
  34. if (!$manualBackflow && (string) $reward->date >= $cutoff) {
  35. throw new RuntimeException('未到发放时间', HttpStatus::CUSTOM_ERROR);
  36. }
  37. if ($scheduledCutoff !== null && (string) $reward->date >= $scheduledCutoff) return false;
  38. if ($type === 'rebate') {
  39. $result = RewardBasisService::rebate($reward);
  40. $basis = $result['effective_amount'];
  41. $reward->betting_amount = $result['betting_amount'];
  42. $reward->effective_betting_amount = $basis;
  43. $reward->rebate_ratio = bccomp($basis, '0', 2) > 0 ? bcdiv($result['amount'], $basis, 6) : '0';
  44. $reward->audited_by = $operator;
  45. $changeType = '笔笔返';
  46. $remark = "日期:{$reward->date};有效投注:{$basis}";
  47. foreach ($result['details'] as $detail) {
  48. if ($detail['rule_id'] === null) continue;
  49. $remark .= ";{$detail['label']}:{$detail['effective_amount']}×{$detail['percentage']}%";
  50. }
  51. } else {
  52. $result = RewardBasisService::backflow($reward);
  53. $reward->loss_amount = $result['loss_amount'];
  54. $reward->backflow_ratio = '0';
  55. $changeType = '回水';
  56. $remark = "日期:{$reward->date};亏损:{$result['loss_amount']}";
  57. foreach ($result['details'] as $detail) {
  58. if ($detail['rule_id'] === null) continue;
  59. $remark .= ";{$detail['label']}亏损{$detail['loss_amount']}奖励{$detail['amount']}";
  60. }
  61. }
  62. $amount = $result['amount'];
  63. $needFlow = $result['need_flow'];
  64. $reward->details = $result['details'];
  65. $reward->need_flow = $needFlow;
  66. // 本次实际发放:笔笔返一次发完;回水只发尚未发放的差额(金额回落时不追回)
  67. $pay = $amount;
  68. $payFlow = $needFlow;
  69. $closed = true;
  70. if ($type === 'backflow') {
  71. $pay = self::positive(bcsub($amount, (string) $reward->granted_amount, 2));
  72. $payFlow = self::positive(bcsub($needFlow, (string) $reward->granted_need_flow, 2));
  73. $closed = (string) $reward->date < $cutoff;
  74. if (!$closed && bccomp($pay, '0', 2) <= 0) {
  75. throw new RuntimeException('暂无可发放金额', HttpStatus::CUSTOM_ERROR);
  76. }
  77. $remark .= ";本次发放:{$pay};累计发放:" . bcadd((string) $reward->granted_amount, $pay, 2);
  78. }
  79. if (bccomp($pay, '0', 2) > 0) {
  80. $wallet = Wallet::where('member_id', $reward->member_id)->lockForUpdate()->first();
  81. if (!$wallet) throw new RuntimeException('会员钱包不存在', HttpStatus::CUSTOM_ERROR);
  82. $before = (string) $wallet->available_balance;
  83. $wallet->available_balance = bcadd($before, $pay, 10);
  84. if (!$wallet->save()) throw new RuntimeException('余额更新失败', HttpStatus::CUSTOM_ERROR);
  85. BalanceLogService::addLog($reward->member_id, $pay, $before, $wallet->available_balance,
  86. $changeType, $reward->id, $remark);
  87. if (bccomp($payFlow, '0', 2) > 0) {
  88. WalletBonus::addData($reward->member_id, $type === 'rebate' ? 4 : 5, $pay, 0, $reward->id, $payFlow);
  89. // 规则配置了打码倍数时计入提现打码量
  90. WageringService::addRequirement($wallet, $payFlow, 'activity:' . $changeType, $reward->id, $remark);
  91. }
  92. }
  93. $reward->amount = $amount;
  94. if ($type === 'backflow') {
  95. $reward->granted_amount = bcadd((string) $reward->granted_amount, $pay, 2);
  96. $reward->granted_need_flow = bcadd((string) $reward->granted_need_flow, $payFlow, 2);
  97. $reward->status = $closed ? Backflow::STATUS_GRANTED : Backflow::STATUS_PARTIAL;
  98. } else {
  99. $reward->status = 1;
  100. }
  101. $reward->grant_time = Carbon::now()->timestamp;
  102. if (!$reward->save()) throw new RuntimeException('发放失败', HttpStatus::CUSTOM_ERROR);
  103. return true;
  104. }, 3);
  105. }
  106. private static function positive(string $value): string
  107. {
  108. return bccomp($value, '0', 2) > 0 ? $value : '0.00';
  109. }
  110. }