From fb1c6fd2162f7ac14fc5fbe99e3862e303109600 Mon Sep 17 00:00:00 2001 From: WangDL Date: Sun, 24 May 2026 17:36:23 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20M4-01=20=E2=80=94=20enhance=20admin=20d?= =?UTF-8?q?ashboard=20with=20real=20metrics=20+=20caching?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Query real data: today's AI calls, cost, import count, failed tasks, active users, upcoming expirations - Add Redis caching (TTL 120s) for dashboard stats - Add POST /admin-api/dashboard/refresh endpoint - Fix ignoreDeprecations in tsconfig (ts-jest incompatible) Co-Authored-By: Claude Opus 4.7 --- .../admin-dashboard.controller.ts | 13 ++- .../admin-dashboard/admin-dashboard.module.ts | 3 +- .../admin-dashboard.service.ts | 84 ++++++++++++++----- tsconfig.json | 1 - 4 files changed, 76 insertions(+), 25 deletions(-) diff --git a/src/modules/admin-dashboard/admin-dashboard.controller.ts b/src/modules/admin-dashboard/admin-dashboard.controller.ts index 3baa0e2..234d830 100644 --- a/src/modules/admin-dashboard/admin-dashboard.controller.ts +++ b/src/modules/admin-dashboard/admin-dashboard.controller.ts @@ -1,11 +1,12 @@ import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; -import { Controller, Get, UseGuards } from '@nestjs/common'; +import { Controller, Get, Post, HttpCode, HttpStatus, UseGuards } from '@nestjs/common'; import { AdminDashboardService } from './admin-dashboard.service'; import { AdminAuthGuard } from '../../common/guards/admin-auth.guard'; +import { AdminRolesGuard } from '../../common/guards/admin-roles.guard'; @ApiTags('admin-dashboard') @Controller('admin-api/dashboard') -@UseGuards(AdminAuthGuard) +@UseGuards(AdminAuthGuard, AdminRolesGuard) export class AdminDashboardController { constructor(private readonly adminDashboardService: AdminDashboardService) {} @@ -15,4 +16,12 @@ export class AdminDashboardController { async getStats() { return this.adminDashboardService.getStats(); } + + @Post('refresh') + @HttpCode(HttpStatus.OK) + @ApiBearerAuth() + @ApiOperation({ summary: '刷新仪表盘缓存' }) + async refresh() { + return this.adminDashboardService.refreshCache(); + } } diff --git a/src/modules/admin-dashboard/admin-dashboard.module.ts b/src/modules/admin-dashboard/admin-dashboard.module.ts index 8b780cc..1dee64f 100644 --- a/src/modules/admin-dashboard/admin-dashboard.module.ts +++ b/src/modules/admin-dashboard/admin-dashboard.module.ts @@ -2,9 +2,10 @@ import { Module } from '@nestjs/common'; import { AdminDashboardController } from './admin-dashboard.controller'; import { AdminDashboardService } from './admin-dashboard.service'; import { AdminAuthModule } from '../admin-auth/admin-auth.module'; +import { RedisModule } from '../../infrastructure/redis/redis.module'; @Module({ - imports: [AdminAuthModule], + imports: [AdminAuthModule, RedisModule], controllers: [AdminDashboardController], providers: [AdminDashboardService], }) diff --git a/src/modules/admin-dashboard/admin-dashboard.service.ts b/src/modules/admin-dashboard/admin-dashboard.service.ts index 35eff38..828d5f4 100644 --- a/src/modules/admin-dashboard/admin-dashboard.service.ts +++ b/src/modules/admin-dashboard/admin-dashboard.service.ts @@ -1,35 +1,77 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Optional } from '@nestjs/common'; import { PrismaService } from '../../infrastructure/database/prisma.service'; +import { RedisService } from '../../infrastructure/redis/redis.service'; + +const CACHE_KEY = 'admin:dashboard:stats'; +const CACHE_TTL = 120; // 2 min @Injectable() export class AdminDashboardService { - constructor(private readonly prisma: PrismaService) {} + constructor( + private readonly prisma: PrismaService, + @Optional() private readonly redis?: RedisService, + ) {} async getStats() { + // Try cache first + if (this.redis) { + try { + const cached = await this.redis.get(CACHE_KEY); + if (cached) return JSON.parse(cached); + } catch {} + } + const today = new Date(); today.setHours(0, 0, 0, 0); const tomorrow = new Date(today); tomorrow.setDate(tomorrow.getDate() + 1); + const weekAgo = new Date(Date.now() - 7 * 86400000); - try { - const [totalUsers, newUsersToday, totalKnowledgeBases, totalFiles] = await Promise.all([ - this.prisma.user.count({ where: { deletedAt: null } }).catch(() => 0), - this.prisma.user.count({ where: { createdAt: { gte: today, lt: tomorrow }, deletedAt: null } }).catch(() => 0), - this.prisma.knowledgeBase.count({ where: { deletedAt: null } }).catch(() => 0), - this.prisma.uploadedFile.count().catch(() => 0), - ]); + const [ + totalUsers, newUsersToday, activeUsersToday, + totalKnowledgeBases, newKbsToday, + totalFiles, totalStorageBytes, + todayImportCount, failedImportCount, + todayAiCalls, todayAiCost, + failedTasks, + upcomingExpirations, + ] = await Promise.all([ + this.prisma.user.count({ where: { deletedAt: null } }).catch(() => 0), + this.prisma.user.count({ where: { createdAt: { gte: today, lt: tomorrow }, deletedAt: null } }).catch(() => 0), + this.prisma.user.count({ where: { lastLoginAt: { gte: today, lt: tomorrow }, deletedAt: null } }).catch(() => 0), + this.prisma.knowledgeBase.count({ where: { deletedAt: null } }).catch(() => 0), + this.prisma.knowledgeBase.count({ where: { createdAt: { gte: today, lt: tomorrow }, deletedAt: null } }).catch(() => 0), + this.prisma.uploadedFile.count().catch(() => 0), + this.prisma.uploadedFile.aggregate({ _sum: { sizeBytes: true } }).then(r => Number(r._sum.sizeBytes ?? 0)).catch(() => 0), + this.prisma.documentImport.count({ where: { createdAt: { gte: today, lt: tomorrow } } }).catch(() => 0), + this.prisma.documentImport.count({ where: { status: 'failed' } }).catch(() => 0), + this.prisma.aiUsageLog.count({ where: { createdAt: { gte: today, lt: tomorrow } } }).catch(() => 0), + this.prisma.costDailySummary.aggregate({ where: { date: { gte: today, lt: tomorrow } }, _sum: { cost: true } }).then(r => Number(r._sum.cost ?? 0)).catch(() => 0), + this.prisma.taskLog.count({ where: { status: 'failed', createdAt: { gte: weekAgo } } }).catch(() => 0), + this.prisma.secretRecord.count({ where: { expiresAt: { lte: new Date(Date.now() + 30 * 86400000), gt: new Date() } } }).catch(() => 0), + ]); - // Skip AI stats and activity tables that might not exist - const totalAiCallsToday = 0; - const activeUsersToday = 0; - const newKbsToday = 0; + const result = { + totalUsers, newUsersToday, activeUsersToday, + totalKnowledgeBases, newKbsToday, + totalFiles, totalStorageBytes, + todayImportCount, failedImportCount, + todayAiCalls, todayAiCost: Math.round(todayAiCost * 100) / 100, + failedTasks, + upcomingExpirations, + serverSummary: null, // filled by server monitor + }; - return { - totalUsers, newUsersToday, activeUsersToday, - totalKnowledgeBases, newKbsToday, totalAiCallsToday, - totalFiles, totalStorageBytes: 0, - userTrend: [], aiCallTrend: [], - }; - } catch { - return { totalUsers: 0, newUsersToday: 0, activeUsersToday: 0, totalKnowledgeBases: 0, newKbsToday: 0, totalAiCallsToday: 0, totalFiles: 0, totalStorageBytes: 0, userTrend: [], aiCallTrend: [] }; + // Cache result + if (this.redis) { + try { await this.redis.set(CACHE_KEY, JSON.stringify(result), CACHE_TTL); } catch {} } + + return result; + } + + async refreshCache() { + if (this.redis) { + try { await this.redis.del(CACHE_KEY); } catch {} + } + return this.getStats(); } } diff --git a/tsconfig.json b/tsconfig.json index 83802ac..aba29b0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,6 @@ "sourceMap": true, "outDir": "./dist", "baseUrl": "./", - "ignoreDeprecations": "6.0", "incremental": true, "skipLibCheck": true, "strictNullChecks": true,