ShareEarningReportService.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. <?php
  2. namespace App\Services;
  3. use App\Models\BalanceLog;
  4. use App\Models\ShareCommissionRecord;
  5. use App\Models\ShareSetting;
  6. use App\Models\User;
  7. use App\Models\UserLogin;
  8. use Illuminate\Support\Facades\DB;
  9. class ShareEarningReportService
  10. {
  11. public function memberDays(array $params): array
  12. {
  13. $range = !empty($params['start_date']) || !empty($params['end_date']) ? $this->dateRange($params) : null;
  14. $page = (int)($params['page'] ?? 1);
  15. $limit = (int)($params['limit'] ?? 20);
  16. $dateQuery = User::query()->whereNotNull('agent_user_code')->where('agent_user_code', '<>', '');
  17. if ($range !== null) {
  18. $dateQuery->whereBetween('created_at', [$range[0] . ' 00:00:00', $range[1] . ' 23:59:59']);
  19. }
  20. $dateQuery->selectRaw('DATE(created_at) AS stat_date')
  21. ->groupByRaw('DATE(created_at)')
  22. ->orderByDesc('stat_date');
  23. $dates = $dateQuery->pluck('stat_date');
  24. $pageDates = $dates->slice(($page - 1) * $limit, $limit)->values();
  25. if ($pageDates->isEmpty()) return ['total' => $dates->count(), 'data' => []];
  26. $pageStart = (string)$pageDates->min();
  27. $pageEnd = (string)$pageDates->max();
  28. $basicRows = User::query()->whereNotNull('agent_user_code')->where('agent_user_code', '<>', '')
  29. ->whereBetween('created_at', [$pageStart . ' 00:00:00', $pageEnd . ' 23:59:59'])
  30. ->selectRaw('DATE(created_at) AS stat_date')
  31. ->selectRaw('COUNT(*) AS registered_members')
  32. ->selectRaw('COUNT(DISTINCT agent_user_code) AS promoter_count')
  33. ->groupByRaw('DATE(created_at)')->get()->keyBy('stat_date');
  34. $inviteeBase = User::query()->whereNotNull('agent_user_code')->where('agent_user_code', '<>', '')
  35. ->whereBetween('created_at', [$pageStart . ' 00:00:00', $pageEnd . ' 23:59:59'])
  36. ->select(['id', 'member_id'])->selectRaw('DATE(created_at) AS stat_date');
  37. $memberMetrics = DB::query()->fromSub($inviteeBase, 'invitee')
  38. ->leftJoin('balance_logs as balance_log', 'balance_log.member_id', '=', 'invitee.member_id')
  39. ->select(['invitee.id', 'invitee.stat_date'])
  40. ->selectRaw('SUM(CASE WHEN change_type IN (?, ?) THEN amount ELSE 0 END) AS deposit', ['充值', '三方充值'])
  41. ->selectRaw('ABS(SUM(CASE WHEN change_type IN (?, ?, ?) THEN amount ELSE 0 END)) AS withdraw', ['提现', '三方提现', '人工提现'])
  42. ->selectRaw('ABS(SUM(CASE WHEN change_type LIKE ? THEN amount ELSE 0 END)) AS consume', ['%投注%'])
  43. ->selectRaw('SUM(CASE WHEN change_type IN (?, ?, ?, ?, ?, ?) THEN amount ELSE 0 END) AS bonus', ['注册赠送', '优惠活动', '返水', '回水', '笔笔返', '比比返'])
  44. ->groupBy(['invitee.id', 'invitee.stat_date']);
  45. $validMin = (float)(ShareSetting::query()->value('valid_member_min_deposit') ?? 0);
  46. $metricRows = DB::query()->fromSub($memberMetrics, 'member_metric')
  47. ->select('stat_date')
  48. ->selectRaw('SUM(deposit) AS deposit_total')
  49. ->selectRaw('SUM(withdraw) AS withdraw_total')
  50. ->selectRaw('SUM(consume) AS consume_total')
  51. ->selectRaw('SUM(bonus) AS bonus_total')
  52. ->selectRaw('SUM(CASE WHEN deposit >= ? THEN 1 ELSE 0 END) AS valid_members', [$validMin])
  53. ->groupBy('stat_date')->get()->keyBy('stat_date');
  54. $pageMemberIds = User::query()->whereNotNull('agent_user_code')->where('agent_user_code', '<>', '')
  55. ->whereBetween('created_at', [$pageStart . ' 00:00:00', $pageEnd . ' 23:59:59'])
  56. ->select('member_id');
  57. $firstLoginIds = UserLogin::query()->where('status', UserLogin::STATUS_SUCCESS)
  58. ->whereIn('user_id', $pageMemberIds)
  59. ->select('user_id')->selectRaw('MIN(id) AS first_login_id')->groupBy('user_id');
  60. $prefix = DB::getTablePrefix();
  61. $userAlias = $prefix . 'invitee_user';
  62. $loginAlias = $prefix . 'first_login_row';
  63. $platformRows = User::query()->from('users as invitee_user')
  64. ->leftJoinSub($firstLoginIds, 'first_login', 'first_login.user_id', '=', 'invitee_user.member_id')
  65. ->leftJoin('user_login as first_login_row', 'first_login_row.id', '=', 'first_login.first_login_id')
  66. ->whereNotNull('invitee_user.agent_user_code')->where('invitee_user.agent_user_code', '<>', '')
  67. ->whereBetween('invitee_user.created_at', [$pageStart . ' 00:00:00', $pageEnd . ' 23:59:59'])
  68. ->selectRaw("DATE({$userAlias}.created_at) AS stat_date")
  69. ->selectRaw("SUM(CASE WHEN {$loginAlias}.platform = ? THEN 1 ELSE 0 END) AS h5_registered", ['H5'])
  70. ->selectRaw("SUM(CASE WHEN {$loginAlias}.platform = ? THEN 1 ELSE 0 END) AS ios_registered", ['iOS'])
  71. ->selectRaw("SUM(CASE WHEN {$loginAlias}.platform = ? THEN 1 ELSE 0 END) AS android_registered", ['Android'])
  72. ->groupByRaw("DATE({$userAlias}.created_at)")->get()->keyBy('stat_date');
  73. $rows = $pageDates->map(function ($day) use ($basicRows, $metricRows, $platformRows) {
  74. $basic = $basicRows->get($day);
  75. $metric = $metricRows->get($day);
  76. $platform = $platformRows->get($day);
  77. $deposit = (float)($metric->deposit_total ?? 0);
  78. $withdraw = (float)($metric->withdraw_total ?? 0);
  79. $bonus = (float)($metric->bonus_total ?? 0);
  80. return [
  81. 'date' => (string)$day,
  82. 'promoter_count' => (int)($basic->promoter_count ?? 0),
  83. 'registered_members' => (int)($basic->registered_members ?? 0),
  84. 'valid_members' => (int)($metric->valid_members ?? 0),
  85. 'h5_registered' => (int)($platform->h5_registered ?? 0),
  86. 'ios_registered' => (int)($platform->ios_registered ?? 0),
  87. 'android_registered' => (int)($platform->android_registered ?? 0),
  88. 'deposit_total' => number_format($deposit, 4, '.', ''),
  89. 'consume_total' => number_format((float)($metric->consume_total ?? 0), 4, '.', ''),
  90. 'company_profit' => number_format($deposit - $withdraw - $bonus, 4, '.', ''),
  91. ];
  92. });
  93. return ['total' => $dates->count(), 'data' => $rows->values()];
  94. }
  95. public function promoterDetails(array $params): array
  96. {
  97. $day = $params['date'];
  98. $promoterCodes = User::query()->whereNotNull('agent_user_code')->where('agent_user_code', '<>', '')
  99. ->whereBetween('created_at', [$day . ' 00:00:00', $day . ' 23:59:59'])
  100. ->select('agent_user_code')->distinct();
  101. $promoterAlias = DB::getTablePrefix() . 'promoter';
  102. $entries = DB::query()->fromSub($promoterCodes, 'referral')
  103. ->leftJoin('users as promoter', 'promoter.user_code', '=', 'referral.agent_user_code')
  104. ->select('referral.agent_user_code')
  105. ->selectRaw("COUNT({$promoterAlias}.id) AS promoter_matches")
  106. ->groupBy('referral.agent_user_code');
  107. // Keep historical codes even when the inviter was removed or changed their code.
  108. // An ambiguous code cannot safely identify one member or their paid commission.
  109. foreach (['id' => 'promoter_id', 'member_id' => 'member_id', 'account' => 'account', 'first_name' => 'nickname'] as $column => $alias) {
  110. $entries->selectRaw("CASE WHEN COUNT({$promoterAlias}.id) = 1 THEN MAX({$promoterAlias}.{$column}) END AS {$alias}");
  111. }
  112. $query = DB::query()->fromSub($entries, 'promoter_entry');
  113. if (!empty($params['member_id'])) $query->where('promoter_entry.member_id', $params['member_id']);
  114. if (!empty($params['account'])) $query->where('promoter_entry.account', 'like', '%' . $params['account'] . '%');
  115. $total = $query->count();
  116. $promoters = $query->orderByDesc('promoter_entry.promoter_id')->orderByDesc('promoter_entry.agent_user_code')
  117. ->forPage((int)($params['page'] ?? 1), (int)($params['limit'] ?? 20))->get();
  118. $invitees = User::query()->whereBetween('created_at', [$day . ' 00:00:00', $day . ' 23:59:59'])
  119. ->whereIn('agent_user_code', $promoters->pluck('agent_user_code'))
  120. ->get(['id', 'member_id', 'agent_user_code']);
  121. $records = ShareCommissionRecord::query()->where('stat_date', $day)->where('status', 1)
  122. ->whereIn('promoter_user_id', $promoters->pluck('promoter_id')->filter())->get()->groupBy('promoter_user_id');
  123. $setting = ShareSetting::query()->first();
  124. $metricsByMember = $this->moneyMetricsByMember($invitees->pluck('member_id')->all());
  125. $rows = $promoters->map(function ($promoter) use ($invitees, $records, $setting, $day, $metricsByMember) {
  126. $children = $invitees->whereStrict('agent_user_code', (string)$promoter->agent_user_code);
  127. $metrics = $this->sumMetrics($metricsByMember, $children->pluck('member_id')->all());
  128. $commission = $records->get($promoter->promoter_id, collect())->where('type', 'commission')->sum('amount');
  129. return [
  130. 'date' => $day,
  131. 'promoter_code' => (string)$promoter->agent_user_code,
  132. 'promoter_status' => (int)$promoter->promoter_matches === 1 ? 'matched'
  133. : ((int)$promoter->promoter_matches === 0 ? 'missing' : 'ambiguous'),
  134. 'member_id' => (string)($promoter->member_id ?? ''),
  135. 'account' => (string)($promoter->account ?? ''),
  136. 'nickname' => (string)($promoter->nickname ?? ''),
  137. 'deposit_total' => $metrics['deposit'],
  138. 'registered_count' => $children->count(),
  139. 'commission' => number_format((float)$commission, 4, '.', ''),
  140. 'current_fee_rate' => $setting ? (string)$setting->tier_one_rate : '0.0000',
  141. ];
  142. });
  143. return ['total' => $total, 'data' => $rows->values()];
  144. }
  145. public function registrationDetails(array $params): array
  146. {
  147. $query = User::query()->whereNotNull('agent_user_code')->where('agent_user_code', '<>', '')
  148. ->whereBetween('created_at', [$params['date'] . ' 00:00:00', $params['date'] . ' 23:59:59']);
  149. if (!empty($params['member_id'])) $query->where('member_id', $params['member_id']);
  150. if (!empty($params['account'])) $query->where('account', 'like', '%' . $params['account'] . '%');
  151. $total = $query->count();
  152. $users = $query->orderByDesc('id')->forPage((int)($params['page'] ?? 1), (int)($params['limit'] ?? 20))->get();
  153. $metricsByMember = $this->moneyMetricsByMember($users->pluck('member_id')->all());
  154. $rows = $users->map(function ($user) use ($params, $metricsByMember) {
  155. $metrics = $metricsByMember[(string)$user->member_id] ?? $this->emptyMetrics();
  156. return [
  157. 'date' => $params['date'],
  158. 'member_id' => (string)$user->member_id,
  159. 'account' => (string)$user->account,
  160. 'nickname' => (string)$user->first_name,
  161. 'deposit_total' => $metrics['deposit'],
  162. 'bet_total' => $metrics['consume'],
  163. 'withdraw_total' => $metrics['withdraw'],
  164. ];
  165. });
  166. return ['total' => $total, 'data' => $rows->values()];
  167. }
  168. public function summary(array $params): array
  169. {
  170. $range = !empty($params['start_date']) || !empty($params['end_date']) ? $this->dateRange($params) : null;
  171. $page = (int)($params['page'] ?? 1);
  172. $limit = (int)($params['limit'] ?? 20);
  173. $prefix = DB::getTablePrefix();
  174. $recordAlias = $prefix . 'share_record';
  175. $query = ShareCommissionRecord::query()->from('share_commission_records as share_record')
  176. ->leftJoin('users as promoter', 'promoter.id', '=', 'share_record.promoter_user_id')
  177. ->where('share_record.status', 1);
  178. if ($range !== null) $query->whereBetween('share_record.stat_date', $range);
  179. if (!empty($params['account'])) $query->where('promoter.account', 'like', '%' . $params['account'] . '%');
  180. $totalQuery = clone $query;
  181. $grouped = $query->select([
  182. 'share_record.stat_date', 'share_record.promoter_user_id',
  183. 'promoter.member_id as promoter_member_id', 'promoter.account as promoter_account',
  184. 'promoter.first_name as promoter_nickname',
  185. ])->selectRaw("SUM(CASE WHEN {$recordAlias}.type = 'commission' THEN {$recordAlias}.base_amount ELSE 0 END) AS deposit")
  186. ->selectRaw("SUM(CASE WHEN {$recordAlias}.type = 'commission' THEN {$recordAlias}.amount ELSE 0 END) AS commission")
  187. ->selectRaw("SUM(CASE WHEN {$recordAlias}.type = 'fee_waiver' THEN {$recordAlias}.base_amount ELSE 0 END) AS sales_amount")
  188. ->selectRaw("MAX(CASE WHEN {$recordAlias}.type = 'fee_waiver' THEN {$recordAlias}.rate ELSE 0 END) AS fee_waiver_rate")
  189. ->groupBy([
  190. 'share_record.stat_date', 'share_record.promoter_user_id',
  191. 'promoter.member_id', 'promoter.account', 'promoter.first_name',
  192. ]);
  193. $total = DB::query()->fromSub(clone $grouped, 'share_summary')->count();
  194. $rows = $grouped->orderByDesc('share_record.stat_date')->orderByDesc('share_record.promoter_user_id')
  195. ->forPage($page, $limit)->get()->map(fn ($row) => [
  196. 'date' => $row->stat_date instanceof \DateTimeInterface
  197. ? $row->stat_date->format('Y-m-d')
  198. : substr((string)$row->stat_date, 0, 10),
  199. 'promoter_member_id' => (string)($row->promoter_member_id ?? ''),
  200. 'promoter_account' => (string)($row->promoter_account ?? ''),
  201. 'promoter_nickname' => (string)($row->promoter_nickname ?? ''),
  202. 'deposit' => number_format((float)$row->deposit, 4, '.', ''),
  203. 'commission' => number_format((float)$row->commission, 4, '.', ''),
  204. 'sales_amount' => number_format((float)$row->sales_amount, 4, '.', ''),
  205. 'fee_waiver_rate' => number_format((float)$row->fee_waiver_rate, 4, '.', ''),
  206. ]);
  207. $totals = $totalQuery->selectRaw("SUM(CASE WHEN {$recordAlias}.type = 'commission' THEN {$recordAlias}.amount ELSE 0 END) AS total_commission")
  208. ->selectRaw("MAX(CASE WHEN {$recordAlias}.type = 'fee_waiver' THEN {$recordAlias}.rate ELSE 0 END) AS highest_fee_waiver_rate")->first();
  209. return [
  210. 'total' => $total,
  211. 'data' => $rows->values(),
  212. 'summary' => [
  213. 'total_commission' => number_format((float)($totals->total_commission ?? 0), 4, '.', ''),
  214. 'highest_fee_waiver_rate' => number_format((float)($totals->highest_fee_waiver_rate ?? 0), 4, '.', ''),
  215. ],
  216. ];
  217. }
  218. private function moneyMetricsByMember(array $memberIds): array
  219. {
  220. $memberIds = array_values(array_unique(array_filter(array_map('strval', $memberIds))));
  221. if (!$memberIds) return [];
  222. $metrics = array_fill_keys($memberIds, $this->emptyMetrics());
  223. $sets = [
  224. 'deposit' => fn ($query) => $query->whereIn('change_type', ['充值', '三方充值']),
  225. 'withdraw' => fn ($query) => $query->whereIn('change_type', ['提现', '三方提现', '人工提现']),
  226. 'consume' => fn ($query) => $query->where('change_type', 'like', '%投注%'),
  227. 'bonus' => fn ($query) => $query->whereIn('change_type', ['注册赠送', '优惠活动', '返水', '回水', '笔笔返', '比比返']),
  228. ];
  229. foreach ($sets as $field => $scope) {
  230. $query = BalanceLog::query()->whereIn('member_id', $memberIds);
  231. $rows = $scope($query)->groupBy('member_id')->get(['member_id', DB::raw('SUM(amount) as total')]);
  232. foreach ($rows as $row) {
  233. $value = (float)$row->total;
  234. $metrics[(string)$row->member_id][$field] = number_format(in_array($field, ['withdraw', 'consume'], true) ? abs($value) : $value, 4, '.', '');
  235. }
  236. }
  237. return $metrics;
  238. }
  239. private function sumMetrics(array $metricsByMember, array $memberIds): array
  240. {
  241. $totals = ['deposit' => 0.0, 'withdraw' => 0.0, 'consume' => 0.0, 'bonus' => 0.0];
  242. foreach (array_unique(array_map('strval', $memberIds)) as $memberId) {
  243. foreach ($totals as $field => $value) {
  244. $totals[$field] += (float)($metricsByMember[$memberId][$field] ?? 0);
  245. }
  246. }
  247. return array_map(fn ($value) => number_format($value, 4, '.', ''), $totals);
  248. }
  249. private function emptyMetrics(): array
  250. {
  251. return ['deposit' => '0.0000', 'withdraw' => '0.0000', 'consume' => '0.0000', 'bonus' => '0.0000'];
  252. }
  253. private function dateRange(array $params): array
  254. {
  255. $end = $params['end_date'] ?? date('Y-m-d');
  256. $start = $params['start_date'] ?? date('Y-m-d', strtotime($end . ' -30 days'));
  257. if (strtotime($start) > strtotime($end)) {
  258. throw new \RuntimeException('结束日期不能早于开始日期');
  259. }
  260. if (strtotime($end) - strtotime($start) > 366 * 86400) {
  261. throw new \RuntimeException('单次查询日期范围不能超过 366 天');
  262. }
  263. return [$start, $end];
  264. }
  265. }