
簡介這是一份基于JavaWebJSPMySQL的BS架構人才招聘網站設計與實現源碼包面向計算機專業畢業設計/課程設計學生以及想通過完整項目學習傳統Java Web開發的入門開發者。包內含全套可運行源碼和詳細配套文檔覆蓋前臺首頁、網站導航、職位信息列表、用戶注冊、后臺登錄、職位信息管理、退出后臺管理等核心模塊并附帶SQL數據庫腳本便于本地部署和二次開發。壓縮包共930個文件以gif、js、html、jar、jsp、java等類型為主包含頁面交互、前端靜態資源、后端邏輯與依賴庫文件構成直觀反映了經典JSP項目的目錄規范整體約19.32MB。已有206人學習下載對希望快速掌握JSPServletMySQL開發流程的人來說是一份能直接運行、邊看文檔邊變實踐的優質參考。1. BS架構下的人才招聘網站從“能跑”到“可以用”的JavaWeb系統用 JavaWeb JSP MySQL 構建 BS 架構的人才招聘網站是課設、畢設和中小企業快速搭建內部招聘平臺最常見的組合。這套技術棧的意義在于瀏覽器只負責渲染 JSP 生成的 HTMLServlet 承擔請求分發與參數校驗MySQL 存放用戶、職位與投遞數據三層職責清晰。大多數拿到這個標題源碼包的人真正想做的事無非四類讀懂已有代碼、改掉一兩個 bug、換掉數據庫連接配置再順手做點二次功能開發。這個系統的體量恰好適合把 JSP 標簽、JDBC 封裝、Servlet 生命周期和 SQL 優化串起來看一遍難度不大坑卻不少。2. 招聘網站的BS架構設計與技術選型JSP、Servlet與MySQL的分層邊界2.1 為什么這門技術組合還值得在 BS 架構里用BS 架構的核心是客戶端無狀態、服務端統一管理會話與數據。JavaWebJSPMySQL 的好處在于JSP 負責動態渲染 HTML瀏覽器端不需要引入 Node 或 Webpack 那套構建鏈。Servlet 負責接收 HTTP 請求并在服務端完成跳轉MySQL 負責把職位、簡歷、投遞記錄持久化。相比直接把 SQL 寫在 JSP 里這個組合最大的價值是 Servlet 作為“中間人”把業務邏輯從頁面里剝離開。這里要澄清一個常見誤區早期很多 JSP 項目把業務代碼全寫進% %腳本塊里頁面一改就觸發 JSP 編譯錯誤維護成本非常高。如果自己從零寫至少拆成 Servlet DAO 兩層Servlet 只取參數、調服務、做跳轉DAO 負責 JDBC 數據訪問。這樣后面加“導出 Excel”或者“批量撤回投遞”這類功能時不用在十幾個 JSP 文件里翻找業務代碼。2.2 三層職責劃分與一次完整請求的流轉路徑一次“求職者搜索職位”的請求在各層的流轉很直白層次典型文件職責邊界表現層job_list.jsp、login.jsp接收表單數據渲染 HTML展示提示信息控制層JobListServlet、LoginServlet取參數、校驗入參、調用 DAO、跳轉或轉發數據層JobDao、UserDao、DBUtil執行 SQL、封裝結果集、處理連接事務對應到運行時請求路徑是瀏覽器提交GET /job/list?keywordjavapage2Tomcat 根據 web.xml 里的映射找到 JobListServletServlet 解析 keyword 和 page調用 JobDao 執行分頁 SQL把結果放進 request 域再forward到 job_list.jsp由 JSP 輸出完整 HTML。這里有個容易忽略的點重定向sendRedirect會丟 request 里的數據所以列表頁必須用 forward 轉發。2.3 項目結構 WEB-INF 約束與 JavaWeb 配置要點拿到源碼包先看目錄結構判斷這個項目到底能不能擴展。合適的劃分是 controller、service、dao、model、util 五類包頁面單獨放 web 目錄下。我一般先找 WEB-INF/web.xml再確認頁面是否被 Servlet 轉發訪問。recruit-web/ ├── src/ │ ├── com/recruit/ │ │ ├── controller/ # Servlet 層接收請求并分發 │ │ ├── service/ # 業務邏輯層處理事務和狀態 │ │ ├── dao/ # JDBC 數據訪問SQL 集中管理 │ │ ├── model/ # 實體類對應數據庫表字段 │ │ └── util/ # DBUtil、MD5Util、PageUtil │ └── db.properties # 數據庫連接配置 ├── web/ │ ├── WEB-INF/ │ │ ├── web.xml # Servlet 映射、過濾器、歡迎頁 │ │ └── lib/ # 第三方 jar 包 │ ├── jsp/ │ │ ├── job/ # 職位模塊頁面 │ │ ├── resume/ # 簡歷模塊頁面 │ │ └── admin/ # 后臺管理頁面 │ └── index.jspWEB-INF 下的 JSP 頁面不能被瀏覽器直接通過 URL 訪問只能經 Servlet forward 進入。所以很多新手把login.jsp放進 WEB-INF 后直接訪問就報 404這是 JavaWeb 配置階段最常見的錯誤。WEB-INF/lib 放的是 mysql-connector-java、jstl 這類依賴包用 Maven 構建的話這里不需要手動放 jarwar 打包時會把依賴裝進來。JavaWeb 配置里另一個容易忽略的是編碼過濾器中文參數在 GET 和 POST 兩條路徑上的亂碼原因不一樣。POST 亂碼靠request.setCharacterEncoding(UTF-8)解決GET 請求是 Tomcat 在server.xml的 Connector 上解析 URI需要單獨加過濾器或改 URIEncoding。簡單做法是在 web.xml 里注冊 Tomcat 自帶的編碼過濾器url-pattern 設為/*同時 POST 和 GET 的亂碼都能兜住。3. MySQL數據庫建模招聘網站的表設計、索引與常用SQL3.1 用戶表、企業表與職位表的 DDL角色字段怎么定招聘網站的常見做法是六到八張表用戶表、企業表、職位表、簡歷表、投遞記錄表再加一張公告表。管理員、企業賬號、求職者賬號可以統一放進一張t_user用 role 字段區分不需要額外做“角色表 用戶角色中間表”。中間表在權限系統里是標準設計但在這里會讓登錄 SQL 多三次 JOIN性能和代碼復雜度都劃不來。CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL COMMENT 登錄名, password VARCHAR(64) NOT NULL COMMENT MD5摘要, role TINYINT NOT NULL DEFAULT 2 COMMENT 0管理員,1企業,2求職者, company_id INT DEFAULT NULL COMMENT 企業賬號關聯公司, status TINYINT DEFAULT 1 COMMENT 1有效,0凍結, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_company ( id INT PRIMARY KEY AUTO_INCREMENT, company_name VARCHAR(120) NOT NULL, industry VARCHAR(50) COMMENT 行業, scale VARCHAR(20) COMMENT 規模, address VARCHAR(200), introduction TEXT ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_job ( id INT PRIMARY KEY AUTO_INCREMENT, company_id INT NOT NULL, title VARCHAR(100) NOT NULL COMMENT 職位名稱, salary_min INT COMMENT 薪資下限單位K, salary_max INT COMMENT 薪資上限單位K, city VARCHAR(50), edu_require VARCHAR(20) COMMENT 學歷要求, work_years VARCHAR(20) COMMENT 經驗要求, description TEXT, status TINYINT DEFAULT 1 COMMENT 1招聘中,0下線, publish_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_company (company_id), INDEX idx_title (title), INDEX idx_publish (publish_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;角色用 TINYINT 而不是字符串占 1 字節查詢時用WHERE role 2比WHERE role jobseeker更快。企業用戶通過company_id關聯公司表職位表再通過company_id關聯公司這樣企業名稱只存一份不會出現職位列表里公司名各寫各的臟數據。這里故意沒有建物理外鍵刪除企業時外鍵會強制檢查子表手寫 SQL 的項目里順序一錯就報錯外鍵的維護成本大于收益關聯一致性靠業務層的 Service 保證。3.2 簡歷表與投遞記錄表多對多關系的落地寫法一個用戶可以有多份簡歷但簡化版系統通常只做一份主簡歷。簡歷表記錄教育背景、工作年限、期望薪資和技能描述用user_id指向用戶表。投遞記錄則是“簡歷”和“職位”之間的多對多關聯表除了兩個外鍵還帶狀態字段。CREATE TABLE t_resume ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, real_name VARCHAR(50), phone VARCHAR(20), email VARCHAR(100), edu_degree VARCHAR(20) COMMENT 最高學歷, work_years TINYINT COMMENT 工作年限, expectation_city VARCHAR(50), expectation_salary VARCHAR(20), skill_desc TEXT, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_delivery ( id INT PRIMARY KEY AUTO_INCREMENT, resume_id INT NOT NULL, job_id INT NOT NULL, status TINYINT DEFAULT 0 COMMENT 0已投遞,1已查看,2邀請面試,3不合適, remark VARCHAR(200) COMMENT 面試備注或反饋, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_resume_job (resume_id, job_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;uk_resume_job這個唯一鍵是整個投遞邏輯的關鍵它保證同一份簡歷對同一個職位只能投一次。如果只在 Servlet 里先SELECT再INSERT并發請求下兩條線程都查不到記錄就會插出重復數據。有了唯一鍵兜底直接插入數據庫會拒掉第二條程序捕獲 Duplicate entry 異常后再拋提示即可。3.3 招聘網站高頻查詢 SQL模糊搜索、分頁與統計搜索職位最常用的語句是LIKE %關鍵詞%這個寫法在數據量小時沒什么感覺職位表到幾十萬行后前導百分號會讓索引失效全表掃描的耗時隨行數線性膨脹。先記住一個結論對小型招聘網站正確做法是控制 JOIN 范圍、給常用過濾字段建聯合索引而不是一上來就上全文檢索。SELECT j.id, j.title, j.salary_min, j.salary_max, j.city, c.company_name, j.publish_time FROM t_job j INNER JOIN t_company c ON j.company_id c.id WHERE j.status 1 AND (j.title LIKE CONCAT(%, ?, %) OR c.company_name LIKE CONCAT(%, ?, %)) AND (? OR j.city ?) ORDER BY j.publish_time DESC LIMIT ?, ?;四個?分別是 keyword、keyword、city、city最后兩個參數是 offset 和 pageSize。LIMIT的 offset 不能在 Servlet 里直接拿用戶傳的頁碼當參數正確算法是offset (page - 1) * size。城市條件寫成(? OR j.city ?)比拼接兩個 SQL 更干凈DAO 層只需要傳同一個 session 字符串。統計企業收到的投遞量是管理后臺的常用功能。這里用LEFT JOIN而不是INNER JOIN因為招聘中的企業即使一條投遞都沒有也要顯示 0而不是被過濾掉。SELECT j.company_id, c.company_name, COUNT(d.id) AS delivery_count FROM t_job j LEFT JOIN t_delivery d ON j.id d.job_id LEFT JOIN t_company c ON j.company_id c.id WHERE j.status 1 GROUP BY j.company_id, c.company_name ORDER BY delivery_count DESC;COUNT(d.id)只統計真實存在的投遞記錄如果t_delivery沒有匹配行左邊記錄也會保留COUNT 結果是 0。GROUP BY 后面帶的列要和 SELECT 中非聚合列一致否則在高版本 MySQL 的 ONLY_FULL_GROUP_BY 模式下會直接報語法錯誤。4. JSPServlet實現核心業務鏈路登錄、職位分頁與簡歷投遞4.1 登錄認證的 Servlet 處理與 Session 生命周期登錄邏輯是理解這套系統最適合的入口它把請求參數、DAO 查詢、Session 存儲和頁面跳轉串在一起。先看最常見的 Servlet 寫法再補一個登錄檢查的過濾器。WebServlet(/login) public class LoginServlet extends HttpServlet { private UserDao userDao new UserDao(); Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8); String username request.getParameter(username); String password MD5Util.md5(request.getParameter(password)); String role request.getParameter(role); User user userDao.findByLogin(username, password, role); if (user ! null) { HttpSession session request.getSession(); session.setAttribute(loginUser, user); session.setMaxInactiveInterval(30 * 60); String target 2.equals(role) ? index.jsp : admin/index.jsp; response.sendRedirect(target); } else { response.getWriter().write( scriptalert(用戶名或密碼錯誤);history.back();/script); } } }登錄前就做 md5 摘要避免明文密碼進入 SQL 語句也避免日志或異常堆棧里帶出密碼。role參數來自登錄表單的下拉框找到用戶后要及時把用戶對象放進 Session后續所有頁面都從 Session 里取當前登錄人。登錄成功后用sendRedirect而不是 forward是為了讓瀏覽器地址欄變成目標頁刷新時不會再次提交表單避免每次刷新都彈“重新發送表單”。登錄檢查 Filter 是第二道防線如果用戶沒有登錄就訪問受保護目錄直接重定向到登錄頁。一個過濾器能讓整站受保護不需要每個 Servlet 都重復判斷 Session 是否為空。public class LoginFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; Object loginUser request.getSession().getAttribute(loginUser); if (loginUser null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(request, response); } }過濾器最常踩的坑是放行規則沒配好。靜態資源、登錄頁和登錄 Servlet 需要排除在外否則正常用戶也會被攔回來。url-pattern用/job/*、/resume/*這類精確目錄范圍比用/*到處放行更安全。4.2 職位列表的分頁查詢與 JSP 渲染參數計算職位列表頁是招聘網站的流量入口涉及三個要素頁碼解析、DAO 分頁查詢、JSP 端循環輸出。先看控制層的分頁代碼。WebServlet(/job/list) public class JobListServlet extends HttpServlet { private JobDao jobDao new JobDao(); Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String keyword request.getParameter(keyword); String pageStr request.getParameter(page); String sizeStr request.getParameter(size); int page pageStr null ? 1 : Integer.parseInt(pageStr); int size sizeStr null ? 8 : Integer.parseInt(sizeStr); // 只允許正向翻頁防止負數頁碼把 offset 算成負數 page Math.max(page, 1); int offset (page - 1) * size; ListJobVO list jobDao.queryPage(keyword, city, offset, size); int total jobDao.countPage(keyword, city); int totalPages (total size - 1) / size; // 超過最大頁時回退到最后一頁避免空列表 if (page totalPages) { page totalPages; offset (page - 1) * size; list jobDao.queryPage(keyword, city, offset, size); } request.setAttribute(jobList, list); request.setAttribute(currentPage, page); request.setAttribute(totalPages, totalPages); request.getRequestDispatcher(/jsp/job/list.jsp).forward(request, response); } }分頁參數計算容易出錯的地方是兩個用戶直接手改 URL 傳page0或page10000第一種用Math.max(page, 1)擋掉第二種在查出總頁數后再做一次越界回退。查詢列表和查詢總數必須用同一組 WHERE 條件否則頁數對不上。JSP 側用 JSTL 的c:forEach渲染避免在頁面里寫 Java 腳本塊。c:if 控制上一頁下一頁是否顯示需要往前端模板傳當前頁和總頁數。c:forEach items${jobList} varjob div classjob-item h3a href${pageContext.request.contextPath}/job/detail?id${job.id} ${job.title}/a/h3 p${job.companyName} | ${job.city} | ${job.salaryMin}K-${job.salaryMax}K/p a classbtn href${ctx}/delivery/add?jobId${job.id}立即投遞/a /div /c:forEach c:if test${currentPage 1} a href${ctx}/job/list?page${currentPage - 1}上一頁/a /c:if span第 ${currentPage} / ${totalPages} 頁/span c:if test${currentPage totalPages} a href${ctx}/job/list?page${currentPage 1}下一頁/a /c:if${ctx}已經在 Servlet 或頁頭用pageContext.request.contextPath注入過作用是拼出項目上下文根路徑。直接寫/job/list在部署到根目錄時沒問題一旦 WAR 包改名后發布到同端口硬編碼路徑就會 404。頁面里不要直接訪問job對象的內部字段JSP 中通過 EL 表達式調用的是 getter 方法字段名大小寫寫錯會直接報 PropertyNotFoundException。4.3 簡歷投遞的冪等控制與不可重復投遞提示投遞動作由一個 Servlet 接收請求檢查登錄態和簡歷完整性最后調用 DAO 插入投遞記錄。這里需要同時處理兩個問題用戶沒登錄直接投遞、用戶重復投遞。先寫 DAO 層。public boolean addDelivery(int resumeId, int jobId) { String sql INSERT INTO t_delivery (resume_id, job_id) VALUES (?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, resumeId); ps.setInt(2, jobId); return ps.executeUpdate() 0; } catch (SQLException e) { if (e.getErrorCode() 1062) { // 1062 是 MySQL 的唯一索引沖突錯誤碼說明重復投遞 return false; } e.printStackTrace(); return false; } }常見錯誤碼含義本項目中對應場景1045數據庫用戶名或密碼錯誤db.properties 配置錯1062唯一鍵沖突同一簡歷重復投遞同一職位1049數據庫不存在建庫腳本沒執行這套代碼并不需要進入數據庫先查一次再說INSERT 唯一鍵沖突時直接返回 false使并發投遞時也只有一條能成功這就是 MySQL 鎖表之外更輕量的冪等方案。Comparator和頁面層檢查 Session 為空時跳登錄頁簡歷不存在時跳編輯頁DAO 返回 false 時用history.back()提示不可重復投遞配合 alert 彈窗處理。5. 部署到Tomcat的版本選擇與上線驗證技巧5.1 WAR 包構建與 Tomcat 9 / 10 版本差異項目在本機跑通和正式部署之間隔著一個版本兼容問題。這套 JSP Servlet 源碼如果用的是javax.servlet包名建議直接選 Tomcat 9。Tomcat 10 之后把命名空間遷到了jakarta.servlet老代碼編譯通過但運行時報ClassNotFoundException這是從黑馬 JavaWeb 筆記到實際部署最常見的錯位點。# Maven 打 war 包并跳過測試 mvn clean package -DskipTests # 拷貝到 Tomcat webapps 目錄 cp target/recruit.war /opt/tomcat/webapps/ # 啟動 /opt/tomcat/bin/startup.sh tail -f /opt/tomcat/logs/catalina.outWAR 包部署后 Tomcat 會自動解壓出recruit/目錄瀏覽器訪問路徑是http://IP:8080/recruit/。如果改完 JSP 后頁面不生效先刪掉tomcat/work/Catalina/localhost/recruit/下的 JSP 編譯緩存目錄這相當于前端強刷之外的 JSP 緩存層。5.2 用一個 bat 腳本帶走 MySQL 每日業務數據部署在 Windows 服務器上的小型招聘網站最靠譜的備份方案不是裝第三方客戶端而是用計劃任務每天跑一次 mysqldump。這樣即使第二天誤刪數據也能在幾分鐘內恢復。echo off set MYSQL_HOMEC:\mysql-8.0.36-winx64\bin set BACKUP_DIRD:\backup\recruit_db set DB_NAMErecruit_db set DB_USERroot set DB_PASS123456 set YYYYMMDD%date:~0,4%%date:~5,2%%date:~8,2% %MYSQL_HOME%\mysqldump -u%DB_USER% -p%DB_PASS% --default-character-setutf8mb4 %DB_NAME% %BACKUP_DIR%\recruit_%YYYYMMDD%.sql forfiles /p %BACKUP_DIR% /m *.sql /d -15 /c cmd /c del path前兩行 set 是 MySQL 安裝目錄和備份目錄按實際環境改。%date:~0,4%取當前日期前四位做年份后面的~5,2和~8,2取月和日前提是系統日期格式為yyyy-MM-dd。首次運行前先執行echo %date%驗證一下區域設置的格式不同取子串的位置要對齊。最后一行保留最近 15 天備份防止磁盤被 sql 文件塞滿。執行這一腳本時遇到 1045 錯誤先檢查賬號密碼是否在命令行里有特殊字符備份出的文件用show tables;快速驗證是否完整再考慮加入 Windows 任務計劃。5.3 上線后的三條驗證路徑部署完成后別急著點功能按順序走三條檢查路徑。第一次檢查 Tomcat 日志與 JSP 編譯產物重啟后訪問首頁并觀察 work 目錄下是否生成了對應的 class 文件第二次用 Navicat 或命令行執行SHOW INDEX FROM t_delivery;確認唯一鍵和常用查詢的索引都存在第三次用兩個不同瀏覽器同時登錄驗證 Session 不會互相踢掉再在其中一個窗口連續點擊兩次投遞按鈕確認第二條請求被攔截。做完這三項整個系統從數據庫到頁面才算真正連通。本文還有配套的精品資源點擊獲取