294 lines
8.3 KiB
TypeScript
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 }
|
|
}
|
|
|