import { prisma } from '@/lib/prisma' export interface MaterialStatsFilters { startDate?: string | null endDate?: string | null departmentKey?: string | null scopeDepartmentName?: string | null } export interface MaterialUserUploadStat { userId: string username: string realName: string | null departmentName: string departmentKey: string uploadCount: number percentage?: number } export interface MaterialDepartmentUploadStat { departmentKey: string departmentId: string | null departmentName: string uploadCount: number userCount: number percentage: number users: MaterialUserUploadStat[] } export interface MaterialTrendPoint { bucket: string count: number } export interface MaterialTypeUploadStat { materialType: string uploadCount: number percentage: number } export interface MaterialTypeDistributionStat { materialType: string uploadCount: number percentage: number users: MaterialUserUploadStat[] } export interface MaterialDistributionSection { granularity: 'month' | 'day' period: string label: string totalUploads: number items: TItem[] } export interface MaterialUploadStatsResult { filters: { startDate: string | null endDate: string | null departmentKey: string | null } summary: { totalUploads: number totalDepartments: number totalUsers: number } departments: MaterialDepartmentUploadStat[] selectedDepartmentName: string selectedUsers: MaterialUserUploadStat[] dailyUploads: MaterialTrendPoint[] monthlyUploads: MaterialTrendPoint[] materialTypeStats: MaterialTypeUploadStat[] departmentDistribution: MaterialDistributionSection materialTypeDistribution: MaterialDistributionSection } interface UserWithDepartment { id: string username: string realName: string | null department: string departmentId: string | null departmentRef: { id: string name: string } | null } const UNASSIGNED_DEPARTMENT_KEY = 'unassigned' const UNASSIGNED_DEPARTMENT_NAME = '未分配部门' 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 normalizeValue(value: unknown): string { if (value === null || value === undefined) return '' return String(value).trim() } function resolveDepartment( user: UserWithDepartment, departmentById: Map, departmentByName: Map ): { departmentKey: string; departmentId: string | null; departmentName: string } { if (user.departmentRef?.id && user.departmentRef?.name) { return { departmentKey: `dept:${user.departmentRef.id}`, departmentId: user.departmentRef.id, departmentName: user.departmentRef.name, } } if (user.departmentId) { const matchedDepartment = departmentById.get(user.departmentId) if (matchedDepartment) { return { departmentKey: `dept:${matchedDepartment.id}`, departmentId: matchedDepartment.id, departmentName: matchedDepartment.name, } } } const departmentNameFromUser = normalizeValue(user.department) if (departmentNameFromUser) { const matchedDepartment = departmentByName.get(departmentNameFromUser) if (matchedDepartment) { return { departmentKey: `dept:${matchedDepartment.id}`, departmentId: matchedDepartment.id, departmentName: matchedDepartment.name, } } return { departmentKey: `name:${departmentNameFromUser}`, departmentId: null, departmentName: departmentNameFromUser, } } return { departmentKey: UNASSIGNED_DEPARTMENT_KEY, departmentId: null, departmentName: UNASSIGNED_DEPARTMENT_NAME, } } function toBucketKey(date: Date, granularity: 'month' | 'day'): string { const year = date.getFullYear() const month = String(date.getMonth() + 1).padStart(2, '0') if (granularity === 'month') { return `${year}-${month}` } const day = String(date.getDate()).padStart(2, '0') return `${year}-${month}-${day}` } function buildDistributionLabel(startDate?: string | null, endDate?: string | null): string { if (startDate && endDate) { return `${startDate} 至 ${endDate}` } if (startDate) { return `${startDate} 至今` } if (endDate) { return `截至 ${endDate}` } return '全部时间' } function resolveSelectedDepartmentName( departmentKey: string | null | undefined, selectedDepartmentName: string | null | undefined, defaultName: string, departmentById: Map ): string { if (!departmentKey) { return defaultName } if (selectedDepartmentName) { return selectedDepartmentName } if (departmentKey === UNASSIGNED_DEPARTMENT_KEY) { return UNASSIGNED_DEPARTMENT_NAME } if (departmentKey.startsWith('name:')) { const name = departmentKey.slice('name:'.length).trim() return name || defaultName } if (departmentKey.startsWith('dept:')) { const departmentId = departmentKey.slice('dept:'.length).trim() const department = departmentId ? departmentById.get(departmentId) : null return department?.name || defaultName } return defaultName } export async function getMaterialUploadStats( filters: MaterialStatsFilters ): Promise { const createdAtFilter: { gte?: Date; lte?: Date } = {} if (filters.startDate) { createdAtFilter.gte = toDateStart(filters.startDate) } if (filters.endDate) { createdAtFilter.lte = toDateEnd(filters.endDate) } const materialWhere: { deletedAt: null; createdAt?: { gte?: Date; lte?: Date } } = { deletedAt: null, } if (createdAtFilter.gte || createdAtFilter.lte) { materialWhere.createdAt = createdAtFilter } const groupedUploads = await prisma.material.groupBy({ by: ['userId'], _count: { id: true }, where: materialWhere, }) const userIds = groupedUploads.map((item) => item.userId) const [users, departments] = await Promise.all([ userIds.length ? prisma.user.findMany({ where: { id: { in: userIds } }, select: { id: true, username: true, realName: true, department: true, departmentId: true, departmentRef: { select: { id: true, name: true, }, }, }, }) : Promise.resolve([]), prisma.department.findMany({ select: { id: true, name: true, }, }), ]) const userMap = new Map(users.map((user) => [user.id, user])) const departmentById = new Map(departments.map((department) => [department.id, department])) const departmentByName = new Map(departments.map((department) => [department.name, department])) const resolvedDepartmentByUserId = new Map() for (const user of users) { resolvedDepartmentByUserId.set(user.id, resolveDepartment(user, departmentById, departmentByName)) } const scopeDepartmentName = normalizeValue(filters.scopeDepartmentName) const allowedUserIds = new Set() const departmentStatsMap = new Map() for (const uploadGroup of groupedUploads) { const user = userMap.get(uploadGroup.userId) if (!user) { continue } const resolvedDepartment = resolvedDepartmentByUserId.get(user.id) if (!resolvedDepartment) { continue } if (scopeDepartmentName && resolvedDepartment.departmentName !== scopeDepartmentName) { continue } allowedUserIds.add(user.id) if (!departmentStatsMap.has(resolvedDepartment.departmentKey)) { departmentStatsMap.set(resolvedDepartment.departmentKey, { departmentKey: resolvedDepartment.departmentKey, departmentId: resolvedDepartment.departmentId, departmentName: resolvedDepartment.departmentName, uploadCount: 0, userCount: 0, percentage: 0, users: [], }) } const uploadCount = uploadGroup._count.id const departmentStat = departmentStatsMap.get(resolvedDepartment.departmentKey) if (!departmentStat) { continue } departmentStat.uploadCount += uploadCount departmentStat.users.push({ userId: user.id, username: user.username, realName: user.realName, departmentName: resolvedDepartment.departmentName, departmentKey: resolvedDepartment.departmentKey, uploadCount, }) } const departmentStats = Array.from(departmentStatsMap.values()) for (const departmentStat of departmentStats) { departmentStat.users.sort((a, b) => b.uploadCount - a.uploadCount) departmentStat.userCount = departmentStat.users.length } departmentStats.sort((a, b) => b.uploadCount - a.uploadCount) const totalUploads = departmentStats.reduce((sum, departmentStat) => sum + departmentStat.uploadCount, 0) for (const departmentStat of departmentStats) { departmentStat.percentage = totalUploads > 0 ? Number(((departmentStat.uploadCount / totalUploads) * 100).toFixed(2)) : 0 for (const user of departmentStat.users) { user.percentage = departmentStat.uploadCount > 0 ? Number(((user.uploadCount / departmentStat.uploadCount) * 100).toFixed(2)) : 0 } } const hasDepartmentFilter = Boolean(normalizeValue(filters.departmentKey)) const selectedDepartment = hasDepartmentFilter ? departmentStats.find((department) => department.departmentKey === filters.departmentKey) : null const allUsers = departmentStats .flatMap((department) => department.users) .sort((a, b) => b.uploadCount - a.uploadCount) const selectedUsers = hasDepartmentFilter ? (selectedDepartment ? selectedDepartment.users : []) : allUsers const selectedUserIds = Array.from(new Set(selectedUsers.map((user) => user.userId))) const materialSnapshots = selectedUserIds.length ? await prisma.material.findMany({ where: { deletedAt: null, userId: { in: selectedUserIds }, ...(materialWhere.createdAt ? { createdAt: materialWhere.createdAt } : {}), }, select: { createdAt: true, materialType: true, userId: true, }, }) : [] const filteredSnapshots = materialSnapshots const selectedTotalUploads = selectedUsers.reduce((sum, user) => sum + user.uploadCount, 0) const dailyMap = new Map() const monthlyMap = new Map() const materialTypeMap = new Map() const departmentDistributionMap = new Map }>() const materialTypeDistributionMap = new Map }>() for (const item of filteredSnapshots) { const createdAt = new Date(item.createdAt) const dayKey = toBucketKey(createdAt, 'day') const monthKey = toBucketKey(createdAt, 'month') const materialTypeKey = normalizeValue(item.materialType) || '未知类型' dailyMap.set(dayKey, (dailyMap.get(dayKey) || 0) + 1) monthlyMap.set(monthKey, (monthlyMap.get(monthKey) || 0) + 1) materialTypeMap.set(materialTypeKey, (materialTypeMap.get(materialTypeKey) || 0) + 1) const user = userMap.get(item.userId) const resolvedDepartment = resolvedDepartmentByUserId.get(item.userId) if (!user || !resolvedDepartment) { continue } if (!departmentDistributionMap.has(resolvedDepartment.departmentKey)) { departmentDistributionMap.set(resolvedDepartment.departmentKey, { departmentKey: resolvedDepartment.departmentKey, departmentId: resolvedDepartment.departmentId, departmentName: resolvedDepartment.departmentName, uploadCount: 0, users: new Map(), }) } const departmentItem = departmentDistributionMap.get(resolvedDepartment.departmentKey) if (!departmentItem) { continue } departmentItem.uploadCount += 1 const existingDepartmentUser = departmentItem.users.get(user.id) if (existingDepartmentUser) { existingDepartmentUser.uploadCount += 1 } else { departmentItem.users.set(user.id, { userId: user.id, username: user.username, realName: user.realName, departmentName: resolvedDepartment.departmentName, departmentKey: resolvedDepartment.departmentKey, uploadCount: 1, }) } if (!materialTypeDistributionMap.has(materialTypeKey)) { materialTypeDistributionMap.set(materialTypeKey, { materialType: materialTypeKey, uploadCount: 0, users: new Map(), }) } const materialTypeItem = materialTypeDistributionMap.get(materialTypeKey) if (!materialTypeItem) { continue } materialTypeItem.uploadCount += 1 const existingMaterialTypeUser = materialTypeItem.users.get(user.id) if (existingMaterialTypeUser) { existingMaterialTypeUser.uploadCount += 1 } else { materialTypeItem.users.set(user.id, { userId: user.id, username: user.username, realName: user.realName, departmentName: resolvedDepartment.departmentName, departmentKey: resolvedDepartment.departmentKey, uploadCount: 1, }) } } const dailyUploads = Array.from(dailyMap.entries()) .map(([bucket, count]) => ({ bucket, count })) .sort((a, b) => a.bucket.localeCompare(b.bucket)) const monthlyUploads = Array.from(monthlyMap.entries()) .map(([bucket, count]) => ({ bucket, count })) .sort((a, b) => a.bucket.localeCompare(b.bucket)) const materialTypeStats = Array.from(materialTypeMap.entries()) .map(([materialType, uploadCount]) => ({ materialType, uploadCount, percentage: selectedTotalUploads > 0 ? Number(((uploadCount / selectedTotalUploads) * 100).toFixed(2)) : 0, })) .sort((a, b) => b.uploadCount - a.uploadCount) const departmentDistributionItemsBase = Array.from(departmentDistributionMap.values()) const departmentDistributionTotal = departmentDistributionItemsBase.reduce((sum, item) => sum + item.uploadCount, 0) const departmentDistributionItems: MaterialDepartmentUploadStat[] = departmentDistributionItemsBase .map((item) => { const users = Array.from(item.users.values()) .sort((a, b) => b.uploadCount - a.uploadCount) .map((user) => ({ ...user, percentage: item.uploadCount > 0 ? Number(((user.uploadCount / item.uploadCount) * 100).toFixed(2)) : 0, })) return { departmentKey: item.departmentKey, departmentId: item.departmentId, departmentName: item.departmentName, uploadCount: item.uploadCount, userCount: users.length, percentage: departmentDistributionTotal > 0 ? Number(((item.uploadCount / departmentDistributionTotal) * 100).toFixed(2)) : 0, users, } }) .sort((a, b) => b.uploadCount - a.uploadCount) const materialTypeDistributionItemsBase = Array.from(materialTypeDistributionMap.values()) const materialTypeDistributionTotal = materialTypeDistributionItemsBase.reduce((sum, item) => sum + item.uploadCount, 0) const materialTypeDistributionItems: MaterialTypeDistributionStat[] = materialTypeDistributionItemsBase .map((item) => { const users = Array.from(item.users.values()) .sort((a, b) => b.uploadCount - a.uploadCount) .map((user) => ({ ...user, percentage: item.uploadCount > 0 ? Number(((user.uploadCount / item.uploadCount) * 100).toFixed(2)) : 0, })) return { materialType: item.materialType, uploadCount: item.uploadCount, percentage: materialTypeDistributionTotal > 0 ? Number(((item.uploadCount / materialTypeDistributionTotal) * 100).toFixed(2)) : 0, users, } }) .sort((a, b) => b.uploadCount - a.uploadCount) return { filters: { startDate: filters.startDate || null, endDate: filters.endDate || null, departmentKey: filters.departmentKey || null, }, summary: { totalUploads: hasDepartmentFilter ? selectedTotalUploads : totalUploads, totalDepartments: hasDepartmentFilter ? (selectedDepartment ? 1 : 0) : departmentStats.length, totalUsers: selectedUsers.length, }, departments: departmentStats, selectedDepartmentName: resolveSelectedDepartmentName( filters.departmentKey || null, selectedDepartment?.departmentName || null, scopeDepartmentName || '全部部门', departmentById ), selectedUsers, dailyUploads, monthlyUploads, materialTypeStats, departmentDistribution: { granularity: 'month', period: '', label: buildDistributionLabel(filters.startDate, filters.endDate), totalUploads: departmentDistributionTotal, items: departmentDistributionItems, }, materialTypeDistribution: { granularity: 'month', period: '', label: buildDistributionLabel(filters.startDate, filters.endDate), totalUploads: materialTypeDistributionTotal, items: materialTypeDistributionItems, }, } }