77 lines
5.4 KiB
TypeScript
77 lines
5.4 KiB
TypeScript
import { prisma } from '@/lib/prisma'
|
|
import { canUserViewMaterial } from '@/lib/department-permission'
|
|
import { buildMaterialPrivacyWhere } from '@/lib/task-detail-privacy'
|
|
import { listDetailBrandProfiles } from '@/lib/detail-generator/brand-profiles'
|
|
import { hasDetailBrandKnowledge } from '@/lib/detail-generator/brand-availability'
|
|
import { findDetailMaterialMatches, readDetailMaterialImage, readDetailMaterialPreview } from '@/lib/detail-generator/materials'
|
|
import { desktopAssert, desktopId } from './contract.mjs'
|
|
|
|
function pagination(params: URLSearchParams) {
|
|
const query = (params.get('query') || '').trim(), page = Number(params.get('page') || 1)
|
|
desktopAssert(query.length <= 100 && Number.isSafeInteger(page) && page >= 1 && page <= 1000, '目录查询参数无效')
|
|
return { query, page, pageSize: 24 }
|
|
}
|
|
function imageMime(bytes: Buffer) {
|
|
if(bytes.subarray(0,8).equals(Buffer.from([137,80,78,71,13,10,26,10])))return 'image/png'
|
|
if(bytes[0]===255&&bytes[1]===216)return 'image/jpeg'
|
|
if(bytes.toString('ascii',0,4)==='RIFF'&&bytes.toString('ascii',8,12)==='WEBP')return 'image/webp'
|
|
desktopAssert(false,'素材不是可用的PNG、JPG或WEBP图片')
|
|
return 'application/octet-stream'
|
|
}
|
|
export async function desktopBrands(params: URLSearchParams) {
|
|
const { query, page, pageSize } = pagination(params)
|
|
const [categories, profiles] = await Promise.all([
|
|
prisma.category.findMany({ where: query ? { name: { contains: query, mode: 'insensitive' } } : {}, orderBy: [{ name: 'asc' }, { id: 'asc' }] }),
|
|
listDetailBrandProfiles(),
|
|
])
|
|
const items = categories.map(category => {
|
|
const row = profiles.find(item => item.profile.name === category.name), ready = !!row && hasDetailBrandKnowledge(row.profile)
|
|
return { id: category.id, name: category.name, ready, reason: ready ? '' : '品牌规范待开发', profileId: row?.id || null, revision: row?.revision || null }
|
|
}).sort((a, b) => Number(b.ready) - Number(a.ready) || a.name.localeCompare(b.name, 'zh-CN'))
|
|
return { items: items.slice((page - 1) * pageSize, page * pageSize), total: items.length, page, hasMore: page * pageSize < items.length }
|
|
}
|
|
export async function desktopBrand(id: string) {
|
|
desktopId(id)
|
|
const category = await prisma.category.findUnique({ where: { id } })
|
|
desktopAssert(category, '品牌不存在', 404)
|
|
const row = (await listDetailBrandProfiles()).find(item => item.profile.name === category!.name)
|
|
desktopAssert(row && hasDetailBrandKnowledge(row.profile), '该品牌规范待开发', 409, 'brand_not_available')
|
|
return { id, name: category!.name, profileId: row!.id, revision: row!.revision, profile: row!.profile }
|
|
}
|
|
export async function desktopMaterials(ownerId: string, params: URLSearchParams) {
|
|
const { query, page, pageSize } = pagination(params)
|
|
desktopAssert(await canUserViewMaterial(undefined, '', undefined, '材质', ownerId), '没有材质目录权限', 403)
|
|
const where = { deletedAt: null, isIncompleteShell: false, materialType: '材质', AND: [await buildMaterialPrivacyWhere(ownerId)],
|
|
...(query ? { productModel: { contains: query, mode: 'insensitive' as const } } : {}) }
|
|
const [items, total] = await Promise.all([
|
|
prisma.material.findMany({ where, select: { id: true, productModel: true }, orderBy: [{ createdAt: 'desc' }, { id: 'desc' }], skip: (page - 1) * pageSize, take: pageSize }),
|
|
prisma.material.count({ where }),
|
|
])
|
|
return { items: items.map(item => ({ id: item.id, name: item.productModel })), total, page, hasMore: page * pageSize < total }
|
|
}
|
|
export async function desktopMaterialThumbnail(ownerId: string, id: string) {
|
|
desktopId(id)
|
|
const bytes = await readDetailMaterialPreview(ownerId, id)
|
|
// Transfer the stored preview; decoding and resizing run on the employee's PC.
|
|
return { bytes, mime: imageMime(bytes) }
|
|
}
|
|
export async function desktopProducts(ownerId: string, params: URLSearchParams) {
|
|
const matches = await findDetailMaterialMatches(ownerId, params.get('identifierType'), params.get('identifier'))
|
|
const rows = await prisma.material.findMany({ where: { id: { in: matches.map(item => item.materialId) } }, select: { id: true, categoryName: true, productCategoryName: true } })
|
|
const names = Array.from(new Set(rows.map(row => row.productCategoryName).filter((name): name is string => !!name)))
|
|
// Material stores the explicit category name; ProductCategory.name is unique.
|
|
// Do not infer category from model text or borrow a similar registered name.
|
|
const categories = names.length ? await prisma.productCategory.findMany({ where: { name: { in: names } }, select: { id: true, name: true } }) : []
|
|
return { items: matches.map(item => {
|
|
const row = rows.find(row => row.id === item.materialId), category = categories.find(category => category.name === row?.productCategoryName)
|
|
return { ...item, brandName: row?.categoryName || '', categoryId: category?.id || '', categoryName: category?.name || '',
|
|
assets: item.assets.map((asset: { id: string; source: string; name: string }) => ({ id: asset.id, kind: asset.source, name: asset.name })) }
|
|
}) }
|
|
}
|
|
export async function desktopProductImage(ownerId: string, materialId: string, kind: string, id: string) {
|
|
desktopId(materialId); desktopId(id)
|
|
const image = await readDetailMaterialImage(ownerId, materialId, kind, id)
|
|
// Original bytes stay private. Decode/type and size validation also occurs on the client.
|
|
return { bytes: image.buffer, mime: imageMime(image.buffer) }
|
|
}
|