GuDingZiChan/backend/src/controllers/borrowController.ts

888 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<string, any>
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<string, any>
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 || '获取借用记录列表失败')
}
}