MemberDailyReportService.php 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. <?php
  2. namespace App\Services;
  3. use App\Models\Agent\Agent;
  4. use App\Models\MemberDailyReport;
  5. use App\Services\Sharing\ShareDefaults;
  6. use Carbon\Carbon;
  7. use Illuminate\Support\Facades\DB;
  8. use Illuminate\Support\Facades\Schema;
  9. /**
  10. * 会员每日盈亏报表(每个会员每天一行)。
  11. * - 资金:充值/提现/人工加扣款/优惠/返水,口径同代理报表(AgentReportService::aggregate);
  12. * - 注单:share_bet_facts 中当天结算、未作废的注单,按结算时间归日,按游戏分类拆分;
  13. * - agent_share:当天合营代理从该会员获得的分成(正为代理收入)。
  14. * 输赢 win_loss 为会员视角(正为会员赢)。
  15. */
  16. class MemberDailyReportService
  17. {
  18. private const MONEY_FIELDS = ['deposit_amount', 'withdraw_amount', 'manual_credit', 'manual_debit', 'bonus_amount',
  19. 'rebate_amount', 'bet_amount', 'valid_bet_amount', 'win_loss', 'agent_share'];
  20. public function refresh(string $date): int
  21. {
  22. $day = Carbon::parse($date)->toDateString();
  23. $start = $day . ' 00:00:00';
  24. $end = $day . ' 23:59:59';
  25. $byUser = [];
  26. $byMember = [];
  27. foreach (DB::table('share_bet_facts')->where('settled', 1)->where('voided', 0)->whereBetween('settled_at', [$start, $end])
  28. ->groupBy('user_id', 'category')
  29. ->selectRaw('user_id, category, COUNT(*) bet_count, COALESCE(SUM(bet_amount),0) bet_amount, COALESCE(SUM(effective_amount),0) valid_bet_amount, COALESCE(SUM(net_profit),0) win_loss')
  30. ->get() as $row) {
  31. $item = &$byUser[(int) $row->user_id];
  32. $item['bet_count'] = ($item['bet_count'] ?? 0) + (int) $row->bet_count;
  33. foreach (['bet_amount', 'valid_bet_amount', 'win_loss'] as $field) {
  34. $item[$field] = bcadd($item[$field] ?? '0', (string) $row->{$field}, 4);
  35. }
  36. $item['categories'][(string) $row->category] = [
  37. 'name' => ShareDefaults::categories()[(string) $row->category] ?? (string) $row->category,
  38. 'bet_count' => (int) $row->bet_count,
  39. 'bet_amount' => bcadd((string) $row->bet_amount, '0', 4),
  40. 'valid_bet_amount' => bcadd((string) $row->valid_bet_amount, '0', 4),
  41. 'win_loss' => bcadd((string) $row->win_loss, '0', 4),
  42. ];
  43. unset($item);
  44. }
  45. if (Schema::hasTable('agent_profit_entries')) {
  46. foreach (DB::table('agent_profit_entries')->where('status', 'counted')->where('settle_date', $day)
  47. ->groupBy('user_id')->selectRaw('user_id, COALESCE(SUM(share_amount),0) total')->get() as $row) {
  48. $byUser[(int) $row->user_id]['agent_share'] = bcadd((string) $row->total, '0', 4);
  49. }
  50. }
  51. $this->addMoney($byMember, DB::table('recharges')->where('status', 1)->whereBetween('created_at', [$start, $end]), 'deposit_amount');
  52. $this->addMoney($byMember, DB::table('withdraws')->where('status', 1)->whereBetween('created_at', [$start, $end]), 'withdraw_amount');
  53. if (Schema::hasTable('payment_orders')) {
  54. $orders = fn () => DB::table('payment_orders')->where('status', PaymentOrderService::STATUS_SUCCESS)->whereBetween('created_at', [$start, $end]);
  55. $this->addMoney($byMember, $orders()->whereIn('type', [PaymentOrderService::TYPE_PAY, PaymentOrderService::TYPE_SELF_PAY]), 'deposit_amount');
  56. $this->addMoney($byMember, $orders()->whereIn('type', [PaymentOrderService::TYPE_PAYOUT, PaymentOrderService::TYPE_SELF_PAYOUT]), 'withdraw_amount');
  57. }
  58. foreach (DB::table('balance_logs')->whereBetween('created_at', [$start, $end])->groupBy('member_id')
  59. ->selectRaw(<<<'SQL'
  60. member_id,
  61. COALESCE(SUM(CASE WHEN ((change_type = '人工充值' AND related_id IS NULL) OR change_type = '订单补偿') AND amount > 0 THEN amount ELSE 0 END), 0) AS manual_credit,
  62. COALESCE(SUM(CASE WHEN change_type = '人工扣款' AND amount < 0 THEN ABS(amount) ELSE 0 END), 0) AS manual_debit,
  63. COALESCE(SUM(CASE WHEN (change_type IN ('注册赠送','优惠活动','即充即送','充值返现','老用户回归') OR (change_type = '人工充值' AND related_id = 0)) AND amount > 0 THEN amount ELSE 0 END), 0) AS bonus_amount,
  64. COALESCE(SUM(CASE WHEN change_type IN ('比比返','笔笔返','返水','回水') AND amount > 0 THEN amount ELSE 0 END), 0) AS rebate_amount
  65. SQL)->get() as $row) {
  66. foreach (['manual_credit', 'manual_debit', 'bonus_amount', 'rebate_amount'] as $field) {
  67. if (bccomp((string) $row->{$field}, '0', 4) === 0) continue;
  68. $byMember[(string) $row->member_id][$field] = bcadd($byMember[(string) $row->member_id][$field] ?? '0', (string) $row->{$field}, 4);
  69. }
  70. }
  71. $users = collect();
  72. foreach (array_chunk(array_keys($byUser), 500) as $ids) {
  73. $users = $users->merge(DB::table('users')->whereIn('id', $ids)->get(['id', 'member_id', 'agent_id']));
  74. }
  75. foreach (array_chunk(array_map('strval', array_keys($byMember)), 500) as $memberIds) {
  76. $users = $users->merge(DB::table('users')->whereIn('member_id', $memberIds)->get(['id', 'member_id', 'agent_id']));
  77. }
  78. $rows = [];
  79. $now = now();
  80. foreach ($users->unique('id') as $user) {
  81. $values = array_merge($byUser[(int) $user->id] ?? [], $byMember[(string) $user->member_id] ?? []);
  82. $row = ['report_date' => $day, 'user_id' => (int) $user->id, 'member_id' => (string) $user->member_id,
  83. 'agent_id' => $user->agent_id ? (int) $user->agent_id : null, 'bet_count' => (int) ($values['bet_count'] ?? 0),
  84. 'categories' => json_encode($values['categories'] ?? new \stdClass(), JSON_UNESCAPED_UNICODE),
  85. 'created_at' => $now, 'updated_at' => $now];
  86. foreach (self::MONEY_FIELDS as $field) $row[$field] = bcadd($values[$field] ?? '0', '0', 4);
  87. $rows[] = $row;
  88. }
  89. DB::transaction(function () use ($day, $rows) {
  90. DB::table('member_daily_reports')->where('report_date', $day)->delete();
  91. foreach (array_chunk($rows, 500) as $chunk) DB::table('member_daily_reports')->insert($chunk);
  92. });
  93. return count($rows);
  94. }
  95. /** 总后台查看全部会员;代理端传入 $agent 只看自己的会员。 */
  96. public function paginate(array $params, ?Agent $agent = null): array
  97. {
  98. $page = max(1, (int) ($params['page'] ?? 1));
  99. $limit = min(100, max(1, (int) ($params['limit'] ?? 20)));
  100. $startDate = (string) ($params['start_date'] ?? now()->subDays(6)->toDateString());
  101. $endDate = (string) ($params['end_date'] ?? now()->toDateString());
  102. if (Carbon::parse($startDate)->diffInDays(Carbon::parse($endDate)) > 92) {
  103. throw new \InvalidArgumentException('单次查询不能超过92天');
  104. }
  105. $query = MemberDailyReport::query()->from('member_daily_reports as r')
  106. ->leftJoin('users as u', 'u.id', '=', 'r.user_id')
  107. ->leftJoin('agents as a', 'a.id', '=', 'r.agent_id')
  108. ->whereBetween('r.report_date', [$startDate, $endDate]);
  109. if ($agent) {
  110. $query->where('r.agent_id', $agent->id);
  111. } else {
  112. if (!empty($params['agent_id'])) $query->where('r.agent_id', (int) $params['agent_id']);
  113. if (!empty($params['agent_type'])) $query->where('a.type', (int) $params['agent_type']);
  114. }
  115. if (!empty($params['member_id'])) $query->where('r.member_id', (string) $params['member_id']);
  116. if (!empty($params['username'])) $query->where('u.username', 'like', '%' . $params['username'] . '%');
  117. // 原生 SQL 中的别名也要带表前缀(r -> bot_r),交给 grammar 包裹。
  118. $grammar = DB::connection()->getQueryGrammar();
  119. $summary = (clone $query)->selectRaw('COUNT(*) row_count, COALESCE(SUM(' . $grammar->wrap('r.bet_count') . '),0) bet_count, '
  120. . implode(', ', array_map(fn ($field) => 'COALESCE(SUM(' . $grammar->wrap("r.{$field}") . "),0) {$field}", self::MONEY_FIELDS)))
  121. ->toBase()->first();
  122. $total = (int) ($summary->row_count ?? 0);
  123. $list = $query->select('r.*', 'u.username', 'u.first_name', 'a.username as agent_username', 'a.type as agent_type')
  124. ->orderByDesc('r.report_date')->orderByDesc('r.id')->forPage($page, $limit)->get();
  125. $totals = ['bet_count' => (int) ($summary->bet_count ?? 0)];
  126. foreach (self::MONEY_FIELDS as $field) $totals[$field] = bcadd((string) ($summary->{$field} ?? 0), '0', 4);
  127. return compact('total', 'page', 'limit', 'totals', 'list');
  128. }
  129. private function addMoney(array &$byMember, $query, string $field): void
  130. {
  131. foreach ($query->groupBy('member_id')->selectRaw('member_id, COALESCE(SUM(amount),0) total')->get() as $row) {
  132. $byMember[(string) $row->member_id][$field] = bcadd($byMember[(string) $row->member_id][$field] ?? '0', (string) $row->total, 4);
  133. }
  134. }
  135. }