initializeBackend($request); if ($response !== null) { return $response; } $scope = $this->channelScopeOrNull(); $todayStart = strtotime(date('Y-m-d')); $todayEnd = $todayStart + 86400 - 1; $yesterdayStart = $todayStart - 86400; $yesterdayEnd = $todayStart - 1; $userTotal = $this->countUsers($scope); $newToday = $this->countUsersInRange($scope, $todayStart, $todayEnd); $newYesterday = $this->countUsersInRange($scope, $yesterdayStart, $yesterdayEnd); $growthPct = null; if ($newYesterday > 0) { $growthPct = round(($newToday - $newYesterday) / $newYesterday * 100, 1); } elseif ($newToday > 0) { $growthPct = 100.0; } $depositAgg = $this->aggregateDepositToday($scope, $todayStart, $todayEnd); $withdrawPending = $this->countWithdrawPending($scope); $betAgg = $this->aggregateBetToday($scope, $todayStart, $todayEnd); $trend = $this->buildSevenDayTrend($scope); $channelShare = $this->buildChannelShare($scope); $depositAmountChannelShare = $this->buildDepositAmountChannelShare($scope); $recentUsers = $this->fetchRecentUsers($scope, 10); return $this->success('', [ 'remark' => get_route_remark(), 'stats' => [ 'user_total' => $userTotal, 'user_new_today' => $newToday, 'user_new_yesterday' => $newYesterday, 'user_new_growth_pct' => $growthPct, 'deposit_today_amount' => $depositAgg['amount'], 'deposit_today_count' => $depositAgg['count'], 'withdraw_pending' => $withdrawPending, 'bet_today_amount' => $betAgg['amount'], 'bet_today_count' => $betAgg['count'], ], 'trend' => $trend, 'channel_share' => $channelShare, 'deposit_amount_channel_share' => $depositAmountChannelShare, 'recent_users' => $recentUsers, ]); } /** * @param int[]|null $scope null=超管不限制;非 null 时 whereIn channel_id */ private function countUsers(?array $scope): int { $q = Db::name('user'); if ($scope !== null) { $q->whereIn('channel_id', $scope); } return intval($q->count()); } /** * @param int[]|null $scope */ private function countUsersInRange(?array $scope, int $start, int $end): int { $q = Db::name('user') ->where('create_time', '>=', $start) ->where('create_time', '<=', $end); if ($scope !== null) { $q->whereIn('channel_id', $scope); } return intval($q->count()); } /** * 今日成功充值:status=1,按创建日落在今日(与 mock 即时成功一致)。 * * @param int[]|null $scope * @return array{count:int, amount:string} */ private function aggregateDepositToday(?array $scope, int $todayStart, int $todayEnd): array { $q = Db::name('deposit_order') ->where('status', 1) ->where('create_time', '>=', $todayStart) ->where('create_time', '<=', $todayEnd); if ($scope !== null) { $q->whereIn('channel_id', $scope); } $rows = $q->fieldRaw('COUNT(*) AS c, COALESCE(SUM(CAST(amount AS DECIMAL(18,4))),0) AS s')->find(); if (!is_array($rows)) { $rows = []; } $count = isset($rows['c']) ? intval($rows['c']) : 0; $sum = isset($rows['s']) ? strval($rows['s']) : '0'; $amount = $this->formatMoney2($sum); return ['count' => $count, 'amount' => $amount]; } /** * @param int[]|null $scope */ private function countWithdrawPending(?array $scope): int { $q = Db::name('withdraw_order')->where('status', 0); if ($scope !== null) { $q->whereIn('channel_id', $scope); } return intval($q->count()); } /** * 今日投注:创建时间在今日且订单未作废(status 1 或 2)。 * * @param int[]|null $scope * @return array{count:int, amount:string} */ private function aggregateBetToday(?array $scope, int $todayStart, int $todayEnd): array { $q = Db::name('bet_order') ->whereIn('status', [1, 2]) ->where('create_time', '>=', $todayStart) ->where('create_time', '<=', $todayEnd); if ($scope !== null) { $q->whereIn('channel_id', $scope); } $rows = $q->fieldRaw('COUNT(*) AS c, COALESCE(SUM(CAST(total_amount AS DECIMAL(18,4))),0) AS s')->find(); if (!is_array($rows)) { $rows = []; } $count = isset($rows['c']) ? intval($rows['c']) : 0; $sum = isset($rows['s']) ? strval($rows['s']) : '0'; return ['count' => $count, 'amount' => $this->formatMoney2($sum)]; } /** * @param int[]|null $scope * @return array{days:string[], new_users:int[], deposit_amount:string[], bet_amount:string[]} */ private function buildSevenDayTrend(?array $scope): array { $days = []; $newUsers = []; $depositAmounts = []; $betAmounts = []; for ($i = 6; $i >= 0; $i--) { $dayStart = strtotime(date('Y-m-d', strtotime('-' . $i . ' day'))); $dayEnd = $dayStart + 86400 - 1; $days[] = date('m-d', $dayStart); $newUsers[] = $this->countUsersInRange($scope, $dayStart, $dayEnd); $dq = Db::name('deposit_order') ->where('status', 1) ->where('create_time', '>=', $dayStart) ->where('create_time', '<=', $dayEnd); if ($scope !== null) { $dq->whereIn('channel_id', $scope); } $drow = $dq->fieldRaw('COALESCE(SUM(CAST(amount AS DECIMAL(18,4))),0) AS s')->find(); $dsum = is_array($drow) && isset($drow['s']) ? strval($drow['s']) : '0'; $depositAmounts[] = $this->formatMoney2($dsum); $bq = Db::name('bet_order') ->whereIn('status', [1, 2]) ->where('create_time', '>=', $dayStart) ->where('create_time', '<=', $dayEnd); if ($scope !== null) { $bq->whereIn('channel_id', $scope); } $brow = $bq->fieldRaw('COALESCE(SUM(CAST(total_amount AS DECIMAL(18,4))),0) AS s')->find(); $bsum = is_array($brow) && isset($brow['s']) ? strval($brow['s']) : '0'; $betAmounts[] = $this->formatMoney2($bsum); } return [ 'days' => $days, 'new_users' => $newUsers, 'deposit_amount' => $depositAmounts, 'bet_amount' => $betAmounts, ]; } /** * 用户按渠道分布(取前 8 名,其余合并为「其他」)。 * * @param int[]|null $scope * @return list */ private function buildChannelShare(?array $scope): array { $q = Db::name('user')->fieldRaw('channel_id, COUNT(*) AS c')->group('channel_id'); if ($scope !== null) { $q->whereIn('channel_id', $scope); } $rows = $q->orderRaw('c DESC')->select()->toArray(); if ($rows === []) { return []; } $channelNames = Db::name('channel')->column('name', 'id'); $list = []; foreach ($rows as $row) { $cid = $row['channel_id']; $cnt = intval($row['c'] ?? 0); if ($cid === null || $cid === '' || intval(strval($cid)) === 0) { $name = '未分配渠道'; } else { $id = intval(strval($cid)); $name = $channelNames[$id] ?? ('#' . strval($id)); } $list[] = ['name' => $name, 'value' => $cnt]; } if (count($list) <= 8) { return $list; } $head = array_slice($list, 0, 8); $rest = array_slice($list, 8); $other = 0; foreach ($rest as $item) { $other += $item['value']; } if ($other > 0) { $head[] = ['name' => '其他', 'value' => $other]; } return $head; } /** * 成功充值金额按订单归属渠道汇总(status=1,受渠道范围限制)。 * * @param int[]|null $scope * @return list value 为两位小数字符串,供前端饼图展示 */ private function buildDepositAmountChannelShare(?array $scope): array { $q = Db::name('deposit_order') ->where('status', 1) ->fieldRaw('channel_id, COALESCE(SUM(CAST(amount AS DECIMAL(18,4))),0) AS s') ->group('channel_id'); if ($scope !== null) { $q->whereIn('channel_id', $scope); } $rows = $q->select()->toArray(); if ($rows === []) { return []; } $channelNames = Db::name('channel')->column('name', 'id'); $list = []; foreach ($rows as $row) { $cid = $row['channel_id']; $sumRaw = isset($row['s']) ? strval($row['s']) : '0'; $amountStr = $this->formatMoney2($sumRaw); if (bccomp($amountStr, '0', 2) <= 0) { continue; } if ($cid === null || $cid === '' || intval(strval($cid)) === 0) { $name = '未分配渠道'; } else { $id = intval(strval($cid)); $name = $channelNames[$id] ?? ('#' . strval($id)); } $list[] = [ 'name' => $name, 'value' => $amountStr, ]; } usort($list, static function (array $a, array $b): int { return bccomp($b['value'], $a['value'], 2); }); if (count($list) <= 8) { return $list; } $head = array_slice($list, 0, 8); $rest = array_slice($list, 8); $other = '0'; foreach ($rest as $item) { $other = bcadd($other, $item['value'], 4); } $otherFormatted = $this->formatMoney2($other); if (bccomp($otherFormatted, '0', 2) > 0) { $head[] = [ 'name' => '其他', 'value' => $otherFormatted, ]; } return $head; } /** * @param int[]|null $scope * @return list */ private function fetchRecentUsers(?array $scope, int $limit): array { $q = Db::name('user') ->field(['id', 'username', 'create_time', 'channel_id', 'head_image']) ->order('id', 'desc') ->limit($limit); if ($scope !== null) { $q->whereIn('channel_id', $scope); } $rows = $q->select()->toArray(); if ($rows === []) { return []; } $channelNames = Db::name('channel')->column('name', 'id'); $out = []; foreach ($rows as $row) { $cid = $row['channel_id'] ?? null; if ($cid === null || $cid === '' || intval(strval($cid)) === 0) { $cname = ''; } else { $id = intval(strval($cid)); $cname = $channelNames[$id] ?? ''; } $out[] = [ 'id' => intval($row['id'] ?? 0), 'username' => strval($row['username'] ?? ''), 'create_time' => intval($row['create_time'] ?? 0), 'channel_name' => $cname, 'head_image' => strval($row['head_image'] ?? ''), ]; } return $out; } private function formatMoney2(string $amount): string { if (!is_numeric($amount)) { return '0.00'; } $normalized = bcadd($amount, '0', 4); return bcadd($normalized, '0', 2); } /** * 非超管:按管理员所属渠道过滤;未绑定渠道时按 channel_id IN (0) 与列表页一致。 * 超管:返回 null,表示 SQL 不加渠道条件。 * * @return int[]|null */ private function channelScopeOrNull(): ?array { if (!$this->auth || $this->auth->isSuperAdmin()) { return null; } $admin = Db::name('admin')->field(['id', 'channel_id'])->where('id', $this->auth->id)->find(); $ids = []; if ($admin && !empty($admin['channel_id'])) { $ids[] = $admin['channel_id']; } return $ids !== [] ? array_values(array_unique($ids)) : [0]; } }