ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

JavaWeb图书管理系统课程设计:从E-R图到借还事务的完整实现

JavaWeb图书管理系统课程设计:从E-R图到借还事务的完整实现

简介:这份资源是面向JavaWeb初学者与课程设计者的图书管理系统完整项目文档,基于JavaWeb技术栈,结合HTML、CSS与SQL Server数据库,帮助读者理解信息管理系统的需求分析、功能设计与数据库建模全过程。压缩包内仅含1个docx文件,大小约12.82MB,以文档形式集中呈现源代码说明、文件目录截图与E-R关系图,便于对照阅读与整理。系统涵盖登录、查询、借还、管理四个子系统,并给出书籍与书库的存放关系、用户与书籍的借阅关系等概念设计,以及关系模式转换思路,适合作为数据库系统开发综合训练的参考案例。目前已有588人学习下载,读者可借此掌握从需求分析到功能模块划分、E-R图绘制与表结构设计的完整流程,快速搭建课程设计或毕业设计的文档框架。

1. 从手工台账到自动化流转:图书管理系统要解决的真实问题

某高校图书馆的借阅台前,管理员还在用纸质台账登记每一本借出的书,读者想查一本书是否在馆,得先翻卡片目录,再跑到书库逐排找。这个场景在课程设计里被反复提起,不是因为它古老,而是因为它把问题暴露得足够清楚:信息分散、状态不同步、超期靠人脑记。基于 JavaWeb 的图书管理系统要解决的正是这三件事——把书籍、读者、借阅记录收进同一套数据库,让借还状态实时更新,让超期规则由程序强制执行。这套方案适合正在做数据库系统开发综合训练的学生,也适合需要一套可演示、可答辩、结构完整的 JavaWeb 课设的开发者。它不追求高并发或分布式,而是把登录、查询、借还、管理四个子系统做扎实,把 E-R 图到关系模式的推导过程讲清楚,让读者能照着搭出自己的版本。

2. 需求拆解与角色划分:四个子系统如何各司其职

2.1 管理员与普通用户的权限边界

系统里有两类角色,边界必须在需求阶段就划死,否则后面写 Servlet 时会出现“这个页面到底谁能看”的反复拉扯。管理员拥有读者管理和图书管理的完整增删改查权限,可以按读者账号查借阅数量,按书号查书名、作者、价格。普通用户只能查书、借书、还书、看自己的借阅记录。这里有一个容易被忽略的约束:当用户存在超出还书期限的未还书籍时,系统必须拒绝新的借书请求。这个规则不是前端弹窗提醒,而是要在借书业务逻辑里做数据库查询判断,否则用户绕过页面直接调接口就能借走。

通常的做法是,在用户表里冗余一个“当前借阅数量”字段,同时在借阅记录表里保留每本书的借出日期和应还日期。借书前先查该用户是否有超期记录,有则直接返回失败,并给出具体是哪本书超期。这样管理员在后台也能一眼看到谁被限制了。

2.2 四个子系统的功能切分

登录子系统负责身份验证和会话保持,管理员和普通用户走同一个登录入口,但根据角色跳转到不同的主页。查询子系统对用户开放按书号、书名、作者的模糊查询,对管理员额外开放读者信息查询。借还子系统是核心业务,借书时校验库存和超期状态,还书时更新库存和借阅记录。管理子系统是管理员的后台,包括图书的增删改查、读者的增删改查、借阅历史的查看。

这四个子系统不是四个独立应用,而是同一套 Web 工程里的不同 Servlet 和 JSP 页面。我一般会把它们放在不同的包下,比如com.library.login、com.library.query、com.library.borrow、com.library.admin,每个包里的 Servlet 只处理自己那部分请求,共用同一个数据库连接工具类。这样后期改借书规则时,不会误伤查询逻辑。

2.3 功能层次图与模块职责

功能层次图在课程设计文档里是必画项,但画的时候容易变成一堆方框堆叠。更实用的画法是按“用户可见操作”来分层:第一层是登录和注册,第二层是查询、借阅、归还、我的图书馆,第三层是管理员后台的图书管理、读者管理、借阅查询。每一层对应一组 JSP 页面和 Servlet 映射。

比如“我的图书馆”页面,它背后要查两张表:借阅记录表和书籍表,把当前用户未归还的书连同借书时间、应还日期一起列出来。这个查询在借还子系统里实现,但页面入口放在用户主页。管理员后台的借阅查询则是另一套 SQL,按读者账号或书号筛选,显示所有历史记录。两者共用同一个借阅记录表,但查询条件和返回字段不同,不要为了省事写一个万能查询,否则后期加筛选条件会越改越乱。

3. 数据库概念设计:从 E-R 图到关系模式的完整推导

3.1 实体识别与属性确定

概念设计阶段要先把实体和属性定下来。这个系统里有四个实体:书籍、书库、普通用户、管理员。书籍的属性包括书号、书名、作者、价格、出版社;书库的属性包括书库编号、书库地址;普通用户的属性包括用户名、密码、性别、借书数量;管理员的属性包括管理员名、密码。

这里有一个设计决策:借阅记录要不要作为独立实体?在 E-R 图里,借阅是用户和书籍之间的多对多联系,联系本身可以带属性,比如借书时间、应还日期、归还状态。但到了关系模式阶段,多对多联系必须转成一张独立的表,所以借阅记录表是必然存在的。同理,书籍和书库之间是一对多联系,一本书只能存放在一个书库,一个书库可以放多本书,这个联系在关系模式里可以通过在书籍表里加一个书库号外键来实现,也可以单独建一张存放表。原文选择了单独建存放表,并带上“数量”属性,这样同一个书库可以存放同一本书的多个副本,更贴近真实图书馆的场景。

3.2 联系类型与 E-R 图绘制要点

“书籍—书库”是 1:n 联系,绘制时在书库端标 1,在书籍端标 n,联系名写“存放”。“用户—书籍”是 m:n 联系,两端都标 n 和 m,联系名写“借阅”。E-R 图里不要出现属性直接连属性的情况,所有属性必须挂在实体或联系上。比如借书时间挂在“借阅”联系上,而不是挂在用户或书籍上。

画 E-R 图时容易翻车的地方是主码标注。书籍的主码是书号,用户的主码是用户名,书库的主码是书库号。借阅联系转成表后,主码是(用户名,书号)的组合,同时用户名引用用户表,书号引用书籍表,两个都是外码。存放表的主码是(书号,书库号)的组合,书库号引用书库表,书号引用书籍表。这些约束在画图时就要想清楚,否则建表时会出现没有主键或者外键指向错误的情况。

3.3 关系模式转换与表结构定义

根据 E-R 图,转换出五张表:

  • 书籍(书号,书名,作者,价格,出版社)
  • 用户(用户名,性别,借阅数量,密码)
  • 书库(书库号,书库地址)
  • 借阅/归还(用户名,书号,借书时间,应还日期,归还状态)
  • 存放(书号,书库号,数量)

原文的关系模式里借阅表只写了(用户名,书号),实际开发中必须加上借书时间和应还日期,否则无法判断超期。归还状态可以用一个字段表示,比如 0 表示未还,1 表示已还。存放表里的数量字段用来记录该库房该书的库存,借书时减一,还书时加一。

建表时 SQL Server 的语法要注意自增列和主键的设置。书号如果是字符串类型,就不能设自增,需要手动输入或按规则生成。用户名的密码字段要留够长度,如果做加密存储,至少 64 位。下面是一段建表参考:

-- 书籍表 CREATE TABLE Book ( BookId VARCHAR(20) PRIMARY KEY, BookName NVARCHAR(100) NOT NULL, Author NVARCHAR(50), Price DECIMAL(10,2), Publisher NVARCHAR(100) ); -- 用户表 CREATE TABLE Users ( UserName VARCHAR(50) PRIMARY KEY, Gender CHAR(2), BorrowCount INT DEFAULT 0, Password VARCHAR(64) NOT NULL ); -- 借阅记录表 CREATE TABLE BorrowRecord ( UserName VARCHAR(50), BookId VARCHAR(20), BorrowDate DATETIME, DueDate DATETIME, ReturnStatus INT DEFAULT 0, PRIMARY KEY (UserName, BookId, BorrowDate), FOREIGN KEY (UserName) REFERENCES Users(UserName), FOREIGN KEY (BookId) REFERENCES Book(BookId) );

借阅记录表的主键用了(用户名,书号,借书时间)的组合,因为同一个用户可能多次借同一本书,只用用户名和书号做主键会导致第二次借阅插不进去。归还状态默认 0,还书时更新为 1。这个设计比原文的关系模式多了一个借书时间字段,是为了支持历史记录查询和超期判断。

4. 开发环境搭建与核心模块实现

4.1 工具链选型与项目结构

开发工具用 Eclipse,数据库用 SQL Server 2012,服务器用 Tomcat 7.0,运行在 Windows 10 上。这套组合在课程设计里很常见,版本不算新,但胜在稳定,资料多,遇到问题容易搜到解决方案。项目结构按标准 JavaWeb 工程来:src下放 Java 源码,WebContent下放 JSP、CSS、JS 和WEB-INF/web.xml。图片资源需要自己找,背景图、按钮图标这些不包含在源码里,但 CSS 里引用的路径要提前规划好,比如统一放在WebContent/images/下。

数据库连接建议写一个工具类,用 JDBC 直连,不要在每个 Servlet 里重复写驱动加载和连接代码。连接字符串里注意 SQL Server 的端口和实例名,如果用的是默认实例,可以写成jdbc:sqlserver://localhost:1433;databaseName=LibraryDB。用户名和密码不要硬编码在 Java 文件里,放到web.xml的<context-param>里,或者用一个db.properties文件加载。

4.2 登录与权限过滤器的实现

登录子系统不只是比对用户名和密码,还要在 Session 里存角色标识,后续每个受保护的页面都要检查这个标识。我一般会写一个AuthFilter,拦截/admin/*和/user/*路径,根据 Session 里的角色决定放行还是跳回登录页。

public class AuthFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; HttpSession session = request.getSession(false); String uri = request.getRequestURI(); // 管理员页面检查 if (uri.contains("/admin/")) { if (session == null || !"admin".equals(session.getAttribute("role"))) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } } // 普通用户页面检查 if (uri.contains("/user/")) { if (session == null || !"user".equals(session.getAttribute("role"))) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } } chain.doFilter(req, res); } }

过滤器的逻辑说明:先拿到当前请求的 URI,判断它属于管理员区域还是用户区域,然后检查 Session 里有没有对应的角色。如果没登录或者角色不对,直接重定向到登录页。这里用request.getSession(false)是为了避免自动创建新 Session,减少无效会话。web.xml里配置过滤器的url-pattern为/admin/*和/user/*,这样静态资源不会被拦截。

4.3 借书与还书的事务处理

借书操作涉及三张表的更新:借阅记录表插入一条新记录,存放表对应库存减一,用户表借阅数量加一。这三步必须在一个事务里完成,否则可能出现库存减了但借阅记录没插进去的情况。JDBC 默认是自动提交,需要手动关掉。

Connection conn = null; PreparedStatement ps1 = null, ps2 = null, ps3 = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 1. 检查是否有超期未还 String checkSql = "SELECT COUNT(*) FROM BorrowRecord WHERE UserName=? AND ReturnStatus=0 AND DueDate < GETDATE()"; ps1 = conn.prepareStatement(checkSql); ps1.setString(1, userName); ResultSet rs = ps1.executeQuery(); if (rs.next() && rs.getInt(1) > 0) { conn.rollback(); return "有超期未还书籍,无法借阅"; } // 2. 插入借阅记录 String borrowSql = "INSERT INTO BorrowRecord(UserName, BookId, BorrowDate, DueDate, ReturnStatus) VALUES(?,?,GETDATE(),DATEADD(DAY,30,GETDATE()),0)"; ps2 = conn.prepareStatement(borrowSql); ps2.setString(1, userName); ps2.setString(2, bookId); ps2.executeUpdate(); // 3. 更新库存和借阅数量 String updateSql = "UPDATE Storage SET Quantity=Quantity-1 WHERE BookId=? AND Quantity>0"; ps3 = conn.prepareStatement(updateSql); ps3.setString(1, bookId); int affected = ps3.executeUpdate(); if (affected == 0) { conn.rollback(); return "库存不足"; } conn.commit(); return "借阅成功"; } catch (Exception e) { if (conn != null) conn.rollback(); e.printStackTrace(); return "系统异常"; } finally { DBUtil.close(conn, ps1, ps2, ps3); }

这段代码的关键点:先查超期,再插记录,最后扣库存。扣库存时加了Quantity>0条件,防止库存扣成负数。还书操作反过来,更新借阅记录的归还状态为 1,库存加一,用户借阅数量减一。还书不需要检查超期,但需要判断这条借阅记录是否存在且未归还。

4.4 查询子系统的模糊查询与分页

用户查书时,输入书名的一部分也能搜到,这需要 SQL 的LIKE配合%。管理员查读者时,按账号精确查询即可。查询结果如果很多,要做分页,否则一个页面列出几百条记录,浏览器渲染会卡。

分页的 SQL Server 写法用OFFSET FETCH,比如每页 10 条,第 2 页的查询是ORDER BY BookId OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY。前端传页码和每页条数,后端拼 SQL 时注意参数化,不要把页码直接拼进字符串,防止 SQL 注入。模糊查询的参数用%拼接后传给PreparedStatement,比如ps.setString(1, "%" + keyword + "%")。

5. 避坑与常见问题排查

5.1 中文乱码从请求到响应逐层排查

现象:JSP 页面输入中文书名,提交后数据库里存的是问号,或者页面显示乱码。原因通常有三层:JSP 页面编码、请求编码、数据库编码。解决顺序是先在 JSP 顶部加<%@ page contentType="text/html;charset=UTF-8" %>,再在 Servlet 里request.setCharacterEncoding("UTF-8"),最后检查 SQL Server 的排序规则是否支持中文,建库时选Chinese_PRC_CI_AS。Tomcat 的server.xml里 Connector 加URIEncoding="UTF-8"也能解决 GET 请求的乱码。

5.2 数据库连接泄漏导致 Tomcat 启动后越来越慢

现象:系统跑一段时间后,页面响应变慢,最后报“Too many connections”。原因是在 Servlet 里拿了 Connection 但没有在 finally 里关闭,或者关闭顺序不对。解决方法是统一用工具类关闭,先关 ResultSet,再关 PreparedStatement,最后关 Connection,每个关闭操作单独 try-catch,避免一个异常导致后面的关不掉。如果用了连接池,要在context.xml里配置maxActive和maxIdle,并确保每次用完归还而不是关闭。

5.3 超期判断的日期边界与数据库时区

现象:用户明明在应还日期当天还书,系统却判定超期。原因是 SQL 里用了DueDate < GETDATE(),而GETDATE()包含时分秒,应还日期如果是当天 00:00:00,到当天下午还书时已经小于当前时间。解决方法是把应还日期存成当天的 23:59:59,或者判断时用CONVERT(DATE, DueDate) < CONVERT(DATE, GETDATE()),只比较日期部分。借书时设置应还日期用DATEADD(DAY, 30, GETDATE()),还书判断时统一转成日期比较。

5.4 库存扣减的并发问题

现象:两个用户同时借同一本书,库存显示还有 1 本,但两个人都借成功了,库存变成 -1。原因是先查库存再扣减,两步之间没有锁。解决方法是在 UPDATE 语句里直接加Quantity>0条件,用executeUpdate()的返回值判断是否扣减成功。如果返回 0,说明库存已经被别人扣完了,当前事务回滚。这个做法比在 Java 里加 synchronized 更可靠,因为锁的范围只在数据库层面,不依赖应用服务器的单实例。

5.5 JSP 页面引入 CSS 和图片路径错误

现象:登录页样式全丢,背景图不显示。原因是 JSP 被 Servlet 转发后,相对路径的基准变了。解决方法是在 JSP 里用<%=request.getContextPath()%>拼绝对路径,比如<link rel="stylesheet" href="<%=request.getContextPath()%>/css/style.css">。图片资源需要自己找,放在WebContent/images/下,CSS 里引用图片也用相对路径,但要注意 CSS 文件本身的位置,如果 CSS 在css/目录下,引用../images/bg.jpg才能找到。

6. 验收与答辩:怎么证明这套系统真的能跑

验收不是把项目跑起来点几下就完事,答辩老师通常会盯着几个关键点问:超期规则怎么实现的、事务有没有回滚、E-R 图到表结构的推导是否一致。我的习惯是提前准备一份验收清单,按功能模块逐条过,每条都留下可复现的操作路径和预期结果。

验收项操作路径预期结果检查点
登录鉴权未登录直接访问 /admin/bookList跳转登录页Session 为空时过滤器生效
超期拦截用户有超期书,尝试借新书提示“有超期未还书籍”借书事务回滚,库存不变
库存扣减两个浏览器同时借同一本书只有一个成功UPDATE 返回值为 0 时回滚
还书更新点击还书后查库存库存加一,借阅记录状态变 1事务提交后数据一致
模糊查询输入书名部分关键字返回匹配列表LIKE 参数化,无 SQL 注入
分页图书列表超过 10 条显示分页导航OFFSET FETCH 语法正确

答辩时如果被问到“为什么用 SQL Server 而不是 MySQL”,可以从课程要求或工具链熟悉度回答,不要贬低另一种数据库。如果被问到“系统有什么不足”,可以说没有做密码加密存储、没有做并发压力测试、前端没有用框架,这些都是可以后续改进的点,但当前版本已经覆盖了需求分析里的全部功能。

我一般会在答辩前把整个流程走三遍:第一遍用管理员账号走增删改查,第二遍用普通用户走借书还书和超期拦截,第三遍故意制造异常,比如借一本库存为 0 的书,看系统是否优雅报错而不是抛 500 页面。从那以后我每次交课程设计都强制走一遍这个清单,避免在演示时翻车。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进