JavaWeb課程設(shè)計(jì):從數(shù)據(jù)建模到業(yè)務(wù)閉環(huán)的完整實(shí)踐指南)
1. 為什么校園卡系統(tǒng)能成為課程設(shè)計(jì)里的常青樹(shù)選題1.1 一個(gè)看似簡(jiǎn)單卻五臟俱全的業(yè)務(wù)場(chǎng)景每年到了課程設(shè)計(jì)和畢業(yè)設(shè)計(jì)的季節(jié)JavaWeb方向的選題翻來(lái)覆去就那么幾類(lèi)圖書(shū)管理系統(tǒng)、學(xué)生管理系統(tǒng)、網(wǎng)上商城、食堂點(diǎn)餐……而校園卡服務(wù)管理系統(tǒng)屬于其中辨識(shí)度很高的一類(lèi)。它不像商城那樣要處理復(fù)雜的購(gòu)物車(chē)和訂單狀態(tài)機(jī)也不像純粹的學(xué)生管理那樣只有簡(jiǎn)單的增刪改查它恰好卡在一個(gè)難度適中但又足夠完整的位置上。從用戶角度看校園卡系統(tǒng)涉及的是每個(gè)人每天都會(huì)遇到的場(chǎng)景食堂吃飯刷卡、超市消費(fèi)、自助充值、卡丟了掛失、補(bǔ)辦新卡、查詢余額和消費(fèi)流水。這些場(chǎng)景大家都有切身體驗(yàn)業(yè)務(wù)規(guī)則非常清晰不需要額外普及背景知識(shí)。從技術(shù)角度看它需要用戶權(quán)限區(qū)分學(xué)生/管理員、一張卡的狀態(tài)流轉(zhuǎn)正常/掛失/凍結(jié)/注銷(xiāo)、充值和消費(fèi)兩條資金流水線、以及基于時(shí)間維度的消費(fèi)統(tǒng)計(jì)報(bào)表。這些功能組合在一起幾乎把JavaWeb課程里要求的Servlet、JSP、Session、JDBC、事務(wù)、過(guò)濾器等知識(shí)點(diǎn)全部覆蓋了。我一直覺(jué)得課程設(shè)計(jì)選題最怕兩種一種是太簡(jiǎn)單兩張表就能做完答辯時(shí)三句話講完老師問(wèn)幾句就露餡另一種是太復(fù)雜比如做個(gè)帶支付網(wǎng)關(guān)的完整電商系統(tǒng)學(xué)生做到一半就卡死在某個(gè)環(huán)節(jié)。校園卡系統(tǒng)正好繞開(kāi)了這兩個(gè)極端——它的核心業(yè)務(wù)閉環(huán)是有限的但每一環(huán)都有值得展開(kāi)講的東西。1.2 這個(gè)選題能鍛煉到的核心能力如果你或者你帶的學(xué)生正在糾結(jié)選題我建議先想清楚做完這個(gè)項(xiàng)目后能回答出哪些為什么。校園卡管理系統(tǒng)不是背幾個(gè)CRUD就能糊弄過(guò)去的第一數(shù)據(jù)建模能力??ê蛯W(xué)生是什么關(guān)系一個(gè)學(xué)生可不可以有多張卡消費(fèi)流水和充值流水為什么不能合成一張表卡狀態(tài)應(yīng)該放在學(xué)生表里還是單獨(dú)一張卡表里這些問(wèn)題在寫(xiě)建表語(yǔ)句之前就要想明白而很多課程設(shè)計(jì)恰恰是在這里翻車(chē)的。第二業(yè)務(wù)閉環(huán)能力。掛失之后卡為什么立刻不能用了如果只是在某個(gè)查詢語(yǔ)句里加一個(gè)狀態(tài)判斷能不能保證所有入口都判斷到補(bǔ)辦新卡之后舊卡的余額怎么遷移流水記錄是保留還是合并這些邏輯不寫(xiě)代碼是發(fā)現(xiàn)不了的。第三項(xiàng)目交付能力。課程設(shè)計(jì)和畢業(yè)設(shè)計(jì)的最終成果是一份報(bào)告加一次答辯演示不只是代碼能跑。數(shù)據(jù)庫(kù)腳本能不能一鍵初始化、README能不能讓別人按著步驟跑起來(lái)、演示時(shí)先點(diǎn)哪個(gè)按鈕后點(diǎn)哪個(gè)按鈕這些非技術(shù)細(xì)節(jié)往往決定了最終評(píng)分。我自己見(jiàn)過(guò)太多這樣的案例代碼功能全做完了但MySQL的建庫(kù)腳本里寫(xiě)著中文注釋導(dǎo)致導(dǎo)入報(bào)錯(cuò)或者Tomcat的JDBC連接串寫(xiě)的是本機(jī)絕對(duì)路徑換臺(tái)機(jī)器就起不來(lái)。這類(lèi)問(wèn)題在答辯現(xiàn)場(chǎng)比功能缺陷更致命。所以這篇就按我平時(shí)幫人review課程設(shè)計(jì)項(xiàng)目的思路把從選型、建庫(kù)、寫(xiě)業(yè)務(wù)邏輯到打包交付的完整過(guò)程捋一遍。2. 技術(shù)選型從JSPServlet到SSM怎么選更穩(wěn)2.1 三種主流方案的真實(shí)差異JavaWeb方向的課程設(shè)計(jì)技術(shù)棧一般集中在下面三種里。很多學(xué)生會(huì)來(lái)問(wèn)我用哪個(gè)更好我的回答通常是先搞清楚你的課程要求是什么再考慮哪個(gè)更好。技術(shù)組合典型構(gòu)成適合場(chǎng)景主要痛點(diǎn)傳統(tǒng)JSPServletJDBCJSP頁(yè)面 Servlet控制器 JDBC操作MySQL部署在Tomcat課程明確要求原生技術(shù)、不允許用框架代碼重復(fù)多連接管理麻煩但最能講清楚原理SSM組合Spring SpringMVC MyBatis前端JSPBootstrap大多數(shù)課程設(shè)計(jì)和畢設(shè)的安全牌配置繁瑣版本兼容問(wèn)題多初學(xué)者容易陷入配置地獄Spring Boot MyBatisSpring Boot簡(jiǎn)化配置 MyBatis持久層畢業(yè)設(shè)計(jì)或想往工程化方向靠如果課程限定JavaWeb且老師要求手寫(xiě)Servlet可能不符要求如果你問(wèn)我的個(gè)人傾向在課程設(shè)計(jì)這個(gè)場(chǎng)景下SSMSpring SpringMVC MyBatis是性價(jià)比最高的選擇。原因有三一是它比純Servlet代碼量少很多學(xué)生能把精力放在業(yè)務(wù)邏輯而不是重復(fù)的請(qǐng)求轉(zhuǎn)發(fā)上二是市面上資料極多遇到問(wèn)題容易搜到解決方案三是它能往Spring Boot平滑遷移答辯時(shí)如果老師問(wèn)下一步怎么改進(jìn)你可以說(shuō)換成Spring Boot 微服務(wù)拆分這個(gè)回答幾乎是標(biāo)準(zhǔn)答案。但有個(gè)前提如果你的課程大綱白紙黑字寫(xiě)了使用JSPServlet不得使用框架那就老老實(shí)實(shí)寫(xiě)原生技術(shù)。這時(shí)候反而有個(gè)優(yōu)勢(shì)——你可以把JDBC連接池、過(guò)濾器實(shí)現(xiàn)登錄攔截、自定義標(biāo)簽這些點(diǎn)講得很深框架版反而不容易講出彩。2.2 前端部分和部署環(huán)境怎么配校園卡系統(tǒng)的前端不需要花哨但也不能太難看。常用的組合是JSP頁(yè)面 Bootstrap jQueryBootstrap負(fù)責(zé)柵格布局和表格樣式j(luò)Query負(fù)責(zé)Ajax局部刷新。要提醒一下Bootstrap 3和Bootstrap 5的寫(xiě)法差異很大如果你在網(wǎng)上找到一份比較老的項(xiàng)目照著抄可能會(huì)遇到class名對(duì)不上的問(wèn)題。建議直接鎖定一個(gè)版本比如Bootstrap 4.6它的兼容性和資料豐富度都比較平衡。環(huán)境方面JDK 1.8 Tomcat 8.5/9 MySQL 5.7是經(jīng)過(guò)大量驗(yàn)證的穩(wěn)定組合。這幾個(gè)版本之間有大量歷史項(xiàng)目的兼容性驗(yàn)證出現(xiàn)問(wèn)題容易排查。非要上JDK 17和最新版Tomcat不是不行但沒(méi)必要——課程設(shè)計(jì)講的是穩(wěn)定跑通不是追新。MySQL的字符集設(shè)置是個(gè)經(jīng)典坑。建庫(kù)時(shí)統(tǒng)一用utf8mb4連接串里也要帶上characterEncodingutf8參數(shù)。我見(jiàn)過(guò)不止一個(gè)項(xiàng)目表里寫(xiě)中文一切正常但Java往數(shù)據(jù)庫(kù)插入中文變成問(wèn)號(hào)最后發(fā)現(xiàn)是連接串沒(méi)加編碼參數(shù)。這類(lèi)問(wèn)題在答辯前夜出現(xiàn)心態(tài)很容易崩。2.3 一個(gè)很多人忽略的選型問(wèn)題要不要做前后端分離這幾年前后端分離的概念很流行有些學(xué)生想用Vue Spring Boot搞一套。我的態(tài)度很明確課程設(shè)計(jì)階段不建議這么做除非你的題目明確要求或你本身對(duì)Vue已經(jīng)很熟。原因很直接前后端分離意味著你至少要同時(shí)管理兩個(gè)工程、處理跨域問(wèn)題、把接口文檔寫(xiě)得足夠規(guī)范這些工作量對(duì)于三五周內(nèi)要完成的項(xiàng)目來(lái)說(shuō)太重了。傳統(tǒng)的JSP頁(yè)面由后端渲染天然沒(méi)有跨域問(wèn)題Session管理也簡(jiǎn)單演示時(shí)直接輸入地址就能看效果對(duì)于答辯場(chǎng)景是最穩(wěn)妥的。如果確實(shí)想展示一點(diǎn)現(xiàn)代感可以在SSM的基礎(chǔ)上用Ajax局部刷新代替整個(gè)頁(yè)面跳轉(zhuǎn)比如充值成功后不刷新整個(gè)頁(yè)面而是彈個(gè)提示然后局部更新余額數(shù)字。這已經(jīng)比大多數(shù)課程設(shè)計(jì)觀感好很多了。3. 數(shù)據(jù)庫(kù)設(shè)計(jì)先讓數(shù)據(jù)模型立得住3.1 核心表結(jié)構(gòu)的拆解思路校園卡系統(tǒng)最核心的表我建議控制在7張左右再多就是給自己找麻煩再少則功能撐不起來(lái)。它們分別是用戶表學(xué)生、管理員表、校園卡表、充值流水表、消費(fèi)流水表、掛失記錄表、操作日志表。用戶表和校園卡表的關(guān)系是重點(diǎn)。很多第一次做的人會(huì)把卡號(hào)直接作為用戶表的一個(gè)字段這看起來(lái)省事但會(huì)帶來(lái)兩個(gè)問(wèn)題第一補(bǔ)辦新卡時(shí)舊卡號(hào)要作廢、新卡號(hào)要寫(xiě)入你需要不停地改用戶表里的卡號(hào)字段歷史消費(fèi)流水卻掛在這張用戶表上新舊卡的數(shù)據(jù)就攪在一起了第二一個(gè)人的卡狀態(tài)正常/掛失/注銷(xiāo)如果放在用戶表里那這和用戶本身的屬性就耦合了比如一個(gè)用戶辦了新卡舊卡的狀態(tài)誰(shuí)來(lái)記錄更合理的做法是用戶表和卡表分開(kāi)。用戶表存學(xué)號(hào)、姓名、密碼、學(xué)院、專業(yè)等相對(duì)靜態(tài)的信息卡表存卡ID、卡號(hào)、所屬用戶、余額、狀態(tài)、發(fā)卡時(shí)間、注銷(xiāo)時(shí)間等動(dòng)態(tài)信息。這樣一個(gè)用戶對(duì)應(yīng)多條卡記錄當(dāng)前生效的卡用狀態(tài)字段標(biāo)出來(lái)舊卡記錄完整保留補(bǔ)辦的業(yè)務(wù)邏輯就順暢多了。3.2 流水表的設(shè)計(jì)這是一張表的兩種面孔充值和消費(fèi)是校園卡的兩條資金流水為什么我堅(jiān)持把它們拆成兩張表因?yàn)樗鼈兊臉I(yè)務(wù)語(yǔ)義完全不同充值只涉及卡的余額增加消費(fèi)涉及具體商戶、消費(fèi)窗口、消費(fèi)項(xiàng)目充值的操作方通常是持卡人自己或自助機(jī)消費(fèi)的操作方是商戶終端。拆成兩張表后查詢某張卡今天的充值總額和某食堂窗口今天的消費(fèi)總額都非常直觀。流水表的核心字段一定有流水號(hào)主鍵、卡ID、發(fā)生時(shí)間、金額、操作類(lèi)型、關(guān)聯(lián)商戶或窗口。金額字段用DECIMAL(10,2)絕對(duì)不要用FLOAT或DOUBLE——浮點(diǎn)數(shù)在累加時(shí)會(huì)產(chǎn)生精度誤差余額對(duì)不上賬是校園卡系統(tǒng)最嚴(yán)重的事故。這是基礎(chǔ)常識(shí)但每年都有人踩。掛失記錄表單獨(dú)建一張也有講究。它不只記錄什么時(shí)候掛失的還要記錄什么時(shí)候解除掛失的、補(bǔ)辦的新卡ID是什么、操作人是誰(shuí)。這張表實(shí)際上承擔(dān)了審計(jì)職責(zé)答辯時(shí)老師大概率會(huì)問(wèn)掛失之后你怎么追溯歷史操作這張表就是答案。3.3 表之間的關(guān)鍵約束和索引主外鍵關(guān)系是必須明確的但MySQL里我建議邏輯外鍵為主、物理外鍵為輔或者干脆不加物理外鍵只在應(yīng)用層維護(hù)一致性。原因不是偷懶而是課程設(shè)計(jì)的演示環(huán)境經(jīng)常要手動(dòng)改數(shù)據(jù)、調(diào)整種子數(shù)據(jù)物理外鍵會(huì)帶來(lái)很多無(wú)法刪除父表記錄的干擾。當(dāng)然應(yīng)用層一定要有校驗(yàn)否則孤兒數(shù)據(jù)會(huì)出現(xiàn)。索引方面流水表一定要對(duì)卡ID 發(fā)生時(shí)間建聯(lián)合索引。消費(fèi)流水表是這幾張表里數(shù)據(jù)增長(zhǎng)最快的沒(méi)有索引的話查某張卡最近10條消費(fèi)記錄會(huì)變成全表掃描數(shù)據(jù)量幾千條時(shí)不覺(jué)得演示時(shí)老師多翻幾頁(yè)就會(huì)卡頓。CREATE TABLE t_card ( card_id INT PRIMARY KEY AUTO_INCREMENT, card_no VARCHAR(20) NOT NULL UNIQUE COMMENT 卡號(hào), user_id INT NOT NULL COMMENT 關(guān)聯(lián)用戶ID, balance DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 當(dāng)前余額, card_status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 2掛失 3凍結(jié) 4注銷(xiāo), create_time DATETIME NOT NULL COMMENT 發(fā)卡時(shí)間, cancel_time DATETIME DEFAULT NULL COMMENT 注銷(xiāo)時(shí)間, INDEX idx_user (user_id), INDEX idx_status (card_status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT校園卡表; CREATE TABLE t_recharge_record ( record_id INT PRIMARY KEY AUTO_INCREMENT, card_id INT NOT NULL COMMENT 卡ID, recharge_no VARCHAR(32) NOT NULL UNIQUE COMMENT 充值單號(hào), amount DECIMAL(10,2) NOT NULL COMMENT 充值金額, recharge_time DATETIME NOT NULL COMMENT 充值時(shí)間, operator_type TINYINT NOT NULL COMMENT 1自助充值 2現(xiàn)金充值 3管理員代充, operator_id INT DEFAULT NULL COMMENT 操作人管理員ID, INDEX idx_card_time (card_id, recharge_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT充值流水表;這是我在實(shí)際項(xiàng)目中常用的建表方式字段注釋寫(xiě)清楚、狀態(tài)用TINYINT加注釋說(shuō)明、時(shí)間字段統(tǒng)一DATETIME。這套習(xí)慣在寫(xiě)報(bào)告時(shí)能省大量力氣每個(gè)字段的物理意義直接對(duì)應(yīng)報(bào)告里的數(shù)據(jù)字典表格。4. 核心業(yè)務(wù)閉環(huán)充值、消費(fèi)、掛失、補(bǔ)辦是怎么串起來(lái)的4.1 充值和消費(fèi)的金額校驗(yàn)鏈路充值的業(yè)務(wù)邏輯看起來(lái)簡(jiǎn)單——余額加金額記錄流水——但實(shí)際寫(xiě)代碼時(shí)要考慮幾個(gè)邊界情況。第一充值金額必須大于0且一般設(shè)置單次上限比如500元。第二充值操作必須包裹在事務(wù)里先插入充值流水再更新卡余額中間任何一步失敗都要回滾。如果先更新余額再插入流水流水插入失敗會(huì)導(dǎo)致余額和流水對(duì)不上如果先插流水再更新余額更新失敗則流水存在但余額沒(méi)變。只有把它們放在同一個(gè)數(shù)據(jù)庫(kù)事務(wù)里用Transactional或手動(dòng)控制connection.commit()才能保證兩條語(yǔ)句要么都成功要么都失敗。消費(fèi)的邏輯更需要注意余額不足的判斷。這里有個(gè)細(xì)節(jié)判斷余額是否充足和扣減余額必須使用UPDATE語(yǔ)句進(jìn)行原子操作不要先SELECT余額到Java代碼里判斷再UPDATE。因?yàn)閮蓚€(gè)請(qǐng)求同時(shí)到達(dá)時(shí)SELECT出來(lái)的值可能是過(guò)期的兩人都判斷余額足夠最后都扣款成功余額就變負(fù)了。正確的寫(xiě)法是UPDATE t_card SET balance balance - ? WHERE card_id ? AND balance ?;通過(guò)受影響行數(shù)判斷是否扣款成功。行數(shù)為1說(shuō)明余額充足、扣減成功行數(shù)為0說(shuō)明余額不足或卡不存在。這個(gè)寫(xiě)法是防并發(fā)超扣的基礎(chǔ)講清楚這一點(diǎn)答辯時(shí)能明顯加分。4.2 掛失狀態(tài)流轉(zhuǎn)與臟卡問(wèn)題掛失在整個(gè)系統(tǒng)里是一個(gè)狀態(tài)切換動(dòng)作把卡的狀態(tài)從正常改為掛失同時(shí)生成一條掛失記錄。聽(tīng)起來(lái)簡(jiǎn)單但真正的難點(diǎn)在于——校園卡的使用場(chǎng)景非常多你必須保證每一處使用到卡的地方都檢查了狀態(tài)。假設(shè)你的代碼里有這么幾處食堂消費(fèi)接口、超市消費(fèi)接口、自助查詢機(jī)接口、充值接口。如果只在消費(fèi)接口里檢查了卡狀態(tài)而充值接口沒(méi)檢查就會(huì)出現(xiàn)卡已經(jīng)掛失了卻能往里充值的荒誕現(xiàn)象。掛失的卡原則上不能充值萬(wàn)一被撿到的人拿去充值再消費(fèi)呢所以充值接口同樣要校驗(yàn)狀態(tài)。更隱蔽的問(wèn)題是掛失之后手里這張物理卡在某個(gè)離線消費(fèi)終端上還可能被刷出去。傳統(tǒng)校園卡的解決方案是消費(fèi)終端定期同步黑名單但這在課程設(shè)計(jì)里講不了那么深。你可以這樣處理在消費(fèi)接口里每次實(shí)時(shí)查庫(kù)校驗(yàn)狀態(tài)同時(shí)在報(bào)告里說(shuō)明真實(shí)場(chǎng)景下需要終端離線驗(yàn)證機(jī)制這里用實(shí)時(shí)校驗(yàn)簡(jiǎn)化處理。這樣既保證了演示效果又體現(xiàn)了業(yè)務(wù)認(rèn)知。補(bǔ)辦新卡是掛失的后續(xù)動(dòng)作。新卡需要復(fù)制舊卡的歷史信息嗎余額怎么辦我的處理方案是舊卡余額清零并標(biāo)記注銷(xiāo)新卡初始化余額為舊卡余額在事務(wù)內(nèi)先查舊卡余額、再創(chuàng)建新卡并把余額寫(xiě)入舊卡的消費(fèi)流水保留新卡生成自己的流水。這樣既保持了歷史數(shù)據(jù)的完整性又不需要修改舊流水記錄。這個(gè)設(shè)計(jì)在報(bào)告的數(shù)據(jù)流轉(zhuǎn)章節(jié)非常出彩。4.3 管理端的統(tǒng)計(jì)報(bào)表從聚合查詢到柱狀圖管理端的統(tǒng)計(jì)報(bào)表是整個(gè)系統(tǒng)觀感上的點(diǎn)睛之筆。最常見(jiàn)的需求是按天統(tǒng)計(jì)全校當(dāng)日的消費(fèi)總額、充值總額按食堂/窗口統(tǒng)計(jì)消費(fèi)排名按學(xué)院統(tǒng)計(jì)消費(fèi)分布。這些統(tǒng)計(jì)的本質(zhì)就是聚合查詢。比如統(tǒng)計(jì)每個(gè)食堂窗口最近7天的消費(fèi)總額SELECT c.window_name, DATE(cr.consum_time) AS day, SUM(cr.amount) AS total_amount FROM t_consum_record cr JOIN t_window c ON cr.window_id c.window_id WHERE cr.consum_time DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY c.window_id, DATE(cr.consum_time) ORDER BY total_amount DESC;拿到聚合結(jié)果后前端用jQuery配合圖表庫(kù)比如ECharts畫(huà)柱狀圖和折線圖。這一塊不用做得很復(fù)雜一張柱狀圖、一張折線圖就足夠撐起管理端的面子了。但要注意圖表庫(kù)引入要提前測(cè)試ECharts的CDN在部分校園網(wǎng)絡(luò)環(huán)境下可能加載慢建議把JS文件下載到本地項(xiàng)目的webapp目錄下。這個(gè)細(xì)節(jié)我反復(fù)強(qiáng)調(diào)過(guò)因?yàn)橛H眼見(jiàn)過(guò)答辯時(shí)圖表加載不出來(lái)的尷尬場(chǎng)面。5. 寫(xiě)代碼和走通流程時(shí)最容易翻車(chē)的幾個(gè)地方5.1 中文亂碼、路徑跳轉(zhuǎn)、事務(wù)失效三大坑這三個(gè)問(wèn)題幾乎是JavaWeb課程設(shè)計(jì)里的三座大山我按踩坑頻率排序說(shuō)一下。中文亂碼的源頭往往不止一處。JSP頁(yè)面要設(shè)置pageEncodingHTML要設(shè)置charsetServlet接收請(qǐng)求參數(shù)時(shí)要設(shè)置request.setCharacterEncoding(UTF-8)數(shù)據(jù)庫(kù)連接串要帶編碼參數(shù)。這些地方的編碼必須統(tǒng)一任何一處漏掉都會(huì)在某個(gè)特定操作上暴露問(wèn)題——最常見(jiàn)的就是表單提交中文姓名變成問(wèn)號(hào)。建議在項(xiàng)目初期寫(xiě)一個(gè)統(tǒng)一的字符編碼過(guò)濾器所有請(qǐng)求都過(guò)一遍UTF-8。WebFilter(/*) public class EncodingFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { request.setCharacterEncoding(UTF-8); response.setCharacterEncoding(UTF-8); chain.doFilter(request, response); } }路徑跳轉(zhuǎn)的問(wèn)題集中在兩個(gè)方面一是JSP頁(yè)面里CSS和JS文件的引入用了絕對(duì)路徑或相對(duì)路徑導(dǎo)致在二級(jí)目錄下的頁(yè)面樣式丟失二是Servlet的forward和redirect用混導(dǎo)致刷新頁(yè)面時(shí)表單重復(fù)提交。統(tǒng)一的解決辦法是頁(yè)面里用${pageContext.request.contextPath}拼接靜態(tài)資源路徑Servlet跳轉(zhuǎn)一律用redirect并攜帶必要參數(shù)不要在doPost里直接forward到結(jié)果頁(yè)面。事務(wù)失效是SSM項(xiàng)目里比較隱蔽的坑。Transactional注解默認(rèn)只在拋出RuntimeException時(shí)回滾如果代碼里捕獲了異常并返回一個(gè)錯(cuò)誤提示對(duì)象事務(wù)是不會(huì)回滾的。我見(jiàn)過(guò)很多學(xué)生寫(xiě)這樣的代碼try { cardService.recharge(...); } catch (Exception e) { // 記了日志但沒(méi)拋出去 return 充值失敗; }充值流水插入了余額更新卻被catch吞掉了異常事務(wù)照樣提交最后數(shù)據(jù)不一致。正確的做法是在catch里手動(dòng)標(biāo)記回滾或把異常重新拋出讓事務(wù)管理器感知到失敗。5.2 演示用的測(cè)試數(shù)據(jù)怎么造才像樣很多項(xiàng)目的演示效果差不是功能有問(wèn)題而是測(cè)試數(shù)據(jù)太假。每個(gè)學(xué)生的卡里都是100.00元整消費(fèi)流水全是同一天刷的統(tǒng)計(jì)報(bào)表畫(huà)出來(lái)就是幾條光禿禿的橫線——老師看著就沒(méi)興趣。我的建議是寫(xiě)一個(gè)init_data.sql腳本專門(mén)用于生成看起來(lái)真實(shí)的數(shù)據(jù)。學(xué)生至少20個(gè)名字用百家姓學(xué)院專業(yè)多樣化校園卡余額要有零有整比如128.50、86.00、250.75消費(fèi)流水要覆蓋最近30天每天每人至少3條消費(fèi)記錄金額從1.5到25不等充值記錄和消費(fèi)記錄在時(shí)間上要對(duì)應(yīng)得上比如月初充值、月中消費(fèi)遞減。造數(shù)據(jù)時(shí)有個(gè)技巧用MySQL的DATE_SUB和RAND()函數(shù)批量生成流水記錄可以一次插入幾百條不用手動(dòng)逐條寫(xiě)。這些數(shù)據(jù)的分布規(guī)律直接決定統(tǒng)計(jì)報(bào)表的畫(huà)質(zhì)一定要花半小時(shí)精心設(shè)計(jì)。5.3 報(bào)告和代碼的對(duì)應(yīng)關(guān)系課程設(shè)計(jì)的報(bào)告通常要求有需求分析、系統(tǒng)設(shè)計(jì)、數(shù)據(jù)庫(kù)設(shè)計(jì)、系統(tǒng)實(shí)現(xiàn)、測(cè)試這幾章。我見(jiàn)過(guò)最普遍的問(wèn)題是報(bào)告畫(huà)了架構(gòu)圖、寫(xiě)了模塊說(shuō)明但代碼里的包結(jié)構(gòu)和報(bào)告里的模塊名對(duì)不上數(shù)據(jù)庫(kù)設(shè)計(jì)截圖和實(shí)際SQL腳本不一致。這個(gè)問(wèn)題很好解決——先定包結(jié)構(gòu)再寫(xiě)報(bào)告的目錄結(jié)構(gòu)。比如你的Java包就按controller、service、dao、entity、filter、util來(lái)分報(bào)告的系統(tǒng)設(shè)計(jì)章節(jié)就完全按同一個(gè)順序講數(shù)據(jù)庫(kù)設(shè)計(jì)章節(jié)里貼的表結(jié)構(gòu)直接截圖自數(shù)據(jù)庫(kù)客戶端而不是手畫(huà)一個(gè)看起來(lái)像的表。報(bào)告先做目錄代碼按目錄來(lái)寫(xiě)兩邊永遠(yuǎn)對(duì)齊。測(cè)試章節(jié)也有講究。不要只寫(xiě)系統(tǒng)運(yùn)行正常要寫(xiě)具體的測(cè)試用例輸入什么數(shù)據(jù)、預(yù)期什么結(jié)果、實(shí)際什么結(jié)果。比如輸入余額為5元的卡消費(fèi)8元預(yù)期提示余額不足不扣款實(shí)際提示正確且余額未變。這類(lèi)測(cè)試用例表既是報(bào)告加分項(xiàng)也能反向幫你驗(yàn)證代碼邏輯一舉兩得。6. 交付篇源碼、數(shù)據(jù)庫(kù)、文檔怎么組織才加分6.1 目錄結(jié)構(gòu)的規(guī)范和注釋的尺度交付項(xiàng)目的時(shí)候壓縮包里的目錄結(jié)構(gòu)決定了別人對(duì)你的第一印象。我建議的最少組織方式campus-card-system/ ├── README.md # 項(xiàng)目說(shuō)明、運(yùn)行步驟、環(huán)境要求 ├── sql/ │ └── init_db.sql # 建庫(kù)建表腳本 │ └── init_data.sql # 測(cè)試數(shù)據(jù)腳本 ├── docs/ │ └── 課程設(shè)計(jì)報(bào)告.docx ├── src/ │ ├── main/ │ │ ├── java/ # 按包分層的Java源碼 │ │ └── webapp/ # JSP頁(yè)面、靜態(tài)資源 │ └── test/ # 選做基礎(chǔ)單元測(cè)試 └── README-source.md # 源碼使用說(shuō)明README.md是很多人忽視的文件。它不需要長(zhǎng)但必須讓別人照著三步能跑起來(lái)第一步導(dǎo)入init_db.sql建庫(kù)第二步修改jdbc.properties里的數(shù)據(jù)庫(kù)賬號(hào)密碼第三步用IDEA導(dǎo)入項(xiàng)目并配置Tomcat啟動(dòng)。如果這三步里有任何一步不明確項(xiàng)目在別人手里就基本跑不起來(lái)了。注釋的尺度我不建議寫(xiě)滿每行代碼。好的注釋是給關(guān)鍵業(yè)務(wù)邏輯寫(xiě)為什么不是給每一行寫(xiě)做了什么。比如充值事務(wù)方法上寫(xiě)充值流水與余額更新必須同事務(wù)失敗回滾比在balance balance amount旁邊寫(xiě)這里給余額加上充值金額有用得多。6.2 數(shù)據(jù)庫(kù)初始化腳本的三個(gè)細(xì)節(jié)init_db.sql必須做到雙擊執(zhí)行不出錯(cuò)這里有幾個(gè)容易翻車(chē)的細(xì)節(jié)。第一腳本開(kāi)頭要DROP DATABASE IF EXISTS campus_card; CREATE DATABASE campus_card;保證可以重復(fù)執(zhí)行而不會(huì)因?yàn)閹?kù)已存在而報(bào)錯(cuò)。第二腳本里不要用存儲(chǔ)過(guò)程和復(fù)雜的觸發(fā)器——課程設(shè)計(jì)的演示環(huán)境大概率是重裝的MySQL越簡(jiǎn)單的SQL越不容易出兼容問(wèn)題。第三在腳本末尾加上幾條SELECT驗(yàn)證語(yǔ)句比如統(tǒng)計(jì)學(xué)生數(shù)量、卡數(shù)量執(zhí)行完一眼就能看出數(shù)據(jù)是否正常。測(cè)試數(shù)據(jù)腳本init_data.sql要和建表腳本分開(kāi)原因是答辯老師可能想看干凈的表結(jié)構(gòu)和有數(shù)據(jù)的演示效果兩種情況分開(kāi)的話隨時(shí)可以切換。6.3 答辯前最有用的三條建議課程設(shè)計(jì)答辯的核心不是代碼講得多么深而是讓老師在三到五分鐘內(nèi)get到他真的做出來(lái)了、他真的理解了。三條建議都是我反復(fù)強(qiáng)調(diào)的第一條準(zhǔn)備一條黃金演示路徑。從學(xué)生登錄開(kāi)始依次展示查詢余額和最近消費(fèi)記錄、模擬消費(fèi)、充值、掛失、管理員審核掛失、補(bǔ)辦新卡。每個(gè)操作前想好要點(diǎn)哪里不要現(xiàn)場(chǎng)現(xiàn)找菜單。掛失之后最好真實(shí)驗(yàn)證一下掛失狀態(tài)下消費(fèi)會(huì)被拒絕這個(gè)小細(xì)節(jié)能讓老師對(duì)你的業(yè)務(wù)完成度印象深刻。第二條把數(shù)據(jù)庫(kù)窗口開(kāi)著。演示時(shí)切換到數(shù)據(jù)庫(kù)管理工具刷新流水表給老師看數(shù)據(jù)確實(shí)寫(xiě)進(jìn)去了——這比口頭說(shuō)我們的系統(tǒng)能記錄流水有力得多。很多同學(xué)演示時(shí)只盯著瀏覽器頁(yè)面老師根本看不到數(shù)據(jù)落庫(kù)的過(guò)程評(píng)分時(shí)對(duì)系統(tǒng)的完整性就打折扣。第三條準(zhǔn)備一個(gè)如果老師問(wèn)了不會(huì)的問(wèn)題的應(yīng)急預(yù)案。比如老師問(wèn)你這個(gè)系統(tǒng)怎么防SQL注入你應(yīng)該能答MyBatis的#{}預(yù)編譯參數(shù)或JDBC的PreparedStatement并且現(xiàn)場(chǎng)指一下代碼。再比如如果同一張卡在多個(gè)窗口同時(shí)消費(fèi)怎么辦就能把上面說(shuō)的原子UPDATE方案講一遍。這些其實(shí)都是你自己寫(xiě)代碼時(shí)一定處理過(guò)的問(wèn)題只要提前組織好語(yǔ)言就行。最后再分享一個(gè)小體會(huì)。校園卡服務(wù)管理系統(tǒng)這個(gè)題目做了這么多年還沒(méi)過(guò)時(shí)是因?yàn)樗岩粋€(gè)真實(shí)系統(tǒng)的核心邏輯壓縮到了課程設(shè)計(jì)的體量里。做完它你學(xué)到的不是某個(gè)框架的API而是從建表到業(yè)務(wù)閉環(huán)再到交付演示的一整條鏈路。這份經(jīng)驗(yàn)對(duì)后續(xù)找工作、做畢設(shè)、接小項(xiàng)目都有直接幫助。如果你正在做這個(gè)題目遇到卡住的地方優(yōu)先從數(shù)據(jù)庫(kù)事務(wù)和狀態(tài)流轉(zhuǎn)兩個(gè)角度去排查——很多時(shí)候代碼沒(méi)錯(cuò)是數(shù)據(jù)模型和業(yè)務(wù)規(guī)則沒(méi)對(duì)齊。