309 lines
10 KiB
TypeScript
309 lines
10 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server'
|
||
import { prisma } from '@/lib/prisma'
|
||
import { getServerSession } from 'next-auth'
|
||
import { authOptions } from '@/lib/auth'
|
||
|
||
export const dynamic = 'force-dynamic'
|
||
|
||
export async function GET(req: NextRequest) {
|
||
try {
|
||
// 获取会话
|
||
let session
|
||
try {
|
||
session = await getServerSession(authOptions)
|
||
} catch (sessionError) {
|
||
console.error('[SystemStatus] Session error:', sessionError)
|
||
return NextResponse.json(
|
||
{ error: '会话获取失败', message: sessionError instanceof Error ? sessionError.message : '未知错误' },
|
||
{ status: 500 }
|
||
)
|
||
}
|
||
|
||
if (!session) {
|
||
return NextResponse.json(
|
||
{ error: '未登录,无法访问系统状态' },
|
||
{ status: 401 }
|
||
)
|
||
}
|
||
|
||
// 并行获取各项统计数据(优化:减少查询次数)
|
||
const thirtyDaysAgoForStats = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
|
||
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
|
||
|
||
let userStats: { _count: { id: number } }
|
||
let materialStats: { _count: { id: number } }
|
||
let systemInfo: { enableRegistration: boolean; enableUserActivation: boolean; siteName: string } | null
|
||
let recentActivity: Array<{ id: string; productModel: string | null; createdAt: Date; user: { username: string; email: string } | null }>
|
||
let totalUsers: number
|
||
let totalMaterials: number
|
||
let activeUsers: number
|
||
let departmentStats: Array<{ department: string | null; _count: { id: number } }>
|
||
let roleDistribution: Array<{ role: string; isSuperAdmin: boolean | null; _count: { id: number } }>
|
||
|
||
try {
|
||
[
|
||
userStats,
|
||
materialStats,
|
||
systemInfo,
|
||
recentActivity,
|
||
totalUsers,
|
||
totalMaterials,
|
||
activeUsers,
|
||
departmentStats,
|
||
roleDistribution
|
||
] = await Promise.all([
|
||
// 用户统计(最近30天)
|
||
prisma.user.aggregate({
|
||
_count: { id: true },
|
||
where: { createdAt: { gte: thirtyDaysAgoForStats } }
|
||
}),
|
||
|
||
// 素材统计(最近30天)
|
||
prisma.material.aggregate({
|
||
_count: { id: true },
|
||
where: { createdAt: { gte: thirtyDaysAgoForStats } }
|
||
}),
|
||
|
||
// 系统设置(使用缓存,减少数据库查询)
|
||
prisma.systemSettings.findFirst({
|
||
select: {
|
||
enableRegistration: true,
|
||
enableUserActivation: true,
|
||
siteName: true,
|
||
}
|
||
}),
|
||
|
||
// 最近活动(限制为10条,减少数据传输)
|
||
prisma.material.findMany({
|
||
take: 10, // 减少到10条
|
||
orderBy: { createdAt: 'desc' },
|
||
select: {
|
||
id: true,
|
||
productModel: true,
|
||
createdAt: true,
|
||
user: {
|
||
select: { username: true, email: true }
|
||
}
|
||
}
|
||
}),
|
||
|
||
// 总用户数
|
||
prisma.user.count(),
|
||
|
||
// 总素材数
|
||
prisma.material.count(),
|
||
|
||
// 活跃用户(最近7天)
|
||
prisma.user.count({
|
||
where: {
|
||
updatedAt: { gte: sevenDaysAgo }
|
||
}
|
||
}),
|
||
|
||
// 部门统计(包括所有部门,包括空字符串和null)
|
||
prisma.user.groupBy({
|
||
by: ['department'],
|
||
_count: { id: true }
|
||
}),
|
||
|
||
// 权限分布统计
|
||
prisma.user.groupBy({
|
||
by: ['role', 'isSuperAdmin'],
|
||
_count: { id: true }
|
||
})
|
||
])
|
||
} catch (dbError) {
|
||
console.error('[SystemStatus] Database query error:', dbError)
|
||
// 如果数据库查询失败,返回默认值
|
||
userStats = { _count: { id: 0 } }
|
||
materialStats = { _count: { id: 0 } }
|
||
systemInfo = null
|
||
recentActivity = []
|
||
totalUsers = 0
|
||
totalMaterials = 0
|
||
activeUsers = 0
|
||
departmentStats = []
|
||
roleDistribution = []
|
||
}
|
||
|
||
// 计算趋势数据
|
||
const thirtyDaysAgo = new Date()
|
||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30)
|
||
|
||
// 获取本月第一天
|
||
const startOfCurrentMonth = new Date()
|
||
startOfCurrentMonth.setDate(1)
|
||
startOfCurrentMonth.setHours(0, 0, 0, 0)
|
||
|
||
let materialTrendDaily: Array<{ date: Date, count: bigint }> = []
|
||
let materialTrendMonthly: Array<{ date: Date, count: bigint }> = []
|
||
let userGrowthTrend: Array<{ date: Date, count: bigint }> = []
|
||
|
||
try {
|
||
// 用户增长趋势(最近30天)
|
||
userGrowthTrend = await prisma.$queryRaw<Array<{ date: Date, count: bigint }>>`
|
||
SELECT
|
||
DATE_TRUNC('day', "createdAt")::date as date,
|
||
COUNT(*)::bigint as count
|
||
FROM "users"
|
||
WHERE "createdAt" >= ${thirtyDaysAgo}
|
||
GROUP BY DATE_TRUNC('day', "createdAt")
|
||
ORDER BY date ASC
|
||
`
|
||
|
||
// 先检查是否有素材数据
|
||
const materialCount = await prisma.material.count()
|
||
console.log('[SystemStatus] Total materials in database:', materialCount)
|
||
|
||
if (materialCount > 0) {
|
||
// 获取一些样本数据,看看createdAt的实际值
|
||
const sampleMaterials = await prisma.material.findMany({
|
||
take: 3,
|
||
select: { id: true, createdAt: true },
|
||
orderBy: { createdAt: 'desc' }
|
||
})
|
||
console.log('[SystemStatus] Sample material createdAt values:', sampleMaterials.map(m => ({
|
||
id: m.id,
|
||
createdAt: m.createdAt,
|
||
createdAtISO: m.createdAt.toISOString(),
|
||
createdAtLocal: m.createdAt.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })
|
||
})))
|
||
}
|
||
|
||
// 素材每天的数据(直接使用createdAt,不转换时区,让PostgreSQL使用数据库时区)
|
||
const dailyResult = await prisma.$queryRaw<Array<{ date: Date, count: bigint }>>`
|
||
SELECT
|
||
"createdAt"::date as date,
|
||
COUNT(*)::bigint as count
|
||
FROM "materials"
|
||
GROUP BY "createdAt"::date
|
||
ORDER BY date DESC
|
||
LIMIT 100
|
||
`
|
||
materialTrendDaily = dailyResult
|
||
console.log('[SystemStatus] Material daily trend count:', dailyResult.length)
|
||
if (dailyResult.length > 0) {
|
||
console.log('[SystemStatus] Sample daily data:', dailyResult.slice(0, 5).map(item => ({
|
||
date: item.date,
|
||
dateStr: item.date.toISOString().split('T')[0],
|
||
count: Number(item.count)
|
||
})))
|
||
} else {
|
||
console.warn('[SystemStatus] No daily trend data found, but material count is:', materialCount)
|
||
}
|
||
|
||
// 素材每月的数据(直接使用createdAt,不转换时区)
|
||
const monthlyResult = await prisma.$queryRaw<Array<{ date: Date, count: bigint }>>`
|
||
SELECT
|
||
DATE_TRUNC('month', "createdAt")::date as date,
|
||
COUNT(*)::bigint as count
|
||
FROM "materials"
|
||
GROUP BY DATE_TRUNC('month', "createdAt")
|
||
ORDER BY date DESC
|
||
LIMIT 24
|
||
`
|
||
materialTrendMonthly = monthlyResult
|
||
console.log('[SystemStatus] Material monthly trend count:', monthlyResult.length)
|
||
if (monthlyResult.length > 0) {
|
||
console.log('[SystemStatus] Sample monthly data:', monthlyResult.slice(0, 5).map(item => ({
|
||
date: item.date,
|
||
dateStr: item.date.toISOString().split('T')[0].substring(0, 7),
|
||
count: Number(item.count)
|
||
})))
|
||
} else {
|
||
console.warn('[SystemStatus] No monthly trend data found, but material count is:', materialCount)
|
||
}
|
||
} catch (queryError) {
|
||
console.error('[SystemStatus] Trend query error:', queryError)
|
||
materialTrendDaily = []
|
||
materialTrendMonthly = []
|
||
userGrowthTrend = []
|
||
}
|
||
|
||
const statusData = {
|
||
overview: {
|
||
totalUsers,
|
||
activeUsers,
|
||
totalMaterials,
|
||
newUsersThisMonth: userStats._count.id,
|
||
newMaterialsThisMonth: materialStats._count.id
|
||
},
|
||
|
||
trends: {
|
||
userGrowth: userGrowthTrend.map(item => ({
|
||
date: item.date.toISOString().split('T')[0], // 转换为 YYYY-MM-DD 格式
|
||
count: Number(item.count)
|
||
})),
|
||
// 兼容旧字段 materialUploads,默认返回月度数据
|
||
materialUploads: materialTrendMonthly.map(item => ({
|
||
date: item.date.toISOString().split('T')[0].substring(0, 7),
|
||
count: Number(item.count)
|
||
})),
|
||
materialUploadsDaily: materialTrendDaily.map(item => ({
|
||
date: item.date.toISOString().split('T')[0], // 转换为 YYYY-MM-DD 格式
|
||
count: Number(item.count)
|
||
})),
|
||
materialUploadsMonthly: materialTrendMonthly.map(item => {
|
||
const dateStr = item.date.toISOString().split('T')[0]
|
||
return {
|
||
date: dateStr.substring(0, 7), // 转换为 YYYY-MM 格式
|
||
count: Number(item.count)
|
||
}
|
||
})
|
||
},
|
||
|
||
distribution: {
|
||
departments: departmentStats.map(item => ({
|
||
department: item.department || '未设置',
|
||
count: item._count.id
|
||
})),
|
||
roles: roleDistribution.map(item => ({
|
||
role: item.role || 'USER',
|
||
isSuperAdmin: item.isSuperAdmin || false,
|
||
count: item._count.id
|
||
}))
|
||
},
|
||
|
||
systemInfo: {
|
||
registrationEnabled: systemInfo?.enableRegistration || false,
|
||
userActivationRequired: systemInfo?.enableUserActivation || false,
|
||
siteName: systemInfo?.siteName || '默认站点'
|
||
},
|
||
|
||
recentActivity: recentActivity
|
||
.filter(activity => activity.user) // 过滤掉没有用户的记录
|
||
.map(activity => ({
|
||
id: activity.id,
|
||
title: activity.productModel || '未命名素材',
|
||
createdAt: activity.createdAt.toISOString(),
|
||
uploadedBy: {
|
||
username: activity.user?.username || '未知用户',
|
||
email: activity.user?.email || ''
|
||
}
|
||
})),
|
||
|
||
// 系统状态
|
||
status: {
|
||
database: 'healthy', // 可以扩展为实际数据库连接检查
|
||
timestamp: new Date().toISOString(),
|
||
uptime: process.uptime()
|
||
}
|
||
}
|
||
|
||
return NextResponse.json(statusData)
|
||
} catch (error) {
|
||
console.error('[SystemStatus] GET error:', error)
|
||
// 输出更详细的错误信息
|
||
if (error instanceof Error) {
|
||
console.error('[SystemStatus] Error message:', error.message)
|
||
console.error('[SystemStatus] Error stack:', error.stack)
|
||
}
|
||
return NextResponse.json(
|
||
{
|
||
error: '获取系统状态失败',
|
||
message: error instanceof Error ? error.message : '未知错误'
|
||
},
|
||
{ status: 500 }
|
||
)
|
||
}
|
||
} |