diff --git a/backend/src/index.js b/backend/src/index.js index da7cfe3..ddd5a81 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -12,6 +12,7 @@ const authRouter = require('./routes/auth'); // 鉴权路由 const readersRouter = require('./routes/readers'); const readerBorrowRouter = require('./routes/reader-borrow'); const announcementsRouter = require('./routes/announcements'); +const adminLoansRouter = require('./routes/admin-loans'); const app = express(); const port = Number(process.env.PORT) || 3001; @@ -32,6 +33,7 @@ app.use('/api/books', booksRouter); app.use('/api/logs', logsRouter); app.use('/api/loans', loansRouter); // 你的借阅历史入口 app.use('/api/announcements', announcementsRouter); +app.use('/api/admin/loans', adminLoansRouter); app.use('/readers', readersRouter); app.use('/loans', loansRouter); app.use('/api/reader', readerBorrowRouter); @@ -68,4 +70,4 @@ process.on('SIGTERM', () => { app.listen(port, () => { console.log(`Server running on port ${port}`); -}); \ No newline at end of file +}); diff --git a/backend/src/routes/admin-loans.js b/backend/src/routes/admin-loans.js new file mode 100644 index 0000000..5729727 --- /dev/null +++ b/backend/src/routes/admin-loans.js @@ -0,0 +1,161 @@ +const express = require('express'); + +const prisma = require('../lib/prisma'); +const { requireAuth } = require('../middleware/auth'); + +const router = express.Router(); + +function requireAdmin(req, res, next) { + if (req.user?.role !== 'ADMIN') { + return res.status(403).json({ message: 'Access denied' }); + } + + return next(); +} + +async function calculateFine(tx, dueDate, returnDate) { + if (!returnDate || returnDate <= dueDate) { + return 0; + } + + const diffDays = Math.ceil((returnDate - dueDate) / (1000 * 60 * 60 * 24)); + const fineRateConfig = await tx.config.findUnique({ + where: { key: 'FINE_RATE_PER_DAY' }, + }); + const rate = fineRateConfig ? Number.parseFloat(fineRateConfig.value) : 0.5; + + return diffDays * rate; +} + +router.get('/active', requireAuth, requireAdmin, async (req, res, next) => { + try { + const loans = await prisma.loan.findMany({ + where: { + returnDate: null, + }, + select: { + id: true, + checkoutDate: true, + dueDate: true, + user: { + select: { + id: true, + name: true, + email: true, + studentId: true, + }, + }, + copy: { + select: { + id: true, + barcode: true, + status: true, + book: { + select: { + id: true, + title: true, + author: true, + isbn: true, + }, + }, + }, + }, + }, + orderBy: [ + { dueDate: 'asc' }, + { checkoutDate: 'desc' }, + ], + }); + + res.json({ loans }); + } catch (error) { + next(error); + } +}); + +router.post('/:loanId/receive-return', requireAuth, requireAdmin, async (req, res, next) => { + try { + const loanId = Number.parseInt(req.params.loanId, 10); + + if (Number.isNaN(loanId)) { + return res.status(400).json({ message: 'Invalid loan id' }); + } + + const loan = await prisma.loan.findUnique({ + where: { id: loanId }, + include: { + copy: { + include: { + book: { + select: { + title: true, + }, + }, + }, + }, + }, + }); + + if (!loan) { + return res.status(404).json({ message: 'Loan record not found' }); + } + + if (loan.returnDate !== null) { + return res.status(400).json({ message: 'Loan record already returned' }); + } + + const returnDate = new Date(); + const fine = await prisma.$transaction(async (tx) => { + const calculatedFine = await calculateFine(tx, loan.dueDate, returnDate); + const updatedLoan = await tx.loan.updateMany({ + where: { + id: loanId, + returnDate: null, + }, + data: { + returnDate, + fineAmount: calculatedFine, + finePaid: false, + }, + }); + + if (updatedLoan.count === 0) { + const error = new Error('Loan record already returned'); + error.statusCode = 400; + throw error; + } + + await tx.copy.update({ + where: { id: loan.copyId }, + data: { status: 'AVAILABLE' }, + }); + + await tx.auditLog.create({ + data: { + userId: req.user.id, + action: 'ADMIN_RECEIVE_RETURN', + entity: 'Loan', + entityId: loan.id, + detail: `Admin ${req.user.id} received return for loan ${loan.id}, copy ${loan.copyId}, book "${loan.copy.book.title}".`, + }, + }); + + return calculatedFine; + }); + + res.json({ + message: + fine > 0 + ? `Book returned late. Fine: ${fine}元` + : 'Book returned successfully', + loanId: loan.id, + returnDate, + fine, + copyStatus: 'AVAILABLE', + }); + } catch (error) { + next(error); + } +}); + +module.exports = router; diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 4997df3..6b8db0d 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -6,6 +6,7 @@ import Login from './pages/Login'; import Register from './pages/Register'; import AdminLogin from './pages/AdminLogin'; import AdminDashboard from './pages/AdminDashboard'; +import AdminReturnBooks from './pages/AdminReturnBooks'; import SystemLogs from './adminLogs/SystemLogs'; import LibrarianApp from './librarian/LibrarianApp'; import Announcements from './pages/Announcements'; @@ -44,6 +45,7 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> @@ -70,4 +72,4 @@ function App() { ); } -export default App; \ No newline at end of file +export default App; diff --git a/frontend/src/pages/AdminDashboard.jsx b/frontend/src/pages/AdminDashboard.jsx index 4362901..bf7507c 100644 --- a/frontend/src/pages/AdminDashboard.jsx +++ b/frontend/src/pages/AdminDashboard.jsx @@ -14,6 +14,11 @@ function AdminDashboard() {

查看系统操作日志

+ +

接收还书

+

处理图书归还并恢复可借状态

+
+

公告查看

查看所有公告

@@ -34,4 +39,4 @@ function AdminDashboard() { ); } -export default AdminDashboard; \ No newline at end of file +export default AdminDashboard; diff --git a/frontend/src/pages/AdminReturnBooks.jsx b/frontend/src/pages/AdminReturnBooks.jsx new file mode 100644 index 0000000..90a591d --- /dev/null +++ b/frontend/src/pages/AdminReturnBooks.jsx @@ -0,0 +1,226 @@ +import { useEffect, useState } from 'react'; + +function AdminReturnBooks() { + const [loans, setLoans] = useState([]); + const [search, setSearch] = useState(''); + const [loading, setLoading] = useState(true); + const [actionLoanId, setActionLoanId] = useState(null); + const [error, setError] = useState(''); + const [message, setMessage] = useState(''); + + const token = localStorage.getItem('adminToken') || localStorage.getItem('token'); + + useEffect(() => { + const storedUser = localStorage.getItem('user'); + + if (!token || !storedUser) { + window.location.href = '/admin-login'; + return; + } + + try { + const user = JSON.parse(storedUser); + + if (user.role !== 'ADMIN') { + window.location.href = '/admin-login'; + return; + } + } catch { + window.location.href = '/admin-login'; + return; + } + + fetchActiveLoans(); + }, []); + + const fetchActiveLoans = async () => { + try { + setLoading(true); + setError(''); + + const response = await fetch('http://localhost:3001/api/admin/loans/active', { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || '获取当前借阅记录失败'); + } + + setLoans(data.loans || []); + } catch (err) { + setError(err.message || '获取当前借阅记录失败'); + } finally { + setLoading(false); + } + }; + + const handleReceiveReturn = async (loanId) => { + const confirmed = window.confirm('确认接收该图书归还并恢复为可借状态吗?'); + + if (!confirmed) { + return; + } + + try { + setActionLoanId(loanId); + setError(''); + setMessage(''); + + const response = await fetch(`http://localhost:3001/api/admin/loans/${loanId}/receive-return`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + }, + }); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || '接收还书失败'); + } + + const fineMessage = data.fine > 0 ? `,罚款 ${data.fine} 元` : ''; + setMessage(`已归还,副本状态已更新为可借${fineMessage}。`); + await fetchActiveLoans(); + } catch (err) { + setError(err.message || '接收还书失败'); + } finally { + setActionLoanId(null); + } + }; + + const isOverdue = (dueDate) => new Date(dueDate) < new Date(); + + const filteredLoans = loans.filter((loan) => { + const keyword = search.trim().toLowerCase(); + + if (!keyword) { + return true; + } + + const fields = [ + String(loan.id), + loan.user?.name || '', + loan.user?.studentId || '', + loan.user?.email || '', + loan.copy?.book?.title || '', + loan.copy?.book?.isbn || '', + loan.copy?.barcode || '', + ]; + + return fields.some((value) => value.toLowerCase().includes(keyword)); + }); + + return ( +
+ + +
+ setSearch(e.target.value)} + style={{ width: '100%', maxWidth: '520px', padding: '10px 12px', border: '1px solid #d1d5db', borderRadius: '4px' }} + /> +
+ + {error &&
{error}
} + {message &&
{message}
} + + {loading ? ( +
加载中...
+ ) : ( + + + + + + + + + + + + + + + + + {filteredLoans.length === 0 ? ( + + + + ) : ( + filteredLoans.map((loan) => ( + + + + + + + + + + + + + )) + )} + +
借阅 ID学生学号图书名ISBN条码借出日期应还日期当前状态操作
+ 当前没有匹配的在借记录 +
{loan.id}{loan.user?.name || '-'}{loan.user?.studentId || '-'}{loan.copy?.book?.title || '-'}{loan.copy?.book?.isbn || '-'}{loan.copy?.barcode || '-'}{new Date(loan.checkoutDate).toLocaleDateString()}{new Date(loan.dueDate).toLocaleDateString()} + + {isOverdue(loan.dueDate) ? '已逾期' : '借出中'} + + + +
+ )} +
+ ); +} + +export default AdminReturnBooks;