gongxiangp2/lib/material-query.ts
2026-07-30 14:39:18 +08:00

294 lines
8.3 KiB
TypeScript

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<string, string[]> = {
'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<string>([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<string>()
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<boolean> {
if (user.isSuperAdmin === true) {
return true
}
return user.id ? await isSuperAdmin(user.id) : false
}
export async function canAccessDeletedMaterials(user: MaterialSessionUser): Promise<boolean> {
if (await isSessionUserSuperAdmin(user)) {
return true
}
return isSystemAdmin(user.role || '')
}
export async function buildMaterialSearchOrConditions(search: string): Promise<any[] | undefined> {
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[] = [
{ id: keyword },
{ productModel: { contains: keyword, mode: 'insensitive' } },
{ reportTitle: { contains: keyword, mode: 'insensitive' } },
{ taskNo: { contains: keyword, mode: 'insensitive' } },
{ dingtalkApprovalNo: { 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<MaterialWhereResult> {
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 }
}