ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

共享图书管理系统实战:JSP+Spring+Oracle 11g 从设计到部署

共享图书管理系统实战:JSP+Spring+Oracle 11g 从设计到部署 简介这份共享图书管理系统设计与实现文档面向计算机相关专业学生与Java Web开发者可作为课程设计、毕业设计或技术练手的完整参考方案。文档围绕图书馆资源利用率低、个性化服务不足等现实问题提出了一套结合智能推荐与多平台访问的管理系统设计思路。资源包共1个docx文件大小约13.15MB内容涵盖绪论、相关理论与技术、系统需求分析等章节涉及面向对象、I/O等基础理论以及JSON、JDBC、JSP、Ajax、Tomcat、Eclipse、Oracle 11g等开发技术与工具并给出模块化功能设计原则与总体功能模块划分。目前已有109人学习适合需要梳理系统开发流程、撰写设计文档或搭建Java Web项目框架的读者参考借鉴。1. 共享图书管理系统从书荒到流转一套 JSPSpring 的落地拆解宿舍楼里总有那么几本书买的时候雄心壮志翻了三页就再也没动过。另一边总有人为了找一本绝版教材跑遍整条街的二手书店。共享图书管理系统要解决的就是这个错配——把闲置的书从一个人的书架搬到另一个人的手里中间不靠微信群喊话不靠 Excel 登记而是靠一套能查、能借、能还、能追责的 Web 系统。这个标题背后是典型的 Java Web 课程设计或中小型公益借阅平台的落地需求技术栈锁定在 JSP Spring Oracle 11g Ajax 这套经典组合上。适合谁看正在做毕设、课设或者要给社区图书馆搭一套轻量管理后台的开发者。下面从选型理由一路讲到能跑通的代码和参数中间踩过的坑一并摊开。2. 为什么是 JSP Spring Oracle 11g选型不是拍脑袋2.1 这套技术栈在共享图书场景下的真实定位共享图书管理系统的核心业务其实不复杂图书录入、用户注册登录、借阅申请、归还确认、逾期提醒、图书状态流转。业务简单意味着不需要上微服务不需要分布式事务一个单体 Web 应用足够扛住几百到几千人的社区或校园场景。JSP 负责页面渲染Spring 负责 IoC 和事务管理Oracle 11g 负责数据持久化Ajax 负责局部刷新和异步交互——四者各司其职没有过度设计。有人会问为什么不用 Spring Boot MyBatis 那套更时髦的组合。答案很直接如果目标环境是学校机房或者老旧服务器Oracle 11g 的驱动和 JSP 容器比如 Tomcat 7/8配合更稳Spring 3.x 或 4.x 的 XML 配置虽然啰嗦但出问题好定位。Spring Boot 的自动配置在这种环境里反而容易因为版本冲突翻车。我一般会建议如果是从零开始的新项目Spring Boot MyBatis 确实更省事但如果是维护老系统或者课程设计有明确技术栈要求JSP Spring Oracle 11g 这套组合的确定性更高。Oracle 11g 在这个场景里的优势是 PL/SQL 写存储过程和触发器方便比如借阅记录的自动逾期计算一个触发器就能搞定不用在 Java 层写定时任务。缺点是安装包大、资源占用高本地开发建议用 Docker 或者轻量版生产环境再上完整版。2.2 数据库表结构设计与 Oracle 11g 的字段类型选择共享图书管理系统的表不用多五张核心表就能撑起来用户表、图书表、借阅记录表、分类表、逾期日志表。下面直接给建表 SQL跑在 Oracle 11g 上。-- 用户表存基本信息和角色 CREATE TABLE lib_user ( user_id NUMBER(10) PRIMARY KEY, user_name VARCHAR2(50) NOT NULL, password VARCHAR2(64) NOT NULL, -- 存 SHA-256 哈希不存明文 role NUMBER(1) DEFAULT 0, -- 0 普通用户1 管理员 create_time DATE DEFAULT SYSDATE ); -- 图书表核心字段是状态和库存 CREATE TABLE lib_book ( book_id NUMBER(10) PRIMARY KEY, book_name VARCHAR2(200) NOT NULL, author VARCHAR2(100), isbn VARCHAR2(20), category_id NUMBER(10), total_count NUMBER(5) DEFAULT 1, -- 总副本数 avail_count NUMBER(5) DEFAULT 1, -- 可借副本数 cover_url VARCHAR2(500), -- 封面图路径 status NUMBER(1) DEFAULT 1 -- 1 上架0 下架 ); -- 借阅记录表一条记录对应一次借还周期 CREATE TABLE lib_borrow ( borrow_id NUMBER(10) PRIMARY KEY, user_id NUMBER(10) NOT NULL, book_id NUMBER(10) NOT NULL, borrow_date DATE DEFAULT SYSDATE, due_date DATE NOT NULL, -- 应还日期借出时算好 return_date DATE, -- 实际归还日期未还为 NULL status NUMBER(1) DEFAULT 0 -- 0 借出中1 已归还2 逾期 ); -- 分类表 CREATE TABLE lib_category ( category_id NUMBER(10) PRIMARY KEY, category_name VARCHAR2(50) NOT NULL ); -- 逾期日志记录每次逾期扣分或罚款 CREATE TABLE lib_overdue_log ( log_id NUMBER(10) PRIMARY KEY, borrow_id NUMBER(10) NOT NULL, overdue_day NUMBER(5), fine_amount NUMBER(10,2), create_time DATE DEFAULT SYSDATE );字段类型选择上有几个点值得说。VARCHAR2是 Oracle 特有的别写成VARCHAR虽然 Oracle 兼容但语义上不推荐。DATE类型存到秒够用如果要做精确到毫秒的日志换TIMESTAMP。NUMBER(10)做主键配合序列使用别用自增Oracle 11g 没有 MySQL 那种AUTO_INCREMENT。序列和触发器要配套建否则插入数据时主键会报空。CREATE SEQUENCE seq_lib_book START WITH 1 INCREMENT BY 1 NOCACHE; CREATE OR REPLACE TRIGGER trg_lib_book_id BEFORE INSERT ON lib_book FOR EACH ROW BEGIN IF :NEW.book_id IS NULL THEN SELECT seq_lib_book.NEXTVAL INTO :NEW.book_id FROM dual; END IF; END; /NOCACHE是为了避免序列跳号课程设计里老师可能会检查连续性。生产环境用CACHE 20性能更好但会出现断号看场景取舍。2.3 Spring 的 XML 配置与事务边界怎么划Spring 在这套系统里主要干三件事管理 Bean、声明式事务、整合 JDBC。用 XML 配置虽然老派但结构清晰适合教学和排查。核心配置文件applicationContext.xml里数据源、事务管理器、DAO 和 Service 的 Bean 都要显式声明。!-- 数据源Oracle 11g 驱动 -- bean iddataSource classorg.apache.commons.dbcp.BasicDataSource property namedriverClassName valueoracle.jdbc.driver.OracleDriver/ property nameurl valuejdbc:oracle:thin:localhost:1521:orcl/ property nameusername valuelib_user/ property namepassword valuelib_pass/ property nameinitialSize value5/ property namemaxActive value20/ /bean !-- 事务管理器 -- bean idtxManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean !-- 开启注解事务 -- tx:annotation-driven transaction-managertxManager/事务边界划在 Service 层借书和还书这两个操作必须在一个事务里完成更新图书可借数量、插入借阅记录、更新用户积分三步要么全成要么全回滚。用Transactional注解在 Service 方法上别在 Controller 上开事务否则事务范围太大数据库连接占用时间过长。Service public class BorrowService { Autowired private BookDao bookDao; Autowired private BorrowDao borrowDao; Transactional(rollbackFor Exception.class) public void borrowBook(Long userId, Long bookId) { // 先扣库存用乐观锁防止超借 int updated bookDao.reduceAvailCount(bookId); if (updated 0) { throw new RuntimeException(库存不足借阅失败); } // 再插借阅记录 borrowDao.insertBorrow(userId, bookId); } }rollbackFor Exception.class是关键默认 Spring 只对RuntimeException回滚受检异常不回滚。借书失败抛的是运行时异常但万一以后改成受检异常不加这个配置就会留下脏数据。3. 从零跑通借阅流程JSP 页面、Ajax 交互与后端接口3.1 JSP 图书列表页与 Ajax 异步加载图书列表页是用户进入系统后看到的第一个页面传统做法是 JSP 里嵌 Java 代码查数据库然后循环输出。但这样每次翻页、筛选都要刷新整个页面体验差。用 Ajax 异步拉数据JSP 只负责渲染骨架数据通过 JavaScript 动态填充。% page contentTypetext/html;charsetUTF-8 languagejava % html head title共享图书列表/title script srcjs/jquery-3.6.0.min.js/script /head body div idbookList/div div idpagination/div script $(function(){ loadBooks(1); }); function loadBooks(pageNum) { $.ajax({ url: book/list.do, type: POST, data: { page: pageNum, size: 10 }, dataType: json, success: function(resp) { if (resp.code 200) { renderBooks(resp.data.list); } else { alert(加载失败 resp.msg); } }, error: function(xhr, status, error) { console.error(Ajax 请求异常, status, error); } }); } function renderBooks(list) { var html ; for (var i 0; i list.length; i) { var book list[i]; html div classbook-item; html img src book.coverUrl width80/; html h3 book.bookName /h3; html p可借 book.availCount / book.totalCount /p; html button onclickborrow( book.bookId )借阅/button; html /div; } $(#bookList).html(html); } /script /body /html这段代码里dataType: json告诉 jQuery 期望返回 JSON后端必须设置Content-Type: application/json;charsetUTF-8否则中文会乱码。data里的参数会自动拼成page1size10的格式发给后端。error回调里打印状态码和错误信息方便排查是 404 还是 500。Ajax 请求参数赋值时注意如果参数值包含特殊字符比如书名里的要用encodeURIComponent包一层否则后端接收到的参数会被截断。3.2 后端接口Spring Controller 接收 Ajax 请求并返回 JSONController 层用ResponseBody把返回值序列化成 JSON配合 Jackson 或 Fastjson。下面是一个完整的图书列表接口。Controller RequestMapping(/book) public class BookController { Autowired private BookService bookService; RequestMapping(value /list.do, method RequestMethod.POST) ResponseBody public MapString, Object list( RequestParam(value page, defaultValue 1) int page, RequestParam(value size, defaultValue 10) int size) { MapString, Object result new HashMap(); try { PageInfoBook pageInfo bookService.queryByPage(page, size); result.put(code, 200); result.put(data, pageInfo); } catch (Exception e) { result.put(code, 500); result.put(msg, 查询异常 e.getMessage()); } return result; } }RequestParam的defaultValue保证前端不传参时也有默认值避免NullPointerException。分页查询在 Oracle 11g 里不能用LIMIT要用ROWNUM嵌套子查询。SELECT * FROM ( SELECT t.*, ROWNUM rn FROM ( SELECT * FROM lib_book WHERE status 1 ORDER BY book_id ) t WHERE ROWNUM #{end} ) WHERE rn #{start}#{start}和#{end}是 MyBatis 或 JdbcTemplate 的参数占位符start (page-1)*sizeend page*size。Oracle 11g 的分页写法比较绕但这是标准做法别想着用OFFSET FETCH那是 12c 才支持的。3.3 借阅操作的完整链路与并发控制借阅操作是系统里唯一需要严格保证数据一致性的地方。用户点“借阅”按钮前端发 Ajax 请求后端在事务里扣库存、插记录。并发场景下两个人同时借同一本书的最后一本必须保证只有一个人成功。// BookDao 里的扣库存方法用乐观锁 public int reduceAvailCount(Long bookId) { String sql UPDATE lib_book SET avail_count avail_count - 1 WHERE book_id ? AND avail_count 0; return jdbcTemplate.update(sql, bookId); }WHERE avail_count 0是乐观锁的核心数据库层面保证不会扣成负数。返回的updated为 0 说明库存已经被抢完Service 层直接抛异常回滚。这种写法比SELECT ... FOR UPDATE轻量适合读多写少的场景。借阅成功后前端要刷新列表页的库存显示不能只弹一个“借阅成功”就完事。我一般会在 Ajax 的success回调里重新调一次loadBooks(currentPage)让用户看到最新的可借数量。4. 部署与联调中容易翻车的五个坑4.1 坑一Oracle 11g 驱动版本与 JDK 不匹配现象Tomcat 启动时报NoClassDefFoundError: oracle/jdbc/driver/OracleDriver或者连接数据库时抛UnsupportedClassVersionError。原因Oracle 11g 的 JDBC 驱动分ojdbc6.jar和ojdbc14.jar前者对应 JDK 6后者对应 JDK 1.4。如果项目用 JDK 8 编译却放了ojdbc14.jar类加载时就会因为字节码版本不兼容而失败。解决JDK 8 环境统一用ojdbc6.jar放在WEB-INF/lib下不要放在 Tomcat 的lib目录避免多个项目冲突。Maven 项目里加依赖时注意groupId是com.oracleartifactId是ojdbc6但 Oracle 的驱动没有在中央仓库需要手动install到本地仓库或者用系统作用域。4.2 坑二Ajax 请求中文参数乱码现象前端传bookNameJava编程思想后端request.getParameter(bookName)拿到的是Java?????。原因Tomcat 默认用 ISO-8859-1 解码 URL 参数而前端用 UTF-8 编码。POST 请求和 GET 请求的乱码原因还不一样。解决POST 请求在web.xml里配CharacterEncodingFilter强制request.setCharacterEncoding(UTF-8)。GET 请求改 Tomcat 的server.xml在Connector标签里加URIEncodingUTF-8。Ajax 请求头里显式设置contentType: application/x-www-form-urlencoded;charsetUTF-8三管齐下才能根治。4.3 坑三JSP 页面图片路径对不上现象图书封面图在列表页显示为裂图F12 看 Network 里图片请求返回 404。原因JSP 里写的srcimages/cover.jpg是相对路径相对于当前页面 URL而不是项目根路径。如果当前页面是/book/list.do浏览器会去请求/book/images/cover.jpg自然找不到。解决用%request.getContextPath()%拼绝对路径写成src%request.getContextPath()%/images/cover.jpg。或者用c:url标签更干净。图片文件放在webapp/images目录下不要放在WEB-INF里面否则外部访问不到。4.4 坑四Spring 事务不生效现象借书时库存扣了但借阅记录没插进去或者反过来数据不一致。原因Transactional注解失效的常见原因有三个——方法不是public的、同类内部方法直接调用、异常被 catch 了没抛出去。Spring 的 AOP 代理只拦截外部调用Service 里this.borrowBook()这样自己调自己事务不会生效。解决确保注解方法都是public同类调用通过AopContext.currentProxy()或者拆到另一个 Service 里。catch 块里如果处理了异常要么重新抛RuntimeException要么手动TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。4.5 坑五Oracle 11g 序列与触发器不同步现象插入图书时报ORA-00001: unique constraint violated主键冲突。原因序列的当前值落后于表里已有的最大主键值。常见于手动插过测试数据或者序列被重建过但表数据没清。解决查一下当前序列值SELECT seq_lib_book.CURRVAL FROM dual和表最大 IDSELECT MAX(book_id) FROM lib_book如果序列值小于最大 ID用ALTER SEQUENCE seq_lib_book INCREMENT BY 100跳过去再改回INCREMENT BY 1。更彻底的做法是写一个存储过程动态重置序列但课程设计里手动调一次就够了。5. 进阶技巧用存储过程做逾期计算与信用分联动逾期计算如果放在 Java 层用定时任务跑需要额外引入 Quartz 或者 Spring Task而且服务器重启期间可能漏算。Oracle 11g 的存储过程可以在数据库层面每天凌晨自动执行不依赖应用服务器。CREATE OR REPLACE PROCEDURE proc_calc_overdue AS v_overdue_day NUMBER; v_fine NUMBER(10,2); BEGIN -- 遍历所有未归还且已过应还日期的记录 FOR rec IN ( SELECT borrow_id, user_id, due_date FROM lib_borrow WHERE status 0 AND due_date SYSDATE ) LOOP v_overdue_day : TRUNC(SYSDATE - rec.due_date); v_fine : v_overdue_day * 0.5; -- 每天 5 毛 -- 更新借阅记录状态为逾期 UPDATE lib_borrow SET status 2 WHERE borrow_id rec.borrow_id; -- 写逾期日志 INSERT INTO lib_overdue_log(log_id, borrow_id, overdue_day, fine_amount) VALUES(seq_overdue_log.NEXTVAL, rec.borrow_id, v_overdue_day, v_fine); -- 扣用户信用分假设用户表有 credit 字段 UPDATE lib_user SET credit credit - v_overdue_day * 2 WHERE user_id rec.user_id; END LOOP; COMMIT; END; /建一个定时任务让这个存储过程每天凌晨 1 点跑一次。BEGIN DBMS_SCHEDULER.CREATE_JOB( job_name job_calc_overdue, job_type STORED_PROCEDURE, job_action proc_calc_overdue, start_date TRUNC(SYSDATE) 1/24, repeat_interval FREQDAILY; BYHOUR1, enabled TRUE ); END; /repeat_interval用的是 iCalendar 语法FREQDAILY; BYHOUR1表示每天凌晨 1 点执行。start_date设为明天凌晨 1 点避免创建后立即触发。信用分联动是共享图书系统里比较实用的一个设计信用分低于 60 的用户不能再借书必须先把逾期图书还了并缴纳罚款。这个逻辑在BorrowService.borrowBook里加一个前置校验就行。// 借书前先查信用分 Integer credit userDao.getCredit(userId); if (credit null || credit 60) { throw new RuntimeException(信用分不足请先归还逾期图书); }信用分的恢复可以做成手动申请管理员审核后加回。也可以设置一个自然恢复机制比如连续 30 天无逾期每天加 1 分。这个看运营策略代码上就是多一个定时任务的事。验证存储过程有没有跑成功直接查日志表。SELECT * FROM lib_overdue_log WHERE create_time SYSDATE - 1;如果查不到记录先确认lib_borrow里确实有逾期数据再检查DBMS_SCHEDULER的 job 日志。SELECT * FROM USER_SCHEDULER_JOB_RUN_DETAILS WHERE JOB_NAME JOB_CALC_OVERDUE ORDER BY LOG_DATE DESC;ERROR#字段不为 0 说明执行报错ADDITIONAL_INFO里会有具体的 ORA 错误码。这套系统我前后搭过三次每次都在 Oracle 的字符集和 Spring 的事务代理上栽跟头。后来养成了一个习惯本地开发环境用 Docker 跑一个 Oracle 11g 的容器字符集固定AL32UTF8数据源配置和线上完全一致联调时少了很多“本地好好的服务器上就乱码”的玄学问题。存储过程能放数据库层就别放 Java 层少一次网络交互少一个出错环节。希望帮到你。本文还有配套的精品资源点击获取
返回列表