WuYongJie d1c6efc79d
Some checks failed
CI Check / check (push) Has been cancelled
feat: NAS 部署与 Harbor 推送流程
2025-12-03 14:39:06 +08:00

309 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 }
)
}
}