實戰(zhàn):從數(shù)據(jù)庫設計到并發(fā)防超賣)
做自習室預約系統(tǒng)這件事我前后折騰了大半個月踩了不少坑也積累了不少心得。這個基于SpringBoot Vue的MVC自習室管理和預約系統(tǒng)大概是很多計算機專業(yè)同學畢業(yè)設計或課程項目的熱門選題也是很多小型創(chuàng)業(yè)團隊做共享空間管理時愿意參考的一套基礎架子。今天我把整套系統(tǒng)的設計思路、核心代碼、數(shù)據(jù)庫方案和部署細節(jié)完整梳理一遍特別是那些網(wǎng)上教程一般不會主動告訴你的坑都一并寫出來。這套系統(tǒng)能做什么簡單說就是三件事管理自習室和座位信息、讓用戶在線預約座位、提供后臺的數(shù)據(jù)統(tǒng)計和基礎管理能力。技術棧非常經(jīng)典后端SpringBoot MyBatis MySQL前端Vue Vue Router Axios整體遵循MVC分層思想。適合正在做課設、畢設的學生也適合想快速搭一套輕量級預約系統(tǒng)的開發(fā)者。1. 為什么做這個系統(tǒng)需求拆解與技術選型1.1 自習室預約到底解決了什么問題去圖書館或者付費自習室搶座位的經(jīng)歷相信不少人都有過。傳統(tǒng)方式要么是線下排隊登記要么是微信群接龍再原始一點就是放一張紙自己簽名。這些問題很典型座位狀態(tài)不透明去了才發(fā)現(xiàn)滿座管理員無法實時掌握使用率座位長期被占但沒人來用戶和管理員之間信息斷層投訴和糾紛不少。所以我做的這套系統(tǒng)核心就是解決三個問題座位資源可視化、預約流程線上化、管理數(shù)據(jù)可統(tǒng)計。用戶端看到的是一個座位圖哪些座位空閑、哪些被占一目了然預約操作點幾下就能完成取消、續(xù)約也能自助處理。管理員端則能看每日預約量、座位利用率、用戶活躍度這些關鍵指標不用再靠Excel手工統(tǒng)計。系統(tǒng)角色劃分也很清晰普通用戶負責預約、取消、查看個人記錄管理員負責自習室和座位維護、預約審核或關閉、查看統(tǒng)計報表。這套RBAC基于角色的訪問控制模型不復雜但足夠支撐起一個完整的業(yè)務閉環(huán)。1.2 SpringBoot Vue MyBatis MySQL這套組合的取舍我在一開始也糾結過技術選型。Node.js Express做后端Python Django最終還是選了SpringBoot這套組合理由非常實際。SpringBoot是目前國內(nèi)企業(yè)級開發(fā)占有率最高的框架之一。它最核心的價值就是簡化配置內(nèi)嵌Tomcat一個jar包就能跑起來不需要額外部署WAR包。配合Maven或Gradle管理依賴幾行配置就能集成MyBatis、MySQL、Redis這些中間件。對于校內(nèi)項目或者中小團隊來說SpringBoot的學習資料最豐富遇到問題的解決方案也最多這個優(yōu)勢在開發(fā)中非常實際。Vue選擇的是Vue 2還是Vue 3我建議新項目直接上Vue 3 Composition API。2025年這個時間點Vue 2已經(jīng)停止維護了生態(tài)里新的UI庫也基本都適配Vue 3。Vue全家桶里真正開發(fā)必須掌握的就三樣Vue Router負責頁面路由Pinia或Vuex負責狀態(tài)管理Axios負責和后端接口通信。MyBatis和MySQL的組合更是經(jīng)典中的經(jīng)典。MySQL是開源關系型數(shù)據(jù)庫里生態(tài)最成熟的安裝部署簡單性能足夠中小規(guī)模應用使用而且和SpringBoot整合非常順滑。MyBatis則讓你保持對SQL的完全控制權復雜查詢不會被ORM框架的限制卡住。有些人會問為什么不用MyBatis-Plus我這里用原生的MyBatis是為了把XML映射和動態(tài)SQL這些底層機制講清楚理解了這一層再上手MyBatis-Plus就是一天的事。2. 數(shù)據(jù)庫設計與核心表結構2.1 核心數(shù)據(jù)表用戶、自習室、座位、預約單數(shù)據(jù)庫設計是這套系統(tǒng)的地基表結構設計得好不好直接影響后續(xù)業(yè)務邏輯的復雜度。我設計的核心表一共有五張用戶表、自習室表、座位表、預約單表還有一張管理員表。下面把每張表的重點字段講清楚。用戶表比較常規(guī)重點字段有id、用戶名、密碼BCrypt加密存儲、手機號、角色標識user/admin、狀態(tài)字段和創(chuàng)建時間。密碼一定不能明文存儲這個是最基本的安全底線。狀態(tài)字段用來表示賬號是否被凍結方便管理員做限制。自習室表相對簡單一個自習室包含名稱、位置、開放時間、座位總數(shù)、描述信息。這里有個小設計點座位總數(shù)不要冗余存儲而是通過統(tǒng)計座位表中某個自習室id的數(shù)量實時計算。如果你想優(yōu)化查詢性能可以在自習室表里加一個total_seats字段做冗余但要注意維護數(shù)據(jù)一致性。座位表是整套系統(tǒng)的核心字段包括id、自習室id、座位編號自習室內(nèi)唯一、座位類型比如單人桌、雙人桌、靠窗座位、安靜區(qū)座位等還有座位狀態(tài)和座位坐標等。為什么加坐標字段因為前端要渲染座位圖沒有坐標你就無法準確把座位畫在對應位置上。預約單表字段最多也是最容易設計出錯的一張表。核心字段有預約單號業(yè)務編號方便人工核驗、用戶id、自習室id、座位id、預約日期、開始時間、結束時間、狀態(tài)字段待使用/已使用/已取消/已過期等、創(chuàng)建時間。這里要特別強調(diào)一個設計經(jīng)驗業(yè)務上需要向用戶展示的編號和數(shù)據(jù)庫主鍵id一定要分開。數(shù)據(jù)庫主鍵autoincrement的id不要直接暴露給用戶因為會泄露系統(tǒng)數(shù)據(jù)量也容易被人遍歷接口。2.2 座位狀態(tài)與預約狀態(tài)的流轉(zhuǎn)設計狀態(tài)設計是最容易做亂的環(huán)節(jié)。我前后改了三版才理清。核心思路是把座位狀態(tài)和預約狀態(tài)拆開不能混在一起。座位只有三種狀態(tài)空閑、占用、禁用。占用表示這個座位在當前時間段被預約了禁用是管理員手動關閉某些座位比如空調(diào)壞了、燈管不亮臨時維修。這里有一個非常容易掉坑的地方座位本身沒有“時間”的概念。同一個座位早上可能是空閑的下午可能是占用的。所以我用的是預約記錄來反推座位狀態(tài)而不是在座位表上存一個update_status_time字段。座位表上的status字段更準確說是“運營狀態(tài)”可用或禁用。真正的實時占用狀態(tài)由一個視圖或查詢接口動態(tài)計算當前時間范圍內(nèi)是否存在未取消的預約記錄。預約狀態(tài)我設計了四個待使用、已完成、已取消、爽約。待使用是預約成功但還沒到時間已完成是使用時間結束后系統(tǒng)自動更新或管理員手動確認已取消是用戶主動取消爽約則定義了這樣一個規(guī)則預約時間開始后30分鐘內(nèi)未簽到自動標記為爽約。這套狀態(tài)機看似簡單但最終落庫的時候要注意狀態(tài)字段不要用數(shù)字盡量用字符串varchar存英文枚舉值比如BOOKED、FINISHED、CANCELLED、NO_SHOW。項目組里新人接手時看到字符串狀態(tài)一看就懂看到0和1還得猜含義。2.3 并發(fā)防超賣唯一約束與樂觀鎖并發(fā)問題是在做預約系統(tǒng)時最需要提前思考的地方。多個用戶同時點擊同一個座位的預約按鈕如果處理不當就可能出現(xiàn)“超賣”——一個座位同一時間被預約給多個人。我用了三層機制解決這個問題。第一層是數(shù)據(jù)庫唯一約束。在預約單表上建一個聯(lián)合唯一索引字段組合是seat_id 預約日期開始時間。這樣數(shù)據(jù)庫層面就保證了同一座位在同一時段只能有一條有效預約記錄。只要用戶取消預約原記錄變成取消狀態(tài)新預約才能再次插入。要注意唯一索引必須帶上狀態(tài)條件嗎MySQL不支持函數(shù)索引的寫法比較復雜所以我的做法是取消的記錄不物理刪除但預約唯一索引只約束status為生效狀態(tài)的記錄。怎么實現(xiàn)這就要用到MyBatis動態(tài)SQL插入前先執(zhí)行一個“活性檢查”查詢確認該座位該時間段沒有狀態(tài)為待使用/已使用的記錄再執(zhí)行插入。數(shù)據(jù)庫唯一索引用來做最后一道兜底它能攔截掉極短時間內(nèi)出現(xiàn)的并發(fā)沖突。第二層是業(yè)務層面的互斥鎖。我選擇在Service層中使用synchronized關鍵字按座位維度加鎖——更嚴謹?shù)淖龇ㄊ鞘褂脭?shù)據(jù)庫的悲觀鎖SELECT ... FOR UPDATE鎖住座位表的行再執(zhí)行插入。但由于自習室預約場景并發(fā)量不會特別高synchronized鎖綁定座位id字符串的intern方法即可滿足需求。不過使用synchronized注意鎖的粒度要細不要鎖整個方法否則所有座位的預約都會互相阻塞性能瞬間崩掉。第三層是兜底的異常處理。即使前面兩層都過去了最后一步插入時如果唯一索引沖突拋了DuplicateKeyException也要捕獲這個異常并轉(zhuǎn)成友好的業(yè)務提示“該座位已被手速更快的小伙伴預約了”。用戶看到這個提示體驗還算可以不會覺得自己點了個假按鈕。3. 后端MVC架構落地與關鍵代碼解析3.1 項目分層Controller、Service、Mapper到底各管什么MVC三層架構在SpringBoot項目里已經(jīng)演化成更細的分層Controller層、Service層、Mapper層再外加一個entity/model層放實體類dto層放參數(shù)對象vo層放返回結果對象。很多人剛學的時候容易把Controller寫成“萬能類”所有邏輯都塞里面這是典型的錯誤姿勢。我的分包結構是標準做法直接照著建就行com.studyroom ├── controller // 接口層接收請求、校驗參數(shù)、返回結果 ├── service // 業(yè)務層處理核心邏輯、事務控制 │ └── impl ├── mapper // MyBatis的數(shù)據(jù)訪問接口 ├── entity // 數(shù)據(jù)庫表對應的實體類 ├── dto // 接收前端參數(shù)的傳輸對象 ├── vo // 返回給前端的結果封裝 ├── config // 配置類比如跨域配置、攔截器配置 ├── common // 通用類統(tǒng)一返回結果、異常處理、常量 └── utils // 工具類Controller層只做三件事接收參數(shù)、調(diào)用Service、返回統(tǒng)一結果對象。它不應該出現(xiàn)任何具體的業(yè)務判斷邏輯。比如預約請求進入Controller后它要做的就是把預約參數(shù)綁定成一個DTO類傳給Service然后返回Result.success(data)。至于校驗座位是否存在、時間是否合法、用戶是否重復預約這些全在Service里完成。Service層是業(yè)務邏輯的核心所有判斷、計算、狀態(tài)流轉(zhuǎn)都在這層完成。這里有件事需要注意涉及多表更新的操作比如取消預約需要同時修改預約狀態(tài)、更新座位運營狀態(tài)、可能要寫一條操作日志必須在Service方法上標注Transactional。否則就算第一句SQL執(zhí)行成功第二句報錯數(shù)據(jù)庫留下臟數(shù)據(jù)排查起來極其痛苦。Mapper層就純粹是數(shù)據(jù)訪問。接口方法的注解或XML里的SQL只負責和數(shù)據(jù)庫打交道不做運算不做判斷把查詢結果原樣返回。我在項目里統(tǒng)一使用XML文件寫SQL因為動態(tài)SQL標簽如if、where、foreach在XML里可讀性更高復雜聯(lián)表查詢也好維護。簡單的單表查詢用注解Select也可以但為了風格統(tǒng)一我全部走了XML。3.2 MyBatis的XML映射與動態(tài)SQLMyBatis的XML文件是這個項目里一眼看上去最繁瑣、但實際最靈活的部分。它最大的優(yōu)勢就是動態(tài)SQL。比如管理員在后臺篩選預約記錄會有多個篩選條件按狀態(tài)查、按日期查、按自習室查、按用戶手機號查。這些條件組合起來可能幾十種情況如果全寫死在Java代碼里那打補丁得累死。我的做法是這樣的用一個Map或一個DTO接收所有查詢參數(shù)然后在XML里用where標簽if標簽動態(tài)拼接。這樣當某個參數(shù)為空時對應的SQL條件片段就不會拼進去查詢語句會自適應變化。下面是一個預約記錄分頁查詢的XML片段select idselectReservationPage resultTypecom.studyroom.vo.ReservationVO SELECT r.id, r.reservation_no, r.room_id, r.seat_id, r.user_id, u.username, u.phone, r.reserve_date, r.start_time, r.end_time, r.status, r.create_time FROM reservation r LEFT JOIN user u ON r.user_id u.id where if teststatus ! null and status ! AND r.status #{status} /if if testroomId ! null AND r.room_id #{roomId} /if if testreserveDate ! null AND r.reserve_date #{reserveDate} /if if testkeyword ! null and keyword ! AND (u.username LIKE CONCAT(%, #{keyword}, %) OR u.phone LIKE CONCAT(%, #{keyword}, %)) /if /where ORDER BY r.create_time DESC LIMIT #{offset}, #{pageSize} /select這里有個細節(jié)值得留意LIKE查詢的寫法。我之前見過有人直接在Java代碼里拼好%...%再傳到SQL里這樣也work但存在注入風險。用CONCAT函數(shù)在SQL層拼接更安全。LIMIT后面的offset和pageSize用#{}參數(shù)占位MyBatis會自動做預編譯防SQL注入。這一點是面試高頻考點也是實際開發(fā)必須養(yǎng)成的好習慣。另外resultType和resultMap的選擇上我大部分聯(lián)表查詢用resultType直接映射到VO對象只要數(shù)據(jù)庫字段名和VO屬性名通過下劃線轉(zhuǎn)駝峰映射就能對上。需要在application.yml里開啟map-underscore-to-camel-case: true這個配置。3.3 預約與取消預約的接口實現(xiàn)預約接口是整個系統(tǒng)最核心的接口它的實現(xiàn)邏輯我拆成了五個步驟參數(shù)校驗、重復檢查、沖突檢查、座位狀態(tài)檢查、插入預約單。下面把關鍵代碼的思維流程講一遍。參數(shù)校驗階段要檢查必傳字段用戶id、自習室id、座位id、預約日期、開始時間和結束時間。開始時間必須早于結束時間預約日期不能是過去日期。這些校驗放在Service里能保證即使繞過前端校驗也攔得住。重復檢查就是檢查同一個用戶同一個時間段內(nèi)是否已經(jīng)有預約。規(guī)則可以做成一個用戶同一時間段只能有一個有效預約避免“占著茅坑不拉屎”的惡意預約行為。沖突檢查則查座位在目標時間段是否已有其他用戶的預約。座位狀態(tài)檢查要查座位的運營狀態(tài)是否可用。如果這個座位被管理員標記為禁用那預約請求要直接拒絕。最后一步才是生成預約單狀態(tài)設為待使用同時返回給前端一個預約成功的提示和預約單號。取消預約的邏輯相對簡單但要仔細考慮時間限制。我設定的規(guī)則是預約開始時間前30分鐘允許取消已經(jīng)超過時間就只能走“超時未到”流程。這里有一個特殊處理取消操作要同步做兩件事改預約單狀態(tài)為已取消同時如果當前沒有其他預約占用該座位座位狀態(tài)恢復為空閑。為了讓讀者更好理解我把核心的Service方法偽代碼寫出來Override Transactional(rollbackFor Exception.class) public ReservationResponse reserve(ReserveRequest request) { // 1. 參數(shù)校驗 if (request.getStartTime().compareTo(request.getEndTime()) 0) { throw new BizException(開始時間必須早于結束時間); } // 2. 檢查用戶是否已有沖突預約 int count reservationMapper.checkUserConflict( request.getUserId(), request.getReserveDate(), request.getStartTime(), request.getEndTime()); if (count 0) { throw new BizException(你已有同時段的預約記錄); } // 3. 檢查座位是否可用 Seat seat seatMapper.selectById(request.getSeatId()); if (seat null || SeatStatus.DISABLED.getCode().equals(seat.getStatus())) { throw new BizException(座位不可用); } // 4. 檢查座位同一時段沖突 int seatConflict reservationMapper.checkSeatConflict( request.getSeatId(), request.getReserveDate(), request.getStartTime(), request.getEndTime()); if (seatConflict 0) { throw new BizException(該座位當前時段已被預約); } // 5. 生成預約單 Reservation reservation new Reservation(); reservation.setReservationNo(generateReservationNo()); // ... set other fields reservationMapper.insert(reservation); return ReservationResponse.from(reservation); }generateReservationNo我是這樣生成的每天日期字符串加六位隨機數(shù)比如20250601 823471。加上唯一索引防止極端情況下兩個單號撞車。這個單號的價值主要體現(xiàn)在線下場景自習室管理員看到單號就能快速在系統(tǒng)里查到對應預約比用數(shù)據(jù)庫id好太多。4. Vue前端實現(xiàn)與交互細節(jié)4.1 項目初始化與路由設計前端用Vue腳手架創(chuàng)建項目后首先要裝依賴vue-router負責路由pinia負責狀態(tài)管理axios負責發(fā)HTTP請求element-plus或vant做UI組件庫。我這套PC端管理類頁面比較多選的是Element Plus。路由設計我采用了動態(tài)路由的思路登錄成功之后根據(jù)后端返回的角色信息動態(tài)添加路由。普通用戶能訪問的路由是首頁、自習室列表、座位預約、我的預約、個人中心管理員能額外訪問座位管理、自習室管理、預約管理、數(shù)據(jù)統(tǒng)計。這樣避免把所有路由一次性注冊用戶手動輸入URL訪問不該進的頁面直接被404或重定向。實現(xiàn)核心是在router.beforeEach的全局前置守衛(wèi)中加邏輯取到用戶的token再根據(jù)角色拼接需要動態(tài)添加的路由。這個方案要注意一個問題刷新頁面時路由會被重置必須在刷新前把路由信息存到pinia或localStorage里下次進入時再重新addRoute。4.2 自習室列表與選座交互用戶進入預約流程的核心路徑是選擇自習室 - 查看座位圖 - 點擊座位 - 確認預約信息 - 提交成功。自習室列表頁比較簡單就是一個卡片列表。每個卡片展示自習室名稱、位置、開放時間、剩余座位數(shù)。剩余座位數(shù)不要單獨寫死而是每次查詢時動態(tài)計算。我這邊是用一個接口獲取所有自習室再返回每個自習室的可用座位數(shù)。座位圖是前端開發(fā)中最需要花心思做的一塊。我設計了一個Canvas繪制的座位平面圖后端返回每個座位在自習室中的相對坐標(x, y)前端根據(jù)坐標繪制座位方塊不同狀態(tài)的座位用不同顏色區(qū)分綠色空閑可點擊、灰色已占用、黃色被選中。用戶點擊空閑座位后彈出側(cè)邊欄顯示預約時間選擇器選好時間段后提交。這個交互看似簡單但雷區(qū)不少。最大的坑是點擊事件綁定。如果你用div渲染多個座位事件冒泡可能導致用戶點一個座位被判定成點擊另一個。解決辦法是為每個座位設置唯一的data-index屬性事件處理時用event.target.dataset.index來鎖定目標而不是依賴事件對象的其它屬性。另外還要給選中的座位做高亮并且一定要處理用戶點擊A座位再點擊B座位的情況上一次選中的狀態(tài)要取消掉。具體到座位坐標返回的接口設計我推薦一次返回該自習室當天所有座位信息的列表包含id、座位編號、x坐標、y坐標、狀態(tài)。前端拿到數(shù)據(jù)后渲染即可。這里不要在選座時才逐個請求座位詳情會造成大量HTTP請求拖慢頁面響應。4.3 狀態(tài)管理用戶信息與預約狀態(tài)Pinia是Vue 3官方推薦的狀態(tài)管理庫相當于是Vuex的進化版。它的代碼比Vuex簡潔很多去掉了mutationsaction里直接改state。我把用戶登錄信息、token、預約篩選條件這些全局共享數(shù)據(jù)都放進了Pinia。實際開發(fā)中比較關鍵的是Token管理。用戶登錄成功后后端返回一個token我用的是JWT前端把token存到localStorage里同時設置axios的請求攔截器在每次請求前自動把token放進請求頭Authorization字段。響應攔截器里做統(tǒng)一錯誤處理后端返回401時自動清除token并跳轉(zhuǎn)登錄頁返回業(yè)務錯誤碼時直接彈出ElMessage提示用戶。預約狀態(tài)的響應式更新也很重要。用戶成功取消一個預約后之前座位圖上對應座位的狀態(tài)還顯示為灰色已占用但數(shù)據(jù)庫里已經(jīng)變成空閑了。這時候必須觸發(fā)數(shù)據(jù)刷新。我的做法是把當前自習室的座位數(shù)據(jù)存到Pinia的管理模塊里取消預約成功后調(diào)用seatStore的fetchSeats方法重新拉取座位數(shù)據(jù)保證UI和數(shù)據(jù)庫狀態(tài)一致。千萬別圖省事只在前端改一個座位狀態(tài)變量一旦刷新頁面就露餡而且并發(fā)情況下用戶看到的可能是過期數(shù)據(jù)。5. 部署運行與常見問題排查5.1 本地快速啟動初始化SQL與配置文件整套系統(tǒng)跑起來的第一步是初始化數(shù)據(jù)庫。提前把建庫建表SQL腳本準備好包括核心表數(shù)據(jù)和幾個測試用戶數(shù)據(jù)。MySQL 8.x版本要注意字符集數(shù)據(jù)庫和表都用utf8mb4而不是utf8因為utf8在MySQL里不是標準的四字節(jié)UTF-8emoji等特殊字符存不進去會報錯。這個問題非常隱蔽排查很久才發(fā)現(xiàn)的。后端配置文件application.yml里我最常被問到的幾個配置server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/study_room?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.studyroom.entity configuration: map-underscore-to-camel-case: trueserverTimezoneAsia/Shanghai是很多新人忽略的地方。MySQL 8.x驅(qū)動默認時區(qū)是UTC如果不指定查出來的時間會比本地時間早8小時預約時間的顯示全部錯亂。allowPublicKeyRetrievaltrue這個參數(shù)是在用caching_sha2_password認證插件時需要的不加會報Public Key Retrieval is not allowed錯誤。5.2 高頻報錯與解決方案我把開發(fā)和部署中遇到的幾個高頻問題整理成表。這些問題都是新手期最容易觸發(fā)、且真實項目里也極有可能再現(xiàn)的。問題現(xiàn)象原因解決方案Failed to configure a DataSourceapplication.yml找不到配置或類掃描路徑不對檢查注解SpringBootApplication所在包是否能掃描到配置類Invalid bound statement (not found)mapper接口和XML文件未綁定檢查XML文件位置是否在mapper-locations配置路徑下方法名是否一致Cannot load driver class: com.mysql.cj.jdbc.DriverMySQL驅(qū)動未引入在pom.xml中引入mysql-connector-java依賴版本需與本地MySQL對應日期錯亂少8小時時區(qū)配置未設置增加serverTimezoneAsia/Shanghai參數(shù)跨域請求被攔截前后端端口不一致在SpringBoot中配置CorsFilter或使用CrossOrigin注解前端請求404或500后端接口路徑或參數(shù)名不一致用瀏覽器開發(fā)者工具的Network面板查看具體請求和響應唯一索引沖突Duplicate entry同一座位同一時間段重復預約在Service中執(zhí)行沖突檢查并捕獲異常返回友好提示跨域問題我要單獨說。我開發(fā)時前端跑在5173端口Vite默認后端跑在8080端口兩者端口不同瀏覽器攔截跨域請求很正常。解決方式有兩種后端統(tǒng)一配置一個CorsFilter允許指定源和指定方法另一個是后端使用Spring Cloud Gateway等網(wǎng)關代理但單機開發(fā)時一個CorsFilter就夠用了。生產(chǎn)環(huán)境建議使用Nginx做反向代理把前端靜態(tài)文件和后端接口放到同一個域名下從根源上規(guī)避跨域。5.3 性能與并發(fā)優(yōu)化心得自習室預約這類系統(tǒng)的并發(fā)量不會特別夸張但是某些高峰時段例如圖書館的期末復習周還是會有一波瞬間高并發(fā)。我的優(yōu)化經(jīng)驗分三檔。第一檔是前端限流。座位圖上的座位數(shù)量有限用戶需要先選擇時間段再提交避免所有人都同時在預約按鈕上狂點。提交按鈕設置60秒的防重復點擊限制用戶操作邏輯上先攔住一部分無效請求。第二檔是后端緩存。熱門自習室的座位狀態(tài)數(shù)據(jù)可以緩存到Redis減少數(shù)據(jù)庫的查詢壓力。但緩存會讓數(shù)據(jù)狀態(tài)有一定的延遲需要配合緩存過期時間或者消息通知主動失效。自習室預約系統(tǒng)的數(shù)據(jù)一致性要求并不那么高因為用戶查詢座位狀態(tài)時看到有一兩秒的延遲完全可以接受但預約提交瞬間必須讀最新數(shù)據(jù)。第三檔才是數(shù)據(jù)庫性能優(yōu)化。給預約單表建立合適的聯(lián)合索引預約日期狀態(tài)自習室id座位表建立自習室id索引用戶表手機號建唯一索引。這幾種索引組合能覆蓋絕大部分業(yè)務查詢場景。不要盲目給所有字段加索引寫多讀少的字段加索引反而拖慢插入速度。還有一個經(jīng)驗是盡量把復雜SQL拆分。比如統(tǒng)計報表需求每日預約量、座位利用率、自習室排行。這類統(tǒng)計查詢在數(shù)據(jù)量小的時候用一條SQL就能搞定但數(shù)據(jù)量一旦上來group by和子查詢會讓數(shù)據(jù)庫壓力劇增。我的做法是把統(tǒng)計任務拆成定時任務每天凌晨用Spring Boot的Scheduled注解生成當天的統(tǒng)計匯總數(shù)據(jù)存到統(tǒng)計表前臺展示時只需查統(tǒng)計表查詢性能提升非常明顯。6. 從課設到真實項目的提升點從一個能跑起來的課設系統(tǒng)到一個稍微接近生產(chǎn)環(huán)境的應用中間還差幾個關鍵動作。整理項目時我用下面幾條標準來衡量系統(tǒng)成熟度也可以對照看看你的項目卡在哪一檔。第一是日志鏈路。開發(fā)階段print()完事直接輸出到控制臺沒問題但部署到服務器后就必須依賴日志定位問題。我用的方案是Logback按天滾動切割ERROR級別和INFO級別分開輸出。預約成功、取消、失敗這些核心操作至少記錄一條INFO日志包含用戶id、座位id和操作結果方便日后做問題排查。第二是參數(shù)校驗的規(guī)范化。不要在前端做了一堆校驗就認為萬事大吉。后端必須用JSR 303的Valid注解做參數(shù)校驗在DTO字段上標注NotNull、NotBlank、Length等注解寫起來零成本但能攔截掉大量無效請求。第三是接口文檔。以前很多小團隊不愛寫接口文檔前后端聯(lián)調(diào)靠口頭溝通效率極低。后來普遍的方案是集成SpringDoc或Swagger啟動項目后自動生成接口文檔。另一個更好維護的方式是寫Apifox或Postman的接口集合把每個接口的請求參數(shù)和返回結果固化下來新成員接手時直接看接口集合就能上手不用讀一遍源碼。第四是數(shù)據(jù)安全。密碼加密使用BCrypt登錄接口加簡單的驗證碼或圖片驗證碼做防爆破。預約的接口要驗證當前登錄用戶和操作對象是否一致防止通過篡改請求參數(shù)操作他人的預約記錄。這個越權問題在課設里常常被忽略但在真實項目中屬于高危漏洞。我用的是攔截器加ThreadLocal保存當前登錄用戶信息Service層取用戶id時統(tǒng)一從這個ThreadLocal取前端傳來的任何user_id都不作為權限判斷依據(jù)。7. 實操中的個人體會整個項目做完回頭復盤我最大的感受是技術選型重要但比技術選型更重要的是對業(yè)務邊界和狀態(tài)變化的清晰認知。這套系統(tǒng)的業(yè)務本質(zhì)并不復雜就是一個座位的占用時間片管理但圍繞這個本質(zhì)展開的字段設計、狀態(tài)流轉(zhuǎn)、并發(fā)控制、交互體驗每一環(huán)都會決定系統(tǒng)的穩(wěn)定性和易用性。對我個人來說動手做一個項目永遠比看書看視頻學得快。遇到問題靠搜索引擎、靠官方文檔、靠上下文調(diào)試慢慢解決踩過坑的記憶最牢固。也建議你把這個項目跑起來之后嘗試加一個功能或者換一種實現(xiàn)方式比如把座位預約改成計數(shù)預約、加一個基于Redis的排隊功能、把統(tǒng)計報表改成圖表可視化。不要照著我寫的代碼抄一遍就完事改造的過程中你才會真正理解哪一步為什么這么設計。