From 6809e9fb462fcc171fa707186c470cac42da4a2a Mon Sep 17 00:00:00 2001 From: aaddaad Date: Wed, 15 Apr 2026 20:45:34 +0800 Subject: [PATCH 1/2] Fix librarian search routes and audit log; add seed librarian records --- backend/prisma/seed.js | 17 ++++ backend/src/routes/loans.js | 196 ++++++++++++++++++++++++++++++++++++ 2 files changed, 213 insertions(+) diff --git a/backend/prisma/seed.js b/backend/prisma/seed.js index c397c37..77e61f6 100644 --- a/backend/prisma/seed.js +++ b/backend/prisma/seed.js @@ -11,6 +11,7 @@ async function main() { await prisma.loan.deleteMany(); await prisma.book.deleteMany(); await prisma.user.deleteMany(); + await prisma.librarian.deleteMany(); await prisma.config.deleteMany(); // 创建用户 @@ -36,6 +37,22 @@ async function main() { }, }); + await prisma.librarian.create({ + data: { + employeeId: 'lib001', + name: '馆员张三', + password: librarianPassword, + }, + }); + + await prisma.librarian.create({ + data: { + employeeId: 'lib002', + name: '馆员李四', + password: librarianPassword, + }, + }); + await prisma.user.create({ data: { name: 'Student One', diff --git a/backend/src/routes/loans.js b/backend/src/routes/loans.js index 26b51e9..99b5cf2 100644 --- a/backend/src/routes/loans.js +++ b/backend/src/routes/loans.js @@ -1,6 +1,7 @@ const express = require('express'); const prisma = require('../lib/prisma'); const { requireAuth } = require('../middleware/auth'); +const { requireLibrarianAuth } = require('../middleware/librarianAuth'); const router = express.Router(); @@ -285,4 +286,199 @@ router.post('/admin/force-borrow/:bookId/:userId', requireAuth, async (req, res, } }); +// 6. 馆员搜索学生 +router.get('/users/search', requireLibrarianAuth, async (req, res, next) => { + try { + const keyword = (req.query.keyword || '').trim(); + if (!keyword) { + return res.status(400).json({ message: 'keyword is required' }); + } + + const students = await prisma.user.findMany({ + where: { + role: 'STUDENT', + OR: [ + { studentId: { contains: keyword } }, + { email: { contains: keyword } }, + { name: { contains: keyword } } + ] + } + }); + + const usersWithStats = await Promise.all(students.map(async (student) => { + const currentBorrowCount = await prisma.loan.count({ + where: { userId: student.id, returnDate: null } + }); + const overdueLoans = await prisma.loan.count({ + where: { + userId: student.id, + returnDate: null, + dueDate: { lt: new Date() } + } + }); + return { + ...student, + currentBorrowCount, + hasOverdue: overdueLoans > 0 + }; + })); + + res.json({ users: usersWithStats }); + } catch (error) { + next(error); + } +}); + +// 7. 馆员搜索图书 +router.get('/books/search', requireLibrarianAuth, async (req, res, next) => { + try { + const keyword = (req.query.keyword || '').trim(); + if (!keyword) { + return res.status(400).json({ message: 'keyword is required' }); + } + + const books = await prisma.book.findMany({ + where: { + OR: [ + { title: { contains: keyword } }, + { isbn: { contains: keyword } }, + { author: { contains: keyword } } + ] + } + }); + + res.json({ books }); + } catch (error) { + next(error); + } +}); + +// 8. 馆员获取当前借阅记录 +router.get('/records', requireLibrarianAuth, async (req, res, next) => { + try { + const loans = await prisma.loan.findMany({ + where: { returnDate: null }, + include: { user: true, book: true }, + orderBy: { checkoutDate: 'desc' } + }); + res.json({ loans }); + } catch (error) { + next(error); + } +}); + +// 9. 馆员借书给学生 +router.post('/lend', requireLibrarianAuth, async (req, res, next) => { + try { + const { userId, bookId } = req.body; + if (!userId || !bookId) { + return res.status(400).json({ message: 'userId and bookId are required' }); + } + + const student = await prisma.user.findUnique({ where: { id: Number(userId) } }); + if (!student || student.role !== 'STUDENT') { + return res.status(404).json({ message: 'Student not found' }); + } + + const book = await prisma.book.findUnique({ where: { id: Number(bookId) } }); + if (!book || book.availableCopies <= 0) { + return res.status(400).json({ message: 'Book not available' }); + } + + const existingLoan = await prisma.loan.findFirst({ + where: { + userId: Number(userId), + bookId: Number(bookId), + returnDate: null + } + }); + if (existingLoan) { + return res.status(400).json({ message: 'Student already borrowed this book' }); + } + + const checkoutDate = new Date(); + const dueDate = new Date(); + dueDate.setDate(dueDate.getDate() + LOAN_DURATION_DAYS); + + const loan = await prisma.loan.create({ + data: { + userId: Number(userId), + bookId: Number(bookId), + checkoutDate, + dueDate, + fineAmount: 0, + finePaid: false, + fineForgiven: false + } + }); + + await prisma.book.update({ + where: { id: Number(bookId) }, + data: { availableCopies: { decrement: 1 } } + }); + + await prisma.auditLog.create({ + data: { + userId: null, + action: 'ADMIN_BORROW', + entity: 'Loan', + entityId: loan.id, + detail: `Librarian ${req.librarian.id} lent book ${bookId} to user ${userId}` + } + }); + + res.status(201).json({ message: 'Borrow successful', loan }); + } catch (error) { + next(error); + } +}); + +// 10. 馆员还书 +router.post('/return', requireLibrarianAuth, async (req, res, next) => { + try { + const { loanId } = req.body; + if (!loanId) { + return res.status(400).json({ message: 'loanId is required' }); + } + + const loan = await prisma.loan.findUnique({ + where: { id: Number(loanId) }, + include: { book: true } + }); + if (!loan) { + return res.status(404).json({ message: 'Loan record not found' }); + } + if (loan.returnDate !== null) { + return res.status(400).json({ message: 'Book already returned' }); + } + + const returnDate = new Date(); + const fine = returnDate > loan.dueDate ? await calculateFine(loan.dueDate, returnDate) : 0; + + await prisma.loan.update({ + where: { id: Number(loanId) }, + data: { returnDate, fineAmount: fine, finePaid: false } + }); + + await prisma.book.update({ + where: { id: loan.bookId }, + data: { availableCopies: { increment: 1 } } + }); + + await prisma.auditLog.create({ + data: { + userId: null, + action: 'ADMIN_RETURN', + entity: 'Loan', + entityId: loan.id, + detail: `Librarian ${req.librarian.id} returned book ${loan.bookId} for user ${loan.userId}` + } + }); + + res.json({ message: fine > 0 ? `Book returned late. Fine: ${fine}元` : 'Book returned successfully', fine, returnDate }); + } catch (error) { + next(error); + } +}); + module.exports = router; \ No newline at end of file From e5e294068d7429f37d18dd69c28821366485ce2b Mon Sep 17 00:00:00 2001 From: mo <26786440642qq.com> Date: Wed, 15 Apr 2026 20:57:41 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=91=98=E8=BF=98=E4=B9=A6=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/librarian/LibrarianApp.jsx | 4 +- frontend/src/librarian/ReturnBook.jsx | 176 ++++++++++++++++++++++++ 2 files changed, 178 insertions(+), 2 deletions(-) create mode 100644 frontend/src/librarian/ReturnBook.jsx diff --git a/frontend/src/librarian/LibrarianApp.jsx b/frontend/src/librarian/LibrarianApp.jsx index 985db14..b931aa9 100644 --- a/frontend/src/librarian/LibrarianApp.jsx +++ b/frontend/src/librarian/LibrarianApp.jsx @@ -2,12 +2,12 @@ import { useState, useEffect } from 'react' import LibrarianLogin from './LibrarianLogin' import LibrarianRegister from './LibrarianRegister' import LibrarianDashboard from './LibrarianDashboard' - +import ReturnBook from './ReturnBook' function LibrarianApp() { const [isLoggedIn, setIsLoggedIn] = useState(false) const [librarian, setLibrarian] = useState(null) const [showRegister, setShowRegister] = useState(false) - + const [currentPage, setCurrentPage] = useState('dashboard') useEffect(() => { const token = localStorage.getItem('librarianToken') const savedLibrarian = localStorage.getItem('librarianInfo') diff --git a/frontend/src/librarian/ReturnBook.jsx b/frontend/src/librarian/ReturnBook.jsx new file mode 100644 index 0000000..14c25d2 --- /dev/null +++ b/frontend/src/librarian/ReturnBook.jsx @@ -0,0 +1,176 @@ +import { useState, useEffect } from 'react' + +const API_URL = 'http://localhost:3001/api' + +export default function ReturnBook() { + // 1. 完全复用你原来的状态结构 + const [userId, setUserId] = useState('') + const [bookName, setBookName] = useState('') + const [loading, setLoading] = useState(false) + const [error, setError] = useState('') + const [message, setMessage] = useState('') + const [fieldErrors, setFieldErrors] = useState({}) + + // 2. 验证逻辑 + const validateUserId = (value) => { + if (!value.trim()) { + setFieldErrors(prev => ({ ...prev, userId: '请输入读者ID' })) + return false + } + setFieldErrors(prev => ({ ...prev, userId: '' })) + return true + } + + const validateBookName = (value) => { + if (!value.trim()) { + setFieldErrors(prev => ({ ...prev, bookName: '请输入书籍名称' })) + return false + } + setFieldErrors(prev => ({ ...prev, bookName: '' })) + return true + } + + // 3. 提交还书 + const handleSubmit = async (e) => { + e.preventDefault() + + const isUserIdValid = validateUserId(userId) + const isBookNameValid = validateBookName(bookName) + + if (!isUserIdValid || !isBookNameValid) { + setError('请填写完整信息') + return + } + + setError('') + setMessage('') + setLoading(true) + + try { + const token = localStorage.getItem('librarianToken') + + const res = await fetch(`${API_URL}/loans/return-by-user-book`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ + userId: userId.trim(), + bookName: bookName.trim() + }) + }) + + const data = await res.json() + + if (!res.ok) { + setError(data.error || '还书失败,请检查信息是否正确') + setLoading(false) + return + } + + // 还书成功:更新书籍状态为可借阅 + setMessage('还书成功!书籍状态已更新为【可借阅】') + setUserId('') + setBookName('') + setLoading(false) + } catch (err) { + setError('网络错误,请确保后端已启动') + setLoading(false) + } + } + + + return ( +
+
+
+

图书归还

+

输入读者ID与书籍名称办理还书

+
+ +
+ {/* 读者ID */} +
+ + { + setUserId(e.target.value) + validateUserId(e.target.value) + }} + disabled={loading} + autoFocus + /> + {fieldErrors.userId && ( +

{fieldErrors.userId}

+ )} +
+ + {/* 书籍名称 */} +
+ + { + setBookName(e.target.value) + validateBookName(e.target.value) + }} + disabled={loading} + /> + {fieldErrors.bookName && ( +

{fieldErrors.bookName}

+ )} +
+ + {/* 错误提示 */} + {error && ( +
+ {error} +
+ )} + + {/* 成功提示 */} + {message && ( +
+ {message} +
+ )} + + {/* 登录按钮 */} + +
+
+
+ ) +} \ No newline at end of file