186 lines
5.4 KiB
TypeScript
186 lines
5.4 KiB
TypeScript
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 }
|
||
)
|
||
}
|
||
}
|
||
|