129 lines
3.7 KiB
TypeScript
129 lines
3.7 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server'
|
||
import { getServerSession } from 'next-auth'
|
||
import { authOptions } from '@/lib/auth'
|
||
import { prisma } from '@/lib/prisma'
|
||
import { deleteFromS3, isS3Url } from '@/lib/s3'
|
||
import { unlink, access } from 'fs/promises'
|
||
|
||
export const dynamic = 'force-dynamic'
|
||
|
||
export async function POST(req: NextRequest) {
|
||
try {
|
||
const session = await getServerSession(authOptions)
|
||
|
||
if (!session || (session.user as any)?.role !== 'ADMIN') {
|
||
return NextResponse.json(
|
||
{ error: '需要管理员权限' },
|
||
{ status: 403 }
|
||
)
|
||
}
|
||
|
||
const results: Record<string, number> = {}
|
||
|
||
// 1. 清理过期的邀请码(已使用且过期超过30天)
|
||
const expiredInvitations = await prisma.$executeRawUnsafe(`
|
||
DELETE FROM invitations
|
||
WHERE "usedAt" IS NOT NULL
|
||
AND "expiresAt" IS NOT NULL
|
||
AND "expiresAt" < NOW() - INTERVAL '30 days'
|
||
`)
|
||
results.expiredInvitations = Number(expiredInvitations)
|
||
|
||
// 2. 清理旧的弹幕数据(超过90天)
|
||
const oldDanmakus = await prisma.$executeRawUnsafe(`
|
||
DELETE FROM danmakus
|
||
WHERE "createdAt" < NOW() - INTERVAL '90 days'
|
||
`)
|
||
results.oldDanmakus = Number(oldDanmakus)
|
||
|
||
// 3. 清理所有素材数据和记录
|
||
// 先获取所有素材记录以便删除文件
|
||
const materials = await prisma.$queryRawUnsafe<Array<{ id: string; "zipFilePath": string; "zipFileName": string }>>(`
|
||
SELECT id, "zipFilePath", "zipFileName" FROM materials
|
||
`)
|
||
|
||
let deletedFiles = 0
|
||
let failedFiles = 0
|
||
|
||
// 删除存储文件(优先S3,其次本地兼容)
|
||
for (const material of materials) {
|
||
if (material.zipFilePath && material.zipFilePath !== '') {
|
||
const filePath = material.zipFilePath
|
||
if (isS3Url(filePath)) {
|
||
try {
|
||
await deleteFromS3(filePath)
|
||
deletedFiles++
|
||
} catch (error) {
|
||
console.error('[Cleanup] Failed to delete S3 object:', filePath, error)
|
||
failedFiles++
|
||
}
|
||
continue
|
||
}
|
||
|
||
try {
|
||
await access(filePath)
|
||
await unlink(filePath)
|
||
deletedFiles++
|
||
} catch (fileError: any) {
|
||
if (fileError.code !== 'ENOENT') {
|
||
failedFiles++
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 删除所有素材记录
|
||
const deletedMaterials = await prisma.$executeRawUnsafe(`
|
||
DELETE FROM materials
|
||
`)
|
||
results.materials = Number(deletedMaterials)
|
||
results.materialFilesDeleted = deletedFiles
|
||
results.materialFilesFailed = failedFiles
|
||
|
||
// 4. 获取清理后的统计信息
|
||
const stats = await prisma.$queryRawUnsafe<Array<{ table_name: string; count: bigint }>>(`
|
||
SELECT
|
||
'invitations' as table_name,
|
||
COUNT(*)::bigint as count
|
||
FROM invitations
|
||
UNION ALL
|
||
SELECT
|
||
'danmakus' as table_name,
|
||
COUNT(*)::bigint as count
|
||
FROM danmakus
|
||
UNION ALL
|
||
SELECT
|
||
'materials' as table_name,
|
||
COUNT(*)::bigint as count
|
||
FROM materials
|
||
UNION ALL
|
||
SELECT
|
||
'users' as table_name,
|
||
COUNT(*)::bigint as count
|
||
FROM users
|
||
`)
|
||
|
||
const statsObj: Record<string, number> = {}
|
||
stats.forEach((stat) => {
|
||
statsObj[stat.table_name] = Number(stat.count)
|
||
})
|
||
|
||
return NextResponse.json({
|
||
message: '数据库清理成功',
|
||
success: true,
|
||
deleted: results,
|
||
remaining: statsObj,
|
||
})
|
||
} catch (error) {
|
||
console.error('[Cleanup] Error:', error)
|
||
return NextResponse.json(
|
||
{
|
||
error: '数据库清理失败',
|
||
details: error instanceof Error ? error.message : '未知错误',
|
||
},
|
||
{ status: 500 }
|
||
)
|
||
}
|
||
}
|
||
|