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) }
}