From f4f2c00b9ba2111b5ad6d0912d85f92147efe725 Mon Sep 17 00:00:00 2001 From: xu-mengru <2062908709@qq.com> Date: Tue, 14 Apr 2026 23:43:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=80=9F=E9=98=85=E5=8E=86=E5=8F=B2?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E5=A2=9E=E5=8A=A0=E5=BA=93=E5=AD=98=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E9=83=A8=E5=88=86=EF=BC=8C=E5=90=88=E5=B9=B6=E5=9B=BE?= =?UTF-8?q?=E4=B9=A6=E6=90=9C=E7=B4=A2=E9=A1=B5=E9=9D=A2=E8=BF=9B=E5=85=A5?= =?UTF-8?q?=E8=AF=BB=E8=80=85=E7=AE=A1=E7=90=86=E7=9A=84=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E9=83=A8=E5=88=86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/prisma/prisma/dev.db | Bin 94208 -> 94208 bytes backend/src/routes/loans.js | 179 +++++++++++++----------------- frontend/src/App.jsx | 45 +++++--- frontend/src/reader/MyHistory.jsx | 33 ++++-- frontend/vite.config.js | 21 +++- 5 files changed, 146 insertions(+), 132 deletions(-) diff --git a/backend/prisma/prisma/dev.db b/backend/prisma/prisma/dev.db index cc34a11b895362e49ac2b1e521be210d44c34414..be2cdc320f2facb7a28910ca2af6c08101911ac8 100644 GIT binary patch delta 158 zcmZp8z}oPDb%Hcw#6%fq#)ypxDgKPCn~(b23-Gb>yD{+h@!#j)$DhmZwpmcYgkN2i zm79T)k(HH|gM))rmYs2K&yu5NjPul$d(D9CrN8^YB9@zT^QU~^l znzx-VfiZ!VzfoJ9mmyqNU9nPHos*GqdO{xKbPiTl#<^XrEdJYN(ilxSSQr=>)@%>W IV_eM$0Mte*)c^nh delta 89 zcmV-f0H*(d;01u-1&|v7RgoM+0adYJWKRJEv&Bz85DWwlZ2%9B58n^I4{fs%AYBi$ vZEuwD4FCWD6b2js4{{Fu4pFxbU;$tSk)W=(9A*JE2n2yC7Y(;nZUL(S { }); // 3. 馆员借出图书给学生 -router.post('/lend', requireAuth, async (req, res, next) => { - try { - if (req.user.role !== 'LIBRARIAN' && req.user.role !== 'ADMIN') { - return res.status(403).json({ message: 'Access denied. Librarian or Admin only.' }); - } - - 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: parseInt(userId) } - }); - if (!student || student.role !== 'STUDENT') { - return res.status(404).json({ message: 'Student not found' }); - } - - // 查询图书 - const book = await prisma.book.findUnique({ - where: { id: parseInt(bookId) } - }); - if (!book) { - return res.status(404).json({ message: 'Book not found' }); - } - if (book.availableCopies <= 0) { - return res.status(400).json({ message: 'No available copies of this book' }); - } - - // 检查是否重复借阅同一本未还 - const existingLoan = await prisma.loan.findFirst({ - where: { - userId: student.id, - bookId: book.id, - returnDate: null - } - }); - if (existingLoan) { - return res.status(400).json({ message: 'Student already borrowed this book and not returned' }); - } +// backend/src/routes/loans.js - // 检查学生资格:借阅数量限制 - const currentCount = await getCurrentBorrowCount(student.id); - if (currentCount >= MAX_BORROW_LIMIT) { - return res.status(400).json({ message: `Student has already borrowed ${MAX_BORROW_LIMIT} books. Cannot lend more.` }); - } - // 检查逾期 - const hasOverdue = await hasOverdueLoans(student.id); - if (hasOverdue) { - return res.status(400).json({ message: 'Student has overdue books. Please return them first.' }); - } - - // 创建借阅记录 - const checkoutDate = new Date(); - const dueDate = new Date(); - dueDate.setDate(dueDate.getDate() + LOAN_DURATION_DAYS); - - const loan = await prisma.loan.create({ - data: { - userId: student.id, - bookId: book.id, - checkoutDate, - dueDate, - fineAmount: 0, - finePaid: false, - fineForgiven: false - } - }); - - // 减少图书可借副本数 - await prisma.book.update({ - where: { id: book.id }, - data: { availableCopies: { decrement: 1 } } - }); +router.post('/lend', requireAuth, async (req, res, next) => { + const { userId, bookId } = req.body; - // 审计日志 - await prisma.auditLog.create({ - data: { - userId: req.user.id, - action: 'LEND_BOOK', - entity: 'Loan', - entityId: loan.id, - detail: `Librarian ${req.user.email} lent "${book.title}" to student ${student.email}. Due date: ${dueDate.toISOString()}` - } + try { + // 使用 Prisma 事务:要么全部成功,要么全部失败 + const result = await prisma.$transaction(async (tx) => { + + // 1. 检查库存 + const book = await tx.book.findUnique({ where: { id: parseInt(bookId) } }); + if (!book) throw new Error('找不到该书籍'); + if (book.availableCopies <= 0) throw new Error('库存不足,无法借阅'); + + // 2. 检查该学生是否已经借过这本书还没还 + const existing = await tx.loan.findFirst({ + where: { userId: parseInt(userId), bookId: parseInt(bookId), returnDate: null } + }); + if (existing) throw new Error('该学生已借阅此书且尚未归还'); + + // 3. 创建借书记录 + const loan = await tx.loan.create({ + data: { + userId: parseInt(userId), + bookId: parseInt(bookId), + checkoutDate: new Date(), + dueDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30天期 + } + }); + + // 4. 【核心】自动减少库存 + await tx.book.update({ + where: { id: parseInt(bookId) }, + data: { availableCopies: { decrement: 1 } } // 自动减 1 + }); + + return loan; }); - res.status(201).json({ - message: 'Book lent successfully', - loan: { - id: loan.id, - bookTitle: book.title, - studentName: student.name, - checkoutDate, - dueDate - } - }); + res.json({ message: '借书成功', loan: result }); } catch (error) { - next(error); + res.status(400).json({ message: error.message }); } }); @@ -221,37 +164,41 @@ router.post('/lend', requireAuth, async (req, res, next) => { // 4. 获取当前登录用户的个人借阅历史 +// backend/src/routes/loans.js + router.get('/my-history', requireAuth, async (req, res, next) => { try { - // 从 requireAuth 中间件获取当前用户的 ID - const userId = req.user.id; + const userId = req.user.id; // 从中间件获取当前登录用户ID const history = await prisma.loan.findMany({ where: { userId: userId, }, include: { + // 关键点:这里决定了返回的数据里包含哪些书籍信息 book: { select: { title: true, author: true, isbn: true, genre: true, + totalCopies: true, // 书籍总馆藏数 + availableCopies: true, // 书籍当前可借数 }, }, }, orderBy: { - checkoutDate: 'desc', // 按借出时间降序排列 + checkoutDate: 'desc', }, }); - // 处理一下数据,增加一个状态字段方便前端显示 + // 处理状态逻辑(已归还/借阅中/逾期) const processedHistory = history.map(loan => { - let status = 'ON_LOAN'; // 借阅中 + let status = 'ON_LOAN'; if (loan.returnDate) { - status = 'RETURNED'; // 已归还 + status = 'RETURNED'; } else if (new Date(loan.dueDate) < new Date()) { - status = 'OVERDUE'; // 已逾期 + status = 'OVERDUE'; } return { @@ -265,7 +212,37 @@ router.get('/my-history', requireAuth, async (req, res, next) => { next(error); } }); +// 5.归还书籍接口 +router.post('/return/:loanId', requireAuth, async (req, res, next) => { + const { loanId } = req.params; + try { + const result = await prisma.$transaction(async (tx) => { + + // 1. 查找这条借书记录 + const loan = await tx.loan.findUnique({ where: { id: parseInt(loanId) } }); + if (!loan) throw new Error('找不到借阅记录'); + if (loan.returnDate) throw new Error('此书已在之前归还'); + + // 2. 更新归还日期 + const updatedLoan = await tx.loan.update({ + where: { id: parseInt(loanId) }, + data: { returnDate: new Date() } + }); + + // 3. 【核心】自动恢复库存 + await tx.book.update({ + where: { id: loan.bookId }, + data: { availableCopies: { increment: 1 } } // 自动加 1 + }); + + return updatedLoan; + }); + res.json({ message: '归还成功,库存已恢复', loan: result }); + } catch (error) { + res.status(400).json({ message: error.message }); + } +}); module.exports = router; \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 1a5c562..5e1f0fa 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,8 +1,12 @@ +import { useState } from 'react'; import MyHistory from './reader/MyHistory'; -import BookSearch from './pages/BookSearch'; // 保留搜索组件 +import BookSearch from './pages/BookSearch'; // 队友的组件 import './App.css'; function App() { + // 定义一个状态,用来记录当前停留在哪个页面,默认显示你的 "history" + const [activeTab, setActiveTab] = useState('history'); + return (
@@ -15,26 +19,33 @@ function App() {
- 图书检索 - 我的借阅 - 个人中心 + {/* 点击时,把状态设置为 search */} + setActiveTab('search')} + className={`cursor-pointer transition-colors pb-1 ${activeTab === 'search' ? 'border-b-2 border-white' : 'hover:text-blue-200'}`} + > + 图书检索 + + + {/* 点击时,把状态设置为 history */} + setActiveTab('history')} + className={`cursor-pointer transition-colors pb-1 ${activeTab === 'history' ? 'border-b-2 border-white' : 'hover:text-blue-200'}`} + > + 我的借阅 + + + + 个人中心 +
- {/* 主体内容区域 - 把两个功能都放进去 */} -
- - {/* 1. 图书搜索功能 */} -
- -
- - {/* 2. 借阅历史功能 */} -
- -
- + {/* 主体内容区域:根据 activeTab 的值,决定显示哪个组件 */} +
+ {activeTab === 'search' && } + {activeTab === 'history' && }
{/* 页脚 */} diff --git a/frontend/src/reader/MyHistory.jsx b/frontend/src/reader/MyHistory.jsx index 473ce99..00f6927 100644 --- a/frontend/src/reader/MyHistory.jsx +++ b/frontend/src/reader/MyHistory.jsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -// 引入 shadcn/ui 组件 (确保之前运行过 npx shadcn@latest add table card badge) +// 引入 shadcn/ui 组件 (已修正路径并删除重复导入) import { Table, TableBody, @@ -7,9 +7,10 @@ import { TableHead, TableHeader, TableRow, -} from "@/components/ui/table"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; +} from "../components/ui/table"; +import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card"; +import { Badge } from "../components/ui/badge"; +// 补全漏掉的图标导入 import { Loader2, BookOpen, AlertCircle } from "lucide-react"; const MyHistory = () => { @@ -21,7 +22,6 @@ const MyHistory = () => { const fetchHistory = async () => { try { setLoading(true); - // 获取 Token (假设存放在 localStorage) const token = localStorage.getItem('token'); const response = await fetch('http://localhost:3001/api/loans/my-history', { @@ -100,7 +100,7 @@ const MyHistory = () => { - 图书详情 + 图书详情 借阅日期 应还期限 归还日期 @@ -112,9 +112,24 @@ const MyHistory = () => { loans.map((loan) => ( -
- {loan.book.title} - {loan.book.author} | ISBN: {loan.book.isbn} +
+ + {loan.book.title} + + + {loan.book.author} | ISBN: {loan.book.isbn} + + +
+ + 馆藏剩余: {loan.book.availableCopies ?? '加载中'} / {loan.book.totalCopies ?? '-'} + + {loan.book.availableCopies === 0 && ( + + 已罄 + + )} +
diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 9930e28..3caf3f9 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -1,10 +1,21 @@ +// frontend/vite.config.js import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' -import tailwindcss from '@tailwindcss/vite' +// 新增:引入 node 的 path 模块 +import path from 'path' +import { fileURLToPath } from 'url' +// "type": "module",需要这样获取 __dirname +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +// https://vite.dev/config/ export default defineConfig({ - plugins: [ - react(), - tailwindcss(), - ], + plugins: [react()], + // --- 新增:配置路径别名 --- + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, }) \ No newline at end of file