NewUserSummaryService.php 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. <?php
  2. namespace App\Services;
  3. use App\Support\AppDomain;
  4. use Illuminate\Support\Facades\DB;
  5. /**
  6. * 新用户汇总:区间集合查询,按天输出
  7. *
  8. * 口径:
  9. * - H5/APP:register_domain 是否为包名风格(com.xxx.yyy),见 AppDomain
  10. * - 活跃会员:当日新注册且当日下注 >= 50
  11. * - 有效会员:当日新注册且当日存款 >= 50
  12. * - 公司盈利:当日新用户当日 投注额 - 派奖
  13. *
  14. * 使用显式表前缀 raw SQL,避免 joinSub 别名前缀 / whereIn 多列 等问题。
  15. */
  16. class NewUserSummaryService
  17. {
  18. public const ACTIVE_BET_THRESHOLD = 50;
  19. public const VALID_DEPOSIT_THRESHOLD = 50;
  20. /**
  21. * @return array{start_time:string,end_time:string,rules:array,data:list<array>}
  22. */
  23. public static function summarize(string $startDate, string $endDate): array
  24. {
  25. $rangeStart = $startDate . ' 00:00:00';
  26. $rangeEnd = $endDate . ' 23:59:59';
  27. $p = DB::getTablePrefix();
  28. $users = $p . 'users';
  29. $logs = $p . 'balance_logs';
  30. // 包名风格 = APP
  31. $isApp = AppDomain::sqlIsApp('u.register_domain');
  32. // 测试代理名下会员不统计
  33. $notTest = \App\Support\StatsScope::rawExcludeTestUsers('u');
  34. // 1) 注册按天
  35. $registerRows = DB::select(
  36. "SELECT
  37. DATE(u.created_at) AS d,
  38. SUM(CASE WHEN {$isApp} THEN 1 ELSE 0 END) AS app_register,
  39. SUM(CASE WHEN {$isApp} THEN 0 ELSE 1 END) AS h5_register
  40. FROM {$users} u
  41. WHERE u.created_at BETWEEN ? AND ?{$notTest}
  42. GROUP BY DATE(u.created_at)",
  43. [$rangeStart, $rangeEnd]
  44. );
  45. $registerByDay = [];
  46. foreach ($registerRows as $r) {
  47. $registerByDay[(string)$r->d] = $r;
  48. }
  49. // 2) 首存:新用户历史上第一次正充值时间 = 注册日
  50. $firstDepositRows = DB::select(
  51. "SELECT
  52. nu.reg_date AS d,
  53. SUM(CASE WHEN nu.is_app = 1 THEN 1 ELSE 0 END) AS app_first_deposit,
  54. SUM(CASE WHEN nu.is_app = 0 THEN 1 ELSE 0 END) AS h5_first_deposit
  55. FROM (
  56. SELECT
  57. DATE(u.created_at) AS reg_date,
  58. COALESCE(NULLIF(u.member_id, ''), u.user_id) AS member_key,
  59. CASE WHEN {$isApp} THEN 1 ELSE 0 END AS is_app
  60. FROM {$users} u
  61. WHERE u.created_at BETWEEN ? AND ?{$notTest}
  62. ) nu
  63. INNER JOIN (
  64. SELECT bl.member_id, MIN(bl.created_at) AS first_at
  65. FROM {$logs} bl
  66. WHERE bl.amount > 0
  67. AND (
  68. bl.change_type IN ('充值', '人工充值', '三方充值')
  69. OR (
  70. bl.change_type LIKE '%充值%'
  71. AND bl.change_type NOT LIKE '%返现%'
  72. AND bl.change_type NOT LIKE '%退%'
  73. )
  74. )
  75. GROUP BY bl.member_id
  76. ) fd ON fd.member_id COLLATE utf8mb4_general_ci = nu.member_key COLLATE utf8mb4_general_ci
  77. WHERE DATE(fd.first_at) = nu.reg_date
  78. GROUP BY nu.reg_date",
  79. [$rangeStart, $rangeEnd]
  80. );
  81. $firstDepositByDay = [];
  82. foreach ($firstDepositRows as $r) {
  83. $firstDepositByDay[(string)$r->d] = $r;
  84. }
  85. // 3) 流水:新用户在其注册日的投注/存款/派奖(每人每天一行)
  86. $memberFlow = DB::select(
  87. "SELECT
  88. nu.reg_date AS d,
  89. bl.member_id,
  90. SUM(CASE
  91. WHEN bl.change_type LIKE '%投注%'
  92. AND bl.change_type NOT LIKE '%退款%'
  93. AND bl.change_type NOT LIKE '%中奖%'
  94. THEN ABS(bl.amount) ELSE 0 END) AS bet_amt,
  95. SUM(CASE
  96. WHEN bl.amount > 0 AND (
  97. bl.change_type IN ('充值', '人工充值', '三方充值')
  98. OR (
  99. bl.change_type LIKE '%充值%'
  100. AND bl.change_type NOT LIKE '%返现%'
  101. AND bl.change_type NOT LIKE '%退%'
  102. )
  103. ) THEN bl.amount ELSE 0 END) AS deposit_amt,
  104. SUM(CASE
  105. WHEN bl.change_type LIKE '%中奖%' THEN bl.amount ELSE 0 END) AS win_amt
  106. FROM (
  107. SELECT
  108. DATE(u.created_at) AS reg_date,
  109. COALESCE(NULLIF(u.member_id, ''), u.user_id) AS member_key
  110. FROM {$users} u
  111. WHERE u.created_at BETWEEN ? AND ?{$notTest}
  112. ) nu
  113. INNER JOIN {$logs} bl
  114. ON bl.member_id COLLATE utf8mb4_general_ci = nu.member_key COLLATE utf8mb4_general_ci
  115. WHERE DATE(bl.created_at) = nu.reg_date
  116. AND bl.created_at BETWEEN ? AND ?
  117. GROUP BY nu.reg_date, bl.member_id",
  118. [$rangeStart, $rangeEnd, $rangeStart, $rangeEnd]
  119. );
  120. $flowByDay = [];
  121. foreach ($memberFlow as $row) {
  122. $d = (string)$row->d;
  123. if (!isset($flowByDay[$d])) {
  124. $flowByDay[$d] = [
  125. 'bet_total' => 0.0,
  126. 'deposit_total' => 0.0,
  127. 'win_total' => 0.0,
  128. 'active_member' => 0,
  129. 'valid_member' => 0,
  130. ];
  131. }
  132. $bet = (float)$row->bet_amt;
  133. $deposit = (float)$row->deposit_amt;
  134. $win = (float)$row->win_amt;
  135. $flowByDay[$d]['bet_total'] += $bet;
  136. $flowByDay[$d]['deposit_total'] += $deposit;
  137. $flowByDay[$d]['win_total'] += $win;
  138. if ($bet >= self::ACTIVE_BET_THRESHOLD) {
  139. $flowByDay[$d]['active_member']++;
  140. }
  141. if ($deposit >= self::VALID_DEPOSIT_THRESHOLD) {
  142. $flowByDay[$d]['valid_member']++;
  143. }
  144. }
  145. $data = [];
  146. // 按日期倒序(新 → 旧)
  147. $cursor = strtotime($endDate);
  148. $startTs = strtotime($startDate);
  149. while ($cursor >= $startTs) {
  150. $d = date('Y-m-d', $cursor);
  151. $reg = $registerByDay[$d] ?? null;
  152. $fd = $firstDepositByDay[$d] ?? null;
  153. $flow = $flowByDay[$d] ?? [
  154. 'bet_total' => 0.0,
  155. 'deposit_total' => 0.0,
  156. 'win_total' => 0.0,
  157. 'active_member' => 0,
  158. 'valid_member' => 0,
  159. ];
  160. $betTotal = round((float)$flow['bet_total'], 2);
  161. $depositTotal = round((float)$flow['deposit_total'], 2);
  162. $winTotal = round((float)$flow['win_total'], 2);
  163. $data[] = [
  164. 'date' => $d,
  165. 'h5_register' => (int)($reg->h5_register ?? 0),
  166. 'app_register' => (int)($reg->app_register ?? 0),
  167. 'h5_first_deposit' => (int)($fd->h5_first_deposit ?? 0),
  168. 'app_first_deposit' => (int)($fd->app_first_deposit ?? 0),
  169. 'active_member' => (int)$flow['active_member'],
  170. 'deposit_total' => $depositTotal,
  171. 'bet_total' => $betTotal,
  172. 'company_profit' => round($betTotal - $winTotal, 2),
  173. 'valid_member' => (int)$flow['valid_member'],
  174. ];
  175. $cursor = strtotime('-1 day', $cursor);
  176. }
  177. return [
  178. 'start_time' => $startDate,
  179. 'end_time' => $endDate,
  180. 'rules' => [
  181. 'active_member' => '新注册会员当日下注>=¥' . self::ACTIVE_BET_THRESHOLD,
  182. 'valid_member' => '新注册会员当日存款>=¥' . self::VALID_DEPOSIT_THRESHOLD,
  183. ],
  184. 'data' => $data,
  185. ];
  186. }
  187. }