import { prisma } from './prisma' import { getUserViewableMaterialTypes, isSuperAdmin, isSystemAdmin, } from './department-permission' import { filterSelectableMaterialTypes } from './material-type-options' import { getCompatibleVideoMaterialTypes, normalizeVideoMaterialType, } from './video-material-types' export type MaterialFilterDimension = | 'materialType' | 'category' | 'scene' | 'platform' | 'productCategory' | 'month' export interface MaterialSessionUser { id?: string | null role?: string | null department?: string | null managedDepartment?: string | null isSuperAdmin?: boolean | null } export interface MaterialWhereResult { where: any empty: boolean } export class MaterialQueryError extends Error { status: number code: string constructor(status: number, code: string, message: string) { super(message) this.status = status this.code = code } } const MATERIAL_TYPE_COMPATIBILITY: Record = { '3D场景': ['3D场景', '3DMAX'], '3DMAX': ['3D场景', '3DMAX'], '任务-详情': ['任务-详情', '产品全案'], '产品全案': ['任务-详情', '产品全案'], '详情模板': ['详情模板', '详情页'], '详情页': ['详情模板', '详情页'], '矢量文件': ['矢量文件', 'AI'], 'AI': ['矢量文件', 'AI'], 'C4D模型': ['C4D模型', 'C4D'], 'C4D': ['C4D模型', 'C4D'], } function unique(values: string[]): string[] { return Array.from(new Set(values.map((value) => value.trim()).filter(Boolean))) } export function normalizeMaterialTypeForUi(materialType?: string | null): string { const trimmed = String(materialType || '').trim() if (!trimmed) { return '' } if (trimmed === '3DMAX') return '3D场景' if (trimmed === '产品全案') return '任务-详情' if (trimmed === '详情页') return '详情模板' return normalizeVideoMaterialType(trimmed) } export function normalizeSelectableMaterialTypes(types: string[]): string[] { return filterSelectableMaterialTypes( unique(types.map((type) => normalizeMaterialTypeForUi(type))), ) } export function getCompatibleMaterialTypes(materialType?: string | null): string[] { const trimmed = String(materialType || '').trim() if (!trimmed) { return [] } const normalized = normalizeMaterialTypeForUi(trimmed) const candidates = new Set([trimmed, normalized]) for (const type of [trimmed, normalized]) { const mapped = MATERIAL_TYPE_COMPATIBILITY[type] if (mapped) { mapped.forEach((item) => candidates.add(item)) } getCompatibleVideoMaterialTypes(type).forEach((item) => candidates.add(item)) } return unique(Array.from(candidates)) } export function expandCompatibleMaterialTypes(types: string[]): string[] { const expanded = new Set() for (const type of types) { getCompatibleMaterialTypes(type).forEach((item) => expanded.add(item)) } return unique(Array.from(expanded)) } export function materialTypeWhere(types: string[]): any { const uniqueTypes = unique(types) if (uniqueTypes.length === 0) { return undefined } return uniqueTypes.length === 1 ? uniqueTypes[0] : { in: uniqueTypes } } export function isDeletedMaterialRequest(searchParams: URLSearchParams): boolean { return searchParams.get('showDeleted') === 'true' || searchParams.get('deleted') === 'true' } export async function isSessionUserSuperAdmin(user: MaterialSessionUser): Promise { if (user.isSuperAdmin === true) { return true } return user.id ? await isSuperAdmin(user.id) : false } export async function canAccessDeletedMaterials(user: MaterialSessionUser): Promise { if (await isSessionUserSuperAdmin(user)) { return true } return isSystemAdmin(user.role || '') } export async function buildMaterialSearchOrConditions(search: string): Promise { const keyword = search.trim() if (!keyword) { return undefined } const [skuMatches, userMatches] = await Promise.all([ prisma.materialSku.findMany({ where: { skuName: { contains: keyword, mode: 'insensitive' }, deletedAt: null, }, select: { materialId: true }, take: 1000, }), prisma.user.findMany({ where: { OR: [ { realName: { contains: keyword, mode: 'insensitive' } }, { username: { contains: keyword, mode: 'insensitive' } }, ], }, select: { id: true }, take: 1000, }), ]) const skuMaterialIds = skuMatches.map((item) => item.materialId) const userIds = userMatches.map((item) => item.id) const orConditions: any[] = [ { productModel: { contains: keyword, mode: 'insensitive' } }, { reportTitle: { contains: keyword, mode: 'insensitive' } }, { taskNo: { contains: keyword, mode: 'insensitive' } }, { categoryName: { contains: keyword, mode: 'insensitive' } }, { sceneName: { contains: keyword, mode: 'insensitive' } }, { authorName: { contains: keyword, mode: 'insensitive' } }, ] if (skuMaterialIds.length > 0) { orConditions.push({ id: { in: skuMaterialIds } }) } if (userIds.length > 0) { orConditions.push({ userId: { in: userIds } }) } return orConditions } function applyMonthFilter(where: any, month: string): void { const [year, monthNum] = month.split('-') const parsedYear = Number.parseInt(year, 10) const parsedMonth = Number.parseInt(monthNum, 10) if (!Number.isFinite(parsedYear) || !Number.isFinite(parsedMonth)) { return } where.createdAt = { gte: new Date(parsedYear, parsedMonth - 1, 1), lt: new Date(parsedYear, parsedMonth, 1), } } export async function buildMaterialWhereFromSearchParams( searchParams: URLSearchParams, user: MaterialSessionUser | null | undefined, options: { exclude?: MaterialFilterDimension } = {}, ): Promise { if (!user?.id) { throw new MaterialQueryError(401, 'UNAUTHORIZED', '未登录') } const where: any = {} const requestedDeleted = isDeletedMaterialRequest(searchParams) if (requestedDeleted) { if (!(await canAccessDeletedMaterials(user))) { throw new MaterialQueryError(403, 'FORBIDDEN_DELETED_MATERIALS', '无权限查看回收站素材') } where.deletedAt = { not: null } } else { where.deletedAt = null } const search = searchParams.get('search') || '' const category = searchParams.get('category') || '' const scene = searchParams.get('scene') || '' const materialType = searchParams.get('materialType') || '' const platform = searchParams.get('platform') || '' const productCategory = searchParams.get('productCategory') || '' const month = searchParams.get('month') || '' const searchOrConditions = await buildMaterialSearchOrConditions(search) if (searchOrConditions) { where.OR = searchOrConditions } if (category && options.exclude !== 'category') { where.categoryName = category } if (scene && options.exclude !== 'scene') { where.sceneName = scene } if (platform && options.exclude !== 'platform') { where.platformName = platform } if (productCategory && options.exclude !== 'productCategory') { where.productCategoryName = productCategory } if (month && options.exclude !== 'month') { applyMonthFilter(where, month) } const requestedTypes = materialType && options.exclude !== 'materialType' ? getCompatibleMaterialTypes(materialType) : [] if (requestedTypes.length > 0) { where.materialType = materialTypeWhere(requestedTypes) } if (await isSessionUserSuperAdmin(user)) { return { where, empty: false } } const viewableTypes = await getUserViewableMaterialTypes( user.department || '', user.role || 'USER', user.managedDepartment || null, user.id, ) const expandedViewableTypes = expandCompatibleMaterialTypes(viewableTypes) if (expandedViewableTypes.length === 0) { return { where, empty: true } } if (requestedTypes.length > 0) { const visibleSet = new Set(expandedViewableTypes) const allowedRequestedTypes = requestedTypes.filter((type) => visibleSet.has(type)) if (allowedRequestedTypes.length === 0) { return { where, empty: true } } where.materialType = materialTypeWhere(allowedRequestedTypes) return { where, empty: false } } where.materialType = materialTypeWhere(expandedViewableTypes) return { where, empty: false } }