import type { Request, Response } from 'express' import prisma from '../utils/prisma' import { errorResponse, successResponse } from '../utils/response' const parseChildrenSnapshot = (value: string | null, rootEquipmentId: number) => { if (!value) return { items: [] as any[], valid: true } try { const parsed = JSON.parse(value) if (!Array.isArray(parsed)) return { items: [] as any[], valid: false } const depthById = new Map([[rootEquipmentId, 0]]) const items = parsed .filter((item) => item && typeof item === 'object') .map((item: Record) => { const id = Number(item.id) const rawParentId = Number(item.parent_id ?? item.parent_equipment_id) const parentId = Number.isInteger(rawParentId) && rawParentId > 0 ? rawParentId : rootEquipmentId const rawDepth = Number(item.depth) const parentDepth = depthById.get(parentId) const depth = Number.isInteger(rawDepth) && rawDepth > 0 ? rawDepth : (parentDepth ?? 0) + 1 if (Number.isInteger(id) && id > 0) depthById.set(id, depth) return { ...item, id, parent_id: parentId, parent_equipment_id: parentId, root_equipment_id: rootEquipmentId, depth, relation_source: 'borrow_snapshot' as const } }) .filter((item) => Number.isInteger(item.id) && item.id > 0) return { items, valid: true } } catch { return { items: [] as any[], valid: false } } } const currentAccessorySelect = { id: true, equipment_code: true, equipment_name: true, equipment_model: true, equipment_type: true, status: true, parent_id: true, mount_status: true, department_id: true, storage_location_id: true } as const const loadCurrentAccessories = async (rootEquipmentIds: number[]) => { const uniqueRootIds = [...new Set(rootEquipmentIds)] const result = new Map(uniqueRootIds.map((id) => [id, []])) const nodeMeta = new Map( uniqueRootIds.map((id) => [id, { rootEquipmentId: id, depth: 0 }]) ) const visited = new Set(uniqueRootIds) let parentIds = uniqueRootIds let depth = 0 while (parentIds.length > 0) { if (depth >= 100) { throw Object.assign(new Error('资产父子关系超过 100 层,请先检查资产关系'), { status: 409 }) } const children = await prisma.equipment.findMany({ where: { parent_id: { in: parentIds } }, select: currentAccessorySelect, orderBy: { id: 'asc' } }) if (children.length === 0) break for (const child of children) { if (visited.has(child.id)) { throw Object.assign( new Error(`资产 ${child.equipment_code} 存在循环或重复父子关系,请先检查资产关系`), { status: 409 } ) } const parentMeta = child.parent_id ? nodeMeta.get(child.parent_id) : null if (!parentMeta) { throw Object.assign( new Error(`资产 ${child.equipment_code} 的父子关系不完整,请先检查资产关系`), { status: 409 } ) } const item = { ...child, parent_equipment_id: child.parent_id, root_equipment_id: parentMeta.rootEquipmentId, depth: parentMeta.depth + 1, relation_source: 'current' as const } visited.add(child.id) nodeMeta.set(child.id, { rootEquipmentId: parentMeta.rootEquipmentId, depth: parentMeta.depth + 1 }) result.get(parentMeta.rootEquipmentId)?.push(item) } parentIds = children.map((child) => child.id) depth += 1 } return result } const getRelationshipFingerprint = (items: Array<{ id: number; parent_id?: number | null }>) => items .map((item) => `${item.id}:${item.parent_id ?? 'null'}`) .sort() .join('|') export const getMyAssets = async (_req: Request, res: Response) => { try { const personId = Number(res.locals.user?.person_id) if (!Number.isInteger(personId) || personId <= 0) { return errorResponse(res, '当前账号尚未绑定员工档案', 403) } const person = await prisma.person.findUnique({ where: { id: personId }, select: { id: true, real_name: true, nickname: true, job_number: true, status: true, source_employee_status: true, mainDepartment: { select: { id: true, name: true } } } }) if (!person || person.status !== 'active') { return errorResponse(res, '绑定的员工档案不存在或已离职', 403) } const records = await prisma.borrowRecord.findMany({ where: { person_id: personId, status: '已借出' }, orderBy: [{ borrow_time: 'desc' }, { id: 'desc' }], include: { equipment: { include: { storageLocation: { select: { id: true, name: true, code: true } }, department: { select: { id: true, name: true } }, children: { orderBy: { id: 'asc' }, select: currentAccessorySelect } } } } }) const currentAccessoriesByRoot = await loadCurrentAccessories( records.map((record) => record.equipment_id) ) successResponse(res, { person, assets: records.map((record) => { const currentAccessories = currentAccessoriesByRoot.get(record.equipment_id) || [] const snapshot = parseChildrenSnapshot(record.children_snapshot, record.equipment_id) const relationshipChanged = !snapshot.valid || getRelationshipFingerprint(currentAccessories) !== getRelationshipFingerprint(snapshot.items) const currentDirectChildren = currentAccessories.filter((item) => item.depth === 1) const legacyChildren = currentDirectChildren.length > 0 ? currentDirectChildren : snapshot.items return { borrow_record_id: record.id, borrow_time: record.borrow_time, expected_return_time: record.expected_return_time, is_long_term: record.is_long_term, notes: record.notes, equipment: { ...record.equipment, children: currentDirectChildren }, children: legacyChildren, current_accessories: currentAccessories, borrowed_accessories_snapshot: snapshot.items, borrowed_accessories_snapshot_valid: snapshot.valid, accessory_relationship_changed: relationshipChanged } }) }, '获取成功') } catch (error: any) { errorResponse(res, error.message || '获取本人资产失败', error.status || 500) } }