import { Request, Response } from 'express' import prisma from '../utils/prisma' import { successResponse, errorResponse } from '../utils/response' const parseNumber = (value: any): number | undefined => { const num = Number(value) return Number.isNaN(num) ? undefined : num } const parseDate = (value?: any): Date | null => { if (!value) return null const date = new Date(value) return Number.isNaN(date.getTime()) ? null : date } const equipmentInclude = { storageLocation: { select: { id: true, name: true, code: true, status: true } } } const getEquipmentByIdentifier = async (equipmentId?: number, equipmentCode?: string) => { if (equipmentId) { return prisma.equipment.findUnique({ where: { id: equipmentId }, include: equipmentInclude }) } if (equipmentCode) { return prisma.equipment.findUnique({ where: { equipment_code: equipmentCode }, include: equipmentInclude }) } return null } const ensurePersonAvailable = async (personId?: number) => { if (!personId) { throw new Error('使用者ID不能为空') } const person = await prisma.person.findUnique({ where: { id: personId } }) if (!person || person.status !== 'active') { throw new Error('使用者不存在或已禁用') } return person } const ensureEquipmentBorrowable = (equipment: any) => { // 检查报废状态 if (equipment.status === '待报废' || equipment.status === '已报废') { throw new Error('资产已报废或待报废,不可借用') } // 检查维护状态 if (equipment.status === '维护中') { throw new Error('资产正在维护中,不可借用') } if (equipment.status !== '可用') { throw new Error('资产状态不可借用') } if (equipment.remaining_quantity <= 0) { throw new Error('资产库存不足') } } const appendLongTermNotes = (notes?: string, isLongTerm?: boolean) => { const longTerm = Boolean(isLongTerm) return longTerm && notes ? `${notes};在职长期` : longTerm ? '在职长期' : notes || undefined } const getChildrenSnapshot = async (equipmentId: number) => { const children = await prisma.equipment.findMany({ where: { parent_id: equipmentId }, select: { id: true, equipment_code: true, equipment_name: true, equipment_model: true, equipment_type: true, unit_price: true } }) return { children, snapshot: children.length > 0 ? JSON.stringify(children) : undefined } } const borrowEquipmentInTransaction = async ( equipment: any, person: any, expectedReturn: Date | null, combinedNotes?: string, isLongTerm?: boolean, childrenSnapshot?: string ) => { return prisma.$transaction(async (tx) => { const record = await tx.borrowRecord.create({ data: { equipment_id: equipment.id, equipment_code: equipment.equipment_code, person_id: person.id, person_name: person.real_name, person_nickname: person.nickname, person_identifier: person.id_number, expected_return_time: expectedReturn, is_long_term: Boolean(isLongTerm), notes: combinedNotes, status: '已借出', children_snapshot: childrenSnapshot } }) const nextStatus = equipment.remaining_quantity === 1 ? '已借出' : '可用' const updateResult = await tx.equipment.updateMany({ where: { id: equipment.id, status: '可用', remaining_quantity: equipment.remaining_quantity }, data: { remaining_quantity: { decrement: 1 }, borrowed_value: { increment: Number(equipment.unit_price) }, available_value: { decrement: Number(equipment.unit_price) }, status: nextStatus } }) if (updateResult.count !== 1) { throw new Error('资产库存已变化,请刷新后重试') } const updatedEquipment = await tx.equipment.findUnique({ where: { id: equipment.id }, include: equipmentInclude }) return { record, updatedEquipment } }) } const findActiveBorrowRecord = async ( equipmentId: number, borrowRecordId?: number, personId?: number ) => { if (borrowRecordId) { return prisma.borrowRecord.findFirst({ where: { id: borrowRecordId, equipment_id: equipmentId, status: '已借出' } }) } if (personId) { return prisma.borrowRecord.findFirst({ where: { equipment_id: equipmentId, person_id: personId, status: '已借出' }, orderBy: { borrow_time: 'desc' } }) } const activeRecords = await prisma.borrowRecord.findMany({ where: { equipment_id: equipmentId, status: '已借出' }, orderBy: { borrow_time: 'desc' }, take: 2 }) if (activeRecords.length > 1) { throw new Error('该资产存在多条未归还记录,请明确选择归还使用者') } return activeRecords[0] || null } const getChildrenDiff = async (equipmentId: number, borrowRecord: any) => { if (!borrowRecord?.children_snapshot) { return null } const borrowedChildren = JSON.parse(borrowRecord.children_snapshot) as any[] const currentChildren = await prisma.equipment.findMany({ where: { parent_id: equipmentId }, select: { id: true, equipment_code: true, equipment_name: true, equipment_model: true, equipment_type: true, unit_price: true } }) const borrowedIds = new Set(borrowedChildren.map((child) => child.id)) const currentIds = new Set(currentChildren.map((child) => child.id)) const removed = borrowedChildren.filter((child) => !currentIds.has(child.id)) const added = currentChildren.filter((child) => !borrowedIds.has(child.id)) if (removed.length === 0 && added.length === 0) { return null } return { removed, added } } const returnEquipmentInTransaction = async ( equipment: any, borrowRecord: any, returnCondition: string, notes?: string ) => { return prisma.$transaction(async (tx) => { const returnTime = new Date() const updatedBorrowRecord = await tx.borrowRecord.updateMany({ where: { id: borrowRecord.id, status: '已借出' }, data: { return_time: returnTime, status: '已归还', return_condition: returnCondition, notes } }) if (updatedBorrowRecord.count !== 1) { throw new Error('该借用记录已归还,请刷新后重试') } const equipmentUpdate = await tx.equipment.updateMany({ where: { id: equipment.id, remaining_quantity: { lt: equipment.total_quantity } }, data: { remaining_quantity: { increment: 1 }, borrowed_value: { decrement: Number(equipment.unit_price) }, available_value: { increment: Number(equipment.unit_price) }, status: '可用' } }) if (equipmentUpdate.count !== 1) { throw new Error('资产库存已达到上限,请检查归还记录') } return { id: borrowRecord.id, person_id: borrowRecord.person_id, return_time: returnTime, status: '已归还' } }) } const mapBorrowRecord = (record: any) => ({ id: record.id, equipment_id: record.equipment_id, equipment_code: record.equipment_code, equipment_name: record.equipment?.equipment_name, storage_location_id: record.equipment?.storage_location_id ?? null, storageLocation: record.equipment?.storageLocation ?? null, person: record.person ? { id: record.person.id, nickname: record.person.nickname, real_name: record.person.real_name, department: record.person.department, phone: record.person.phone } : { id: record.person_id, nickname: record.person_nickname, real_name: record.person_name }, person_id: record.person_id, person_name: record.person_name, person_nickname: record.person_nickname, person_identifier: record.person_identifier, borrow_time: record.borrow_time, expected_return_time: record.expected_return_time, is_long_term: record.is_long_term, return_time: record.return_time, status: record.status, return_condition: record.return_condition, notes: record.notes, children_snapshot: record.children_snapshot }) // 借用资产 export const borrowEquipment = async (req: Request, res: Response) => { try { const { equipment_id, equipment_code, person_id, expected_return_time, notes, is_long_term } = req.body const equipmentIdNum = parseNumber(equipment_id) const equipment = await getEquipmentByIdentifier(equipmentIdNum, equipment_code) if (!equipment) { return errorResponse(res, '资产不存在', 404) } if (equipmentIdNum && equipment_code && equipment.equipment_code !== equipment_code) { return errorResponse(res, '资产编号与ID不匹配', 422) } let person try { person = await ensurePersonAvailable(parseNumber(person_id)) } catch (error: any) { return errorResponse(res, error.message || '使用者验证失败', 422) } try { ensureEquipmentBorrowable(equipment) } catch (error: any) { return errorResponse(res, error.message) } const longTerm = Boolean(is_long_term) const expectedReturn = longTerm ? null : parseDate(expected_return_time) const combinedNotes = appendLongTermNotes(notes, longTerm) const { children, snapshot } = await getChildrenSnapshot(equipment.id) const { record: borrowRecord } = await borrowEquipmentInTransaction( equipment, person, expectedReturn, combinedNotes, longTerm, snapshot ) successResponse( res, { id: borrowRecord.id, borrow_time: borrowRecord.borrow_time, children_count: children.length }, '借用成功' ) } catch (error: any) { console.error('[借用资产失败]', { error: error?.message || error, stack: error?.stack, body: req.body, user: res.locals.user }) errorResponse(res, error.message || '借用资产失败') } } // 扫码出库 export const scanBorrow = async (req: Request, res: Response) => { try { const { equipment_code, person_id, expected_return_time, notes, is_long_term } = req.body const equipment = await getEquipmentByIdentifier(undefined, equipment_code) if (!equipment) { return errorResponse(res, '资产不存在', 404) } let person try { person = await ensurePersonAvailable(parseNumber(person_id)) } catch (error: any) { return errorResponse(res, error.message || '使用者验证失败', 422) } try { ensureEquipmentBorrowable(equipment) } catch (error: any) { return errorResponse(res, error.message) } const longTerm = Boolean(is_long_term) const expectedReturn = longTerm ? null : parseDate(expected_return_time) const combinedNotes = appendLongTermNotes(notes, longTerm) const { children, snapshot } = await getChildrenSnapshot(equipment.id) const { record: borrowRecord, updatedEquipment } = await borrowEquipmentInTransaction( equipment, person, expectedReturn, combinedNotes, longTerm, snapshot ) successResponse( res, { id: borrowRecord.id, equipment_code: equipment.equipment_code, equipment_name: equipment.equipment_name, borrow_time: borrowRecord.borrow_time, status: '已借出', remaining_quantity: updatedEquipment?.remaining_quantity ?? 0, equipment_status: updatedEquipment?.status ?? '已借出', storage_location_id: equipment.storage_location_id, storageLocation: equipment.storageLocation, children_count: children.length }, '出库成功' ) } catch (error: any) { console.error('[扫码出库失败]', { error: error?.message || error, stack: error?.stack, body: req.body, user: res.locals.user }) errorResponse(res, error.message || '出库失败') } } // 批量扫码出库 export const scanBorrowBatch = async (req: Request, res: Response) => { try { const { equipment_codes, person_id, expected_return_time, notes, is_long_term } = req.body as { equipment_codes: string[] person_id: number expected_return_time?: string notes?: string is_long_term?: boolean } if (!Array.isArray(equipment_codes) || equipment_codes.length === 0) { return errorResponse(res, '资产编码列表不能为空', 422) } let person try { person = await ensurePersonAvailable(parseNumber(person_id)) } catch (error: any) { return errorResponse(res, error.message || '使用者验证失败', 422) } const longTerm = Boolean(is_long_term) const expectedReturn = longTerm ? null : parseDate(expected_return_time) const combinedNotes = appendLongTermNotes(notes, longTerm) const results: any[] = [] let successCount = 0 let failedCount = 0 for (const code of equipment_codes) { try { const equipment = await getEquipmentByIdentifier(undefined, code) if (!equipment) { results.push({ equipment_code: code, status: 'failed', message: '资产不存在' }) failedCount++ continue } try { ensureEquipmentBorrowable(equipment) } catch (err: any) { results.push({ equipment_code: code, status: 'failed', message: err.message }) failedCount++ continue } const { snapshot } = await getChildrenSnapshot(equipment.id) await borrowEquipmentInTransaction( equipment, person, expectedReturn, combinedNotes, longTerm, snapshot ) results.push({ equipment_code: code, status: 'success', message: '出库成功' }) successCount++ } catch (error: any) { results.push({ equipment_code: code, status: 'failed', message: error.message || '出库失败' }) failedCount++ } } successResponse( res, { success_count: successCount, failed_count: failedCount, results }, '批量出库完成' ) } catch (error: any) { console.error('[批量出库失败]', { error: error?.message || error, stack: error?.stack, body: req.body, user: res.locals.user }) errorResponse(res, error.message || '批量出库失败') } } // 归还资产 export const returnEquipment = async (req: Request, res: Response) => { try { const { equipment_id, equipment_code, borrow_record_id, person_id, return_condition = '正常', notes, confirm_children_diff } = req.body as Record const equipmentIdNum = parseNumber(equipment_id) const equipment = await getEquipmentByIdentifier(equipmentIdNum, equipment_code) if (!equipment) { return errorResponse(res, '资产不存在', 404) } let borrowRecord try { borrowRecord = await findActiveBorrowRecord( equipment.id, parseNumber(borrow_record_id), parseNumber(person_id) ) } catch (error: any) { return errorResponse(res, error.message || '未找到对应的借用记录', 422) } if (!borrowRecord) { return errorResponse(res, '未找到对应的借用记录') } const childrenDiff = await getChildrenDiff(equipment.id, borrowRecord) if (childrenDiff && !confirm_children_diff) { return successResponse(res, { need_confirm: true, children_diff: childrenDiff, message: '配件与借出时不一致,请确认后继续' }, '配件差异提醒') } const finalNotes = childrenDiff ? [notes, `配件变更:移除${childrenDiff.removed.length}个,新增${childrenDiff.added.length}个`] .filter(Boolean) .join(';') : notes await returnEquipmentInTransaction(equipment, borrowRecord, return_condition, finalNotes) successResponse( res, { id: borrowRecord.id, return_time: new Date(), children_diff: childrenDiff }, '归还成功' ) } catch (error: any) { console.error('[归还资产失败]', { error: error?.message || error, stack: error?.stack, body: req.body, user: res.locals.user }) errorResponse(res, error.message || '归还资产失败') } } // 扫码入库 export const scanReturn = async (req: Request, res: Response) => { try { const { equipment_code, return_condition = '正常', notes, person_id, borrow_record_id, confirm_children_diff } = req.body as Record const equipment = await getEquipmentByIdentifier(undefined, equipment_code) if (!equipment) { return errorResponse(res, '资产不存在', 404) } let borrowRecord = null try { borrowRecord = await findActiveBorrowRecord( equipment.id, parseNumber(borrow_record_id), parseNumber(person_id) ) } catch (error: any) { return errorResponse(res, error.message || '未找到对应的借用记录,请选择正确的使用者', 422) } if (!borrowRecord) { return errorResponse(res, '未找到对应的借用记录,请选择正确的使用者', 404) } const childrenDiff = await getChildrenDiff(equipment.id, borrowRecord) if (childrenDiff && !confirm_children_diff) { return successResponse(res, { need_confirm: true, children_diff: childrenDiff, message: '配件与借出时不一致,请确认后继续' }, '配件差异提醒') } const finalNotes = childrenDiff ? [notes, `配件变更:移除${childrenDiff.removed.length}个,新增${childrenDiff.added.length}个`] .filter(Boolean) .join(';') : notes const updatedRecord = await returnEquipmentInTransaction( equipment, borrowRecord, return_condition, finalNotes ) successResponse( res, { id: updatedRecord.id, equipment_code: equipment.equipment_code, equipment_name: equipment.equipment_name, return_time: updatedRecord.return_time, status: '已归还', person_id: updatedRecord.person_id }, '入库成功' ) } catch (error: any) { console.error('[扫码入库失败]', { error: error?.message || error, stack: error?.stack, body: req.body, user: res.locals.user }) errorResponse(res, error.message || '入库失败') } } // 批量扫码入库 export const scanReturnBatch = async (req: Request, res: Response) => { try { const { equipment_codes, return_condition = '正常', notes } = req.body as { equipment_codes: string[]; return_condition?: string; notes?: string } if (!Array.isArray(equipment_codes) || equipment_codes.length === 0) { return errorResponse(res, '资产编码列表不能为空', 422) } const results: any[] = [] let successCount = 0 let failedCount = 0 for (const code of equipment_codes) { try { const equipment = await getEquipmentByIdentifier(undefined, code) if (!equipment) { results.push({ equipment_code: code, status: 'failed', message: '资产不存在' }) failedCount++ continue } const activeBorrowRecords = await prisma.borrowRecord.findMany({ where: { equipment_id: equipment.id, status: '已借出' }, orderBy: { borrow_time: 'desc' }, take: 2 }) const borrowRecord = activeBorrowRecords[0] if (!borrowRecord) { results.push({ equipment_code: code, status: 'failed', message: '未找到对应的借用记录' }) failedCount++ continue } if (activeBorrowRecords.length > 1) { results.push({ equipment_code: code, status: 'failed', message: '存在多条未归还记录,请使用单个扫码归还' }) failedCount++ continue } const childrenDiff = await getChildrenDiff(equipment.id, borrowRecord) if (childrenDiff) { results.push({ equipment_code: code, status: 'failed', message: '配件与借出时不一致,请使用单个扫码归还确认' }) failedCount++ continue } await returnEquipmentInTransaction(equipment, borrowRecord, return_condition, notes) results.push({ equipment_code: code, status: 'success', message: '入库成功' }) successCount++ } catch (error: any) { results.push({ equipment_code: code, status: 'failed', message: error.message || '入库失败' }) failedCount++ } } successResponse( res, { success_count: successCount, failed_count: failedCount, results }, '批量入库完成' ) } catch (error: any) { console.error('[批量入库失败]', { error: error?.message || error, stack: error?.stack, body: req.body, user: res.locals.user }) errorResponse(res, error.message || '批量入库失败') } } // 根据资产编码查询借用记录 export const getBorrowRecordByCode = async (req: Request, res: Response) => { try { const { equipment_code } = req.params const equipment = await getEquipmentByIdentifier(undefined, equipment_code) if (!equipment) { return errorResponse(res, '资产不存在', 404) } const borrowRecords = await prisma.borrowRecord.findMany({ where: { equipment_id: equipment.id, status: '已借出' }, orderBy: { borrow_time: 'desc' }, include: { person: true, equipment: { select: { equipment_name: true, storage_location_id: true, storageLocation: { select: { id: true, name: true, code: true, status: true } } } } } }) if (borrowRecords.length === 0) { return errorResponse(res, '未找到对应的借用记录', 404) } successResponse( res, { equipment_code: equipment.equipment_code, equipment_name: equipment.equipment_name, unit_price: Number(equipment.unit_price), equipment_value: Number(equipment.unit_price), storage_location_id: equipment.storage_location_id, storageLocation: equipment.storageLocation, borrow_records: borrowRecords.map(mapBorrowRecord), can_return: true, message: '资产已借出,可以选择对应使用者归还' }, '查询成功' ) } catch (error: any) { errorResponse(res, error.message || '查询借用记录失败') } } // 获取借用记录列表 export const getBorrowRecordList = async (req: Request, res: Response) => { try { const { page = 1, pageSize = 10, status, person_name, person_id } = req.query const filters: any[] = [] if (status) { filters.push({ status }) } const personIdNum = parseNumber(person_id) if (personIdNum) { filters.push({ person_id: personIdNum }) } if (person_name) { const kw = person_name as string filters.push({ OR: [ { person_name: { contains: kw } }, { person_nickname: { contains: kw } }, { person_identifier: { contains: kw } } ] }) } const where = filters.length ? { AND: filters } : undefined const skip = (Number(page) - 1) * Number(pageSize) const [list, total] = await Promise.all([ prisma.borrowRecord.findMany({ where, skip, take: Number(pageSize), orderBy: { borrow_time: 'desc' }, include: { person: true, equipment: { select: { equipment_name: true, storage_location_id: true, storageLocation: { select: { id: true, name: true, code: true, status: true } } } } } }), prisma.borrowRecord.count({ where }) ]) successResponse( res, { list: list.map(mapBorrowRecord), total, page: Number(page), pageSize: Number(pageSize) }, '获取成功' ) } catch (error: any) { errorResponse(res, error.message || '获取借用记录列表失败') } }