Ver Fonte

feat: add ConcurrentSessionsCard to dashboard and logs pages

- Integrated ConcurrentSessionsCard component into both the Dashboard and Usage Logs pages for enhanced session tracking.
- Updated next.config.ts and instrumentation.ts to ensure proper configuration and formatting.

This update improves the user interface by providing real-time insights into concurrent sessions.
ding113 há 3 meses atrás
pai
commit
c31c3e31cd

+ 3 - 0
next.config.ts

@@ -5,3 +5,6 @@ const nextConfig: NextConfig = {
 }
 
 export default nextConfig
+
+
+

+ 23 - 0
src/actions/concurrent-sessions.ts

@@ -0,0 +1,23 @@
+"use server";
+
+import { getActiveConcurrentSessions } from "@/lib/redis";
+import type { ActionResult } from "./types";
+
+/**
+ * 获取当前并发 session 数量(5分钟窗口)
+ */
+export async function getConcurrentSessions(): Promise<ActionResult<number>> {
+  try {
+    const count = await getActiveConcurrentSessions();
+    return {
+      ok: true,
+      data: count,
+    };
+  } catch (error) {
+    console.error('Failed to get concurrent sessions:', error);
+    return {
+      ok: false,
+      error: '获取并发数失败',
+    };
+  }
+}

+ 5 - 0
src/app/dashboard/logs/page.tsx

@@ -3,6 +3,7 @@ import { redirect } from "next/navigation";
 import { getSession } from "@/lib/auth";
 import { Section } from "@/components/section";
 import { UsageLogsView } from "./_components/usage-logs-view";
+import { ConcurrentSessionsCard } from "@/components/customs/concurrent-sessions-card";
 import { getUsers } from "@/actions/users";
 import { getProviders } from "@/actions/providers";
 
@@ -27,6 +28,10 @@ export default async function UsageLogsPage({
 
   return (
     <div className="space-y-6">
+      <div className="grid gap-4 md:grid-cols-4">
+        <ConcurrentSessionsCard />
+      </div>
+
       <Section
         title="使用记录"
         description="查看 API 调用日志和使用统计"

+ 5 - 0
src/app/dashboard/page.tsx

@@ -7,6 +7,7 @@ import { getUserStatistics } from "@/actions/statistics";
 import { hasPriceTable } from "@/actions/model-prices";
 import { ListErrorBoundary } from "@/components/error-boundary";
 import { StatisticsWrapper } from "./_components/statistics";
+import { ConcurrentSessionsCard } from "@/components/customs/concurrent-sessions-card";
 import { DEFAULT_TIME_RANGE } from "@/types/statistics";
 
 export const dynamic = "force-dynamic";
@@ -26,6 +27,10 @@ export default async function DashboardPage() {
 
   return (
     <div className="space-y-6">
+      <div className="grid gap-4 md:grid-cols-4">
+        <ConcurrentSessionsCard />
+      </div>
+
       <div>
         <StatisticsWrapper
           initialData={statistics.ok ? statistics.data : undefined}

+ 46 - 0
src/components/customs/concurrent-sessions-card.tsx

@@ -0,0 +1,46 @@
+"use client";
+
+import * as React from "react";
+import { useQuery } from "@tanstack/react-query";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Activity } from "lucide-react";
+import { getConcurrentSessions } from "@/actions/concurrent-sessions";
+
+const REFRESH_INTERVAL = 5000; // 5秒刷新一次
+
+async function fetchConcurrentSessions(): Promise<number> {
+  const result = await getConcurrentSessions();
+  if (!result.ok) {
+    throw new Error(result.error || '获取并发数失败');
+  }
+  return result.data;
+}
+
+/**
+ * 并发 Session 数显示卡片
+ * 显示最近 5 分钟内的活跃 session 数量
+ */
+export function ConcurrentSessionsCard() {
+  const { data = 0 } = useQuery<number, Error>({
+    queryKey: ["concurrent-sessions"],
+    queryFn: fetchConcurrentSessions,
+    refetchInterval: REFRESH_INTERVAL,
+  });
+
+  return (
+    <Card>
+      <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+        <CardTitle className="text-sm font-medium">
+          当前并发
+        </CardTitle>
+        <Activity className="h-4 w-4 text-muted-foreground" />
+      </CardHeader>
+      <CardContent>
+        <div className="text-2xl font-bold">{data}</div>
+        <p className="text-xs text-muted-foreground">
+          最近 5 分钟活跃 Session
+        </p>
+      </CardContent>
+    </Card>
+  );
+}

+ 2 - 1
src/instrumentation.ts

@@ -27,4 +27,5 @@ export async function register() {
       console.log('================================\n');
     }
   }
-}
+}
+

+ 1 - 0
src/lib/redis/index.ts

@@ -1 +1,2 @@
 export { getRedisClient, closeRedis } from './client';
+export { getActiveConcurrentSessions } from './session-stats';

+ 46 - 0
src/lib/redis/session-stats.ts

@@ -0,0 +1,46 @@
+import { getRedisClient } from './client';
+
+/**
+ * 获取当前活跃的并发 session 数量
+ *
+ * 统计最近 5 分钟内的活跃 session(基于 Redis TTL)
+ * session key 格式:session:{sessionId}:last_seen
+ *
+ * @returns 当前并发 session 数量(Redis 不可用时返回 0)
+ */
+export async function getActiveConcurrentSessions(): Promise<number> {
+  const redis = getRedisClient();
+
+  if (!redis) {
+    // Redis 未启用,返回 0
+    return 0;
+  }
+
+  try {
+    let cursor = '0';
+    let count = 0;
+    const pattern = 'session:*:last_seen';
+
+    do {
+      // 使用 SCAN 避免阻塞
+      const result = await redis.scan(
+        cursor,
+        'MATCH',
+        pattern,
+        'COUNT',
+        100
+      );
+
+      cursor = result[0];
+      count += result[1].length;
+
+      // 如果没有更多键,退出循环
+      if (cursor === '0') break;
+    } while (true);
+
+    return count;
+  } catch (error) {
+    console.error('[SessionStats] Failed to get concurrent sessions:', error);
+    return 0; // Fail Open
+  }
+}