RewardGrantService.php 4.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. <?php
  2. namespace App\Services;
  3. use App\Constants\HttpStatus;
  4. use App\Models\Backflow;
  5. use App\Models\Config;
  6. use App\Models\Rebate;
  7. use App\Models\Wallet;
  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. if ((int) $reward->status !== 0) {
  24. if ($skipPaid) return false;
  25. throw new RuntimeException('奖励已发放,无需再次发放', HttpStatus::CUSTOM_ERROR);
  26. }
  27. $cutoff = $type === 'rebate'
  28. ? Carbon::now('America/New_York')->format('Y-m-d')
  29. : Carbon::now('Asia/Shanghai')->format('Y-m');
  30. if ((string) $reward->date >= $cutoff) {
  31. throw new RuntimeException('未到发放时间', HttpStatus::CUSTOM_ERROR);
  32. }
  33. if ($scheduledCutoff !== null && (string) $reward->date >= $scheduledCutoff) return false;
  34. if ($type === 'rebate') {
  35. $ratio = self::configDecimal('rebate');
  36. $basis = (string) $reward->effective_betting_amount;
  37. if (bccomp($basis, '0', 2) < 0) throw new RuntimeException('有效投注金额异常', HttpStatus::CUSTOM_ERROR);
  38. $amount = bcmul($basis, $ratio, 2);
  39. $reward->rebate_ratio = $ratio;
  40. $reward->audited_by = $operator;
  41. $changeType = '笔笔返';
  42. $remark = "日期:{$reward->date};有效投注:{$basis};比例:" . bcmul($ratio, '100', 2) . '%';
  43. } else {
  44. // Preserve the existing monthly formula: abs(recharge + signed withdrawal).
  45. $basis = bcadd((string) $reward->recharge_amount, (string) $reward->withdrawal_amount, 2);
  46. if (bccomp($basis, '0', 2) < 0) $basis = bcsub('0', $basis, 2);
  47. $amount = '0.00';
  48. $ratio = (string) $reward->backflow_ratio;
  49. if (bccomp($basis, self::configDecimal('huishui_restriction'), 2) >= 0) {
  50. $ratio = self::configDecimal('huishui_percentage');
  51. $reward->backflow_ratio = $ratio;
  52. $amount = bcmul($basis, $ratio, 2);
  53. }
  54. $changeType = '回水';
  55. $remark = "日期:{$reward->date};盈亏:-{$basis};回水比例:" . bcmul($ratio ?: '0', '100', 2) . '%';
  56. }
  57. if (bccomp($amount, '0', 2) > 0) {
  58. $wallet = Wallet::where('member_id', $reward->member_id)->lockForUpdate()->first();
  59. if (!$wallet) throw new RuntimeException('会员钱包不存在', HttpStatus::CUSTOM_ERROR);
  60. $before = (string) $wallet->available_balance;
  61. $wallet->available_balance = bcadd($before, $amount, 10);
  62. if (!$wallet->save()) throw new RuntimeException('余额更新失败', HttpStatus::CUSTOM_ERROR);
  63. BalanceLogService::addLog($reward->member_id, $amount, $before, $wallet->available_balance,
  64. $changeType, $reward->id, $remark);
  65. }
  66. $reward->amount = $amount;
  67. $reward->status = 1;
  68. $reward->grant_time = Carbon::now()->timestamp;
  69. if (!$reward->save()) throw new RuntimeException('发放失败', HttpStatus::CUSTOM_ERROR);
  70. return true;
  71. }, 3);
  72. }
  73. private static function configDecimal(string $field): string
  74. {
  75. $value = trim((string) Config::where('field', $field)->value('val'));
  76. if (!preg_match('/^\d+(?:\.\d+)?$/D', $value)) {
  77. throw new RuntimeException('奖励比例或门槛配置异常', HttpStatus::CUSTOM_ERROR);
  78. }
  79. return $value;
  80. }
  81. }