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() {
查看系统操作日志
+ +处理图书归还并恢复可借状态
+ +查看所有公告
@@ -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 ( +查看当前在借记录,并在收到图书后更新副本状态。
+| 借阅 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) ? '已逾期' : '借出中'} + + | ++ + | +