生選課管理系統(tǒng):從業(yè)務(wù)設(shè)計到并發(fā)控制的完整實踐)
每年到了畢業(yè)設(shè)計選題季技術(shù)社區(qū)里總會出現(xiàn)同一個問題“Java 畢設(shè)選什么題好”而“基于 SpringBoot 的學(xué)生選課管理系統(tǒng)”幾乎是所有候選列表里的常客。乍一看這個題目有點老套——選課管理網(wǎng)上源碼一大把還有什么好研究的但如果你真的動手做過一版就會發(fā)現(xiàn)事情沒那么簡單課程時間沖突怎么判斷選課人數(shù)會不會超并發(fā)情況下會不會有人同時搶到同一門課的最后一個名額退課后名額要不要釋放這些看起來很小的問題每一個都能讓一個沒有任何項目經(jīng)驗的人卡上好幾天。這篇文章不打算復(fù)讀任何一份源碼的每一行而是想聊透一件事以 SpringBoot 學(xué)生選課管理系統(tǒng)為例一個畢設(shè)項目從選題、拆需求、設(shè)計表、寫業(yè)務(wù)、做文檔到準備答辯完整走下來到底在練什么、容易死在哪、怎么避免。如果你正在做這個選題或者正在為一套網(wǎng)上找來的源碼做二次開發(fā)這里面的思路應(yīng)該能幫你少走不少彎路。1. 為什么“選課管理系統(tǒng)”能成為畢設(shè)選題常青樹1.1 表面上是業(yè)務(wù)系統(tǒng)實際上是軟件開發(fā)全流程的縮影選課管理系統(tǒng)在技術(shù)上沒有太高的天花板但它有一個其他題目很難替代的優(yōu)勢業(yè)務(wù)邏輯足夠清晰又能覆蓋軟件開發(fā)的大多數(shù)關(guān)鍵環(huán)節(jié)。需求階段你需要和學(xué)生、教師、管理員三類角色打交道設(shè)計階段你需要畫用例圖、ER 圖、數(shù)據(jù)庫表結(jié)構(gòu)實現(xiàn)階段你要寫增刪改查、寫事務(wù)、寫校驗、寫權(quán)限控制測試階段你要模擬選課沖突、退課、滿員等多種情況最后還要寫出一份論文文檔把“為什么這樣設(shè)計”講清楚。換句話說這不是在做一套業(yè)務(wù)系統(tǒng)而是在做一次完整的軟件工程演練。這個判斷基本定義了我對這類題目的態(tài)度它的價值不在“功能多新穎”而在“流程很完整”。1.2 這類系統(tǒng)的學(xué)習(xí)價值在于邊界清晰、反饋直接選課系統(tǒng)的業(yè)務(wù)邊界非常明確學(xué)生、課程、選課記錄。它不像電商系統(tǒng)那樣牽扯支付、庫存、物流、推薦一大堆外部依賴也不像內(nèi)容管理系統(tǒng)那樣需求可以無限膨脹。你可以在兩周內(nèi)跑通一個最小可用版本也能在兩個月里往里面加權(quán)限、加日志、加 Redis、加消息隊列。可進可退這是它作為畢設(shè)題目最大的優(yōu)點。對于剛開始接觸真實項目的同學(xué)來說“反饋直接”也非常重要。寫完選課接口立刻通過接口工具驗證選課是否成功、事務(wù)是否回滾、數(shù)據(jù)是否正確。這種即時反饋能幫助建立程序調(diào)試的直覺猜測哪里出了問題驗證再修正再驗證。這個循環(huán)本身就是做工程的核心能力。1.3 什么情況下不建議選這個題目反過來也要說清楚。如果你從一開始就打算完全靠網(wǎng)上源碼“拼”出一個項目連數(shù)據(jù)庫表結(jié)構(gòu)都看不懂那選這個題目反而會放大問題——因為答辯老師看到這種題目實在太多了他們非常清楚哪些點值得深挖。一旦被問到“為什么選課記錄表要單獨建一張”“并發(fā)時怎么防止超選”答不上來項目做得再花哨也沒有意義。所以這個題目適合愿意真正動手跑一遍的人不適合只想交差的人。如果你現(xiàn)在沒時間或者沒有興趣理解業(yè)務(wù)邏輯我建議換個更簡單、更生的題目。選課系統(tǒng)太常見了常見到老師一眼就能看出你是不是真的懂。2. 動手之前先把選課系統(tǒng)的業(yè)務(wù)邊界畫清楚2.1 角色與權(quán)限三類賬號各管什么一個標準的選課管理系統(tǒng)至少有三類角色學(xué)生查看課程列表、選課、退課、查看已選課程和成績。教師查看自己教授的課程、查看選課學(xué)生名單、錄入成績。管理員管理學(xué)生和教師賬號、維護課程信息、設(shè)置選課時間窗口、統(tǒng)計選課數(shù)據(jù)。這里要提醒一句權(quán)限控制在論文里是加分項但在實現(xiàn)上不要一上來就引入復(fù)雜的權(quán)限框架。先用role字段區(qū)分角色在接口層做簡單的攔截判斷跑通之后再考慮引入更完整的方案。對畢設(shè)來說先保證業(yè)務(wù)正確再考慮架構(gòu)優(yōu)雅順序不能反。2.2 核心業(yè)務(wù)流程從排課到成績完整流程通常是這樣管理員維護課程信息包括課程名稱、授課教師、學(xué)分、上課時間、容量、選課起止時間。管理員在指定時間窗口發(fā)布選課。學(xué)生登錄瀏覽可選課程提交選課請求。系統(tǒng)檢查課程是否存在、選課時間是否在窗口內(nèi)、學(xué)生是否已選過該課、課程是否還有剩余名額。校驗通過后系統(tǒng)創(chuàng)建選課記錄同時把課程已選人數(shù)加一。學(xué)生在規(guī)定時間內(nèi)可以退課退課后名額釋放。選課結(jié)束后教師錄入成績學(xué)生查看成績。這個流程圖看起來簡單但第 4 步和第 5 步之間藏著整個系統(tǒng)最容易出錯的地方后面會單獨展開。你現(xiàn)在要做的不是在代碼里背下這個流程而是把自己想象成一個學(xué)生把每一步操作可能出現(xiàn)的意外都問一遍。2.3 容易被忽略的隱藏需求很多同學(xué)做的選課系統(tǒng)功能列表看起來齊全但一遇到邊界情況就崩。最容易忽略的需求包括課程時間是否沖突學(xué)生選了周一第一節(jié)的《高等數(shù)學(xué)》還能不能選同一時間段的《大學(xué)英語》同一門課能不能重復(fù)選必須靠數(shù)據(jù)庫約束兜底不能只依賴前端按鈕變灰。退課后名額釋放正在等待的學(xué)生什么時候能看到名額選課窗口關(guān)閉后學(xué)生還能退課嗎教師已錄入成績的課程學(xué)生退課要怎么辦成績還能改嗎這些問題不需要在一開始全部實現(xiàn)但要在需求分析階段列成清單。答辯時老師問“你考慮過哪些異常情況”你直接拿出這張清單比背十頁概念都有說服力。這也能體現(xiàn)你是自己在思考業(yè)務(wù)而不是在搬代碼。3. 數(shù)據(jù)庫設(shè)計選課系統(tǒng)的七成功力在這里3.1 五張核心表的結(jié)構(gòu)與關(guān)系到數(shù)據(jù)庫設(shè)計這一步很多人才意識到前面需求分析的價值。一個典型的選課系統(tǒng)數(shù)據(jù)庫通常包含student學(xué)生表學(xué)號、姓名、學(xué)院、專業(yè)、年級、密碼。teacher教師表教師工號、姓名、學(xué)院、職稱、密碼。course課程表課程編號、課程名稱、授課教師、學(xué)分、上課時間、上課地點、容量、已選人數(shù)、選課開始時間、選課結(jié)束時間。course_selection選課記錄表選課編號、學(xué)號、課程編號、選課時間、成績、狀態(tài)。admin管理員表管理員賬號、密碼。表和表之間的關(guān)系在 ER 圖中要能畫清楚學(xué)生和課程是多對多通過course_selection表關(guān)聯(lián)教師和課程是一對多一門課只有一個主講教師但一個教師可以上多門課。3.2 為什么選課記錄表要單獨設(shè)計很多剛?cè)腴T的朋友會想既然學(xué)生和課程是多對多那是不是在student表里放一個course_ids字段把選的課程編號用逗號拼起來這個想法在小型 demo 里也能跑但一旦需要統(tǒng)計、查詢、退課、判重就會變得非常難受。單獨設(shè)計一張選課記錄表讓每一行代表“一個學(xué)生選了一門課”這一次行為后續(xù)所有查詢都會變得直接查某個學(xué)生的選課記錄就是按學(xué)號過濾查某門課被誰選了就是按課程編號過濾。選課記錄表里還可以預(yù)留狀態(tài)字段標記“已選”“已退”“成績已錄入”比直接刪除記錄更符合業(yè)務(wù)語義也方便在論文里寫“系統(tǒng)支持選課記錄的完整追溯”。這個小小的設(shè)計決策在答辯時其實是一個很好的加分點它說明你理解為什么要保留歷史數(shù)據(jù)而不是只做物理刪除。3.3 唯一約束是防止重復(fù)選課的第一道關(guān)這是數(shù)據(jù)庫設(shè)計里最容易漏、但對選課系統(tǒng)最重要的一點。為了防止學(xué)生重復(fù)選同一門課建議在course_selection表上建立聯(lián)合唯一索引ALTER TABLE course_selection ADD UNIQUE INDEX uk_student_course (student_id, course_id);這樣即使應(yīng)用層校驗漏了數(shù)據(jù)庫也會兜底報錯告訴你“這個學(xué)生已經(jīng)選過這門課”。答辯時能說出這句說明你真的理解了數(shù)據(jù)庫約束和應(yīng)用校驗之間的區(qū)別。很多生產(chǎn)事故的教訓(xùn)就是應(yīng)用層邏輯可以寫錯但數(shù)據(jù)庫約束錯了直接就是臟數(shù)據(jù)。4. SpringBoot 技術(shù)落地從零搭出一個可運行項目4.1 技術(shù)選型與依賴搭配在這類畢設(shè)項目里SpringBoot 往往搭配 MyBatis 或 MyBatis-Plus 使用。前者的 SQL 掌控感更強后者的開發(fā)效率更高。如果你希望論文里有東西可寫MyBatis 的 XML 文件會提供不少可以分析的細節(jié)如果時間緊張想把功能先完整跑起來MyBatis-Plus 的BaseMapper可以省掉大量重復(fù)代碼。依賴的大致結(jié)構(gòu)通常是spring-boot-starter-web提供 Web 能力。spring-boot-starter-validation參數(shù)校驗。mybatis-plus-boot-starter或mybatis-spring-boot-starter持久層。mysql-connector-java數(shù)據(jù)庫驅(qū)動。lombok簡化實體類代碼。spring-boot-starter-test單元測試。有一個非常常見的坑SpringBoot 3.x 要求 JDK 17 及以上很多網(wǎng)上的教程還是基于 SpringBoot 2.x 寫的照著抄啟動都會失敗。第一次創(chuàng)建項目時如果 IDEA 下載依賴卡住或者超時通常是網(wǎng)絡(luò)源的問題優(yōu)先考慮配置 Maven 鏡像源而不是反復(fù)重建項目。這些聽起來很基礎(chǔ)但實際會決定你整個項目能不能順利起步。4.2 項目分層與目錄結(jié)構(gòu)一個常見的項目結(jié)構(gòu)如下com.example.course ├── controller // 接收請求返回結(jié)果 ├── service // 業(yè)務(wù)邏輯 ├── mapper // 數(shù)據(jù)訪問層 ├── entity // 數(shù)據(jù)庫實體 ├── dto // 請求和響應(yīng)對象 ├── config // 配置類 └── common // 統(tǒng)一返回結(jié)果、異常處理分層的意義不是為了好看而是讓每一條代碼路徑都有明確的責(zé)任邊界。Controller 只做參數(shù)接收和結(jié)果包裝Service 做業(yè)務(wù)判斷和事務(wù)控制Mapper 只寫 SQL。將來出現(xiàn) bug 時你知道去哪個層查而不是從 Controller 一路翻到 Mapper 然后懷疑人生。4.3 最小可運行流程先跑通再擴展不要一開始就想著把所有功能寫完。按這個順序推進會舒服很多配置好數(shù)據(jù)源能連上本地 MySQL。寫一個最簡單的接口確認項目能啟動。創(chuàng)建學(xué)生表和課程表先寫一個查詢課程列表的接口。實現(xiàn)第一個完整流程學(xué)生登錄、查看課程、選課、查看已選課程。再補權(quán)限、退課、成績、統(tǒng)計等功能。每一步都驗證成功后再進入下一步。這樣即使中途出了問題你也能確定問題只出現(xiàn)在最近一步里不需要滿項目找 bug。這一步看起來慢實際是最快的方式。5. 選課核心邏輯與并發(fā)問題這是最容易翻車的地方5.1 選課接口的業(yè)務(wù)流程選課接口的邏輯看起來簡單但每一步都有講究。一個典型的流程是接收學(xué)號和課程編號。校驗學(xué)生是否存在、狀態(tài)是否正常。校驗課程是否存在、選課時間是否在窗口內(nèi)。校驗學(xué)生是否已經(jīng)選過該課程。檢查課程剩余名額是否大于 0。創(chuàng)建選課記錄。課程已選人數(shù)加一。單用戶測試時這個流程完全沒問題。但一旦多個學(xué)生同時選同一門課第 5 步和第 6 步之間就可能出問題。這也是為什么這類項目被反復(fù)拿來做畢設(shè)它業(yè)務(wù)簡單但問題不簡單。5.2 事務(wù)邊界校驗、扣減、插入要放在一起第 5、6、7 步必須放在同一個事務(wù)里。如果先插入選課記錄再更新課程人數(shù)第二步失敗時事務(wù)要能回滾否則會出現(xiàn)“記錄存在但人數(shù)沒變”的臟數(shù)據(jù)。在 Spring 里最簡單的方式是在 Service 方法上加TransactionalTransactional public void selectCourse(Long studentId, Long courseId) { Course course courseMapper.selectById(courseId); // 校驗選課時間、是否重復(fù)、剩余名額 // 插入選課記錄 // 更新課程已選人數(shù) }注意事務(wù)的粒度不能太大。比如把“讀取學(xué)生信息”之類的前置查詢也放進同一個大事務(wù)會拉長事務(wù)持有時間但畢設(shè)場景下通常問題不大。更重要的反而是理解為什么這些操作必須是一個原子操作。5.3 并發(fā)選課時如何防止超選如果你在論文里只寫到事務(wù)這一步老師大概率會追問兩個人同時讀到剩余名額為 1同時通過校驗同時插入怎么辦這就是經(jīng)典的并發(fā)超賣問題在選課場景里叫“超選”。解決思路從簡到繁有幾種我列一個對比方案核心思路優(yōu)點缺點適合場景數(shù)據(jù)庫樂觀鎖用版本號控制更新實現(xiàn)簡單不鎖數(shù)據(jù)高并發(fā)下失敗率高并發(fā)量中等的畢設(shè)項目數(shù)據(jù)庫悲觀鎖通過SELECT ... FOR UPDATE鎖住課程行數(shù)據(jù)絕對安全并發(fā)下等待時間長并發(fā)量不大、追求穩(wěn)定Redis 分布式鎖在內(nèi)存中預(yù)扣減名額性能最高需要額外組件復(fù)雜度高集群部署、高并發(fā)生產(chǎn)環(huán)境對畢設(shè)級別來說我建議先實現(xiàn)樂觀鎖或悲觀鎖中的一種。理論基礎(chǔ)可以這樣寫樂觀鎖適合“沖突不頻繁”的業(yè)務(wù)悲觀鎖適合“沖突頻繁、必須串行”的業(yè)務(wù)。選課系統(tǒng)通常在開選瞬間沖突很高所以悲觀鎖反而可能更直觀但如果想展示對性能的理解用樂觀鎖配合失敗提示也是完整的方案。關(guān)鍵不是選哪個而是你清楚知道為什么在自己設(shè)計的表結(jié)構(gòu)和查詢方式下這個方案能解決并發(fā)問題。5.4 一個推薦的循序漸進實現(xiàn)路徑如果你的項目還在起步階段我建議按下面的路徑走先不加任何并發(fā)控制把整個流程跑通。加上事務(wù)和數(shù)據(jù)庫唯一約束保證不產(chǎn)生臟數(shù)據(jù)。用接口壓測工具并發(fā)請求同一個選課接口觀察是否出現(xiàn)超選。加上樂觀鎖或悲觀鎖重新跑并發(fā)測試對比結(jié)果。這套流程做下來你不僅完成了一個功能還完成了一次完整的性能優(yōu)化演示。答辯時甚至可以打開壓測工具現(xiàn)場跑一遍這是很多項目都拿不出來的實證。6. 從“能跑”到“能答辯”文檔、測試與演示路線6.1 單測和接口調(diào)試不能省很多畢設(shè)項目根本沒有單元測試但這其實是一個很容易拿分的地方。不需要多復(fù)雜的測試用例只要覆蓋幾條核心業(yè)務(wù)線就行正常選課成功。重復(fù)選同一門課被拒絕。課程滿員時選課失敗。不在選課時間窗口內(nèi)選課失敗。退課后名額恢復(fù)。并發(fā)選同一門課時只有預(yù)期的人數(shù)能成功。用spring-boot-starter-test里的SpringBootTest和接口測試組件就能寫出覆蓋核心業(yè)務(wù)的集成測試。代碼能跑不能證明它沒錯測試用例能穩(wěn)定通過才是可以寫進論文里的證據(jù)。這一步非常推薦做它也是你和“只會抄源碼”的人之間最容易拉開差距的地方。6.2 論文和文檔報告的結(jié)構(gòu)建議畢設(shè)論文一般會包含需求分析、系統(tǒng)設(shè)計、數(shù)據(jù)庫設(shè)計、核心模塊實現(xiàn)、系統(tǒng)測試這幾大塊。這里有一個容易被忽視的點文檔里的圖和代碼必須和實際項目保持一致。很多同學(xué)先寫完論文再補代碼或者對著網(wǎng)上的代碼寫論文結(jié)果答辯前發(fā)現(xiàn)對不上只能熬夜改稿。更穩(wěn)妥的順序是先讓代碼跑通再回頭寫文檔或者邊寫代碼邊同步截圖、邊記錄設(shè)計決策。數(shù)據(jù)庫表結(jié)構(gòu)這類核心內(nèi)容一旦改了論文里的 ER 圖和表結(jié)構(gòu)說明都要跟著改這個同步成本很高。6.3 答辯演示的演示路徑答辯時間通常只有 5 到 10 分鐘演示不要從頭點菜單到尾。建議走一條能展現(xiàn)關(guān)鍵判斷的路徑用管理員賬號演示創(chuàng)建課程和設(shè)置選課時間窗口。用學(xué)生賬號演示選課、退課。用教師賬號演示查看選課名單、錄入成績。如果有做并發(fā)優(yōu)化演示兩三個賬號同時選同一門課的數(shù)據(jù)變化。打開數(shù)據(jù)庫工具或接口文檔展示數(shù)據(jù)是怎么寫的。演示時不要念代碼也不要只講“我點了這個按鈕”。每做一個操作就說明這一步解決了什么問題、背后涉及哪張表、哪條業(yè)務(wù)邏輯。老師真正想聽到的是“你理解這個東西”不是“你會點按鈕”。7. 給正在做這個選題的同學(xué)幾句實在話7.1 先跑通再優(yōu)化最后才考慮炫技這個項目最容易出現(xiàn)的狀態(tài)是功能列表寫得很滿權(quán)限框架、Redis 緩存、消息隊列都加了一遍但數(shù)據(jù)庫表設(shè)計是抄的核心選課邏輯根本沒跑通。等到了答辯前夜才發(fā)現(xiàn)最基礎(chǔ)的功能反而不穩(wěn)定。先跑通一個哪怕最簡單的版本你會發(fā)現(xiàn)后面加功能的速度會快很多出了問題也知道去哪個模塊查。先跑通是底線優(yōu)化是加分炫技是最后才考慮的事這個順序不能反。7.2 適合與不適合的邊界這個選題適合想完整經(jīng)歷一遍 SpringBoot 項目開發(fā)流程、愿意花時間理解數(shù)據(jù)庫設(shè)計、希望在答辯時能講出清晰業(yè)務(wù)故事的人。不適合想靠某個開箱即用的項目“速通”畢業(yè)設(shè)計的人。不是因為題難而是因為老師對這個題目的套路足夠熟悉一問就能知道你理解多少。你可以在項目里少做幾個功能但自己真正動手實現(xiàn)的每一條鏈路都要能從頭講到尾。7.3 長期看起來這次經(jīng)歷真正的收獲是什么從長期看選課管理系統(tǒng)留給你的不是一份能交差的代碼而是一條完整的工程思考鏈如何從一個模糊的題目出發(fā)拆解出清晰的需求設(shè)計出可靠的表結(jié)構(gòu)寫出有邊界的事務(wù)邏輯再用文檔和演示把思考過程表達清楚。這套能力放到任何系統(tǒng)開發(fā)里都成立哪怕你以后不做 Java、不寫 SpringBoot這套“先想清楚再動手”的方式也不會過時。如果你現(xiàn)在正盯著 IDEA 里的報錯或者對著數(shù)據(jù)庫的表結(jié)構(gòu)發(fā)愁我的建議很簡單先停一下把“學(xué)生、課程、選課記錄”這三張表的關(guān)系想明白把選課和退課的流程在紙上畫出來再回來寫代碼。你會發(fā)現(xiàn)最麻煩的問題往往在寫代碼之前就已經(jīng)解決了。