:時(shí)間沖突檢測(cè)與狀態(tài)機(jī)設(shè)計(jì)實(shí)戰(zhàn))
我去年完整做了一套汽車租賃管理系統(tǒng)技術(shù)棧就是標(biāo)題里那套SpringBoot Vue3 MyBatis MySQL前后端分離。做之前以為就是一個(gè)普通的業(yè)務(wù)管理系統(tǒng)做順了才發(fā)現(xiàn)租賃類業(yè)務(wù)和常規(guī)CRUD完全是兩碼事——它有一個(gè)繞不開(kāi)的核心約束同一輛車在同一時(shí)間段只能被一個(gè)訂單占用。這個(gè)時(shí)間窗口的沖突檢測(cè)加上訂單從下單到還車的全生命周期狀態(tài)流轉(zhuǎn)才是這套系統(tǒng)真正有含金量的地方。這篇文章把我從頭到尾的設(shè)計(jì)思路、表結(jié)構(gòu)設(shè)計(jì)、核心接口邏輯、前端交互方案以及實(shí)測(cè)中踩過(guò)的坑完整復(fù)盤一遍。不管你是正在準(zhǔn)備Java全棧項(xiàng)目、想充實(shí)簡(jiǎn)歷的應(yīng)屆生還是公司要快速落地一套租賃類業(yè)務(wù)后臺(tái)這套設(shè)計(jì)都能直接參考。1. 技術(shù)棧選型為什么是SpringBootVue3MyBatis而不是別家組合1.1 四個(gè)組件各自解決什么問(wèn)題這套技術(shù)棧放到今天已經(jīng)不算新潮但它依舊是Java企業(yè)級(jí)項(xiàng)目里最穩(wěn)的組合。SpringBoot負(fù)責(zé)把后端的集成工作簡(jiǎn)化到極致嵌入式Tomcat、自動(dòng)配置、starter機(jī)制省去大量XML配置MyBatis負(fù)責(zé)SQL的可控性租賃系統(tǒng)里查詢條件特別多車型、價(jià)格區(qū)間、狀態(tài)、租期組合查詢一多動(dòng)態(tài)SQL的價(jià)值就體現(xiàn)出來(lái)了Vue3配合Vite和Element Plus做后臺(tái)管理界面和用戶端頁(yè)面都很順手MySQL負(fù)責(zé)數(shù)據(jù)持久化輕量、穩(wěn)定、資料多作為中小型業(yè)務(wù)系統(tǒng)的數(shù)據(jù)庫(kù)完全夠用。有人會(huì)問(wèn)現(xiàn)在MyBatis-Plus這么流行為什么不用兩個(gè)原因。第一這個(gè)項(xiàng)目體量不大原生MyBatis完全能把SQL控制住不會(huì)出現(xiàn)幾百行動(dòng)態(tài)SQL的極端場(chǎng)景第二MyBatis的動(dòng)態(tài)SQL和XML映射是Java面試??键c(diǎn)用原生方式把這塊吃透對(duì)面試幫助更大。實(shí)際開(kāi)發(fā)中團(tuán)隊(duì)用MP還是原生那是團(tuán)隊(duì)習(xí)慣問(wèn)題但對(duì)于個(gè)人項(xiàng)目我建議至少要能獨(dú)立寫出XML里的動(dòng)態(tài)SQL。1.2 項(xiàng)目邊界這個(gè)系統(tǒng)到底做什么做一個(gè)系統(tǒng)之前先把邊界劃清楚否則容易越做越失控。我的設(shè)計(jì)是兩類角色、兩條主線。普通用戶能做的事注冊(cè)登錄、瀏覽車輛、選擇租期下單、在線支付押金和租金、查看個(gè)人訂單、還車結(jié)算。管理員能做的事車輛增刪改查、車輛上架下架、訂單審核、確認(rèn)取車、確認(rèn)還車、訂單結(jié)算。這個(gè)邊界劃好后前后端的功能范圍就定了接口數(shù)量也能數(shù)得出來(lái)。全系統(tǒng)核心接口大概二十來(lái)個(gè)后端分用戶模塊、車輛模塊、訂單模塊前端分用戶端和管理端兩個(gè)獨(dú)立視圖。邊界清晰的好處是開(kāi)發(fā)過(guò)程中不會(huì)出現(xiàn)“要不要加個(gè)站內(nèi)信”、“要不要做積分系統(tǒng)”這種沒(méi)完沒(méi)了的需求蔓延。1.3 前后端分離的關(guān)鍵收益前后端分離不是趕時(shí)髦對(duì)這個(gè)項(xiàng)目來(lái)說(shuō)有幾個(gè)實(shí)際收益。第一開(kāi)發(fā)過(guò)程中后端不需要關(guān)心頁(yè)面長(zhǎng)什么樣只返回JSON前端也不需要關(guān)心SQL怎么查只負(fù)責(zé)渲染和交互我可以并行推進(jìn)第二部署上后端是一個(gè)jar包前端是一堆靜態(tài)文件可以單獨(dú)發(fā)布前端改樣式不用重新打包后端第三將來(lái)如果要做小程序端或者App端后端接口可以直接復(fù)用不用重寫。代價(jià)也有就是聯(lián)調(diào)成本變高了。前端需要知道后端接口返回的數(shù)據(jù)結(jié)構(gòu)后端需要知道前端需要什么字段。我的辦法是后端統(tǒng)一返回結(jié)構(gòu)所有數(shù)據(jù)都包在Result對(duì)象里前端拿到后按約定解析。這個(gè)統(tǒng)一約定后面專門有一節(jié)說(shuō)。2. 數(shù)據(jù)庫(kù)模型車輛、訂單、用戶三張核心表的設(shè)計(jì)邏輯2.1 核心表結(jié)構(gòu)與建表SQL數(shù)據(jù)庫(kù)設(shè)計(jì)是這套系統(tǒng)的地基。我用的核心表就三張用戶表、車輛表、訂單表。不要加太多冗余表更不要用物理外鍵業(yè)務(wù)關(guān)系靠代碼邏輯維護(hù)索引來(lái)保證查詢性能。用戶表id、用戶名、密碼、手機(jī)號(hào)、身份證號(hào)、角色、狀態(tài)、創(chuàng)建時(shí)間。密碼我用了BCrypt加密存儲(chǔ)明文存密碼在真實(shí)項(xiàng)目里是絕對(duì)紅線。數(shù)據(jù)庫(kù)字段類型金額用decimal時(shí)間用datetime狀態(tài)用tinyint。車輛表這里信息量比較大我貼一下實(shí)際的建表SQLCREATE TABLE car ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 車輛ID, brand VARCHAR(50) NOT NULL COMMENT 品牌, model VARCHAR(50) NOT NULL COMMENT 車型, plate_number VARCHAR(20) NOT NULL UNIQUE COMMENT 車牌號(hào), category TINYINT NOT NULL DEFAULT 0 COMMENT 車型分類 0-經(jīng)濟(jì)型 1-SUV 2-商務(wù)型 3-豪華型, daily_price DECIMAL(10,2) NOT NULL COMMENT 日租金, deposit DECIMAL(10,2) NOT NULL COMMENT 押金, status TINYINT NOT NULL DEFAULT 0 COMMENT 狀態(tài) 0-可租 1-已租出 2-維修中 3-已下架, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 創(chuàng)建時(shí)間 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT車輛表; CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 訂單ID, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 訂單編號(hào), user_id BIGINT NOT NULL COMMENT 用戶ID, car_id BIGINT NOT NULL COMMENT 車輛ID, start_time DATETIME NOT NULL COMMENT 取車時(shí)間, end_time DATETIME NOT NULL COMMENT 還車時(shí)間, daily_price DECIMAL(10,2) NOT NULL COMMENT 下單時(shí)日租金快照, total_days INT NOT NULL COMMENT 租期天數(shù), amount DECIMAL(10,2) NOT NULL COMMENT 租金金額, deposit DECIMAL(10,2) NOT NULL COMMENT 押金金額, status TINYINT NOT NULL DEFAULT 0 COMMENT 狀態(tài) 0-待支付 1-已支付待取車 2-租賃中 3-已完成 4-已取消, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 下單時(shí)間, pay_time DATETIME NULL COMMENT 支付時(shí)間, finish_time DATETIME NULL COMMENT 完成時(shí)間, INDEX idx_user_id (user_id), INDEX idx_car_id (car_id), INDEX idx_start_time (start_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT訂單表;有幾個(gè)字段容易被忽略我單獨(dú)說(shuō)。orders表里的daily_price字段是“下單時(shí)日租金快照”這個(gè)很重要。車輛日常價(jià)格會(huì)變比如節(jié)假日調(diào)價(jià)訂單一旦創(chuàng)建租金就得按下單那一刻的價(jià)格算不能在結(jié)算時(shí)又去讀車輛表的最新價(jià)格否則用戶會(huì)投訴。這是一個(gè)很典型的訂單設(shè)計(jì)原則金額類信息必須做快照不做實(shí)時(shí)關(guān)聯(lián)。訂單編號(hào)我用的是當(dāng)前時(shí)間加隨機(jī)數(shù)生成格式類似yyyyMMddHHmmss 4位隨機(jī)數(shù)保證同一秒內(nèi)并發(fā)也不會(huì)重復(fù)。生產(chǎn)環(huán)境可以用雪花ID或者Redis自增個(gè)人項(xiàng)目這個(gè)簡(jiǎn)化方案完全夠。2.2 車輛和訂單的狀態(tài)機(jī)設(shè)計(jì)狀態(tài)字段我全部用tinyint從0開(kāi)始遞增。為什么不用字符串三個(gè)原因省空間、查詢快、代碼里定義常量類后可讀性也不差。真正要注意的是別把魔法數(shù)字寫死在代碼里我在后端建了一個(gè)枚舉類把所有狀態(tài)做成枚舉前端再用映射字典把數(shù)字翻譯成中文標(biāo)簽。訂單狀態(tài)流轉(zhuǎn)是最核心的業(yè)務(wù)規(guī)則。從用戶角度看是這個(gè)鏈路待支付 - 已支付待取車 - 租賃中 - 已完成任意狀態(tài)下取消則進(jìn)入已取消。從管理員角度看這里面有兩次人工確認(rèn)動(dòng)作確認(rèn)取車和確認(rèn)還車。為什么要人工確認(rèn)因?yàn)榫€下要驗(yàn)車車況沒(méi)問(wèn)題才交接這個(gè)動(dòng)作必須由管理員觸發(fā)不能全自動(dòng)。狀態(tài)流轉(zhuǎn)用表格表示當(dāng)前狀態(tài)觸發(fā)動(dòng)作下一狀態(tài)誰(shuí)觸發(fā)0 待支付支付成功1 已支付待取車用戶0 待支付取消訂單4 已取消用戶1 已支付待取車管理員確認(rèn)取車2 租賃中管理員2 租賃中管理員確認(rèn)還車3 已完成管理員1 已支付待取車超時(shí)未取車4 已取消系統(tǒng)/管理員這套狀態(tài)機(jī)定義清晰后代碼里每個(gè)狀態(tài)下的按鈕、接口邏輯、車輛狀態(tài)配合都變得非常明確不需要在每個(gè)接口里臨時(shí)判斷一堆if。2.3 時(shí)間沖突檢測(cè)租賃系統(tǒng)最核心的業(yè)務(wù)校驗(yàn)這是整套系統(tǒng)最有含金量的地方。車輛表里有status字段標(biāo)記是否在租但只有這個(gè)還不夠——一個(gè)訂單還沒(méi)創(chuàng)建時(shí)車是“可租”的可一旦有人在某個(gè)時(shí)間段已經(jīng)預(yù)定了又沒(méi)到取車時(shí)間車已經(jīng)處于被占用的窗口期。這個(gè)時(shí)候需要靠訂單表來(lái)判斷。判斷邏輯可以歸結(jié)為一句話新訂單的租期 [newStart, newEnd] 與某個(gè)已占用訂單的租期 [oldStart, oldEnd] 是否存在重疊區(qū)間。SQL寫法是這樣的SELECT COUNT(*) FROM orders WHERE car_id #{carId} AND status IN (1, 2) AND start_time #{newEndTime} AND end_time #{newStartTime}這里status IN (1, 2)排除了已取消和已完成的訂單。這個(gè)區(qū)間重疊判斷覆蓋了四種情況新訂單完全包含舊訂單、新訂單被舊訂單包含、新訂單左端與舊訂單右端重疊、新訂單右端與舊訂單左端重疊。凡是這四種情況里任意一種count都大于0就不能下單。邊界情況要注意處理有人會(huì)問(wèn)前一個(gè)訂單還車時(shí)間是9:00新訂單取車時(shí)間也是9:00算不算沖突嚴(yán)格說(shuō)這取決于業(yè)務(wù)規(guī)則。我的規(guī)則是不允許邊界緊貼所以SQL里用的是和不是和。如果業(yè)務(wù)上允許9點(diǎn)整還車9點(diǎn)整租出那要用和處理。這個(gè)細(xì)節(jié)最好和業(yè)務(wù)方確認(rèn)清楚再定。3. 后端實(shí)現(xiàn)里容易翻車的三個(gè)點(diǎn)金額、狀態(tài)、動(dòng)態(tài)SQL3.1 金額計(jì)算與BigDecimal的精度陷阱租金計(jì)算看起來(lái)簡(jiǎn)單日租金 × 天數(shù) 押金但實(shí)操里有兩個(gè)坑。第一個(gè)坑是數(shù)據(jù)類型金額一律用BigDecimal計(jì)算數(shù)據(jù)庫(kù)里用decimal(10,2)絕不能用float或double。0.1 0.2 在double里的結(jié)果是0.30000000000000004這個(gè)精度錯(cuò)誤在金額上會(huì)造成分賬差錯(cuò)測(cè)試時(shí)還不容易發(fā)現(xiàn)。第二個(gè)坑是租期天數(shù)的計(jì)算邏輯兩個(gè)datetime之間做差得到毫秒數(shù)再除以一天的毫秒數(shù)這里牽扯時(shí)區(qū)問(wèn)題取整規(guī)則也要定好。我這里的使用者是按整天租賃的所以天數(shù)計(jì)算采用向上取整當(dāng)天取當(dāng)晚還也算一天。代碼很簡(jiǎn)單long diffMs endTime.getTime() - startTime.getTime(); int totalDays (int) Math.ceil(diffMs / (1000.0 * 3600 * 24)); BigDecimal amount dailyPrice.multiply(BigDecimal.valueOf(totalDays));這個(gè)計(jì)算邏輯后端寫好后前端也必須要寫一模一樣的預(yù)估價(jià)邏輯。我的做法是在前端展示預(yù)估費(fèi)用但最終費(fèi)用以后端訂單詳情里的金額為準(zhǔn)。如果兩邊口徑不一致用戶看到預(yù)估100元下單后變成120元體驗(yàn)非常差。3.2 防止“搶車”條件更新比先查再改可靠租賃系統(tǒng)并發(fā)場(chǎng)景下容易出現(xiàn)一個(gè)bug兩個(gè)用戶同時(shí)看中同一輛車同時(shí)提交訂單結(jié)果兩個(gè)訂單都創(chuàng)建成功。解決辦法是兩層防護(hù)。第一層創(chuàng)建訂單前執(zhí)行上面那個(gè)沖突檢測(cè)SQLcount為0才允許創(chuàng)建第二層創(chuàng)建訂單時(shí)會(huì)同步把車輛狀態(tài)改為已租出這個(gè)更新操作必須加條件UPDATE car SET status 1 WHERE id #{carId} AND status 0注意這里的關(guān)鍵點(diǎn)update語(yǔ)句里帶了AND status 0這個(gè)條件。如果車輛已經(jīng)被其他訂單改成了已租出這條update受影響的行數(shù)就是0通過(guò)Java代碼判斷affectedRows如果為0就拋出異常讓當(dāng)前事務(wù)回滾這樣即使兩個(gè)用戶同時(shí)提交也只有一個(gè)能成功。這個(gè)設(shè)計(jì)比“先SELECT再UPDATE”的常規(guī)寫法更可靠因?yàn)樵诟卟l(fā)環(huán)境下select之后、update之前的那段時(shí)間里車輛狀態(tài)可能已經(jīng)被別人改了。條件更新本質(zhì)上是一種樂(lè)觀鎖思路用狀態(tài)本身做版本校驗(yàn)。3.3 MyBatis動(dòng)態(tài)SQL和事務(wù)邊界MyBatis的動(dòng)態(tài)SQL是查詢條件不確定時(shí)的核心武器。比如車輛列表頁(yè)用戶可以按品牌模糊查、按車型分類查、按價(jià)格區(qū)段查、按狀態(tài)查這些條件任意組合。用XML的 標(biāo)簽加 標(biāo)簽就能輕松搞定select idlistCars resultTypecom.example.entity.Car SELECT * FROM car where if testbrand ! null and brand ! AND brand LIKE CONCAT(%, #{brand}, %) /if if testcategory ! null AND category #{category} /if if teststatus ! null AND status #{status} /if /where ORDER BY create_time DESC /select標(biāo)簽的好處是如果所有條件都不滿足它不會(huì)生成WHERE關(guān)鍵字如果第一個(gè)條件成立它會(huì)自動(dòng)去掉多出來(lái)的AND。這里要注意 里的判空不僅要判null還要判空字符串尤其前端傳空字符串的時(shí)候brand ! null攔不住。事務(wù)邊界是另一個(gè)容易翻車的地方。創(chuàng)建訂單這個(gè)操作涉及三件事插入訂單記錄、更新車輛狀態(tài)為已租出、記錄支付流水。這三個(gè)操作必須在一個(gè)事務(wù)里任何一個(gè)失敗都要全部回滾否則會(huì)出現(xiàn)訂單存在但車輛狀態(tài)沒(méi)改的臟數(shù)據(jù)。實(shí)現(xiàn)方式就是在service方法上添加Transactional注解。但要注意一個(gè)Spring事務(wù)自調(diào)用失效的問(wèn)題同一個(gè)類里方法A調(diào)用方法BB上面有Transactional是無(wú)效的事務(wù)不會(huì)開(kāi)啟。事務(wù)方法是代理對(duì)象調(diào)用才生效所以事務(wù)邏輯要設(shè)計(jì)在獨(dú)立的service類里別在controller里寫業(yè)務(wù)事務(wù)也別在一個(gè)類內(nèi)部調(diào)來(lái)調(diào)去。4. Vue3前端用戶端和管理端分開(kāi)設(shè)計(jì)4.1 前端工程結(jié)構(gòu)和路由設(shè)計(jì)前端我用Vite初始化Vue3項(xiàng)目用TypeScript還是JavaScript這個(gè)項(xiàng)目我用的是JavaScript團(tuán)隊(duì)項(xiàng)目用TS另說(shuō)個(gè)人項(xiàng)目里JS改起來(lái)更快。UI庫(kù)用Element Plus狀態(tài)管理用PiniaHTTP請(qǐng)求用Axios。目錄結(jié)構(gòu)比較常規(guī)src/ api/ 接口請(qǐng)求定義 store/ Pinia狀態(tài)管理存token和用戶信息 router/ 路由配置 views/user/ 用戶端頁(yè)面 views/admin/ 管理端頁(yè)面 utils/ axios實(shí)例和工具函數(shù)路由這塊用前端路由守衛(wèi)配合后端鑒權(quán)雙層控制。前端路由分為用戶端和管理端兩組管理員登錄后才顯示管理端菜單普通用戶訪問(wèn)管理端路由直接跳首頁(yè)。后端的攔截器還要再校驗(yàn)一次兩層的權(quán)限攔截不能互相替代。4.2 用戶端選車、下單、支付的交互鏈路用戶端的核心交互是四個(gè)步驟瀏覽車輛 - 查看詳情 - 選時(shí)間下單 - 支付。車輛列表頁(yè)用卡片展示每輛車顯示名稱、分類標(biāo)簽、日租金、押金、當(dāng)前狀態(tài)。狀態(tài)為“可租”的車輛才可以點(diǎn)擊查看詳情其他狀態(tài)置灰。這個(gè)細(xì)節(jié)很重要用戶看到一輛在租的車點(diǎn)進(jìn)去發(fā)現(xiàn)下不了單體驗(yàn)很差。車輛詳情頁(yè)是交互最關(guān)鍵的地方。頁(yè)面里放了兩個(gè)日期選擇器分別是取車時(shí)間和還車時(shí)間默認(rèn)取車時(shí)間是明天還車時(shí)間是后天。用戶一選完時(shí)間前端立刻計(jì)算預(yù)估費(fèi)用并展示出來(lái)。計(jì)算公式和后端完全一致const diffMs new Date(endTime).getTime() - new Date(startTime).getTime() const days Math.ceil(diffMs / (1000 * 3600 * 24)) const estimate dailyPrice * days deposit用戶點(diǎn)擊下單前端把carId、startTime、endTime提交到后端。后端返回訂單ID和應(yīng)付金額前端彈一個(gè)支付確認(rèn)框上面顯示明細(xì)租金多少、押金多少、合計(jì)多少。本項(xiàng)目用模擬支付點(diǎn)確認(rèn)就調(diào)用支付接口把訂單狀態(tài)從待支付改成已支付待取車。真實(shí)項(xiàng)目中這一步要對(duì)接微信或支付寶邏輯上只是把模擬支付的接口換成真正的支付回調(diào)。4.3 管理端用狀態(tài)驅(qū)動(dòng)按鈕顯隱管理端訂單管理頁(yè)是最能體現(xiàn)狀態(tài)機(jī)設(shè)計(jì)價(jià)值的地方。頁(yè)面用表格展示所有訂單每一行根據(jù)訂單狀態(tài)顯示不同的操作按鈕訂單狀態(tài)顯示的操作按鈕0 待支付查看詳情、取消1 已支付待取車確認(rèn)取車2 租賃中確認(rèn)還車3 已完成查看詳情4 已取消查看詳情Element Plus的el-table里可以通過(guò)條件渲染動(dòng)態(tài)控制按鈕是否顯示操作列根據(jù)狀態(tài)對(duì)象里的屬性來(lái)判斷。這樣做的好處是代碼邏輯非常簡(jiǎn)單不會(huì)出現(xiàn)一堆亂七八糟的v-if每個(gè)狀態(tài)對(duì)應(yīng)什么操作是固定的數(shù)據(jù)驅(qū)動(dòng)的思路。車輛管理頁(yè)相對(duì)簡(jiǎn)單表格展示車輛列表新增和編輯用彈窗表單。這里容易被忽略的是車輛分類和日租金的表單校驗(yàn)分類用一個(gè)下拉框取值范圍要和后端枚舉對(duì)齊。還有“上架/下架”操作下架的車不允許再下單這個(gè)操作本質(zhì)就是更新車輛status字段為3。4.4 前端狀態(tài)管理和權(quán)限控制用戶登錄后的token、用戶基本信息、角色信息我都放在Pinia里。Axios的請(qǐng)求攔截器統(tǒng)一從store里取token加到Authorization請(qǐng)求頭響應(yīng)攔截器判斷后端返回code如果不是200就彈出錯(cuò)誤提示如果是401表示token過(guò)期清除本地登錄狀態(tài)跳轉(zhuǎn)登錄頁(yè)。這個(gè)模式幾乎是Vue3前后端分離項(xiàng)目里固定套路但值得注意的一點(diǎn)是響應(yīng)攔截器的錯(cuò)誤提示不能太生硬要區(qū)分是網(wǎng)絡(luò)錯(cuò)誤還是業(yè)務(wù)錯(cuò)誤業(yè)務(wù)錯(cuò)誤比如庫(kù)存不足要用后端的message內(nèi)容提示網(wǎng)絡(luò)錯(cuò)誤則統(tǒng)一提示“網(wǎng)絡(luò)異常請(qǐng)稍后重試”。5. 前后端聯(lián)調(diào)的關(guān)鍵細(xì)節(jié)統(tǒng)一返回結(jié)構(gòu)、JWT鑒權(quán)、跨域5.1 統(tǒng)一接口返回結(jié)構(gòu)前后端分離的項(xiàng)目最忌諱各寫各的后端返回原生對(duì)象字段名一個(gè)叫createTime前端需要的卻是createdAt聯(lián)調(diào)時(shí)改來(lái)改去。我從一開(kāi)始就定了統(tǒng)一的返回結(jié)構(gòu){ code: 200, message: success, data: {} }code為200表示成功非200表示業(yè)務(wù)異常message里放給用戶看的提示信息data里放業(yè)務(wù)數(shù)據(jù)。后端所有接口都返回這個(gè)Result對(duì)象泛型保證data的類型安全。這么做的好處是前端的響應(yīng)攔截器可以全局統(tǒng)一處理code為200時(shí)直接返回data非200時(shí)統(tǒng)一彈message。前端每個(gè)請(qǐng)求都不用重復(fù)寫錯(cuò)誤處理代碼。5.2 JWT登錄態(tài)設(shè)計(jì)前后端分離后服務(wù)端不再維護(hù)Session登錄態(tài)用什么方案我用的是JWT。流程是登錄接口校驗(yàn)用戶名密碼成功后后端生成一個(gè)token返回給前端前端存到localStorage里之后每次請(qǐng)求都在Authorization頭帶上這個(gè)token后端攔截器解析token獲取用戶信息。生成token我用的是jjwt庫(kù)token里可以放userId和role過(guò)期時(shí)間設(shè)24小時(shí)。后端寫一個(gè)攔截器在HandlerInterceptor的preHandle里解析token解析成功就放行失敗就返回401。要注意的是攔截器放行的白名單要放好登錄、注冊(cè)、車輛列表查詢這些接口是不需要登錄就能訪問(wèn)的不能攔截但下單、支付、管理端所有接口必須登錄后才能訪問(wèn)。管理員的接口校驗(yàn)要加角色判斷普通用戶拿自己的token調(diào)管理員接口也必須拒絕。5.3 開(kāi)發(fā)環(huán)境和生產(chǎn)環(huán)境的跨域處理開(kāi)發(fā)階段前后端分離前端跑在5173端口后端跑在8080端口跨域問(wèn)題避免不了。我的方案是開(kāi)發(fā)環(huán)境用Vite的proxy代理把前端的/api請(qǐng)求轉(zhuǎn)發(fā)到后端// vite.config.js server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }生產(chǎn)環(huán)境則用Nginx反向代理讓前端靜態(tài)頁(yè)面和后端接口在同一個(gè)域名下徹底避免跨域問(wèn)題。Nginx配置里把/api路徑轉(zhuǎn)發(fā)到后端服務(wù)其他路徑返回前端dist目錄的靜態(tài)文件。我在部署時(shí)會(huì)加上try_files配置后面踩坑記錄里會(huì)詳細(xì)說(shuō)。6. 打包部署與實(shí)測(cè)踩坑記錄6.1 前后端打包流程后端打包用Maven命令很簡(jiǎn)單mvn clean package -DskipTests生成的jar在target目錄直接java -jar跑起來(lái)。前端打包用Vitenpm run build生成dist目錄。生產(chǎn)部署我是把后端jar和前端dist都放到同一臺(tái)服務(wù)器上后端跑SpringBoot服務(wù)監(jiān)聽(tīng)8080端口前端dist目錄由Nginx托管。6.2 我在實(shí)測(cè)中遇到的真坑第一個(gè)坑是MySQL時(shí)區(qū)導(dǎo)致的日期錯(cuò)亂。系統(tǒng)上線后發(fā)現(xiàn)訂單的創(chuàng)建時(shí)間比本地時(shí)間晚了8個(gè)小時(shí)原因是JDBC連接串里沒(méi)配時(shí)區(qū)。解決方法是連接串加上參數(shù)jdbc:mysql://localhost:3306/car_rental?serverTimezoneAsia/ShanghaicharacterEncodingutf8useSSLfalse這個(gè)坑在本地開(kāi)發(fā)時(shí)可能不出現(xiàn)因?yàn)楸镜豈ySQL時(shí)區(qū)恰好和系統(tǒng)一致部署到云服務(wù)器或者用Docker跑MySQL時(shí)就會(huì)暴露。遇到時(shí)間相關(guān)的問(wèn)題第一反應(yīng)就是查時(shí)區(qū)配置。第二個(gè)坑是Linux下MySQL表名大小寫敏感。本地Windows開(kāi)發(fā)時(shí)一切正常部署到Linux服務(wù)器后程序報(bào)“Table car_rental.CAR doesnt exist”因?yàn)槲业腟QL里寫的是大寫表名Windows下MySQL默認(rèn)不區(qū)分大小寫Linux下區(qū)分。解決方法兩種要么所有SQL統(tǒng)一小寫表名要么在MySQL配置里加lower_case_table_names1。個(gè)人項(xiàng)目改SQL統(tǒng)一比較穩(wěn)妥。第三個(gè)坑是Vue打包部署后刷新頁(yè)面出現(xiàn)404。原因很簡(jiǎn)單前端路由用的是history模式Nginx默認(rèn)配置下刷新 /orders/123 這樣的路徑時(shí)Nginx去磁盤上找這個(gè)路徑對(duì)應(yīng)的文件找不到就404。要在Nginx配置里加上location / { try_files $uri $uri/ /index.html; }這個(gè)配置的意思是如果請(qǐng)求的路徑不存在就回退到index.html讓前端路由自己解析。第四個(gè)坑是業(yè)務(wù)層面的訂單取消或還車后車輛狀態(tài)要記得恢復(fù)。這個(gè)看起來(lái)理所當(dāng)然但我在開(kāi)發(fā)時(shí)漏過(guò)用戶下單后取消訂單訂單狀態(tài)改成已取消但車輛狀態(tài)還是已租出這輛車就永遠(yuǎn)租不出去了。后來(lái)在service方法里嚴(yán)格按狀態(tài)機(jī)流轉(zhuǎn)訂單取消的同時(shí)把車輛狀態(tài)改回可租并且放在同一個(gè)事務(wù)里才算徹底解決。6.3 這個(gè)項(xiàng)目的后續(xù)擴(kuò)展空間系統(tǒng)做完了功能上是完整的但還有幾個(gè)方向可以繼續(xù)擴(kuò)展。第一個(gè)是把前端預(yù)估費(fèi)用的邏輯抽出來(lái)獨(dú)立成一個(gè)費(fèi)用計(jì)算工具后續(xù)如果支持半天租、按時(shí)租前后端只改這個(gè)工具就行。第二個(gè)是給車輛加價(jià)格日歷節(jié)假日和旺季可以動(dòng)態(tài)調(diào)整日租金這個(gè)功能在真正的租車公司里幾乎必備。第三個(gè)是增加一些統(tǒng)計(jì)報(bào)表比如車輛出租率、訂單收入趨勢(shì)、熱門車型排行管理端用ECharts畫幾個(gè)圖表項(xiàng)目看起來(lái)也更完整。第四個(gè)是把管理端的角色再細(xì)分比如操作員和超級(jí)管理員操作員只能處理訂單超級(jí)管理員才能改車輛配置。說(shuō)回到這套系統(tǒng)本身我個(gè)人實(shí)測(cè)下來(lái)最大的體會(huì)是租賃類業(yè)務(wù)系統(tǒng)的難度不在增刪改查而在業(yè)務(wù)約束的完整性。一輛車的狀態(tài)從可租到被租再到還車背后牽扯的是時(shí)間段沖突檢測(cè)、事務(wù)邊界、狀態(tài)流轉(zhuǎn)的異?;謴?fù)這些邏輯捋清楚了代碼寫起來(lái)會(huì)非常順。這套設(shè)計(jì)不止適用于汽車租賃共享單車、酒店房間預(yù)約、機(jī)械設(shè)備租借核心模型都可以直接平移過(guò)去。如果你正在找項(xiàng)目練手我建議在跑通基礎(chǔ)功能后真實(shí)地給自己設(shè)計(jì)幾個(gè)并發(fā)場(chǎng)景去壓一壓比如同時(shí)搶同一輛車、訂單取消的瞬間又有新用戶下單把這些場(chǎng)景都處理干凈這個(gè)項(xiàng)目就算真正吃透了。