374 lines
12 KiB
TypeScript
374 lines
12 KiB
TypeScript
import { createError, defineEventHandler, getQuery } from 'h3'
|
|
import { db } from '~/drizzle/db'
|
|
import { schedules, semesters, songBlacklists, songs, users } from '~/drizzle/schema'
|
|
import { and, count, eq, gte, lt } from 'drizzle-orm'
|
|
import { cacheService } from '../../services/cacheService'
|
|
import dayjs from 'dayjs'
|
|
import utc from 'dayjs/plugin/utc.js'
|
|
import timezone from 'dayjs/plugin/timezone.js'
|
|
|
|
dayjs.extend(utc)
|
|
dayjs.extend(timezone)
|
|
|
|
const BEIJING_TIMEZONE = 'Asia/Shanghai'
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
// 检查认证和权限
|
|
const user = event.context.user
|
|
if (!user || !['ADMIN', 'SUPER_ADMIN'].includes(user.role)) {
|
|
throw createError({
|
|
statusCode: 403,
|
|
message: '需要管理员权限'
|
|
})
|
|
}
|
|
|
|
const query = getQuery(event)
|
|
const semester = query.semester as string
|
|
|
|
try {
|
|
// 尝试从缓存获取数据
|
|
const cachedStats = await cacheService.getAdminStats(semester)
|
|
if (cachedStats) {
|
|
console.log('[Cache] 管理员统计数据缓存命中')
|
|
return cachedStats
|
|
}
|
|
// 获取当前时间相关的日期
|
|
const now = new Date()
|
|
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
|
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
|
|
const twoWeeksAgo = new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000)
|
|
|
|
// 构建查询条件
|
|
const where = semester && semester !== 'all' ? { semester: semester } : {}
|
|
|
|
// 构建排期查询条件
|
|
const scheduleBaseWhere = semester && semester !== 'all' ? { song: { semester: semester } } : {}
|
|
|
|
// 添加日期条件到今日排期查询
|
|
const todayScheduleWhere = {
|
|
...scheduleBaseWhere,
|
|
playDate: {
|
|
gte: today,
|
|
lt: new Date(today.getTime() + 24 * 60 * 60 * 1000)
|
|
}
|
|
}
|
|
|
|
// 并行获取所有统计数据
|
|
const [
|
|
totalSongs,
|
|
totalUsers,
|
|
todaySchedules,
|
|
totalSchedules,
|
|
weeklyRequests,
|
|
previousWeekRequests,
|
|
songsThisWeek,
|
|
songsLastWeek,
|
|
usersThisWeek,
|
|
usersLastWeek,
|
|
currentSemester,
|
|
blacklistCount
|
|
] = await Promise.all([
|
|
// 总歌曲数
|
|
(async () => {
|
|
const whereCondition =
|
|
semester && semester !== 'all' ? eq(songs.semester, semester) : undefined
|
|
const result = await db.select({ count: count() }).from(songs).where(whereCondition)
|
|
return result[0].count
|
|
})(),
|
|
|
|
// 总用户数
|
|
(async () => {
|
|
const result = await db.select({ count: count() }).from(users)
|
|
return result[0].count
|
|
})(),
|
|
|
|
// 今日排期数 (按天计算)
|
|
(async () => {
|
|
const query = db.select({ playDate: schedules.playDate }).from(schedules)
|
|
|
|
const whereConditions = [
|
|
gte(schedules.playDate, today),
|
|
lt(schedules.playDate, new Date(today.getTime() + 24 * 60 * 60 * 1000))
|
|
]
|
|
|
|
if (semester && semester !== 'all') {
|
|
query.innerJoin(songs, eq(schedules.songId, songs.id))
|
|
whereConditions.push(eq(songs.semester, semester))
|
|
}
|
|
|
|
const todaySchedulesList = await query.where(and(...whereConditions))
|
|
|
|
// 按北京时间日期去重计算天数
|
|
const uniqueDates = new Set(
|
|
todaySchedulesList.map((s) => dayjs(s.playDate).tz(BEIJING_TIMEZONE).format('YYYY-MM-DD'))
|
|
)
|
|
return uniqueDates.size
|
|
})(),
|
|
|
|
// 总排期数 (按天计算)
|
|
(async () => {
|
|
const query = db.select({ playDate: schedules.playDate }).from(schedules)
|
|
|
|
if (semester && semester !== 'all') {
|
|
query.innerJoin(songs, eq(schedules.songId, songs.id)).where(eq(songs.semester, semester))
|
|
}
|
|
|
|
const allSchedules = await query
|
|
|
|
// 按北京时间日期去重计算天数
|
|
const uniqueDates = new Set(
|
|
allSchedules.map((s) => dayjs(s.playDate).tz(BEIJING_TIMEZONE).format('YYYY-MM-DD'))
|
|
)
|
|
return uniqueDates.size
|
|
})(),
|
|
|
|
// 本周点歌数
|
|
(async () => {
|
|
const whereConditions = [gte(songs.createdAt, weekAgo)]
|
|
if (semester && semester !== 'all') {
|
|
whereConditions.push(eq(songs.semester, semester))
|
|
}
|
|
const result = await db
|
|
.select({ count: count() })
|
|
.from(songs)
|
|
.where(and(...whereConditions))
|
|
return result[0].count
|
|
})(),
|
|
|
|
// 上周点歌数
|
|
(async () => {
|
|
const whereConditions = [gte(songs.createdAt, twoWeeksAgo), lt(songs.createdAt, weekAgo)]
|
|
if (semester && semester !== 'all') {
|
|
whereConditions.push(eq(songs.semester, semester))
|
|
}
|
|
const result = await db
|
|
.select({ count: count() })
|
|
.from(songs)
|
|
.where(and(...whereConditions))
|
|
return result[0].count
|
|
})(),
|
|
|
|
// 本周新增歌曲
|
|
(async () => {
|
|
const whereConditions = [gte(songs.createdAt, weekAgo)]
|
|
if (semester && semester !== 'all') {
|
|
whereConditions.push(eq(songs.semester, semester))
|
|
}
|
|
const result = await db
|
|
.select({ count: count() })
|
|
.from(songs)
|
|
.where(and(...whereConditions))
|
|
return result[0].count
|
|
})(),
|
|
|
|
// 上周新增歌曲
|
|
(async () => {
|
|
const whereConditions = [gte(songs.createdAt, twoWeeksAgo), lt(songs.createdAt, weekAgo)]
|
|
if (semester && semester !== 'all') {
|
|
whereConditions.push(eq(songs.semester, semester))
|
|
}
|
|
const result = await db
|
|
.select({ count: count() })
|
|
.from(songs)
|
|
.where(and(...whereConditions))
|
|
return result[0].count
|
|
})(),
|
|
|
|
// 本周新增用户
|
|
(async () => {
|
|
const result = await db
|
|
.select({ count: count() })
|
|
.from(users)
|
|
.where(gte(users.createdAt, weekAgo))
|
|
return result[0].count
|
|
})(),
|
|
|
|
// 上周新增用户
|
|
(async () => {
|
|
const result = await db
|
|
.select({ count: count() })
|
|
.from(users)
|
|
.where(and(gte(users.createdAt, twoWeeksAgo), lt(users.createdAt, weekAgo)))
|
|
return result[0].count
|
|
})(),
|
|
|
|
// 当前学期
|
|
(async () => {
|
|
const result = await db
|
|
.select({ name: semesters.name })
|
|
.from(semesters)
|
|
.where(eq(semesters.isActive, true))
|
|
.limit(1)
|
|
return result[0]
|
|
})(),
|
|
|
|
// 黑名单项目数
|
|
(async () => {
|
|
const blacklistCount = await db
|
|
.select({ count: count() })
|
|
.from(songBlacklists)
|
|
.where(eq(songBlacklists.isActive, true))
|
|
return blacklistCount[0].count
|
|
})()
|
|
])
|
|
|
|
// 计算变化百分比
|
|
const requestsChange =
|
|
previousWeekRequests > 0
|
|
? Math.round(((weeklyRequests - previousWeekRequests) / previousWeekRequests) * 100)
|
|
: weeklyRequests > 0
|
|
? 100
|
|
: 0
|
|
|
|
const songsChange = songsThisWeek - songsLastWeek
|
|
const usersChange = usersThisWeek - usersLastWeek
|
|
|
|
// 获取趋势数据 (最近7天)
|
|
const trendData = await Promise.all([
|
|
// 歌曲趋势
|
|
(async () => {
|
|
const trends = []
|
|
for (let i = 6; i >= 0; i--) {
|
|
const date = new Date(now.getTime() - i * 24 * 60 * 60 * 1000)
|
|
const startOfDay = new Date(date.getFullYear(), date.getMonth(), date.getDate())
|
|
const endOfDay = new Date(startOfDay.getTime() + 24 * 60 * 60 * 1000)
|
|
|
|
const whereConditions = [gte(songs.createdAt, startOfDay), lt(songs.createdAt, endOfDay)]
|
|
if (semester && semester !== 'all') {
|
|
whereConditions.push(eq(songs.semester, semester))
|
|
}
|
|
const result = await db
|
|
.select({ count: count() })
|
|
.from(songs)
|
|
.where(and(...whereConditions))
|
|
const countValue = result[0].count
|
|
|
|
trends.push({
|
|
date: dayjs(startOfDay).tz(BEIJING_TIMEZONE).format('YYYY-MM-DD'),
|
|
count: countValue
|
|
})
|
|
}
|
|
return trends
|
|
})(),
|
|
|
|
// 用户趋势
|
|
(async () => {
|
|
const trends = []
|
|
for (let i = 6; i >= 0; i--) {
|
|
const date = new Date(now.getTime() - i * 24 * 60 * 60 * 1000)
|
|
const startOfDay = new Date(date.getFullYear(), date.getMonth(), date.getDate())
|
|
const endOfDay = new Date(startOfDay.getTime() + 24 * 60 * 60 * 1000)
|
|
|
|
const result = await db
|
|
.select({ count: count() })
|
|
.from(users)
|
|
.where(and(gte(users.createdAt, startOfDay), lt(users.createdAt, endOfDay)))
|
|
const countValue = result[0].count
|
|
|
|
trends.push({
|
|
date: dayjs(startOfDay).tz(BEIJING_TIMEZONE).format('YYYY-MM-DD'),
|
|
count: countValue
|
|
})
|
|
}
|
|
return trends
|
|
})(),
|
|
|
|
// 排期趋势
|
|
(async () => {
|
|
const trends = []
|
|
for (let i = 6; i >= 0; i--) {
|
|
const date = new Date(now.getTime() - i * 24 * 60 * 60 * 1000)
|
|
const startOfDay = new Date(date.getFullYear(), date.getMonth(), date.getDate())
|
|
const endOfDay = new Date(startOfDay.getTime() + 24 * 60 * 60 * 1000)
|
|
|
|
const query = db.select({ playDate: schedules.playDate }).from(schedules)
|
|
|
|
const whereConditions = [
|
|
gte(schedules.playDate, startOfDay),
|
|
lt(schedules.playDate, endOfDay)
|
|
]
|
|
|
|
if (semester && semester !== 'all') {
|
|
query.innerJoin(songs, eq(schedules.songId, songs.id))
|
|
whereConditions.push(eq(songs.semester, semester))
|
|
}
|
|
|
|
const schedulesList = await query.where(and(...whereConditions))
|
|
|
|
// 按北京时间日期去重计算天数
|
|
const uniqueDates = new Set(
|
|
schedulesList.map((s) => dayjs(s.playDate).tz(BEIJING_TIMEZONE).format('YYYY-MM-DD'))
|
|
)
|
|
|
|
trends.push({
|
|
date: dayjs(startOfDay).tz(BEIJING_TIMEZONE).format('YYYY-MM-DD'),
|
|
count: uniqueDates.size
|
|
})
|
|
}
|
|
return trends
|
|
})(),
|
|
|
|
// 点歌请求趋势
|
|
(async () => {
|
|
const trends = []
|
|
for (let i = 6; i >= 0; i--) {
|
|
const date = new Date(now.getTime() - i * 24 * 60 * 60 * 1000)
|
|
const startOfDay = new Date(date.getFullYear(), date.getMonth(), date.getDate())
|
|
const endOfDay = new Date(startOfDay.getTime() + 24 * 60 * 60 * 1000)
|
|
|
|
const whereConditions = [gte(songs.createdAt, startOfDay), lt(songs.createdAt, endOfDay)]
|
|
if (semester && semester !== 'all') {
|
|
whereConditions.push(eq(songs.semester, semester))
|
|
}
|
|
const result = await db
|
|
.select({ count: count() })
|
|
.from(songs)
|
|
.where(and(...whereConditions))
|
|
const countValue = result[0].count
|
|
|
|
trends.push({
|
|
date: dayjs(startOfDay).tz(BEIJING_TIMEZONE).format('YYYY-MM-DD'),
|
|
count: countValue
|
|
})
|
|
}
|
|
return trends
|
|
})()
|
|
])
|
|
|
|
const result = {
|
|
totalSongs,
|
|
totalUsers,
|
|
todaySchedules,
|
|
totalSchedules,
|
|
weeklyRequests,
|
|
songsChange,
|
|
usersChange,
|
|
requestsChange,
|
|
currentSemester: currentSemester?.name || null,
|
|
blacklistCount,
|
|
// 趋势数据
|
|
songsTrend: trendData[0],
|
|
usersTrend: trendData[1],
|
|
schedulesTrend: trendData[2],
|
|
requestsTrend: trendData[3]
|
|
}
|
|
|
|
// 缓存结果
|
|
await cacheService.setAdminStats(semester, result)
|
|
|
|
// 只在Redis可用时显示缓存提示
|
|
const { isRedisReady } = await import('../../utils/redis')
|
|
if (isRedisReady()) {
|
|
console.log('[Cache] 管理员统计数据已缓存')
|
|
}
|
|
|
|
return result
|
|
} catch (error) {
|
|
console.error('获取统计数据失败:', error)
|
|
throw createError({
|
|
statusCode: 500,
|
|
message: '获取统计数据失败'
|
|
})
|
|
}
|
|
})
|