177 lines
5.5 KiB
TypeScript
177 lines
5.5 KiB
TypeScript
import type { NextRequest } from 'next/server'
|
|
import { NextResponse } from 'next/server'
|
|
import { getServerSession } from 'next-auth'
|
|
import { authOptions } from '@/lib/auth'
|
|
import { isDepartmentAdmin, isSystemAdmin } from '@/lib/department-permission'
|
|
import { getMaterialUploadStats } from '@/lib/admin-material-stats'
|
|
import { prisma } from '@/lib/prisma'
|
|
|
|
interface StatsSessionUser {
|
|
role?: string | null
|
|
managedDepartment?: string | null
|
|
department?: string | null
|
|
}
|
|
|
|
function toDateStart(dateString: string): Date {
|
|
return new Date(`${dateString}T00:00:00`)
|
|
}
|
|
|
|
function toDateEnd(dateString: string): Date {
|
|
return new Date(`${dateString}T23:59:59.999`)
|
|
}
|
|
|
|
function escapeCsvValue(value: unknown): string {
|
|
if (value === null || value === undefined) {
|
|
return ''
|
|
}
|
|
|
|
const stringValue = String(value)
|
|
if (/[",\n]/.test(stringValue)) {
|
|
return `"${stringValue.replace(/"/g, '""')}"`
|
|
}
|
|
|
|
return stringValue
|
|
}
|
|
|
|
function formatDateTime(value: Date): string {
|
|
return value.toLocaleString('sv-SE', {
|
|
timeZone: 'Asia/Shanghai',
|
|
hour12: false,
|
|
})
|
|
}
|
|
|
|
export async function GET(request: NextRequest) {
|
|
try {
|
|
const session = await getServerSession(authOptions)
|
|
const sessionUser = session?.user as StatsSessionUser | undefined
|
|
const role = sessionUser?.role
|
|
const canViewAllDepartments = isSystemAdmin(role)
|
|
const canViewDepartmentScope = isDepartmentAdmin(role)
|
|
const canViewStats = canViewAllDepartments || canViewDepartmentScope
|
|
const scopeDepartmentName = canViewDepartmentScope
|
|
? String(sessionUser?.managedDepartment || sessionUser?.department || '').trim()
|
|
: null
|
|
|
|
if (!session?.user || !canViewStats) {
|
|
return NextResponse.json({ error: '无权限' }, { status: 403 })
|
|
}
|
|
|
|
if (canViewDepartmentScope && !scopeDepartmentName) {
|
|
return NextResponse.json({ error: '当前账号未绑定部门,无法导出统计数据' }, { status: 400 })
|
|
}
|
|
|
|
const { searchParams } = new URL(request.url)
|
|
const startDate = searchParams.get('startDate')
|
|
const endDate = searchParams.get('endDate')
|
|
const departmentKey = searchParams.get('departmentKey')
|
|
|
|
const stats = await getMaterialUploadStats({
|
|
startDate,
|
|
endDate,
|
|
departmentKey,
|
|
scopeDepartmentName,
|
|
})
|
|
|
|
const selectedUserIds = Array.from(new Set(stats.selectedUsers.map((user) => user.userId)))
|
|
if (selectedUserIds.length === 0) {
|
|
const lines = ['素材ID,上传时间,产品型号,素材类型,店铺,场景,平台,品类,作者,上传账号,上传姓名,上传部门,下载次数,点赞数']
|
|
const csvContent = `\uFEFF${lines.join('\n')}`
|
|
const rangeStart = startDate || 'all'
|
|
const rangeEnd = endDate || 'all'
|
|
const fileName = `material-details-${rangeStart}-${rangeEnd}.csv`
|
|
|
|
return new NextResponse(csvContent, {
|
|
status: 200,
|
|
headers: {
|
|
'Content-Type': 'text/csv; charset=utf-8',
|
|
'Content-Disposition': `attachment; filename="${encodeURIComponent(fileName)}"`,
|
|
},
|
|
})
|
|
}
|
|
|
|
const createdAtFilter: { gte?: Date; lte?: Date } = {}
|
|
if (startDate) {
|
|
createdAtFilter.gte = toDateStart(startDate)
|
|
}
|
|
if (endDate) {
|
|
createdAtFilter.lte = toDateEnd(endDate)
|
|
}
|
|
|
|
const materials = await prisma.material.findMany({
|
|
where: {
|
|
deletedAt: null,
|
|
userId: { in: selectedUserIds },
|
|
...(createdAtFilter.gte || createdAtFilter.lte ? { createdAt: createdAtFilter } : {}),
|
|
},
|
|
orderBy: { createdAt: 'desc' },
|
|
select: {
|
|
id: true,
|
|
productModel: true,
|
|
materialType: true,
|
|
categoryName: true,
|
|
sceneName: true,
|
|
platformName: true,
|
|
productCategoryName: true,
|
|
authorName: true,
|
|
createdAt: true,
|
|
downloadCount: true,
|
|
likeCount: true,
|
|
user: {
|
|
select: {
|
|
username: true,
|
|
realName: true,
|
|
department: true,
|
|
departmentRef: {
|
|
select: {
|
|
name: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
const lines: string[] = []
|
|
lines.push('素材ID,上传时间,产品型号,素材类型,店铺,场景,平台,品类,作者,上传账号,上传姓名,上传部门,下载次数,点赞数')
|
|
for (const material of materials) {
|
|
const departmentName = material.user.departmentRef?.name || material.user.department || '未分配部门'
|
|
lines.push(
|
|
[
|
|
material.id,
|
|
formatDateTime(material.createdAt),
|
|
material.productModel,
|
|
material.materialType,
|
|
material.categoryName,
|
|
material.sceneName,
|
|
material.platformName,
|
|
material.productCategoryName,
|
|
material.authorName,
|
|
material.user.username,
|
|
material.user.realName || '',
|
|
departmentName,
|
|
material.downloadCount,
|
|
material.likeCount,
|
|
]
|
|
.map(escapeCsvValue)
|
|
.join(',')
|
|
)
|
|
}
|
|
|
|
const csvContent = `\uFEFF${lines.join('\n')}`
|
|
const rangeStart = startDate || 'all'
|
|
const rangeEnd = endDate || 'all'
|
|
const fileName = `material-details-${rangeStart}-${rangeEnd}.csv`
|
|
|
|
return new NextResponse(csvContent, {
|
|
status: 200,
|
|
headers: {
|
|
'Content-Type': 'text/csv; charset=utf-8',
|
|
'Content-Disposition': `attachment; filename="${encodeURIComponent(fileName)}"`,
|
|
},
|
|
})
|
|
} catch (error) {
|
|
console.error('[Admin Data Stats Export] GET error:', error)
|
|
return NextResponse.json({ error: '导出 CSV 失败' }, { status: 500 })
|
|
}
|
|
}
|