2026-06-02 10:16:28 +08:00

186 lines
5.4 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 { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { prisma } from '@/lib/prisma'
import { isCurrentSuperAdmin } from '@/lib/admin-permissions'
import { canUseSystemSettings } from '@/lib/license'
import {
collectMaterialStorageUrls,
deleteMaterialStorageFiles,
} from '@/lib/material-storage-cleanup'
export const dynamic = 'force-dynamic'
export async function POST(req: NextRequest) {
try {
const session = await getServerSession(authOptions)
if (!session || !(await isCurrentSuperAdmin(session))) {
return NextResponse.json(
{ error: '仅最高权限账号可执行数据库清理' },
{ status: 403 }
)
}
if (!(await canUseSystemSettings())) {
return NextResponse.json(
{ error: '当前许可证未包含系统设置功能,请联系管理员处理授权' },
{ status: 403 }
)
}
const body = await req.json().catch(() => ({}))
if (body?.confirmPhrase !== '清理全部素材') {
return NextResponse.json(
{
error: '数据库清理是高风险操作,请输入确认短语后再执行',
requiredConfirmPhrase: '清理全部素材',
},
{ status: 400 }
)
}
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 | null
previewUrl: string | null
previewUrls: string | null
displayImages: string | null
videoSourceUrl: string | null
videoPreview540Url: string | null
videoPreviewClipUrl: string | null
videoCoverUrl: string | null
}>>(`
SELECT
id,
"zipFilePath",
"previewUrl",
"previewUrls",
"displayImages",
"videoSourceUrl",
"videoPreview540Url",
"videoPreviewClipUrl",
"videoCoverUrl"
FROM materials
`)
let deletedFiles = 0
let skippedFiles = 0
let failedFiles = 0
const failedMaterials: Array<{
id: string
failedFiles: number
failures: Array<{ fileUrl: string; reason: string }>
}> = []
// 删除存储文件。文件删除失败时保留数据库记录,避免清库后无法追踪残留对象。
for (const material of materials) {
const cleanupResult = await deleteMaterialStorageFiles(collectMaterialStorageUrls(material), {
logPrefix: `[Cleanup] Material ${material.id}`,
})
deletedFiles += cleanupResult.deletedFiles
skippedFiles += cleanupResult.skippedFiles
failedFiles += cleanupResult.failedFiles
if (cleanupResult.failedFiles > 0) {
failedMaterials.push({
id: material.id,
failedFiles: cleanupResult.failedFiles,
failures: cleanupResult.failures.slice(0, 10),
})
}
}
if (failedMaterials.length > 0) {
return NextResponse.json(
{
error: '素材文件删除失败,数据库记录已保留,请处理存储连接后重试',
deleted: {
...results,
materialFilesDeleted: deletedFiles,
materialFilesSkipped: skippedFiles,
materialFilesFailed: failedFiles,
},
failedMaterials,
},
{ status: 500 }
)
}
// 删除所有素材记录
const deletedMaterials = await prisma.$executeRawUnsafe(`
DELETE FROM materials
`)
results.materials = Number(deletedMaterials)
results.materialFilesDeleted = deletedFiles
results.materialFilesSkipped = skippedFiles
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 }
)
}
}