:從源碼部署到問題排查全指南)
簡(jiǎn)介一套基于JavaWeb實(shí)現(xiàn)的健身房管理系統(tǒng)畢業(yè)設(shè)計(jì)源碼面向計(jì)算機(jī)相關(guān)專業(yè)學(xué)生和Java開發(fā)者可作為畢業(yè)設(shè)計(jì)、課程設(shè)計(jì)或期末大作業(yè)的完整參考方案。壓縮包共274個(gè)文件涵蓋48個(gè)Java核心邏輯文件、19個(gè)JSP頁(yè)面及18個(gè)CSS樣式同時(shí)包含JavaScript腳本、gif演示動(dòng)圖、SQL數(shù)據(jù)庫(kù)腳本等資源總量2.72MB整體結(jié)構(gòu)清晰便于快速導(dǎo)入運(yùn)行。目前已有116人瀏覽學(xué)習(xí)源碼經(jīng)嚴(yán)格調(diào)試并獲95分以上評(píng)審成績(jī)確??蛇\(yùn)行。下載后可獲得完整項(xiàng)目源碼、數(shù)據(jù)庫(kù)腳本以及各頁(yè)面操作錄屏既能用于理解健身房會(huì)員管理、課程預(yù)約、器材管理等模塊的實(shí)現(xiàn)思路也能直接作為畢設(shè)答辯與功能演示的基礎(chǔ)工程。1. “基于JavaWeb實(shí)現(xiàn)的健身房管理系統(tǒng)”能跑起來才算數(shù)先理解它在解決什么問題“基于JavaWeb實(shí)現(xiàn)的健身房管理系統(tǒng)”在畢業(yè)設(shè)計(jì)里屬于出場(chǎng)率極高但最能保底的一類選題。它把JavaWeb課程幾個(gè)核心考點(diǎn)全占了JSP/Servlet做頁(yè)面交互、JDBC連MySQL數(shù)據(jù)庫(kù)做增刪改查、三層架構(gòu)承載會(huì)員辦卡、課程預(yù)約、教練排班、到期續(xù)費(fèi)提醒這些真實(shí)業(yè)務(wù)。對(duì)準(zhǔn)備答辯的人來說真正難的從來不是“看懂代碼”而是打開壓縮包之后讓源碼和數(shù)據(jù)庫(kù)在十分鐘內(nèi)從自己電腦上跑起來。這篇文章就按這個(gè)順序講壓縮包里先看什么、數(shù)據(jù)庫(kù)表怎么建模、環(huán)境和數(shù)據(jù)庫(kù)連接池怎么配、業(yè)務(wù)代碼怎么改、運(yùn)行時(shí)五個(gè)高頻坑在哪。適合正在做JavaWeb課程設(shè)計(jì)、Java課程設(shè)計(jì)案例源碼或畢業(yè)設(shè)計(jì)的同學(xué)也適合想用一個(gè)完整案例把業(yè)務(wù)邏輯真正吃透的初學(xué)者。2. 先拆包再寫代碼JavaWeb健身房管理系統(tǒng)的文件結(jié)構(gòu)、三層架構(gòu)與數(shù)據(jù)庫(kù)建模健身房管理系統(tǒng)這類源碼包最怕一解壓就盲目點(diǎn)“運(yùn)行”。很多老工程不是Maven標(biāo)準(zhǔn)結(jié)構(gòu)而是用IDEA的Java EE向?qū)傻钠胀╓eb工程部署方式和源碼存放路徑跟教科書示例不太一樣。我建議按下面的順序拆能少走一半彎路。2.1 壓縮包里通常有哪些文件先分清源碼目錄、數(shù)據(jù)庫(kù)腳本和說明文檔打開zip后大概率會(huì)看到四類東西。別急著點(diǎn)開任何代碼先按表格里的檢查點(diǎn)過一遍。路徑/文件作用動(dòng)手前確認(rèn)點(diǎn)src 或 src/main/java存放Servlet、Service、DAO、實(shí)體類看包名有沒有分層找JDBC工具類在哪WebContent / web / src/main/webappJSP頁(yè)面、CSS、JS、圖片看login.jsp和index.jsp確認(rèn)登錄入口.sql 文件 / database目錄建庫(kù)建表腳本與測(cè)試數(shù)據(jù)看建表語句有沒有DROP TABLE和INSERT說明文檔/配置文檔環(huán)境版本、部署步驟確認(rèn)JDK/Tomcat/MySQL版本要求如果工程帶pom.xml說明作者把工程改成了Maven結(jié)構(gòu)依賴統(tǒng)一依賴maven管理。如果沒有pom.xml你通常會(huì)在WEB-INF/lib下看到mysql-connector-java.jar這類依賴。這里最容易踩坑的是“源碼放對(duì)了但依賴少一個(gè)jar”后文第5章專門講。數(shù)據(jù)庫(kù)腳本有的叫g(shù)ym.sql有的叫fitness_manager.sql打開先看前三行確認(rèn)數(shù)據(jù)庫(kù)名、字符集以及表名大小寫。絕大多數(shù)畢設(shè)工程用MySQL“基于JavaWeb實(shí)現(xiàn)的健身房管理系統(tǒng)”里的“數(shù)據(jù)庫(kù)”基本就是指MySQL腳本按MySQL來準(zhǔn)備環(huán)境不會(huì)錯(cuò)。2.2 三層架構(gòu)與MVC為什么JavaWeb項(xiàng)目要把控制器、業(yè)務(wù)、DAO分開健身房管理系統(tǒng)的代碼好不好改取決于它有沒有把三層架構(gòu)分開。常見的分層如下視圖層JSP頁(yè)面。負(fù)責(zé)展示和收集表單頁(yè)面里盡量不寫Java業(yè)務(wù)代碼??刂破鲗覵ervlet。負(fù)責(zé)接收請(qǐng)求、調(diào)用Service方法、跳轉(zhuǎn)頁(yè)面。業(yè)務(wù)層Service。負(fù)責(zé)辦卡、預(yù)約、續(xù)費(fèi)提醒這類業(yè)務(wù)規(guī)則事務(wù)通常寫在這一層。數(shù)據(jù)訪問層DAO JDBC。負(fù)責(zé)數(shù)據(jù)庫(kù)增刪改查只談數(shù)據(jù)不談業(yè)務(wù)。如果工程里一個(gè)Servlet把查詢和頁(yè)面輸出全干了系統(tǒng)也能運(yùn)行但你要加一個(gè)“會(huì)員到期自動(dòng)提醒”就會(huì)很痛苦因?yàn)槟悴恢罃?shù)據(jù)從哪來、該改哪里。MVC和三層架構(gòu)不是同一個(gè)概念但在JavaWeb里經(jīng)常一起出現(xiàn)三層架構(gòu)管職責(zé)劃分MVC管頁(yè)面交互方式。JSP充當(dāng)ViewServlet充當(dāng)ControllerModel由Service、DAO和實(shí)體類共同組成。啟動(dòng)項(xiàng)目前建議先看兩個(gè)入口會(huì)員管理的Servlet和它的DAO這是整條鏈路的骨架。提示如果看到JSP里直接寫了JDBC代碼說明工程沒有分層。能跑但改起來風(fēng)險(xiǎn)高答辯時(shí)也容易被追問“為什么不分層”。建議至少把數(shù)據(jù)庫(kù)訪問抽到一個(gè)工具類里否則后續(xù)改造的每一處都可能是雷。2.3 數(shù)據(jù)庫(kù)表怎么建模會(huì)員卡、課程預(yù)約與教練排班的表結(jié)構(gòu)健身房管理系統(tǒng)的核心不是頁(yè)面是數(shù)據(jù)庫(kù)設(shè)計(jì)。常見的表至少有這些會(huì)員表、員工表/教練表、課程表、預(yù)約記錄表、卡券訂單表。下面是簡(jiǎn)化版表結(jié)構(gòu)很多畢設(shè)工程的核心表就長(zhǎng)這樣-- 會(huì)員表存卡種和有效期 CREATE TABLE member ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登錄名, phone VARCHAR(20), card_type VARCHAR(20) COMMENT 月卡/季卡/年卡, card_start DATE NOT NULL, card_end DATE NOT NULL, status TINYINT DEFAULT 1 COMMENT 1正常 0停用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 課程表 CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, course_name VARCHAR(100) NOT NULL, coach_name VARCHAR(50), max_num INT DEFAULT 10 COMMENT 最大預(yù)約人數(shù), remain INT DEFAULT 10 COMMENT 剩余名額, course_time DATETIME ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 預(yù)約記錄表 CREATE TABLE booking ( id INT PRIMARY KEY AUTO_INCREMENT, course_id INT NOT NULL, member_id INT NOT NULL, booking_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_course_member (course_id, member_id), CONSTRAINT fk_booking_course FOREIGN KEY (course_id) REFERENCES course(id), CONSTRAINT fk_booking_member FOREIGN KEY (member_id) REFERENCES member(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;這段SQL里有幾個(gè)關(guān)鍵參數(shù)值得注意。ENGINEInnoDB必須保留因?yàn)橐С质聞?wù)課程預(yù)約扣減名額和插入預(yù)約記錄得在同一個(gè)事務(wù)里完成。CHARSETutf8mb4解決中文和特殊符號(hào)存儲(chǔ)問題老工程里常見的utf8在MySQL 8下已經(jīng)不夠用。card_start和card_end選DATE而不是DATETIME因?yàn)檗k卡只關(guān)心日期不關(guān)心時(shí)刻DATE類型還能直接配合DATEDIFF做到期提醒。UNIQUE KEY uk_course_member是防重復(fù)預(yù)約的數(shù)據(jù)庫(kù)層保障后面第4章的課程預(yù)約事務(wù)會(huì)用到它。如果你的壓縮包里數(shù)據(jù)庫(kù)腳本沒有這些約束建議在理解表關(guān)系后手動(dòng)補(bǔ)上這是相冊(cè)管理系統(tǒng)最值得留的一層保險(xiǎn)。3. 把源碼和數(shù)據(jù)庫(kù)跑起來JDK、Tomcat、MySQL和IDEA的完整復(fù)位很多老JavaWeb項(xiàng)目跑不起來不是代碼問題是環(huán)境版本錯(cuò)位。尤其是JDK、Tomcat、MySQL三方版本互相不兼容時(shí)日志里全是ClassNotFound和Connection refused你根本分不清是配置錯(cuò)還是依賴缺。這一章給出一套最穩(wěn)的組合和復(fù)現(xiàn)步驟。3.1 環(huán)境選型JDK 1.8、Tomcat 8.5、MySQL 5.7為什么最省事這里直接給參數(shù)對(duì)比照著選能省下大量排錯(cuò)時(shí)間。組件推薦版本備選踩坑點(diǎn)JDK1.8Java 811老工程編譯級(jí)別如果是1.8用JDK 11要改Project Structure和Language levelTomcat8.59.0Tomcat 10起包名從javax改成jakarta老JavaWeb代碼啟動(dòng)直接報(bào)ClassNotFoundExceptionMySQL5.78.0用MySQL 8時(shí)驅(qū)動(dòng)版本要跟著換URL還要加serverTimezone參數(shù)IDEA2020.3及以上社區(qū)版社區(qū)版需要插件或使用外部Tomcat專業(yè)版自帶Tomcat集成為什么首選Java 8因?yàn)榻^大多數(shù)JavaWeb畢設(shè)源碼都是當(dāng)年用JDK 8寫的字節(jié)碼版本和IDEA默認(rèn)編譯級(jí)別能對(duì)上。JDK 11能跑但老項(xiàng)目里如果有JAXB相關(guān)依賴會(huì)缺包屬于自找麻煩。Tomcat 8.5兼容javax.*包Servlet 3.1規(guī)范是這套技術(shù)棧的舒適區(qū)。MySQL 5.7與老版mysql-connector-java的配合最沒有幺蛾子只要數(shù)據(jù)庫(kù)腳本本身沒有用到MySQL 8才有的語法就能直接導(dǎo)。如果你手里只有MySQL 8.0驅(qū)動(dòng)務(wù)必?fù)Q成8.0.xURL里加上serverTimezoneAsia/Shanghai否則第5章會(huì)專門講這個(gè)坑。3.2 導(dǎo)入數(shù)據(jù)庫(kù)用兩行命令把SQL腳本灌進(jìn)MySQL拿到gym.sql之類腳本后我一般不用Navicat的導(dǎo)入按鈕先用命令行導(dǎo)入。原因是命令行能直接看到報(bào)錯(cuò)圖形工具經(jīng)常把錯(cuò)誤吞到一個(gè)日志文件里反而增加定位成本。# 第一步創(chuàng)建數(shù)據(jù)庫(kù)指定utf8mb4字符集 mysql -uroot -p -e CREATE DATABASE IF NOT EXISTS gym_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; # 第二步把SQL腳本灌進(jìn)gym_db mysql -uroot -p -D gym_db gym.sql參數(shù)說明-u指定用戶名-p讓MySQL提示輸入密碼-e直接執(zhí)行后面的SQL語句。-D表示選擇當(dāng)前數(shù)據(jù)庫(kù)等價(jià)于USE gym_db。是shell重定向把文件內(nèi)容作為mysql命令的輸入。如果腳本第一行已經(jīng)有CREATE DATABASE和USE語句第二步可以簡(jiǎn)化成mysql -uroot -p gym.sql省得指定-D。導(dǎo)入完成后用mysql -uroot -p -D gym_db -e SHOW TABLES;檢查表是否齊全數(shù)一下表數(shù)量跟說明文檔對(duì)得上再進(jìn)入下一步。注意SQL文件路徑如果包含空格或中文建議把文件先移到D:/gym.sql這類簡(jiǎn)單路徑否則某些Windows終端會(huì)把路徑截?cái)鄬?dǎo)致導(dǎo)入失敗。3.3 修改數(shù)據(jù)源與數(shù)據(jù)庫(kù)連接池db.properties里每個(gè)參數(shù)都有用傳統(tǒng)的JavaWeb工程通常用一個(gè)db.properties或jdbc.properties保存數(shù)據(jù)庫(kù)配置再用一個(gè)工具類讀取。下面是最常見的內(nèi)容jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/gym_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456 jdbc.initialSize5 jdbc.maxActive20逐個(gè)說參數(shù)。jdbc.driver對(duì)應(yīng)MySQL驅(qū)動(dòng)的類名5.x版本用com.mysql.jdbc.Driver8.x版本用com.mysql.cj.jdbc.Driver寫錯(cuò)啟動(dòng)就報(bào)ClassNotFoundException。jdbc.url里的useUnicodetruecharacterEncodingutf8是解決中文亂碼的關(guān)鍵useSSLfalse是關(guān)閉SSL握手否則MySQL 8會(huì)默認(rèn)嘗試SSL導(dǎo)致連接變慢。serverTimezoneAsia/Shanghai是MySQL 8.0以上版本必須加的時(shí)區(qū)參數(shù)。最后的initialSize和maxActive是連接池初始連接數(shù)和最大連接數(shù)沒有連接池的工程這兩個(gè)參數(shù)不會(huì)出現(xiàn)。如果項(xiàng)目里用的是Druid或者commons-dbcp的連接池工具類的初始化方式不同但properties里這幾個(gè)值一般都要保留。配套的JDBC工具類常見寫法是public class DbUtil { private static final Properties PROPS new Properties(); static { try (InputStream in DbUtil.class.getClassLoader() .getResourceAsStream(db.properties)) { PROPS.load(in); Class.forName(PROPS.getProperty(jdbc.driver)); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { // 每次調(diào)用從DriverManager拿新連接適合demo生產(chǎn)建議改用連接池 return DriverManager.getConnection( PROPS.getProperty(jdbc.url), PROPS.getProperty(jdbc.username), PROPS.getProperty(jdbc.password)); } }這段代碼的核心邏輯在靜態(tài)代碼塊里加載properties文件、注冊(cè)驅(qū)動(dòng)保證整個(gè)應(yīng)用只初始化一次。getResourceAsStream(db.properties)從classpath根目錄找文件所以db.properties要放在src目錄或resources目錄下而不是WEB-INF里面。代碼里注釋也寫了每次DriverManager.getConnection在并發(fā)高時(shí)是性能瓶頸畢設(shè)如果被問到“怎么優(yōu)化”答案就是換Druid或C3P0連接池把getConnection改成從池里借連接。3.4 在IDEA里配置Tomcat并部署JavaWeb工程這是“idea運(yùn)行javaweb項(xiàng)目配置”的熱搜問題也是最容易卡殼的一步。老工程部署步驟相對(duì)固定打開IDEA用File - Open直接選中解壓后的工程目錄等待索引完成。確認(rèn)Project Structure里的SDK是1.8Language level是8。如果工程不是Maven項(xiàng)目還需要在Project Structure - Modules - Dependencies里把WEB-INF/lib下的jar全部加入。新建Tomcat運(yùn)行配置Run - Edit Configurations - - Tomcat Server - Local。如果IDEA里沒有Tomcat選項(xiàng)進(jìn)入Settings - Build, Execution, Deployment - Application Servers點(diǎn)把本地的Tomcat安裝目錄指定進(jìn)去。在Deployment標(biāo)簽頁(yè)點(diǎn) - Artifact選項(xiàng)目名:war exploded。這里建議用war exploded改完JSP不用重新打包刷新頁(yè)面就能看到變化。把Server標(biāo)簽頁(yè)里的HTTP port改成8080或者改成一個(gè)不容易沖突的端口。點(diǎn)擊運(yùn)行瀏覽器訪問http://localhost:8080/項(xiàng)目名/login.jsp。配置完成后不要急著點(diǎn)運(yùn)行先單獨(dú)跑一下連接檢查把數(shù)據(jù)庫(kù)配置錯(cuò)誤和Tomcat部署問題分開日志里才不會(huì)什么都混雜在一起public class CheckDb { public static void main(String[] args) { // 只驗(yàn)證數(shù)據(jù)庫(kù)連通性不啟動(dòng)Tomcat try (Connection conn DbUtil.getConnection(); Statement st conn.createStatement()) { ResultSet rs st.executeQuery(SELECT COUNT(*) AS total FROM member); rs.next(); System.out.println(member count rs.getInt(total)); } catch (Exception e) { // 如果這里報(bào)錯(cuò)先檢查db.properties再檢查MySQL服務(wù)是否啟動(dòng) e.printStackTrace(); } } }這個(gè)類的價(jià)值是把“數(shù)據(jù)庫(kù)連不上”和“項(xiàng)目部署不上”兩個(gè)變量分離開。運(yùn)行它如果能打出member count 0說明JDBC、驅(qū)動(dòng)、連接URL、數(shù)據(jù)庫(kù)腳本全都沒問題問題只可能出在Tomcat側(cè)。如果它報(bào)錯(cuò)就先別碰Tomcat回頭查數(shù)據(jù)源配置。這個(gè)習(xí)慣能幫你把運(yùn)行JavaWeb項(xiàng)目時(shí)的黑匣子至少拆成兩塊。4. 業(yè)務(wù)模塊動(dòng)手改造從會(huì)員到期提醒到課程預(yù)約事務(wù)環(huán)境跑通只是開始畢設(shè)答辯和實(shí)際維護(hù)真正看重的是業(yè)務(wù)代碼。這一章挑健身房管理系統(tǒng)里三個(gè)最常被問到的業(yè)務(wù)點(diǎn)來動(dòng)手改到期提醒、課程預(yù)約搶名額、按月統(tǒng)計(jì)收入。4.1 會(huì)員到期提醒日期函數(shù)與PreparedStatement配合會(huì)員模塊除了增刪改查最常見的業(yè)務(wù)是“查出一周內(nèi)到期的會(huì)員”。這個(gè)功能用SQL的日期函數(shù)最省事SELECT id, username, phone, card_type, card_end FROM member WHERE status 1 AND card_end BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 7 DAY) ORDER BY card_end ASC;CURDATE()返回當(dāng)前日期INTERVAL 7 DAY表示7天區(qū)間。BETWEEN配合DATE_ADD可以查出從今天開始到未來7天內(nèi)到期的會(huì)員嚴(yán)格意義上是8天窗口今天到第7天。如果你只想要未來7天可以把區(qū)間改成 CURDATE() AND card_end DATE_ADD(CURDATE(), INTERVAL 7 DAY)。在Java代碼里通常不會(huì)把7寫死在SQL里而是用占位符public ListMember listExpiringMembers(int days) throws SQLException { // 用占位符?代替天數(shù)避免拼接SQL導(dǎo)致注入風(fēng)險(xiǎn) String sql SELECT id, username, phone, card_end FROM member WHERE status 1 AND card_end BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL ? DAY); ListMember list new ArrayList(); try (Connection conn DbUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, days); // 參數(shù)索引從1開始 try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Member m new Member(); m.setId(rs.getInt(id)); m.setUsername(rs.getString(username)); m.setPhone(rs.getString(phone)); m.setCardEnd(rs.getDate(card_end)); list.add(m); } } } return list; }這里用PreparedStatement而不是Statement有兩個(gè)原因一是不用手動(dòng)拼SQL避免會(huì)員名字里的單引號(hào)把SQL弄壞二是數(shù)據(jù)庫(kù)可以復(fù)用執(zhí)行計(jì)劃循環(huán)調(diào)用多條SQL時(shí)性能更好。調(diào)用時(shí)傳入7由Service層字段判斷是“一周內(nèi)”還是“三天內(nèi)”。如果你希望提醒更準(zhǔn)確可以把DATE_ADD的間隔改成DATEDIFF(card_end, CURDATE()) 7這是很多后掛的報(bào)表SQL里常見的寫法二者效果等價(jià)。4.2 課程預(yù)約防超賣事務(wù)與唯一索引一起上健身房課程預(yù)約最容易出的問題是一個(gè)熱門課被重復(fù)預(yù)約或超員。很多人只做了前端不可點(diǎn)擊就沒管后端實(shí)際上并發(fā)場(chǎng)景下兩個(gè)請(qǐng)求都能通過檢查導(dǎo)致超賣。數(shù)據(jù)庫(kù)層已經(jīng)加了UNIQUE KEY uk_course_memberJava這邊還要把“扣名額”和“插入預(yù)約記錄”放進(jìn)同一個(gè)事務(wù)public boolean bookCourse(int courseId, int memberId) throws SQLException { Connection conn DbUtil.getConnection(); PreparedStatement deduct null; PreparedStatement insert null; try { conn.setAutoCommit(false); // 開啟事務(wù) // 先扣減剩余名額只影響remain0的行返回0說明沒名額了 String deductSql UPDATE course SET remain remain - 1 WHERE id ? AND remain 0; deduct conn.prepareStatement(deductSql); deduct.setInt(1, courseId); int rows deduct.executeUpdate(); if (rows 0) { conn.rollback(); return false; } // 再插入預(yù)約記錄唯一索引會(huì)在重復(fù)預(yù)約時(shí)拋異常觸發(fā)回滾 String insertSql INSERT INTO booking(course_id, member_id) VALUES(?, ?); insert conn.prepareStatement(insertSql); insert.setInt(1, courseId); insert.setInt(2, memberId); insert.executeUpdate(); conn.commit(); return true; } catch (SQLException e) { conn.rollback(); // 回滾已扣減的剩余名額 throw e; } finally { if (insert ! null) insert.close(); if (deduct ! null) deduct.close(); conn.setAutoCommit(true); conn.close(); } }這段代碼的次序是有講究的。先執(zhí)行UPDATE扣減并且?guī)螦ND remain 0讓數(shù)據(jù)庫(kù)自己判斷剩余名額避免“先SELECT再UPDATE”的競(jìng)態(tài)窗口。executeUpdate返回影響行數(shù)0就說明當(dāng)前沒有余量。插入預(yù)約記錄時(shí)如果同一人重復(fù)預(yù)約UNIQUE KEY會(huì)拋出重復(fù)鍵異常進(jìn)入rollback()把剛才扣掉的名額還回來。最后finally里把a(bǔ)utoCommit設(shè)回true并關(guān)閉連接不然連接歸還到連接池后可能殘留事務(wù)狀態(tài)。提示如果你看到的源碼是先查再看即先SELECT確認(rèn)名額再INSERT這在并發(fā)下會(huì)翻車。兩個(gè)請(qǐng)求同時(shí)通過SELECT都看到remain1然后兩個(gè)都執(zhí)行INSERT最終實(shí)際超約。務(wù)必改成“UPDATE條件里帶剩余名額判斷”。4.3 收入月報(bào)GROUP BY配合日期格式化統(tǒng)計(jì)營(yíng)業(yè)額健身房管理系統(tǒng)的后臺(tái)通常有一個(gè)“經(jīng)營(yíng)報(bào)表”統(tǒng)計(jì)每月賣卡和課程收入。核心SQL很經(jīng)典SELECT DATE_FORMAT(create_time, %Y-%m) AS month, COUNT(*) AS order_count, SUM(pay_amount) AS total_income FROM card_order WHERE pay_status 1 GROUP BY DATE_FORMAT(create_time, %Y-%m) ORDER BY month DESC;DATE_FORMAT(create_time, %Y-%m)把2025-03-15 10:30:00格式化成2025-03這樣GROUP BY就能把同月訂單聚合到一起。%Y是四位年份%m是兩位月份。如果寫成%y-%m年份會(huì)變成25和月份組合后排序就會(huì)錯(cuò)亂。SUM(pay_amount)統(tǒng)計(jì)已支付訂單的金額總和數(shù)據(jù)來自卡券訂單表。ORDER BY month DESC讓最新的月份排在最前面。這段SQL的參數(shù)變化點(diǎn)主要是統(tǒng)計(jì)粒度如果想看日?qǐng)?bào)把%Y-%m改成%Y-%m-%d想看按教練維度的業(yè)務(wù)量把DATE_FORMAT(create_time, %Y-%m)換成coach_name。畢設(shè)答辯時(shí)面試官大概率會(huì)問“如果數(shù)據(jù)量大怎么優(yōu)化”可以先答“給create_time建索引”再看是否需要按年份做分表或匯總表。實(shí)際工程很少直接在業(yè)務(wù)表上跑這種聚合統(tǒng)計(jì)而是定時(shí)任務(wù)把日?qǐng)?bào)寫進(jìn)統(tǒng)計(jì)表但畢設(shè)階段用這條SQL已經(jīng)足夠說明你懂?dāng)?shù)據(jù)庫(kù)分組和聚合。5. 常見問題排查五個(gè)從亂碼到端口占用的真坑這一章里的每一條都是真實(shí)跑項(xiàng)目時(shí)反復(fù)遇到的按照“現(xiàn)象、原因、解決”來寫遇到問題直接對(duì)號(hào)入座。5.1 導(dǎo)入SQL后頁(yè)面和數(shù)據(jù)庫(kù)全是中文亂碼現(xiàn)象登錄頁(yè)面能打開但頁(yè)面上會(huì)員姓名顯示成???或者數(shù)據(jù)庫(kù)里查出來是中文字符網(wǎng)頁(yè)上卻是亂碼。這個(gè)場(chǎng)景在Windows下極其常見幾乎每個(gè)用MySQL的人都撞過。原因字符集三個(gè)環(huán)節(jié)沒對(duì)齊。數(shù)據(jù)庫(kù)創(chuàng)建時(shí)用了默認(rèn)latin1或utf8JDBC連接URL沒帶characterEncodingutf8JSP頁(yè)面本身的pageEncoding也不是UTF-8。三處只要有一處不一致中文就會(huì)在寫入或讀取時(shí)變成亂碼。解決按順序檢查。第一步確認(rèn)數(shù)據(jù)庫(kù)是utf8mb4ALTER DATABASE gym_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;。第二步檢查db.properties里連接URL是否帶了useUnicodetruecharacterEncodingutf8沒有就補(bǔ)上。第三步確認(rèn)JSP文件頭有% page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 %。如果還是亂碼把IDEA右下角的文件編碼也切到UTF-8有的老工程源碼文件本身是GBK編碼讀取時(shí)按UTF-8解碼就亂這是IDE層面的坑和運(yùn)行環(huán)境無關(guān)。修復(fù)順序按數(shù)據(jù)庫(kù)、連接、頁(yè)面依次做別跳到中間瞎改不然越改越亂。5.2 Tomcat啟動(dòng)報(bào)ClassNotFoundException: javax.servlet.xxx現(xiàn)象把工程部署到Tomcat后啟動(dòng)日志里第一段異常就是ClassNotFoundException: javax.servlet.ServletException或者java.lang.NoClassDefFoundError: javax/servlet/http/HttpServlet。很多人第一反應(yīng)是缺jar往WEB-INF/lib里塞servlet-api.jar結(jié)果越塞越亂。原因新下載的Tomcat 10.x把命名空間從javax.*改成了jakarta.*老JavaWeb代碼里的javax.servlet包路徑完全找不到了。這不是缺依賴是Tomcat版本和Servlet規(guī)范不匹配往lib里塞舊jar也不能生效因?yàn)門omcat自己加載的servlet-api和lib里的jar沖突。解決換成Tomcat 8.5或9.0這是老畢設(shè)項(xiàng)目最兼容的容器。如果不想換就得把項(xiàng)目里所有import javax.servlet改成import jakarta.servlet還要把JSP的依賴也同步換掉工作量不小。所以我建議初始環(huán)境選型時(shí)直接鎖定Tomcat 8.5避免這個(gè)版本沖突。評(píng)論區(qū)里常見“Tomcat 9能跑嗎”的疑問答案是可以但要用支持Servlet 4.0規(guī)范的那版包名仍是javax。5.3 啟動(dòng)時(shí)Communications link failureMySQL 8的時(shí)區(qū)與SSL現(xiàn)象運(yùn)行連接檢查或啟動(dòng)Tomcat時(shí)日志報(bào)Communications link failure末尾帶The last packet successfully received from the server was ...。有時(shí)候連接前幾次成功過幾分鐘就斷。原因最常見的是MySQL 8.0默認(rèn)開啟了useSSLtrue而驅(qū)動(dòng)版本沒跟上其次是MySQL 8要求URL顯式指定時(shí)區(qū)不指定就用服務(wù)器默認(rèn)時(shí)區(qū)如果服務(wù)器時(shí)區(qū)是UTC客戶端和數(shù)據(jù)庫(kù)端時(shí)間就會(huì)出現(xiàn)偏移甚至直接拒絕連接。另一個(gè)可能是mysql-connector-java的版本還是5.1.x連不上MySQL 8的認(rèn)證插件。解決在JDBC URL上補(bǔ)參數(shù)?useSSLfalseserverTimezoneAsia/Shanghai。驅(qū)動(dòng)版本換成mysql-connector-java-8.0.x.jar。如果用的還是DriverManager啟動(dòng)時(shí)確保MySQL服務(wù)是開啟狀態(tài)Windows下看服務(wù)列表里的MySQLmacOS或Linux下用systemctl status mysql確認(rèn)。這里有一個(gè)排查技巧先用Navicat或命令行連一次同一個(gè)數(shù)據(jù)庫(kù)如果能連上但Java連不上問題一定在URL或驅(qū)動(dòng)版本如果命令行也連不上是MySQL服務(wù)或賬號(hào)權(quán)限問題別在Java代碼里浪費(fèi)時(shí)間。這條經(jīng)驗(yàn)我反復(fù)用過Java側(cè)的連接報(bào)錯(cuò)十有八九是配置和版本問題而不是網(wǎng)絡(luò)問題。5.4 8080端口被占用Tomcat起不來的真兇現(xiàn)象IDEA運(yùn)行Tomcat時(shí)日志出現(xiàn)Address already in use: JVM_Bind或者瀏覽器訪問8080打開的是別的頁(yè)面。這個(gè)場(chǎng)景在公司和學(xué)校機(jī)房特別多因?yàn)槠渌?wù)或之前IDE殘留的Tomcat實(shí)例還沒釋放端口。原因本機(jī)已經(jīng)有進(jìn)程監(jiān)聽8080端口。常見占用成功的情況包括另一個(gè)Tomcat實(shí)例還在后臺(tái)運(yùn)行、占用端口的程序、以及Windows下經(jīng)常見到的javaw.exe僵尸進(jìn)程。解決先確認(rèn)誰占用了端口。Windows下在命令行執(zhí)行netstat -ano | findstr 8080看到監(jiān)聽PID后用tasklist | findstr PID查是哪個(gè)進(jìn)程如果是遺留的javaw.exe直接taskkill /F /PID PID結(jié)束掉。如果不想動(dòng)進(jìn)程就改Tomcat端口。注意改端口要?jiǎng)觕onf/server.xml里的三處Server port8005 shutdownSHUTDOWN、Connector port8080、Connector port8009。只改8080不動(dòng)其他兩處也會(huì)出現(xiàn)“Tomcat能起但停不掉”的情況。IDEA里還容易忽略的一處是如果配置了Tomcat Server LocalServer標(biāo)簽頁(yè)的HTTP port和JMX port是兩個(gè)不同端口JMX端口沖突也會(huì)報(bào)錯(cuò)但報(bào)錯(cuò)信息里不會(huì)直接寫8080。遇到端口相關(guān)異常優(yōu)先看完整堆棧區(qū)分是Connector端口還是JMX端口。5.5 導(dǎo)出war包后JSP頁(yè)面打不開或樣式全丟現(xiàn)象在IDEA里運(yùn)行一切正常但導(dǎo)出war包再部署到另一臺(tái)Tomcat后登錄頁(yè)能顯示HTML但CSS和圖片全丟了或者點(diǎn)擊菜單直接404。這是一個(gè)很經(jīng)典的“打包即翻車”場(chǎng)景。原因IDEA的Artifacts配置里war包只打包了編譯后的class和部分web資源WEB-INF/lib下的第三方j(luò)ar沒有被打進(jìn)war。JSP文件如果沒放在web目錄下也會(huì)漏掉。靜態(tài)資源CSS、JS和圖片丟失通常是目錄引用問題JSP里用了/image/logo.png這種絕對(duì)路徑而部署后項(xiàng)目上下文路徑變成了/項(xiàng)目名/image/logo.png路徑自然就404了。解決在IDEA里打開Project Structure - Artifacts選中當(dāng)前的war exploded配置在Output Layout標(biāo)簽頁(yè)檢查WEB-INF/lib下是否有所有依賴jar。沒有的話右鍵Available Elements里的jar選擇Put into WEB-INF/lib。同時(shí)把web資源目錄完整加入artifact。路徑問題方面JSP里引用靜態(tài)資源建議用${pageContext.request.contextPath}/image/logo.png或者用base標(biāo)簽統(tǒng)一前綴。如果你打算把war部署到Linux服務(wù)器的Tomcat這些細(xì)節(jié)比代碼本身更容易讓你在演示當(dāng)天翻車。6. 給健身房管理系統(tǒng)加一個(gè)數(shù)據(jù)快照答辯演示前的回滾技巧所有改造和排錯(cuò)做完之后最后補(bǔ)一個(gè)我自己的習(xí)慣給這個(gè)系統(tǒng)做一個(gè)數(shù)據(jù)快照腳本。健身房管理系統(tǒng)里有會(huì)員、訂單、預(yù)約記錄演示之前你永遠(yuǎn)不知道評(píng)委或自己會(huì)不會(huì)手滑刪掉數(shù)據(jù)數(shù)據(jù)和代碼分開治理才能讓演示環(huán)境隨時(shí)重新來過。#!/bin/bash # 備份按日期生成SQL文件當(dāng)作后悔藥 mysqldump -uroot -p gym_db backup_$(date %Y%m%d_%H%M%S).sql # 恢復(fù)先刪庫(kù)再建庫(kù)避免舊數(shù)據(jù)殘留 mysql -uroot -p -e DROP DATABASE IF EXISTS gym_db; CREATE DATABASE gym_db DEFAULT CHARACTER SET utf8mb4; mysql -uroot -p -D gym_db backup_20250315_100000.sqlmysqldump的參數(shù)和mysql命令類似-u、-p指定賬號(hào)密碼后面跟庫(kù)名。備份文件名用date %Y%m%d_%H%M%S生成時(shí)間戳避免覆蓋上一次備份?;謴?fù)腳本里DROP DATABASE必須謹(jǐn)慎但演示環(huán)境里這是最快回到初始狀態(tài)的方式。如果你不想每次輸入密碼可以在命令里寫-p123456注意這種寫法不會(huì)隱藏密碼shell的history里會(huì)留下明文自己本機(jī)用可以演示機(jī)不建議。建議在正式答辯或匯報(bào)前跑一次完整恢復(fù)然后按照“會(huì)員辦卡 - 預(yù)約課程 - 查詢?nèi)請(qǐng)?bào)”的主流程走一遍。這套系統(tǒng)的表不多五張表以內(nèi)的工程手動(dòng)重置也麻煩腳本化以后恢復(fù)數(shù)據(jù)從十分鐘壓縮到十幾秒。我第一次帶人用這個(gè)工程時(shí)演示前直接在真實(shí)數(shù)據(jù)上改卡號(hào)把一條會(huì)員記錄改壞了又忘記錄原來的值最后只能重新導(dǎo)庫(kù)。從那以后我養(yǎng)成了每次演示前強(qiáng)制備份的習(xí)慣不管系統(tǒng)多簡(jiǎn)單先備份再動(dòng)手。希望這篇從拆包到排錯(cuò)的筆記能幫你在JavaWeb健身房管理系統(tǒng)這條路上少踩幾個(gè)坑也祝你的項(xiàng)目一次跑通。本文還有配套的精品資源點(diǎn)擊獲取