:數(shù)據(jù)庫設(shè)計(jì)與Servlet+JSP+JDBC實(shí)戰(zhàn))
簡介這是一套面向計(jì)算機(jī)、通信、人工智能、自動(dòng)化等相關(guān)專業(yè)學(xué)生的JavaWeb試題庫管理系統(tǒng)完整項(xiàng)目可直接用于期末課程設(shè)計(jì)、課程大作業(yè)或畢業(yè)設(shè)計(jì)。項(xiàng)目由個(gè)人獨(dú)立設(shè)計(jì)完成答辯評(píng)審分達(dá)99分代碼經(jīng)過調(diào)試測試確保可運(yùn)行適合小白學(xué)習(xí)與進(jìn)階也便于基礎(chǔ)較好的同學(xué)在此基礎(chǔ)上修改擴(kuò)展功能。資源包共375個(gè)文件涵蓋24個(gè)java源文件、78個(gè)class編譯文件、28個(gè)jar依賴包、24個(gè)properties配置、10個(gè)xml配置以及docx文檔說明等包含源碼、數(shù)據(jù)庫與文檔說明壓縮包約147.4MB目錄結(jié)構(gòu)清晰便于按模塊查閱。系統(tǒng)圍繞管理員、組卷、題庫、知識(shí)點(diǎn)等界面模塊展開可幫助讀者理解試題管理、組卷邏輯與權(quán)限劃分的實(shí)現(xiàn)思路。目前已有87人學(xué)習(xí)下載適合需要完整項(xiàng)目參考、快速搭建課程設(shè)計(jì)框架并查漏補(bǔ)缺的同學(xué)。1. 期末大作業(yè)選試題庫管理系統(tǒng)為什么 JavaWeb 這套組合拳最穩(wěn)期末大作業(yè)選題十個(gè)里有六個(gè)繞不開「管理系統(tǒng)」。但真正能拿到 95 分以上的往往不是功能堆得最多的那個(gè)而是技術(shù)棧完整、數(shù)據(jù)庫設(shè)計(jì)規(guī)范、文檔能自圓其說的那一個(gè)?;?JavaWeb 的試題庫管理系統(tǒng)就是典型它同時(shí)踩中了 JavaWeb、MySQL、JDBC/Servlet 這幾條教學(xué)主線老師一眼就能看出你是不是真動(dòng)手寫了。這個(gè)系統(tǒng)的核心訴求很明確——教師能錄題、組卷、看成績學(xué)生能刷題、交卷、查分管理員能管用戶和科目。適合誰做適合已經(jīng)學(xué)過 Servlet、JSP、JDBC但還沒完整串過一個(gè) CRUD 項(xiàng)目的同學(xué)。它不追求高并發(fā)追求的是流程閉環(huán)和代碼可讀性而這恰恰是答辯時(shí)最容易被追問的地方。2. 試題庫管理系統(tǒng)的數(shù)據(jù)庫設(shè)計(jì)從 ER 圖到建表腳本2.1 先想清楚「誰在什么條件下做什么」很多同學(xué)一上來就打開 IDEA 建項(xiàng)目結(jié)果寫到一半發(fā)現(xiàn)表不夠用回頭改實(shí)體類改到崩潰。血淚經(jīng)驗(yàn)是先把業(yè)務(wù)動(dòng)作列全再反推表結(jié)構(gòu)。試題庫管理系統(tǒng)的核心動(dòng)作有這些管理員創(chuàng)建科目比如 Java、數(shù)據(jù)庫原理并指定該科目的教師教師向某科目下錄入題目題目分單選、多選、判斷、簡答教師從題庫中按條件抽題生成一張?jiān)嚲韺W(xué)生選擇試卷作答系統(tǒng)自動(dòng)判客觀題主觀題留給教師批閱系統(tǒng)記錄每次考試的成績支持按學(xué)生、按試卷查詢把這些動(dòng)作翻譯成實(shí)體至少需要用戶表、角色表、科目表、題目表、試卷表、試卷題目關(guān)聯(lián)表、考試記錄表、答題詳情表。其中試卷題目關(guān)聯(lián)表和答題詳情表是最容易被漏掉的漏了它們組卷和判分就無從談起。2.2 建表腳本與字段說明下面是一份可以直接在 MySQL 5.7/8.0 里執(zhí)行的建表腳本庫名用exam_db字符集統(tǒng)一utf8mb4。-- 創(chuàng)建數(shù)據(jù)庫 CREATE DATABASE IF NOT EXISTS exam_db DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE exam_db; -- 用戶表管理員、教師、學(xué)生共用用 role 區(qū)分 CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登錄名, password VARCHAR(100) NOT NULL COMMENT 建議存 MD5 或 BCrypt, real_name VARCHAR(50) NOT NULL COMMENT 真實(shí)姓名, role TINYINT NOT NULL COMMENT 1管理員 2教師 3學(xué)生, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT用戶表; -- 科目表 CREATE TABLE t_subject ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT 科目名稱, teacher_id INT COMMENT 負(fù)責(zé)教師ID, remark VARCHAR(255), FOREIGN KEY (teacher_id) REFERENCES t_user(id) ) ENGINEInnoDB COMMENT科目表; -- 題目表type 區(qū)分題型options 存 JSON 字符串 CREATE TABLE t_question ( id INT PRIMARY KEY AUTO_INCREMENT, subject_id INT NOT NULL, type TINYINT NOT NULL COMMENT 1單選 2多選 3判斷 4簡答, content TEXT NOT NULL COMMENT 題干, options TEXT COMMENT 選項(xiàng)JSON 格式, answer VARCHAR(255) NOT NULL COMMENT 標(biāo)準(zhǔn)答案, score INT DEFAULT 5 COMMENT 默認(rèn)分值, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (subject_id) REFERENCES t_subject(id) ) ENGINEInnoDB COMMENT題目表; -- 試卷表 CREATE TABLE t_paper ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL, subject_id INT NOT NULL, total_score INT DEFAULT 100, duration INT DEFAULT 60 COMMENT 考試時(shí)長(分鐘), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (subject_id) REFERENCES t_subject(id) ) ENGINEInnoDB COMMENT試卷表; -- 試卷-題目關(guān)聯(lián)表記錄每題在試卷中的順序和分值 CREATE TABLE t_paper_question ( id INT PRIMARY KEY AUTO_INCREMENT, paper_id INT NOT NULL, question_id INT NOT NULL, sort_no INT DEFAULT 0 COMMENT 題目順序, score INT DEFAULT 5 COMMENT 該題在本卷中的分值, FOREIGN KEY (paper_id) REFERENCES t_paper(id), FOREIGN KEY (question_id) REFERENCES t_question(id) ) ENGINEInnoDB COMMENT試卷題目關(guān)聯(lián)表; -- 考試記錄表 CREATE TABLE t_exam_record ( id INT PRIMARY KEY AUTO_INCREMENT, paper_id INT NOT NULL, student_id INT NOT NULL, start_time DATETIME, submit_time DATETIME, total_score INT DEFAULT 0 COMMENT 最終得分, status TINYINT DEFAULT 0 COMMENT 0未交 1已交 2已批閱, FOREIGN KEY (paper_id) REFERENCES t_paper(id), FOREIGN KEY (student_id) REFERENCES t_user(id) ) ENGINEInnoDB COMMENT考試記錄表; -- 答題詳情表 CREATE TABLE t_answer_detail ( id INT PRIMARY KEY AUTO_INCREMENT, record_id INT NOT NULL, question_id INT NOT NULL, stu_answer TEXT COMMENT 學(xué)生答案, is_correct TINYINT DEFAULT 0 COMMENT 0錯(cuò) 1對(duì) 2待批閱, get_score INT DEFAULT 0, FOREIGN KEY (record_id) REFERENCES t_exam_record(id), FOREIGN KEY (question_id) REFERENCES t_question(id) ) ENGINEInnoDB COMMENT答題詳情表;參數(shù)說明與設(shè)計(jì)取舍t(yī)_user.role用TINYINT而不是字符串查詢時(shí)WHERE role 3比WHERE role student更快也省空間。t_question.options存 JSON 字符串而不是拆成單獨(dú)的選項(xiàng)表。原因是選項(xiàng)數(shù)量固定最多 6 個(gè)拆表會(huì)讓查詢變復(fù)雜得不償失。MySQL 5.7 以上可以直接用JSON類型但為了兼容老版本這里用TEXT。t_paper_question里冗余了score字段因?yàn)橥坏李}在不同試卷里分值可能不同不能只依賴題目表的默認(rèn)分值。所有外鍵都顯式命名方便后期用dbx數(shù)據(jù)庫工具或 Navicat 查看依賴關(guān)系。提示如果學(xué)校機(jī)房 MySQL 版本低于 5.7把DATETIME DEFAULT CURRENT_TIMESTAMP改成TIMESTAMP否則建表會(huì)報(bào)錯(cuò)。2.3 初始化數(shù)據(jù)與自增主鍵的坑建完表后至少插入一個(gè)管理員賬號(hào)否則你連登錄都進(jìn)不去。INSERT INTO t_user (username, password, real_name, role) VALUES (admin, e10adc3949ba59abbe56e057f20f883e, 系統(tǒng)管理員, 1);這里的e10adc...是123456的 MD5 值。不要存明文密碼答辯時(shí)老師看到明文會(huì)直接扣分。如果你用 BCrypt長度要改成VARCHAR(100)以上。自增主鍵的坑在于如果你手動(dòng)插入了id 1的記錄再讓數(shù)據(jù)庫自增下一條可能從 2 開始也可能沖突。穩(wěn)妥做法是永遠(yuǎn)不要手動(dòng)指定 id讓AUTO_INCREMENT自己管。3. 用 IDEA 跑通 JavaWeb 項(xiàng)目Servlet JSP JDBC 最小閉環(huán)3.1 項(xiàng)目結(jié)構(gòu)與依賴配置熱搜里「idea運(yùn)行javaweb項(xiàng)目配置」一直居高不下說明很多同學(xué)卡在環(huán)境上。我一般用Maven Tomcat 8.5/9.0的組合JDK 選 1.8 或 11別用太新的版本否則javax.servlet包名會(huì)變成jakarta.servlet老代碼全報(bào)錯(cuò)。pom.xml里至少要有這些依賴dependencies !-- Servlet APIscope 必須是 provided因?yàn)?Tomcat 自帶 -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency !-- JSP API -- dependency groupIdjavax.servlet.jsp/groupId artifactIdjavax.servlet.jsp-api/artifactId version2.3.3/version scopeprovided/scope /dependency !-- MySQL 驅(qū)動(dòng) -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.28/version /dependency !-- JSTLJSP 里用 c:forEach 必須加 -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency /dependencies參數(shù)說明scopeprovided的意思是「編譯時(shí)用打包時(shí)不帶」因?yàn)?Tomcat 的lib目錄里已經(jīng)有這些 jar。如果你寫成默認(rèn)的compile部署時(shí)會(huì)報(bào)ClassCastException或NoSuchMethodError這是新手翻車最多的地方。3.2 JDBC 工具類一個(gè)能復(fù)用的 DBUtil不要在每個(gè) Servlet 里都寫DriverManager.getConnection抽一個(gè)工具類出來。public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/exam_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USER root; private static final String PWD 你的密碼; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new RuntimeException(MySQL 驅(qū)動(dòng)加載失敗, e); } } public static Connection getConn() throws SQLException { return DriverManager.getConnection(URL, USER, PWD); } public static void close(Connection conn, Statement st, ResultSet rs) { try { if (rs ! null) rs.close(); } catch (SQLException ignored) {} try { if (st ! null) st.close(); } catch (SQLException ignored) {} try { if (conn ! null) conn.close(); } catch (SQLException ignored) {} } }邏輯說明static塊只執(zhí)行一次保證驅(qū)動(dòng)只加載一次。close方法按ResultSet → Statement → Connection的順序關(guān)閉反過來關(guān)會(huì)報(bào)錯(cuò)。serverTimezoneAsia/Shanghai不加的話插入時(shí)間會(huì)差 8 小時(shí)這是 MySQL 8.0 的經(jīng)典坑。3.3 登錄 Servlet 與 JSP 頁面先寫一個(gè)最簡單的登錄流程把「瀏覽器 → Servlet → 數(shù)據(jù)庫 → JSP」這條鏈路跑通。WebServlet(/login) public class LoginServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username req.getParameter(username); String password req.getParameter(password); // 前端傳明文后端轉(zhuǎn) MD5 再比對(duì) String md5Pwd MD5Util.encode(password); String sql SELECT id, real_name, role FROM t_user WHERE username? AND password?; try (Connection conn DBUtil.getConn(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, md5Pwd); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { User user new User(); user.setId(rs.getInt(id)); user.setRealName(rs.getString(real_name)); user.setRole(rs.getInt(role)); req.getSession().setAttribute(user, user); // 按角色跳不同首頁 if (user.getRole() 1) { resp.sendRedirect(admin/index.jsp); } else if (user.getRole() 2) { resp.sendRedirect(teacher/index.jsp); } else { resp.sendRedirect(student/index.jsp); } } else { req.setAttribute(msg, 用戶名或密碼錯(cuò)誤); req.getRequestDispatcher(login.jsp).forward(req, resp); } } } catch (SQLException e) { throw new ServletException(e); } } }參數(shù)說明用PreparedStatement而不是Statement是為了防 SQL 注入。req.getSession().setAttribute(user, user)把用戶信息存進(jìn) session后續(xù)頁面用session.getAttribute(user)取。跳轉(zhuǎn)用sendRedirect而不是forward避免刷新頁面時(shí)重復(fù)提交表單。對(duì)應(yīng)的login.jsp里表單action寫loginmethod寫post密碼框typepassword。如果登錄失敗用${msg}顯示錯(cuò)誤提示。注意JSP 頁面頂部要加% page contentTypetext/html;charsetUTF-8 languagejava %否則中文會(huì)亂碼。4. 試題庫管理系統(tǒng)的避坑與排查5 個(gè)真實(shí)翻車現(xiàn)場4.1 中文亂碼從請(qǐng)求到響應(yīng)全鏈路排查現(xiàn)象登錄后頁面顯示「?3??????????‘?」這種亂碼或者提交的題目內(nèi)容變成問號(hào)。原因亂碼可能出現(xiàn)在三個(gè)環(huán)節(jié)——請(qǐng)求編碼、數(shù)據(jù)庫編碼、響應(yīng)編碼。任何一個(gè)沒設(shè)對(duì)都會(huì)出問題。解決按順序檢查。第一數(shù)據(jù)庫和表的字符集必須是utf8mb4用SHOW CREATE TABLE t_question;確認(rèn)。第二JDBC URL 里加characterEncodingutf8。第三在 Servlet 的doPost第一行加req.setCharacterEncoding(UTF-8);在doGet里加resp.setContentType(text/html;charsetUTF-8);。第四JSP 頁面pageEncodingUTF-8。四個(gè)地方全對(duì)亂碼才會(huì)消失。4.2 組卷時(shí)題目重復(fù)關(guān)聯(lián)表沒有去重現(xiàn)象教師手動(dòng)組卷同一道題被抽了兩次試卷里出現(xiàn)兩個(gè)一模一樣的題目。原因t_paper_question表沒有對(duì)(paper_id, question_id)做唯一約束前端也沒有做重復(fù)校驗(yàn)。解決在關(guān)聯(lián)表上加唯一索引UNIQUE KEY uk_paper_q (paper_id, question_id)插入前先SELECT COUNT(*)判斷。如果業(yè)務(wù)允許同一題在不同試卷里出現(xiàn)那沒問題但同一張?jiān)嚲砝镏貜?fù)一定是 bug。4.3 自動(dòng)判分把多選題判錯(cuò)答案順序不一致現(xiàn)象學(xué)生選了 A、C標(biāo)準(zhǔn)答案是 C、A系統(tǒng)判錯(cuò)。原因多選題的答案存成字符串AC直接equals比較順序不同就判錯(cuò)。解決存答案前先排序或者比較時(shí)把兩個(gè)字符串都轉(zhuǎn)成Set再比。我一般會(huì)在t_question.answer里強(qiáng)制按字母序存比如AC而不是CA學(xué)生提交的答案也排序后再比。4.4 Tomcat 啟動(dòng)報(bào) 404web.xml 版本不匹配現(xiàn)象IDEA 里 Tomcat 啟動(dòng)成功但訪問http://localhost:8080/login報(bào) 404。原因web.xml的version和 Servlet 版本不匹配或者WebServlet注解沒生效。解決如果用注解web.xml里metadata-completefalse或者干脆刪掉web.xml里的 Servlet 配置。如果用web.xml配置url-pattern要和訪問路徑一致。另外檢查pom.xml里servlet-api的scope是不是provided寫成compile會(huì)導(dǎo)致類加載沖突。4.5 數(shù)據(jù)庫連接池耗盡Connection 沒關(guān)現(xiàn)象系統(tǒng)用一會(huì)兒就卡死報(bào)Too many connections。原因每個(gè) Servlet 里getConn()之后沒有在finally里關(guān)閉連接越積越多。解決用try-with-resources語法把Connection、PreparedStatement、ResultSet都放在try()里Java 會(huì)自動(dòng)關(guān)閉。如果項(xiàng)目規(guī)模大引入 Druid 或 HikariCP 連接池在druid.properties里配maxActive20別用默認(rèn)的 8。5. 從 95 分到滿分文檔說明與答辯演示的加分技巧5.1 文檔說明怎么寫才不像湊字?jǐn)?shù)老師看文檔重點(diǎn)看三樣數(shù)據(jù)庫設(shè)計(jì)、核心流程、測試用例。數(shù)據(jù)庫設(shè)計(jì)部分把 ER 圖可以用 draw.io 畫和建表腳本放上去每張表下面用一句話說明用途。核心流程部分挑「組卷」和「判分」兩個(gè)最復(fù)雜的流程用文字加截圖說明。測試用例部分列一個(gè)表格寫清楚輸入、預(yù)期輸出、實(shí)際輸出。測試項(xiàng)輸入預(yù)期輸出實(shí)際輸出登錄成功admin/123456跳轉(zhuǎn)管理員首頁一致登錄失敗admin/000000提示密碼錯(cuò)誤一致組卷選 10 道題生成試卷總分 50一致自動(dòng)判分單選全對(duì)得分等于客觀題總分一致5.2 答辯演示的 3 個(gè)技巧第一提前準(zhǔn)備好數(shù)據(jù)。別現(xiàn)場錄題錄一道題要填五六個(gè)字段時(shí)間根本不夠。提前在數(shù)據(jù)庫里插 20 道題、2 張?jiān)嚲怼? 個(gè)學(xué)生賬號(hào)。第二演示順序按角色走。先管理員登錄展示用戶管理再教師登錄展示錄題和組卷最后學(xué)生登錄展示答題和查分。這樣邏輯最順老師也容易跟上。第三主動(dòng)說一個(gè)坑。比如「這里我一開始用 Statement 查登錄后來發(fā)現(xiàn) SQL 注入風(fēng)險(xiǎn)改成了 PreparedStatement」。主動(dòng)暴露問題并給出解決方案比藏著掖著得分高。5.3 一個(gè)我踩過的坑我第一次做這個(gè)系統(tǒng)時(shí)把試卷總分寫死在代碼里totalScore 100結(jié)果老師臨時(shí)要求「這次試卷總分 50」我改代碼改了半小時(shí)。后來我把總分改成從t_paper_question里SUM(score)動(dòng)態(tài)算再也沒翻過車。能算出來的值就不要寫死這是做管理系統(tǒng)的鐵律。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取