Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion backend/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -68,4 +70,4 @@ process.on('SIGTERM', () => {

app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
});
161 changes: 161 additions & 0 deletions backend/src/routes/admin-loans.js
Original file line number Diff line number Diff line change
@@ -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;
4 changes: 3 additions & 1 deletion frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -44,6 +45,7 @@ function App() {
<Route path="/librarian-login" element={<LibrarianApp />} />
<Route path="/admin-login" element={<AdminLogin />} />
<Route path="/admin-dashboard" element={<AdminDashboard />} />
<Route path="/admin-returns" element={<AdminReturnBooks />} />
<Route path="/admin-logs" element={<SystemLogs />} />
<Route path="/announcements" element={<Announcements />} />
<Route path="/admin/announcements" element={<AdminAnnouncements />} />
Expand All @@ -70,4 +72,4 @@ function App() {
);
}

export default App;
export default App;
7 changes: 6 additions & 1 deletion frontend/src/pages/AdminDashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ function AdminDashboard() {
<p style={{ margin: '10px 0 0', opacity: 0.9 }}>查看系统操作日志</p>
</a>

<a href="/admin-returns" style={{ display: 'block', padding: '30px', background: '#10b981', color: 'white', borderRadius: '8px', textDecoration: 'none', textAlign: 'center' }}>
<h3 style={{ margin: 0, fontSize: '20px' }}>接收还书</h3>
<p style={{ margin: '10px 0 0', opacity: 0.9 }}>处理图书归还并恢复可借状态</p>
</a>

<a href="/announcements" style={{ display: 'block', padding: '30px', background: '#10b981', color: 'white', borderRadius: '8px', textDecoration: 'none', textAlign: 'center' }}>
<h3 style={{ margin: 0, fontSize: '20px' }}>公告查看</h3>
<p style={{ margin: '10px 0 0', opacity: 0.9 }}>查看所有公告</p>
Expand All @@ -34,4 +39,4 @@ function AdminDashboard() {
);
}

export default AdminDashboard;
export default AdminDashboard;
Loading