initializeBackend($request); if ($response !== null) { return $response; } $ownerAdminId = $this->ownerAdminIdOrNull(); $todayStart = strtotime(date('Y-m-d')); $todayEnd = $todayStart + 86400 - 1; $yesterdayStart = $todayStart - 86400; $yesterdayEnd = $todayStart - 1; $userTotal = $this->countUsers($ownerAdminId); $newToday = $this->countUsersInRange($ownerAdminId, $todayStart, $todayEnd); $newYesterday = $this->countUsersInRange($ownerAdminId, $yesterdayStart, $yesterdayEnd); $growthPct = null; if ($newYesterday > 0) { $growthPct = round(($newToday - $newYesterday) / $newYesterday * 100, 1); } elseif ($newToday > 0) { $growthPct = 100.0; } $depositAgg = $this->aggregateDepositToday($ownerAdminId, $todayStart, $todayEnd); $withdrawPending = $this->countWithdrawPending($ownerAdminId); $betAgg = $this->aggregateBetToday($ownerAdminId, $todayStart, $todayEnd); $trend = $this->buildSevenDayTrend($ownerAdminId); $channelShare = $this->buildChannelShare($ownerAdminId); $depositAmountChannelShare = $this->buildDepositAmountChannelShare($ownerAdminId); $recentUsers = $this->fetchRecentUsers($ownerAdminId, 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 $ownerAdminId null=超管不限制;非 null 时 where admin_id */ private function countUsers(?int $ownerAdminId): int { $q = Db::name('user'); if ($ownerAdminId !== null) { $q->where('admin_id', '=', $ownerAdminId); } return intval($q->count()); } /** * @param int|null $ownerAdminId */ private function countUsersInRange(?int $ownerAdminId, int $start, int $end): int { $q = Db::name('user') ->where('create_time', '>=', $start) ->where('create_time', '<=', $end); if ($ownerAdminId !== null) { $q->where('admin_id', '=', $ownerAdminId); } return intval($q->count()); } /** * 今日成功充值:status=1,按创建日落在今日(与 mock 即时成功一致)。 * * @param int|null $ownerAdminId * @return array{count:int, amount:string} */ private function aggregateDepositToday(?int $ownerAdminId, int $todayStart, int $todayEnd): array { $q = Db::name('deposit_order') ->where('status', 1) ->where('create_time', '>=', $todayStart) ->where('create_time', '<=', $todayEnd); if ($ownerAdminId !== null) { $q->whereIn('user_id', $this->scopedUserIds($ownerAdminId)); } $rows = $q->fieldRaw('COUNT(*) AS c, COALESCE(SUM(CAST(amount AS DECIMAL(18,2))),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 $ownerAdminId */ private function countWithdrawPending(?int $ownerAdminId): int { $q = Db::name('withdraw_order')->where('status', 0); if ($ownerAdminId !== null) { $q->whereIn('user_id', $this->scopedUserIds($ownerAdminId)); } return intval($q->count()); } /** * 今日投注:创建时间在今日且订单未作废(status 1 或 2)。 * * @param int|null $ownerAdminId * @return array{count:int, amount:string} */ private function aggregateBetToday(?int $ownerAdminId, int $todayStart, int $todayEnd): array { $q = Db::name('bet_order') ->whereIn('status', [1, 2]) ->where('create_time', '>=', $todayStart) ->where('create_time', '<=', $todayEnd); if ($ownerAdminId !== null) { $q->whereIn('user_id', $this->scopedUserIds($ownerAdminId)); } $rows = $q->fieldRaw('COUNT(*) AS c, COALESCE(SUM(CAST(total_amount AS DECIMAL(18,2))),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 $ownerAdminId * @return array{days:string[], new_users:int[], deposit_amount:string[], bet_amount:string[]} */ private function buildSevenDayTrend(?int $ownerAdminId): 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($ownerAdminId, $dayStart, $dayEnd); $dq = Db::name('deposit_order') ->where('status', 1) ->where('create_time', '>=', $dayStart) ->where('create_time', '<=', $dayEnd); if ($ownerAdminId !== null) { $dq->whereIn('user_id', $this->scopedUserIds($ownerAdminId)); } $drow = $dq->fieldRaw('COALESCE(SUM(CAST(amount AS DECIMAL(18,2))),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 ($ownerAdminId !== null) { $bq->whereIn('user_id', $this->scopedUserIds($ownerAdminId)); } $brow = $bq->fieldRaw('COALESCE(SUM(CAST(total_amount AS DECIMAL(18,2))),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 $ownerAdminId * @return list */ private function buildChannelShare(?int $ownerAdminId): array { $q = Db::name('user')->fieldRaw('channel_id, COUNT(*) AS c')->group('channel_id'); if ($ownerAdminId !== null) { $q->where('admin_id', '=', $ownerAdminId); } $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 $ownerAdminId * @return list value 为两位小数字符串,供前端饼图展示 */ private function buildDepositAmountChannelShare(?int $ownerAdminId): array { $q = Db::name('deposit_order') ->where('status', 1) ->fieldRaw('channel_id, COALESCE(SUM(CAST(amount AS DECIMAL(18,2))),0) AS s') ->group('channel_id'); if ($ownerAdminId !== null) { $q->whereIn('user_id', $this->scopedUserIds($ownerAdminId)); } $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'], 2); } $otherFormatted = $this->formatMoney2($other); if (bccomp($otherFormatted, '0', 2) > 0) { $head[] = [ 'name' => '其他', 'value' => $otherFormatted, ]; } return $head; } /** * @param int|null $ownerAdminId * @return list */ private function fetchRecentUsers(?int $ownerAdminId, int $limit): array { $q = Db::name('user') ->field(['id', 'username', 'create_time', 'channel_id', 'head_image']) ->order('id', 'desc') ->limit($limit); if ($ownerAdminId !== null) { $q->where('admin_id', '=', $ownerAdminId); } $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', 2); return bcadd($normalized, '0', 2); } /** * 非超管:按当前管理员名下用户过滤。 * 超管:返回 null,表示 SQL 不加管理员条件。 * * @return int|null */ private function ownerAdminIdOrNull(): ?int { if (!$this->auth || $this->auth->isSuperAdmin()) { return null; } $idRaw = $this->auth->id; if ($idRaw === null || $idRaw === '' || !is_numeric(strval($idRaw))) { return 0; } $id = intval(strval($idRaw)); return $id > 0 ? $id : 0; } /** * @return int[] */ private function scopedUserIds(int $ownerAdminId): array { $ids = Db::name('user')->where('admin_id', '=', $ownerAdminId)->column('id'); $ids = array_map('intval', $ids); return $ids === [] ? [0] : array_values(array_unique($ids)); } }