自習室預約管理系統(tǒng):從需求到部署)
自習室預約這套題說實話已經(jīng)被做成“畢業(yè)設(shè)計常青樹”了。但凡帶點管理系統(tǒng)的課設(shè)、畢設(shè)十有八九繞不開這個選題。我這次做的就是一個基于SpringBootVue的MVC架構(gòu)自習室管理和預約系統(tǒng)技術(shù)棧落在JavaMySQLMyBatis上屬于典型的全棧分離項目。整個系統(tǒng)拆成用戶端和管理端兩套界面用戶在手機上或者瀏覽器里能看到自習室列表、樓層分區(qū)、座位占用情況選個座位預約一段時間管理員在后臺維護自習室、座位、預約規(guī)則還能看統(tǒng)計報表。這篇文章我就按自己做這個項目的完整過程來寫從需求拆解、技術(shù)選型、數(shù)據(jù)庫設(shè)計到后端接口、前端交互、部署運行再到我實際踩過的坑都會交代清楚。適合正在做課設(shè)/畢設(shè)、想復現(xiàn)一個完整前后端分離項目的同學參考也適合想入門SpringBootVue全棧開發(fā)的人照著搭一套練手。源碼層面我會把關(guān)鍵代碼片段貼出來不是那種“只給目錄不給細節(jié)”的分享。1. 項目到底要解決什么問題1.1 自習室預約的核心痛點學校或者社會自習室的座位資源看起來是“公共的”但實際運營起來全是問題。最常見的場景就是考研季、期末周座位嚴重不夠用第二天一早門口排隊搶座甚至有人拿書占座幾天不來。反過來非高峰期大量座位空著資源閑置。自習室管理員也很頭疼紙質(zhì)登記本翻起來麻煩誰占著座位、什么時候走完全靠人肉盯。這個系統(tǒng)的核心價值就是把“座位狀態(tài)數(shù)字化”。每一張座位在系統(tǒng)里都有明確狀態(tài)空閑、已預約、已簽到、暫離、禁用。用戶預約之前一眼看清哪些座位能用預約之后到現(xiàn)場簽到入座離開時釋放座位。管理員不用再跑現(xiàn)場后臺就能看到實時占用率還能設(shè)置每個自習室的開放時段、預約時長上限。本質(zhì)上這就是一個帶時間維度的資源管理系統(tǒng)和會議室預約、健身房場地預約的邏輯是相通的。1.2 功能拆解與用戶角色設(shè)計用戶角色我分成三類普通學生用戶、管理員、超級管理員。沒有做“教師”角色因為自習室場景里教師通常只參與審批或者設(shè)備管理沒必要為了湊功能硬加。普通用戶側(cè)的關(guān)鍵功能注冊登錄、個人信息維護查看自習室列表按樓棟、區(qū)域篩選查看每個自習室的座位圖區(qū)分不同座位狀態(tài)預約座位選擇日期和起止時間簽到入座、暫離、釋放座位查看自己的預約記錄、違規(guī)記錄管理員側(cè)的關(guān)鍵功能自習室管理新增、編輯、啟停用座位管理批量生成座位、設(shè)置座位類型靠窗/普通/帶電源、禁用某個座位預約規(guī)則管理設(shè)置可預約天數(shù)、單次最長時長、簽到時限預約記錄查詢與統(tǒng)計導出表格用戶管理封禁、解封、重置密碼開發(fā)展示中最容易被問的就是“這個系統(tǒng)有什么亮點”。我當時給出來的方案是“座位狀態(tài)實時同步預約沖突兜底”。也就是用戶在選座時看到的座位狀態(tài)是準實時的提交預約時后端再做一次防沖突校驗避免兩個人同時鎖到同一個座位。2. 技術(shù)選型與架構(gòu)設(shè)計思路2.1 為什么是SpringBootVue這套組合先說后端。SpringBoot在這個場景里幾乎是無可爭議的選擇原因很簡單內(nèi)嵌Tomcat不需要額外部署Web容器自動配置把大量樣板代碼省掉生態(tài)成熟社區(qū)資料多遇到問題基本都能搜到答案。相比之下如果再用傳統(tǒng)的SSHSpringStrutsHibernate那一套光是寫各種XML配置就能勸退一堆初學者。我用的版本組合比較穩(wěn)SpringBoot 2.7.x JDK 1.8 MySQL 5.7/8.0 MyBatis 2.x。為什么要鎖這個版本因為SpringBoot 3.x強制要求JDK 17而且部分老項目用的MyBatis、PageHelper插件在3.x下需要額外適配如果你照著網(wǎng)上的教程抄作業(yè)大概率會踩版本不一致的坑。所以我的建議是除非你有明確理由否則畢設(shè)/課設(shè)別追新版本。前端選Vue就更好理解了。Vue 2.x Element UI是當前中文社區(qū)里資料最多、最穩(wěn)定的組合Vue 3 Element Plus雖然新但有些組件用法變動比較大網(wǎng)上的老舊教程容易誤導。我這次用的是Vue 2.6 Element UI 2.15配合vue-router和axios足夠支撐這種后臺管理用戶選座的場景。2.2 MVC分層在后端怎么落地標題里特意提到MVC這個必須解釋清楚。MVC本身是一個設(shè)計思想不是一套固定框架。我后端的代碼結(jié)構(gòu)就是按照Controller、Service、MapperDAO三個層次來的Model用實體類和VO來表現(xiàn)。典型的調(diào)用鏈路是這樣的瀏覽器發(fā)起請求到Controller層Controller負責參數(shù)接收、簡單校驗然后調(diào)用Service層Service層處理業(yè)務(wù)邏輯比如校驗預約沖突、計算座位狀態(tài)Service調(diào)用Mapper接口Mapper對應(yīng)XML里的SQLMySQL返回結(jié)果Mapper映射成實體類經(jīng)過Service組裝成VO最后由Controller返回JSON給前端這樣的分層好處很明顯至少有三點各層職責單一代碼可讀性好。別人拿到項目不需要翻完整套代碼只看Controller就知道系統(tǒng)提供了哪些接口。Service層獨立后業(yè)務(wù)規(guī)則可以單獨做單元測試。比如預約沖突檢測這種核心邏輯不依賴Controller和前端就能跑通。后期替換技術(shù)方案更方便。比如把MyBatis換成MyBatis-Plus或者把MySQL換成PostgreSQL底層影響可以被控制在Mapper層。實體類這塊我做了一個區(qū)分數(shù)據(jù)庫表對應(yīng)的叫Entity接口返回前端的叫VO前端提交參數(shù)用DTO。之前的項目吃了不區(qū)分的虧查了一個用戶信息順便把密碼hash也返回了前端雖然前端不顯示但這事很不專業(yè)。2.3 前端Vue項目的目錄組織Vue項目不是簡單的“一個頁面一個組件”就完了。我按功能模塊拆分成view頁面級組件和component可復用組件兩大類。src/apiaxios請求封裝按模塊拆分成user.js、seat.js、reservation.jssrc/router路由配置文件區(qū)分需要登錄的頁面和不需要登錄的頁面src/storeVuex存放用戶登錄信息、當前選中的自習室狀態(tài)src/views頁面級組件比如Login.vue、SeatMap.vue、Admin/ReservationManage.vuesrc/components通用組件比如座位格子SeatItem.vue、分頁組件封裝的PaginationTable.vue實際開發(fā)中很多初學者會把所有邏輯全部塞進一個頁面組件幾百行代碼堆在一個Vue文件里后面想改一個選座邏輯都要翻半天。我這次強行把座位圖抽成了獨立組件狀態(tài)由父頁面?zhèn)魅虢换ネㄟ^事件回調(diào)這樣座位圖組件可以單獨測試也能復用到管理端的座位預覽功能里。3. 數(shù)據(jù)庫設(shè)計預約系統(tǒng)的命門3.1 核心表結(jié)構(gòu)設(shè)計數(shù)據(jù)庫設(shè)計是這種系統(tǒng)里最考驗功力的部分。表設(shè)計不好后面寫業(yè)務(wù)代碼就是連環(huán)坑。我規(guī)劃了這幾張核心表sys_user用戶表字段包括id、username、password、real_name、role、status、create_timestudy_room自習室表字段包括id、name、location、floor、open_time、close_time、status、capacityseat座位表字段包括id、room_id、seat_no、type、position_x、position_y、statusreservation預約記錄表字段包括id、user_id、seat_id、room_id、reserve_date、start_time、end_time、statusviolation_record違規(guī)記錄表記錄超時未簽到、超時未釋放等情況用戶表里role字段我直接用的字符串USER、ADMIN、SUPER_ADMIN。也有人用int類型再在代碼里映射枚舉我個人覺得字符串更直觀查數(shù)據(jù)庫的時候一眼能看懂也不容易被數(shù)字映射搞糊涂。座位表里的position_x和position_y是干嘛的這是為了前端渲染座位圖準備的。每張座位在自習室的平面圖里有一個坐標前端拿到數(shù)據(jù)后可以直接按坐標渲染到對應(yīng)位置比后端拼接好HTML再返回強多了。當然純粹做簡單系統(tǒng)也可以讓前端寫死布局但那樣教室里臨時加把椅子就得改代碼。3.2 預約時間沖突的處理思路預約系統(tǒng)的核心難點不是CRUD而是“同一座位同一時間段不能被兩個人預約”。這個沖突檢測我一開始想用純SQL來做后來發(fā)現(xiàn)反而復雜干脆在Service層加鎖校驗。邏輯是用戶提交預約時后端根據(jù)seat_id和reserve_date去查已經(jīng)存在的預約記錄只要新預約的時間段和已有記錄存在重疊就拒絕。SQL大概是這樣的select idselectConflict resultTypejava.lang.Integer SELECT COUNT(*) FROM reservation WHERE seat_id #{seatId} AND reserve_date #{reserveDate} AND status IN (BOOKED, CHECKED_IN, AWAY) AND NOT ( #{newEndTime} lt; start_time OR #{newStartTime} gt; end_time ) /select這里的時間重疊判斷就是兩個區(qū)間的交集判斷新預約開始時間不早于已有記錄的結(jié)束時間或者新預約結(jié)束時間不晚于已有記錄的開始時間才算不沖突。反過來只要兩個時間段存在任何交叉區(qū)域就說明有人占用了。只做查詢校驗還不夠。兩個請求同時提交理論上都能通過校驗然后都插入成功這就產(chǎn)生了并發(fā)問題。解決辦法一個是給座位加行鎖用SELECT ... FOR UPDATE把座位記錄鎖住再校驗另一個是對reservation表加唯一索引配合狀態(tài)字段。我最終采用的是“事務(wù)座位行鎖”的方案這也是商用系統(tǒng)中比較穩(wěn)妥的做法。3.3 索引設(shè)計索引不是用來炫技的而是要命中實際查詢場景。這個系統(tǒng)里查詢頻率最高的場景就兩個用戶查詢自己某一時間段的預約記錄按座位查詢某個日期的預約占用情況所以我在reservation表上建了兩個復合索引idx_user_time (user_id, reserve_date, start_time)idx_seat_time (seat_id, reserve_date, start_time)建索引的理由很直接預約記錄表會越來越大沒有索引的話WHERE user_id ? AND reserve_date BETWEEN ? AND ?這種查詢就是全表掃描數(shù)據(jù)量到幾萬條之后響應(yīng)時間會明顯變差。加了復合索引后查詢可以走索引快速定位排序也順手能利用上索引。再強調(diào)一個細節(jié)不要給每個字段單獨建索引那樣不但不加速反而拖慢寫入速度。MySQL的索引機制決定了復合索引的字段順序也有講究最常用的等值條件字段放前面范圍條件字段放后面。4. 后端核心實現(xiàn)與關(guān)鍵細節(jié)4.1 登錄認證與用戶上下文登錄這塊我用的方案是JWTJSON Web Token。跟Session那套比JWT最大的好處是無狀態(tài)后端不用存儲會話記錄橫向擴展的時候不用做會話同步。對畢設(shè)項目來說前端拿到token后存到localStorage里每次請求在axios攔截器里帶上Authorization頭就行。具體實現(xiàn)上我用JWT生成token時把user_id作為payload核心字段。用戶每次請求受保護接口時后端攔截器解析token拿到user_id后去redis或者直接查數(shù)據(jù)庫取用戶信息放入ThreadLocal里作為本次請求的用戶上下文。注意我在這里沒有用Spring Security因為對自習室預約這種場景來說Spring Security的學習成本和配置成本有點重。自己寫一個HandlerInterceptor 自定義注解RequireRole代碼量不大邏輯還更好懂。自定義注解是這樣一個思路Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RequireRole { String value() default USER; }在攔截器中根據(jù)Controller方法上的注解判斷當前用戶是否擁有對應(yīng)角色。管理端接口標記RequireRole(ADMIN)用戶端接口標記RequireRole(USER)超級管理員兩個角色都能訪問。4.2 預約與取消的核心業(yè)務(wù)邏輯預約的核心邏輯我在Service里用事務(wù)包裹起來大致流程是這樣的校驗用戶是否有預約權(quán)限是否已被封禁、是否已經(jīng)約了同一時段鎖定座位記錄SELECT ... FOR UPDATE查詢沖突預約判斷時間段是否重疊插入一條預約記錄狀態(tài)為BOOKED更新座位狀態(tài)為OCCUPIED為什么要先鎖座位再查沖突因為如果不鎖并發(fā)場景下兩個人同時查到“無沖突”然后都插入成功就出現(xiàn)了超賣問題。鎖座位雖然犧牲了一點并發(fā)量但對于自習室這種場景完全沒有壓力單把椅子一天最多也就被約幾次沒有效能瓶頸。取消預約的規(guī)則有個細節(jié)距離預約開始時間不足30分鐘不允許用戶在線取消必須到現(xiàn)場找管理員操作。這個規(guī)則在管理端可以配置但默認值我是直接寫在Service層常量里。為了這個規(guī)則我在事務(wù)里能直接判斷當前時間是否滿足條件很快也不用額外設(shè)計提醒表。流程是刪除或標記預約記錄為CANCELLED同時釋放座位狀態(tài)。我選擇的是保留預約記錄、把狀態(tài)改成CANCELLED而不是物理刪除。這樣保留歷史數(shù)據(jù)后面做統(tǒng)計分析比如“哪些座位最受歡迎”才有數(shù)據(jù)可用。4.3 MyBatis動態(tài)SQL與緩存配置MyBatis在這個項目里承擔所有數(shù)據(jù)庫訪問工作。我坦白說一開始我也糾結(jié)要不要用MyBatis-Plus因為它真的能省掉大量單表CRUD代碼。后來考慮到很多課設(shè)答辯的老師要求“使用MyBatis實現(xiàn)”所以我保留了原生MyBatis的XML寫法把多表查詢和動態(tài)SQL都寫清楚了。動態(tài)SQL在預約記錄查詢里特別好用。管理端查詢預約記錄時篩選條件可能是“按日期查”“按自習室查”“按用戶姓名查”也可能全部不選查全量。這種場景用動態(tài)SQL最合適select idselectReservationPage resultTypecom.example.entity.Reservation SELECT r.*, u.username, s.seat_no FROM reservation r LEFT JOIN sys_user u ON r.user_id u.id LEFT JOIN seat s ON r.seat_id s.id where if testroomId ! null AND r.room_id #{roomId} /if if testreserveDate ! null AND r.reserve_date #{reserveDate} /if if teststatus ! null and status ! AND r.status #{status} /if /where ORDER BY r.reserve_date DESC, r.start_time DESC /selectwhere標簽會自動處理“第一個條件前面的AND會被去掉”的問題不用自己手動拼接字符串避免SQL注入的同時代碼也干凈很多。MyBatis的二級緩存我也聊一下。默認情況下MyBatis的二級緩存是關(guān)閉的我沒有在全局開啟而是只對seat表的Mapper開了二級緩存。原因很簡單預約記錄和用戶表的實時性要求高緩存命中率低而且一旦數(shù)據(jù)變更還要考慮緩存刷新容易臟讀。座位表相對穩(wěn)定狀態(tài)變更頻率不高開啟二級緩存后可以少打幾次數(shù)據(jù)庫。不過要注意現(xiàn)在很多項目直接用Spring Cache Redis做緩存MyBatis的二級緩存用得少了因為多級緩存的一致性維護成本偏高。4.4 管理端功能實現(xiàn)管理端的核心功能是自習室和座位管理我用了一個比較省事的思路座位批量生成。一個自習室可能有幾十上百個座位一個個人工錄入不現(xiàn)實。我的方案是在新增自習室時讓管理員輸入“行數(shù)”和“列數(shù)”系統(tǒng)自動生成座位編號例如A01、A02B01、B02并按照坐標規(guī)則寫入seat表。這樣既省了管理員的工作量又保證了前端座位圖的位置數(shù)據(jù)是完整的。座位批量生成的核心代碼大致是for (int row 1; row rowCount; row) { for (int col 1; col colCount; col) { Seat seat new Seat(); seat.setRoomId(roomId); seat.setSeatNo(String.format(%s%02d, (char)(A row - 1), col)); seat.setPositionX(col); seat.setPositionY(row); seat.setStatus(FREE); seatMapper.insert(seat); } }管理端還有一個實用功能是今日統(tǒng)計。首頁展示“今日預約數(shù)”“當前在場人數(shù)”“自習室占用率”。占用率的計算方式是當前已占用座位數(shù) / 總座位數(shù)查詢時先分組查每個自習室的座位總數(shù)再查當前狀態(tài)為OCCUPIED的座位數(shù)兩個數(shù)據(jù)在Service層合并成統(tǒng)計VO返回前端。5. 前端頁面與交互實現(xiàn)5.1 Vue路由與頁面結(jié)構(gòu)前端路由用的是Vue Router。路由設(shè)計上我分了兩層不需要登錄就能訪問的和必須登錄才能訪問的。不需要登錄的路由/login登錄頁/register注冊頁必須登錄的路由放在/layout父路由的children里因為這一部分頁面都有共同的頂部導航和側(cè)邊欄布局。{ path: /layout, component: Layout, redirect: /layout/home, children: [ { path: home, component: Home, meta: { title: 首頁 } }, { path: rooms, component: RoomList, meta: { title: 自習室 } }, { path: seat-map, component: SeatMap, meta: { title: 選座 } }, { path: my-reservation, component: MyReservation, meta: { title: 我的預約 } } ] }管理端路由單獨建了一份掛在/admin下面比如/admin/room-manage、/admin/seat-manage、/admin/reservation-manage。這里要注意的是前端路由的權(quán)限控制只是用戶體驗層面的真正的權(quán)限校驗必須依賴后端接口。前端不能訪問的頁面只是看不到入口如果直接調(diào)后端接口沒有權(quán)限后端必須返回403。路由守衛(wèi)方面我在router.beforeEach里檢查本地有沒有tokenrouter.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.meta.requiresAuth !token) { next(/login); } else { next(); } });5.2 座位選座交互選座界面是整個前端最核心的交互頁面我在設(shè)計上參考了影院選座、演唱會選座那一套一個平面圖座位格子一個個平鋪在上面用顏色區(qū)分狀態(tài)。座位狀態(tài)的配色我定的是空閑白色/淺灰色已預約黃色已簽到綠色暫離橙色禁用深灰色打叉用戶點擊一個空閑座位后右側(cè)彈出預約面板選擇日期和起止時間點擊確認后走預約接口。這個交互邏輯我封裝在SeatMap.vue里數(shù)據(jù)來源是后端接口返回的座位列表和座位狀態(tài)列表。這里有一個交互細節(jié)用戶選中座位后要在前端本地標記“正在選中”狀態(tài)但這個狀態(tài)不能和后端實時同步等到真正提交時才返回給后端。所以為了防止兩個用戶同時選同一座位前端顯示的狀態(tài)只能作為參考后端提交時的沖突校驗才是兜底。座位圖渲染的模板大概是div v-forseat in seatList :keyseat.id classseat-item :classgetSeatClass(seat) :style{ left: (seat.positionX - 1) * seatSize px, top: (seat.positionY - 1) * seatSize px } clickhandleSeatClick(seat) {{ seat.seatNo }} /div座位尺寸我固定用44px×44px間距6px這樣一張A4紙寬度的屏幕能放下十幾個座位體驗比較自然。如果自習室座位特別多還可以在右側(cè)加一個縮小比例的迷你地圖用于快速定位。5.3 Axios封裝與權(quán)限控制axios不能每個組件里單獨引一遍要統(tǒng)一封裝。我在src/api/request.js里做了以下事情創(chuàng)建axios實例設(shè)置baseURL和timeoutbaseURL指向后端接口地址比如http://localhost:8080/api請求攔截器里從localStorage讀取token放到請求頭響應(yīng)攔截器里統(tǒng)一處理HTTP狀態(tài)碼401跳轉(zhuǎn)登錄頁403提示無權(quán)限500提示服務(wù)器錯誤把后端返回的業(yè)務(wù)狀態(tài)碼也做了統(tǒng)一處理比如code500的提示語統(tǒng)一用Element UI的Message組件展示這種封裝的好處是后面如果后端接口地址變了只需要改一個文件如果后端的鑒權(quán)方式變了比如改為請求參數(shù)攜帶token也只需要改一個攔截器組件里的業(yè)務(wù)代碼完全不用動。6. 從零到一完整部署運行步驟6.1 本地環(huán)境準備先說一下我本地的環(huán)境JDK 1.8如果你是JDK 17也可以跑SpringBoot 2.7但部分老插件可能報錯Maven 3.6MySQL 5.7或8.0Node.js 14Vue 2項目不建議用Node 18會有兼容問題IDE后端用IDEA前端用VS Code環(huán)境這塊我吃過虧尤其Node版本。Vue 2的老項目用Node 17跑npm install經(jīng)常報Error: error:0308010C:digital envelope routines::unsupported這是因為新版Node的OpenSSL策略變了。解決辦法要么把Node降級到14/16要么在package.json的啟動腳本里加一句NODE_OPTIONS--openssl-legacy-provider。我更推薦前者降級到Node 14一了百了。6.2 數(shù)據(jù)庫初始化數(shù)據(jù)庫這塊我用Navicat新建一個study_room_db數(shù)據(jù)庫字符集選utf8mb4為什么不用utf8因為utf8在MySQL里是utf8mb3不支持emoji和一些特殊字符雖然系統(tǒng)里不太用emoji但保不準用戶昵稱里有特殊符號。然后直接執(zhí)行init.sql腳本里面包含建表語句和初始數(shù)據(jù)。初始數(shù)據(jù)至少要有一個超級管理員賬號admin/admin123兩個自習室數(shù)據(jù)每個自習室對應(yīng)的座位數(shù)據(jù)我直接用批量插入SQL寫死了幾條演示用的預約記錄方便前端展示效果關(guān)于賬號密碼我在user表里存的是MD5加密后的值。明說一句MD5現(xiàn)在已經(jīng)不夠安全了真正商用至少要用BCrypt。我這里圖省事用了MD5但代碼里把加密算法抽成了工具類想替換成BCrypt很容易。6.3 后端啟動后端項目導入IDEA后第一步修改application.yml里的數(shù)據(jù)庫連接配置server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/study_room_db?useUnicodetruecharacterEncodingutf8mb4useSSLfalseserverTimezoneAsia/Shanghai username: root password: 你的數(shù)據(jù)庫密碼 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.entity configuration: map-underscore-to-camel-case: truemap-underscore-to-camel-case這個配置一定要開啟否則數(shù)據(jù)庫字段seat_no映射到Java屬性seatNo會失敗查詢結(jié)果全是null。這是個非常典型的新手問題排錯能排半天。配置改完后先啟動MySQL再運行StudyRoomApplication主類。后端啟動后瀏覽器直接訪問http://localhost:8080/api/health如果能返回正常結(jié)果說明后端已經(jīng)跑起來了。6.4 前端啟動前端項目打開終端按順序執(zhí)行npm install npm run devnpm install如果速度慢可以換成淘寶鏡像源先執(zhí)行npm config set registry https://registry.npmmirror.com再裝。啟動后開發(fā)服務(wù)器默認跑在http://localhost:8081我特意把端口改到8081避免和后端8080混淆。Vue開發(fā)服務(wù)器的端口可以在vue.config.js里配置module.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }這里用proxying而不是直接寫死http://localhost:8080作為baseURL是為了避免開發(fā)階段的跨域問題。瀏覽器訪問8081端口請求通過Vue開發(fā)服務(wù)器轉(zhuǎn)發(fā)到8080同源策略就不會攔截了。如果不用proxy要么后端配置CORS要么前端調(diào)接口時直接帶完整地址但會被瀏覽器攔截所以這個配置很關(guān)鍵。7. 常見問題與排查經(jīng)驗7.1 端口占用與配置不一致啟動后端時最常遇到的報錯就是Web server failed to start. Port 8080 was already in use.端口被占用的原因五花八門可能是你自己之前啟動過一次沒關(guān)掉也可能是其他軟件搶占了端口。排查方法Windows下用netstat -ano | findstr 8080查看占用進程PID用taskkill /PID 進程號 /F強制殺掉或者在application.yml里換一個端口但我更建議換端口而不是殺進程因為殺掉的進程有可能是別人正在用的服務(wù)。比如你機器上已經(jīng)跑了一個Tomcat在8080你非要用8080最好還是改自己的項目端口。7.2 數(shù)據(jù)庫連接失敗排查數(shù)據(jù)庫相關(guān)的報錯幾乎占了新手問題的一半。常見的幾個Communications link failure這個報錯十有八九是數(shù)據(jù)庫沒啟動或者連接地址寫錯。先確認MySQL服務(wù)在系統(tǒng)服務(wù)列表里是“運行中”然后在命令行用mysql -u root -p直接連一下確認賬號密碼沒問題。Unknown database study_room_db這個就是數(shù)據(jù)庫沒創(chuàng)建或者名字寫錯了。去Navicat里看看庫名是不是和url里的一致注意大小寫。The server time zone value ?D1ú±ê×?ê±?? is unrecognized這個報錯是因為MySQL時區(qū)設(shè)置問題在url上加serverTimezoneAsia/Shanghai就能解決。如果還報錯就在MySQL的配置文件my.ini里加上default-time-zone 08:00。7.3 跨域問題我用了devServer的proxy方案之后開發(fā)階段基本不會遇到跨域。但如果你把前端打包成靜態(tài)文件在Nginx里部署或者直接打開dist/index.html跨域問題就會冒出來。本地開發(fā)時如果不用proxy就必須在后端加CORS配置。一個簡單的做法是在Controller上或者全局配置類上加CrossOrigin或CorsFilter。CORS配置時要指定允許的來源、方法和請求頭不能直接寫*加允許所有請求頭那是懶人配置不嚴謹。前端部署時更推薦Nginx反向代理的方式把/api/開頭的請求轉(zhuǎn)給后端服務(wù)。Nginx配置大致是location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }7.4 MyBatis相關(guān)坑MyBatis使用中我踩過最深刻的坑就是“SQL沒有問題但查詢結(jié)果全是null”。這個我在前面提過就是map-underscore-to-camel-case沒開啟。如果你不喜歡全局開這個配置也可以在XML里的resultMap里手動映射每一列但是那樣的代碼量翻倍不推薦。另外一個坑是參數(shù)傳遞問題。當Mapper接口方法有兩個或以上參數(shù)時必須用Param注解標注參數(shù)名否則MyBatis不知道#{userId}對應(yīng)哪個參數(shù)。我見過太多人寫這樣的代碼然后報Parameter xxx not found的錯誤。還有MyBatis的if判斷字符串比較時寫成if teststatus BOOKED有時候不生效原因在于MyBatis解析OGNL表達式時單引號內(nèi)的內(nèi)容會被解析成字符而非字符串。正確寫法是if teststatus BOOKED外層用單引號內(nèi)層用雙引號。這個真是很陰間的細節(jié)。7.5 預約并發(fā)沖突實戰(zhàn)最后說一個真實遇到過的業(yè)務(wù)問題。系統(tǒng)上線模擬壓力測試時兩個用戶同時點預約同一個座位結(jié)果兩個都成功了。我排查后發(fā)現(xiàn)問題出在事務(wù)邊界上我第一次把座位狀態(tài)校驗放到了事務(wù)外等事務(wù)內(nèi)插入時才去更新座位中間丟了一個校驗窗口。修正方案是在Service方法上加Transactional進入事務(wù)后先執(zhí)行SELECT ... FOR UPDATE鎖定該座位記錄再執(zhí)行沖突查詢和插入操作。加上行鎖之后模擬并發(fā)請求就不會再出現(xiàn)超賣場景了。這個問題的通用排查思路就是凡是涉及“資源唯一性”的業(yè)務(wù)座位、庫存、優(yōu)惠券都必須考慮并發(fā)。只有業(yè)務(wù)校驗沒有數(shù)據(jù)庫鎖或者唯一約束遲早出問題。8. 項目擴展與個人心得做完這套系統(tǒng)再回頭去看我會覺得技術(shù)棧本身并不復雜真正花時間的其實是業(yè)務(wù)規(guī)則的設(shè)計和落地。預約時間沖突判斷、簽到時限、暫離時長限制每一個規(guī)則邊界都需要想清楚否則上線之后運營人員會被各種邊界case折磨。很多人以為做一個管理系統(tǒng)就是增刪改查做完才發(fā)現(xiàn)增刪改查只是最外面一層皮真正的核心在于“狀態(tài)機怎么流轉(zhuǎn)”“并發(fā)下如何保持一致”“用戶體驗怎么做得好”。如果你也正在做類似的系統(tǒng)我的建議是先把業(yè)務(wù)流程圖畫明白把每個狀態(tài)之間的跳轉(zhuǎn)條件和限制想清楚再動手寫代碼。數(shù)據(jù)庫設(shè)計可以花一整天反復推敲這比代碼寫了一半再改表結(jié)構(gòu)要劃算得多。這個項目后續(xù)可以擴展的方向也很多。比如加入一個基于WebSocket的實時座位狀態(tài)推送有人釋放座位時其他在線用戶立刻看到或者引入Redis緩存熱點數(shù)據(jù)減輕數(shù)據(jù)庫壓力再或者把前端升級成Vue 3 TypeScript代碼的可維護性會再上一個臺階。但這些都是增量優(yōu)化核心骨架不變。最后說一個實際開發(fā)里的細節(jié)我在項目里對所有時間字段統(tǒng)一用了LocalDateTime而不是Date。這樣在做時間段比較的時候比Date要順手很多也避免了時區(qū)偏移導致的坑。如果你正在選型階段一開始就統(tǒng)一用LocalDateTime能省掉后面不少麻煩。