RewardRuleService.php 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. <?php
  2. namespace App\Services;
  3. use App\Services\Sharing\ShareDefaults;
  4. use App\Support\MaintenancePolicy;
  5. use Illuminate\Support\Facades\DB;
  6. use Illuminate\Validation\Rule;
  7. use Illuminate\Validation\ValidationException;
  8. use InvalidArgumentException;
  9. /** Per-category tiers for 笔笔返 (rebate) and 回水 (backflow); each save replaces one category's list. */
  10. class RewardRuleService
  11. {
  12. private const TABLES = ['rebate' => 'rebate_rules', 'backflow' => 'backflow_rules'];
  13. private const BACKFLOW_EXCLUDED_RULE_CATEGORIES = ['self:lottery', 'third:3'];
  14. public static function categories(string $type = 'rebate'): array
  15. {
  16. $categories = ShareDefaults::categories();
  17. if ($type === 'backflow') {
  18. $categories = array_diff_key($categories, array_flip(self::BACKFLOW_EXCLUDED_RULE_CATEGORIES));
  19. foreach (ConfigService::sortedList('game_list') as $item) {
  20. if (!is_string($item['game'] ?? null)) continue;
  21. $category = MaintenancePolicy::lotterySubcategory($item);
  22. if ($category === null || isset($categories[$category])) continue;
  23. $label = is_string($item['name'] ?? null) ? trim($item['name']) : '';
  24. $categories[$category] = $label === '' ? $category : $label;
  25. }
  26. }
  27. return $categories;
  28. }
  29. public static function get(string $type): array
  30. {
  31. $table = self::table($type);
  32. $labels = self::categories($type);
  33. $categories = $lotteries = [];
  34. foreach ($labels as $value => $label) {
  35. $option = ['value' => $value, 'label' => $label];
  36. if ($type === 'backflow' && MaintenancePolicy::parentScope($value) === 'self:lottery') {
  37. $lotteries[] = $option;
  38. } else {
  39. $categories[] = $option;
  40. }
  41. }
  42. if ($type === 'backflow') {
  43. $categories[] = ['value' => 'self:lottery', 'label' => ShareDefaults::categories()['self:lottery'], 'children' => $lotteries];
  44. }
  45. $rules = array_fill_keys(array_keys($labels), []);
  46. $query = DB::table($table)->orderBy('category');
  47. if ($type === 'rebate') $query->orderBy('level')->orderBy('min_effective_amount');
  48. else $query->orderBy('min_loss_amount');
  49. foreach ($query->get() as $row) {
  50. if (!array_key_exists($row->category, $rules)) continue;
  51. $rules[$row->category][] = self::present($type, $row);
  52. }
  53. $result = ['type' => $type, 'categories' => $categories, 'rules' => $rules];
  54. if ($type === 'rebate') {
  55. $result['levels'] = DB::table('level')->orderBy('level')->get(['level', 'level_name'])
  56. ->map(fn ($row) => ['level' => (int) $row->level, 'level_name' => (string) $row->level_name])->all();
  57. }
  58. return $result;
  59. }
  60. public static function save(string $type, array $params): array
  61. {
  62. $table = self::table($type);
  63. $plain = 'regex:/^\d{1,12}(?:\.\d{1,4})?$/D';
  64. $money = 'regex:/^\d{1,12}(?:\.\d{1,2})?$/D';
  65. $amount = ['required', 'numeric', $money, 'min:0'];
  66. $rules = ['category' => ['required', 'string', Rule::in(array_keys(self::categories($type)))],
  67. 'rules' => ['present', 'array', 'max:200'],
  68. 'rules.*' => ['array'],
  69. 'rules.*.turnover_multiple' => ['required', 'numeric', $money, 'min:0', 'max:999999']];
  70. if ($type === 'rebate') {
  71. $rules['rules.*.level'] = ['required', 'integer', Rule::in(DB::table('level')->pluck('level')->map(fn ($v) => (int) $v)->all())];
  72. $rules['rules.*.min_effective_amount'] = $amount;
  73. $rules['rules.*.percentage'] = ['required', 'numeric', $plain, 'min:0', 'max:100'];
  74. } else {
  75. $rules['rules.*.min_loss_amount'] = $amount;
  76. $rules['rules.*.reward_amount'] = $amount;
  77. }
  78. $params = validator($params, $rules, [], ['rules.*.level' => '会员等级', 'rules.*.percentage' => '返回百分比',
  79. 'rules.*.min_effective_amount' => '有效投注', 'rules.*.min_loss_amount' => '亏损金额',
  80. 'rules.*.reward_amount' => '奖励金额', 'rules.*.turnover_multiple' => '需要打码量'])->validate();
  81. $rows = $seen = [];
  82. $now = now();
  83. foreach ($params['rules'] as $rule) {
  84. $row = ['category' => $params['category'], 'turnover_multiple' => self::decimal($rule['turnover_multiple']),
  85. 'created_at' => $now, 'updated_at' => $now];
  86. if ($type === 'rebate') {
  87. $row += ['level' => (int) $rule['level'], 'min_effective_amount' => self::decimal($rule['min_effective_amount']),
  88. 'percentage' => bcadd((string) $rule['percentage'], '0', 4)];
  89. $key = $row['level'] . '|' . $row['min_effective_amount'];
  90. } else {
  91. $row += ['min_loss_amount' => self::decimal($rule['min_loss_amount']),
  92. 'reward_amount' => self::decimal($rule['reward_amount'])];
  93. $key = $row['min_loss_amount'];
  94. }
  95. if (isset($seen[$key])) {
  96. throw ValidationException::withMessages(['rules' => $type === 'rebate'
  97. ? '同一会员等级的有效投注门槛不能重复' : '亏损金额门槛不能重复']);
  98. }
  99. $seen[$key] = true;
  100. $rows[] = $row;
  101. }
  102. DB::transaction(function () use ($table, $params, $rows) {
  103. DB::table($table)->where('category', $params['category'])->delete();
  104. if ($rows) DB::table($table)->insert($rows);
  105. });
  106. return self::get($type);
  107. }
  108. /** Rules of one type grouped by category, highest threshold first, for settlement. */
  109. public static function tiers(string $type): array
  110. {
  111. $tiers = [];
  112. $threshold = $type === 'rebate' ? 'min_effective_amount' : 'min_loss_amount';
  113. $query = DB::table(self::table($type))->orderByDesc($threshold);
  114. if ($type === 'backflow') $query->whereNotIn('category', self::BACKFLOW_EXCLUDED_RULE_CATEGORIES);
  115. foreach ($query->get() as $row) {
  116. $tiers[$row->category][] = self::present($type, $row);
  117. }
  118. return $tiers;
  119. }
  120. private static function present(string $type, object $row): array
  121. {
  122. $data = ['id' => (int) $row->id, 'category' => (string) $row->category];
  123. if ($type === 'rebate') {
  124. $data += ['level' => (int) $row->level, 'min_effective_amount' => self::decimal($row->min_effective_amount),
  125. 'percentage' => bcadd((string) $row->percentage, '0', 4)];
  126. } else {
  127. $data += ['min_loss_amount' => self::decimal($row->min_loss_amount), 'reward_amount' => self::decimal($row->reward_amount)];
  128. }
  129. return $data + ['turnover_multiple' => self::decimal($row->turnover_multiple)];
  130. }
  131. private static function decimal($value): string
  132. {
  133. return bcadd((string) $value, '0', 2);
  134. }
  135. private static function table(string $type): string
  136. {
  137. if (!isset(self::TABLES[$type])) throw new InvalidArgumentException('奖励类型错误');
  138. return self::TABLES[$type];
  139. }
  140. }