| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149 |
- <?php
- namespace App\Services;
- use App\Services\Sharing\EffectiveBet;
- use Carbon\Carbon;
- use Illuminate\Support\Facades\DB;
- /**
- * 笔笔返/回水的计算口径:按分类汇总 share_bet_facts,再套用 RewardRuleService 的档位。
- * 笔笔返按纽约自然日,回水按北京时间自然月,与原有 rebates.date / backflow.date 一致。
- */
- class RewardBasisService
- {
- public static function user(string $memberId): ?object
- {
- return DB::table('users')->where('member_id', $memberId)->first(['id', 'member_id', 'level', 'username', 'first_name']);
- }
- /** Category totals for one New York day: bet (non-void), effective and profit (settled only). */
- public static function rebateBasis(object $user, string $nyDate): array
- {
- $start = Carbon::parse($nyDate . ' 00:00:00', 'America/New_York')->setTimezone('Asia/Shanghai');
- $end = $start->copy()->addDay();
- return self::sum(DB::table('share_bet_facts')->where('user_id', $user->id)
- ->whereBetween('bet_date', [$start->toDateString(), $end->toDateString()])
- ->where('bet_at', '>=', $start->format('Y-m-d H:i:s'))->where('bet_at', '<', $end->format('Y-m-d H:i:s')));
- }
- /** Category totals for one Asia/Shanghai month (Y-m). */
- public static function backflowBasis(object $user, string $month): array
- {
- $start = Carbon::parse($month . '-01', 'Asia/Shanghai');
- return self::sum(DB::table('share_bet_facts')->where('user_id', $user->id)
- ->whereBetween('bet_date', [$start->toDateString(), $start->copy()->endOfMonth()->toDateString()]));
- }
- /** Highest tier of the member's level whose threshold the category's effective bet reaches. */
- public static function rebateReward(array $basis, int $level, ?array $tiers = null): array
- {
- $tiers ??= RewardRuleService::tiers('rebate');
- $result = self::empty($basis);
- foreach ($basis as $category => $row) {
- $detail = self::detail($category, $row) + ['level' => $level];
- if (bccomp($row['effective_amount'], '0', 2) > 0) {
- foreach ($tiers[$category] ?? [] as $tier) {
- if ($tier['level'] !== $level || bccomp($row['effective_amount'], $tier['min_effective_amount'], 2) < 0) continue;
- $amount = bcmul($row['effective_amount'], bcdiv($tier['percentage'], '100', 8), 2);
- $detail = self::matched($detail, $tier, $amount) + ['min_effective_amount' => $tier['min_effective_amount'],
- 'percentage' => $tier['percentage']];
- break;
- }
- }
- $result = self::add($result, $detail);
- }
- return $result;
- }
- /** Highest tier whose loss threshold the category's monthly net loss reaches; the reward is a fixed amount. */
- public static function backflowReward(array $basis, ?array $tiers = null): array
- {
- $tiers ??= RewardRuleService::tiers('backflow');
- $result = self::empty($basis) + ['loss_amount' => '0.00'];
- foreach ($basis as $category => $row) {
- $loss = bccomp($row['profit'], '0', 2) < 0 ? bcsub('0', $row['profit'], 2) : '0.00';
- $detail = self::detail($category, $row) + ['loss_amount' => $loss];
- if (bccomp($loss, '0', 2) > 0) {
- $result['loss_amount'] = bcadd($result['loss_amount'], $loss, 2);
- foreach ($tiers[$category] ?? [] as $tier) {
- if (bccomp($loss, $tier['min_loss_amount'], 2) < 0) continue;
- $detail = self::matched($detail, $tier, $tier['reward_amount']) + ['min_loss_amount' => $tier['min_loss_amount']];
- break;
- }
- }
- $result = self::add($result, $detail);
- }
- return $result;
- }
- /** Fresh rebate result for a stored row; legacy PC28 days without any facts keep their stored effective bet. */
- public static function rebate(object $reward): array
- {
- $user = self::user((string) $reward->member_id);
- $basis = $user ? self::rebateBasis($user, (string) $reward->date) : [];
- if ($basis === [] && bccomp((string) $reward->effective_betting_amount, '0', 2) > 0) {
- $basis = ['self:lottery' => ['bet_amount' => self::money($reward->betting_amount),
- 'effective_amount' => self::money($reward->effective_betting_amount), 'profit' => self::money($reward->profit ?? 0)]];
- }
- return self::rebateReward($basis, (int) ($user->level ?? 0));
- }
- public static function backflow(object $reward): array
- {
- $user = self::user((string) $reward->member_id);
- return self::backflowReward($user ? self::backflowBasis($user, (string) $reward->date) : []);
- }
- private static function sum($query): array
- {
- $currency = strtoupper(trim((string) config('third_game.currency', 'CNY')));
- $rows = $query->where('voided', 0)->where('currency', $currency)->groupBy('category')
- ->selectRaw('category, SUM(bet_amount) AS bet_amount, SUM(effective_amount) AS effective_amount, SUM(net_profit) AS profit')
- ->orderBy('category')->get();
- $basis = [];
- foreach ($rows as $row) {
- $basis[$row->category] = ['bet_amount' => self::money($row->bet_amount),
- 'effective_amount' => self::money($row->effective_amount), 'profit' => self::money($row->profit)];
- }
- return $basis;
- }
- private static function empty(array $basis): array
- {
- return ['betting_amount' => '0.00', 'effective_amount' => '0.00', 'profit' => '0.00',
- 'amount' => '0.00', 'need_flow' => '0.00', 'details' => [], 'has_facts' => $basis !== []];
- }
- private static function detail(string $category, array $row): array
- {
- return ['category' => $category, 'label' => RewardRuleService::categories()[$category] ?? $category,
- 'bet_amount' => $row['bet_amount'], 'effective_amount' => $row['effective_amount'], 'profit' => $row['profit'],
- 'rule_id' => null, 'turnover_multiple' => '0.00', 'amount' => '0.00', 'need_flow' => '0.00'];
- }
- private static function matched(array $detail, array $tier, string $amount): array
- {
- return array_merge($detail, ['rule_id' => $tier['id'], 'turnover_multiple' => $tier['turnover_multiple'],
- 'amount' => $amount, 'need_flow' => bcmul($amount, $tier['turnover_multiple'], 2)]);
- }
- private static function add(array $result, array $detail): array
- {
- $result['betting_amount'] = bcadd($result['betting_amount'], $detail['bet_amount'], 2);
- $result['effective_amount'] = bcadd($result['effective_amount'], $detail['effective_amount'], 2);
- $result['profit'] = bcadd($result['profit'], $detail['profit'], 2);
- $result['amount'] = bcadd($result['amount'], $detail['amount'], 2);
- $result['need_flow'] = bcadd($result['need_flow'], $detail['need_flow'], 2);
- $result['details'][] = $detail;
- return $result;
- }
- /** SUM() is an exact string on MySQL but may be a float on SQLite. */
- private static function money($value): string
- {
- if ($value === null || $value === '') return '0.00';
- if (is_float($value) || is_int($value)) $value = number_format((float) $value, 10, '.', '');
- return bcadd(EffectiveBet::decimal($value), '0', 2);
- }
- }
|