RewardRuleService.php 5.7 KB

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