全棧開(kāi)發(fā)實(shí)戰(zhàn))
做汽車租賃系統(tǒng)這個(gè)選題的人這幾年確實(shí)不少尤其是課程設(shè)計(jì)和畢業(yè)設(shè)計(jì)我接觸到的絕大多數(shù)同學(xué)都掛在“技術(shù)棧太多不知道從哪里下手”這一關(guān)。標(biāo)題里從 PHP、ASP.NET、Java 一路列到 Spring Boot、SSM、Vue3看起來(lái)是讓你任選但恰恰因?yàn)檫x項(xiàng)太多很多人光搭環(huán)境就耗掉一周最后連一個(gè)能演示的主流程都跑不通。這篇文章就圍繞這個(gè)選題講清楚一件事如果你想在較短時(shí)間內(nèi)做出一個(gè)功能完整、能正常演示、拿得出手的汽車租賃系統(tǒng)最穩(wěn)妥的路徑是什么。我會(huì)從技術(shù)選型、數(shù)據(jù)庫(kù)設(shè)計(jì)、后端接口、前端頁(yè)面到最終打包部署一條龍拆開(kāi)講代碼和配置直接給到可直接復(fù)制改用的程度重點(diǎn)解釋每個(gè)關(guān)鍵設(shè)計(jì)背后的原因而不是只丟一段“能跑就行”的代碼。適合正在做這個(gè)課題的在校生也適合想用完整業(yè)務(wù)練手前后端分離開(kāi)發(fā)的初學(xué)者。1. 技術(shù)方案選型與整體設(shè)計(jì)思路1.1 多個(gè)技術(shù)棧怎么選為什么 Java Vue3 是最大公約數(shù)這個(gè)選題本質(zhì)上是一個(gè)典型的業(yè)務(wù)管理系統(tǒng)核心是“用戶登錄、車輛展示、下單租車、訂單管理、后臺(tái)維護(hù)”這一條完整鏈路。用 PHP、ASP.NET 或者 Java 都能做出來(lái)區(qū)別在于后續(xù)的擴(kuò)展性、資料查找成本以及和你找工作時(shí)技術(shù)棧的匹配度。先說(shuō) PHP。PHP 在傳統(tǒng) Web 開(kāi)發(fā)里確實(shí)很輕寫(xiě)個(gè)頁(yè)面、查個(gè)數(shù)據(jù)庫(kù)非??鞄啄暌郧暗恼n設(shè)很多都是 PHP 一把梭。但現(xiàn)在的課程設(shè)計(jì)如果要求前后端分離PHP 這邊的前端交互、接口鑒權(quán)、部署方式反而不如 Java 體系順手。更現(xiàn)實(shí)的問(wèn)題是你搜“PHP Vue 前后端分離”的教程質(zhì)量參差不齊很多還是老式混編思路容易把新手繞暈。ASP.NET 同理技術(shù)在 Windows 系環(huán)境下沒(méi)問(wèn)題但如果你身邊同學(xué)用的都是 Mac 或者統(tǒng)一 Linux 環(huán)境部署踩坑概率會(huì)明顯增加。另外網(wǎng)上的資料總量和問(wèn)題的命中率也明顯低于 Java。Java Spring Boot Vue3 是這幾年最“劃算”的組合。Spring Boot 把 SSM 那套 Spring SpringMVC MyBatis 的配置復(fù)雜度降到很低MyBatis 依然是國(guó)內(nèi)最主流的持久層框架Vue3 又是前端招聘里的常見(jiàn)要求。所以別貪多求全選這條主線。標(biāo)題里寫(xiě)“SSM”也不用慌Spring Boot 項(xiàng)目里引入 MyBatis 就是現(xiàn)代化的 SSM 組合本質(zhì)一樣只是不再需要手動(dòng)寫(xiě)一堆 XML 配置文件。1.2 系統(tǒng)模塊劃分前臺(tái)、后臺(tái)、服務(wù)端三層各管什么汽車租賃系統(tǒng)再怎么擴(kuò)展核心也就三個(gè)使用方普通用戶、業(yè)務(wù)管理員、系統(tǒng)管理員。課設(shè)階段不需要把角色拆得太碎一般兩級(jí)角色就夠了普通用戶操作前臺(tái)頁(yè)面管理員操作后臺(tái)管理頁(yè)。前臺(tái)面向用戶的功能包括注冊(cè)登錄個(gè)人資料維護(hù)瀏覽車輛列表按品牌、座位數(shù)、日租金等條件篩選查看車輛詳情包括參數(shù)、圖片、價(jià)格選擇取車/還車日期創(chuàng)建租車訂單在線支付押金與租金課設(shè)里模擬支付即可查看我的訂單取消訂單、確認(rèn)還車后臺(tái)面向管理員的功能包括車輛管理新增車輛、編輯、上下架、維護(hù)狀態(tài)訂單管理查看所有訂單執(zhí)行“確認(rèn)出車”和“確認(rèn)還車”等操作用戶管理查看已注冊(cè)用戶禁用異常賬號(hào)服務(wù)端就是一個(gè) Spring Boot 應(yīng)用提供 RESTful API前端用 axios 調(diào)接口完成數(shù)據(jù)交互。這樣的分層結(jié)構(gòu)既符合主流前后端分離的開(kāi)發(fā)方式也方便在答辯時(shí)講清楚“前端展示層—接口層—業(yè)務(wù)層—數(shù)據(jù)層”的完整架構(gòu)。1.3 技術(shù)棧清單與版本踩坑預(yù)期我常用的組合是這樣的后端JDK 8 或 11、Spring Boot 2.7.x、Maven、MyBatis、MySQL 8前端Vue3 Vite Element Plus Pinia Vue Router Axios權(quán)限方案JWT 攔截器開(kāi)發(fā)調(diào)試前端 Vite 代理把 /api 轉(zhuǎn)發(fā)到后端 8080為什么后端不建議一開(kāi)始就上 Spring Boot 3.x因?yàn)?Spring Boot 3 把 javax 包名遷到了 jakarta很多老教程、老依賴不兼容新手遇到報(bào)錯(cuò)容易懵。先用 2.7.x 跑通主流程后期想折騰再升級(jí)。關(guān)于版本坑我在第 5 節(jié)會(huì)詳細(xì)說(shuō)。2. 數(shù)據(jù)庫(kù)設(shè)計(jì)與核心業(yè)務(wù)邏輯2.1 三張核心表用戶、車輛、訂單汽車租賃的業(yè)務(wù)本質(zhì)是“人對(duì)車輛的臨時(shí)占用”所以最重要的表就三張用戶表、車輛表、訂單表。先把這三張表設(shè)計(jì)清楚系統(tǒng)就穩(wěn)定了一半。用戶表sys_userCREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, nickname VARCHAR(50), phone VARCHAR(20), id_card VARCHAR(30), driver_no VARCHAR(30), role TINYINT DEFAULT 1, -- 1普通用戶 2管理員 status TINYINT DEFAULT 1, -- 1正常 0禁用 create_time DATETIME DEFAULT CURRENT_TIMESTAMP );車輛表carCREATE TABLE car ( id BIGINT PRIMARY KEY AUTO_INCREMENT, brand VARCHAR(50) NOT NULL, model VARCHAR(100) NOT NULL, plate_no VARCHAR(20) NOT NULL UNIQUE, daily_price DECIMAL(10,2) NOT NULL, deposit DECIMAL(10,2) NOT NULL, seat_count INT DEFAULT 5, gearbox VARCHAR(20), -- 自動(dòng)擋/手動(dòng)擋 energy_type VARCHAR(20), -- 燃油/新能源 cover_image VARCHAR(255), status TINYINT DEFAULT 0, -- 0空閑 1租賃中 2維修中 3已下架 create_time DATETIME DEFAULT CURRENT_TIMESTAMP );訂單表rental_orderCREATE TABLE rental_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, user_id BIGINT NOT NULL, car_id BIGINT NOT NULL, start_date DATE NOT NULL, end_date DATE NOT NULL, total_days INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0, -- 0待支付 1待取車 2租賃中 3待還車 4已完成 5已取消 create_time DATETIME DEFAULT CURRENT_TIMESTAMP );這里有兩個(gè)細(xì)節(jié)值得注意。第一表名不要用 orderorder 是 SQL 里的關(guān)鍵字雖然加反引號(hào)也能用但后面寫(xiě)查詢語(yǔ)句時(shí)到處都是反引號(hào)煩得很直接叫 rental_order 最省心。第二金額字段必須用 DECIMAL不要用 DOUBLE。租車金額涉及押金、租金、超時(shí)費(fèi)用浮點(diǎn)計(jì)算容易出現(xiàn) 0.1 0.2 不等于 0.3 這種詭異現(xiàn)象后端一旦統(tǒng)一用 BigDecimal 就不會(huì)有這個(gè)問(wèn)題。另外字段名我特意用下劃線風(fēng)格配合 MyBatis 的 map-underscore-to-camel-case 配置查詢結(jié)果能自動(dòng)映射成駝峰命名實(shí)體類后面寫(xiě)代碼會(huì)非常順。2.2 訂單狀態(tài)機(jī)從下單到還車的完整流轉(zhuǎn)訂單狀態(tài)是整個(gè)系統(tǒng)最核心的流程任何一個(gè)環(huán)節(jié)斷了演示就會(huì)出問(wèn)題。我的設(shè)計(jì)里訂單狀態(tài)從 0 到 5流轉(zhuǎn)方向大概是用戶選車、選日期創(chuàng)建訂單 → 狀態(tài) 0待支付用戶模擬支付成功后 → 狀態(tài) 1待取車管理員在后臺(tái)點(diǎn)擊“確認(rèn)出車” → 狀態(tài) 2租賃中用戶實(shí)際還車或管理員點(diǎn)擊“確認(rèn)還車” → 狀態(tài) 3待還車管理員完成結(jié)算確認(rèn)無(wú)額外費(fèi)用 → 狀態(tài) 4已完成用戶在支付前主動(dòng)取消或超時(shí)未支付 → 狀態(tài) 5已取消狀態(tài)流轉(zhuǎn)看起來(lái)瑣碎但它是后臺(tái)訂單管理頁(yè)面的操作依據(jù)。管理員每個(gè)按鈕對(duì)應(yīng)一次狀態(tài)變更比如“確認(rèn)出車”按鈕只在狀態(tài)為 1 時(shí)顯示“確認(rèn)還車”按鈕只在狀態(tài)為 2 時(shí)顯示。前端控制展示后端控制校驗(yàn)雙重保障。價(jià)格計(jì)算的規(guī)則總金額 日租金 × 租車天數(shù)押金單獨(dú)展示不包含在訂單總額里。租車天數(shù)我用 ChronoUnit.DAYS.between(startDate, endDate) 計(jì)算。這里要注意用戶在頁(yè)面上選擇的可能是日期而不是“天數(shù)”那么按自然日算會(huì)比按 24 小時(shí)算更直觀。比如 1 月 1 日取車、1 月 3 日還車租車天數(shù)是 2 天而不是 48 小時(shí)。這樣既方便計(jì)算也符合租車行業(yè)“按天計(jì)費(fèi)”的慣例。2.3 車輛時(shí)間沖突校驗(yàn)避免同一輛車被重復(fù)預(yù)訂這是整個(gè)系統(tǒng)里最值得拿出來(lái)講的業(yè)務(wù)點(diǎn)也經(jīng)常是答辯時(shí)老師會(huì)問(wèn)的“你的系統(tǒng)有沒(méi)有考慮并發(fā)”的地方。其實(shí)邏輯很簡(jiǎn)單和酒店訂房重疊判斷一模一樣兩個(gè)時(shí)間段 [start1, end1] 和 [start2, end2] 存在重疊當(dāng)且僅當(dāng) start1 end2 AND start2 end1。對(duì)應(yīng)到租車場(chǎng)景用戶要租某輛車如果存在一個(gè)狀態(tài)不是已取消的訂單它的 startDate 和 endDate 與用戶選擇的日期段重疊就說(shuō)明這輛車在這個(gè)時(shí)間段已經(jīng)被占了。用 MyBatis 查詢時(shí)SQL 可以這樣寫(xiě)SELECT COUNT(*) FROM rental_order WHERE car_id #{carId} AND status NOT IN (5) AND start_date #{endDate} AND end_date #{startDate}count 大于 0 就直接提示用戶“該車輛在本時(shí)段已被預(yù)訂請(qǐng)更換日期或選擇其他車輛”。這個(gè)判斷放在創(chuàng)建訂單的 Service 層里采用數(shù)據(jù)庫(kù)查詢判斷的方式對(duì)課設(shè)級(jí)別完全夠用。如果真要做到高并發(fā)防重那就要對(duì)車輛加分布式鎖或者唯一約束不是這類項(xiàng)目該糾結(jié)的點(diǎn)。3. 后端實(shí)現(xiàn)Spring Boot SSM 的分層落地3.1 項(xiàng)目初始化與依賴配置創(chuàng)建 Spring Boot 項(xiàng)目建議直接用 Spring Initializr 網(wǎng)頁(yè)端生成選擇 Maven、Java 8、Spring Boot 2.7.x依賴?yán)镞x Spring Web 和 MySQL Driver 就行MyBatis 和 JWT 手動(dòng)加。我常用的 pom.xml 核心依賴是這樣的dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdcom.auth0/groupId artifactIdjava-jwt/artifactId version4.4.0/version /dependency dependency groupIdcn.hutool/groupId artifactIdhutool-all/artifactId version5.8.25/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependencyHutool 是個(gè)好東西后面生成訂單號(hào)、做 BCrypt 密碼加密、寫(xiě)日期工具類都用得上省掉不少重復(fù)代碼。引入了這堆依賴之后application.yml 里把數(shù)據(jù)源和 MyBatis 配置好spring: datasource: url: jdbc:mysql://localhost:3306/car_rental?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: configuration: map-underscore-to-camel-case: true mapper-locations: classpath:mapper/*.xml這里 map-underscore-to-camel-case 必須開(kāi)否則你數(shù)據(jù)庫(kù)里查出來(lái)的 start_date 映射不到 startDate 實(shí)體字段上接口返回的全是 null排查起來(lái)特別迷惑。3.2 Controller、Service、Mapper 三層的標(biāo)準(zhǔn)寫(xiě)法后端代碼結(jié)構(gòu)我習(xí)慣分成 controller、service、mapper、entity、common 五個(gè)包。common 里放 Result、JWT 工具類、全局異常處理這些公共部分。以創(chuàng)建訂單這個(gè)核心接口為例Controller 只負(fù)責(zé)接收參數(shù)和返回結(jié)果PostMapping(/order/create) public Result createOrder(RequestBody OrderCreateDTO dto, RequestHeader(Authorization) String token) { Long userId JwtUtil.getUserId(token); return Result.ok(orderService.createOrder(userId, dto)); }Service 里完成核心校驗(yàn)和業(yè)務(wù)邏輯Transactional public Order createOrder(Long userId, OrderCreateDTO dto) { Car car carMapper.selectById(dto.getCarId()); if (car null || car.getStatus() ! 0) { throw new BusinessException(車輛不存在或不可租用); } int days (int) ChronoUnit.DAYS.between(dto.getStartDate(), dto.getEndDate()); if (days 0) { throw new BusinessException(還車日期必須晚于取車日期); } int count orderMapper.countOverlap(dto.getCarId(), dto.getStartDate(), dto.getEndDate()); if (count 0) { throw new BusinessException(該車輛在本時(shí)段已被預(yù)訂); } Order order new Order(); order.setOrderNo(R System.currentTimeMillis()); order.setUserId(userId); order.setCarId(dto.getCarId()); order.setStartDate(dto.getStartDate()); order.setEndDate(dto.getEndDate()); order.setTotalDays(days); order.setTotalAmount(car.getDailyPrice().multiply(BigDecimal.valueOf(days))); order.setStatus(0); orderMapper.insert(order); return order; }這里有三層校驗(yàn)車輛必須存在且空閑、日期必須合法、時(shí)間段不能沖突。校驗(yàn)順序也有講究先查車輛再算天數(shù)再查沖突避免用戶隨意提交非法數(shù)據(jù)打到數(shù)據(jù)庫(kù)。加 Transactional 保證事務(wù)安全萬(wàn)一后面插入失敗不會(huì)留下臟數(shù)據(jù)。Mapper 接口配合 XML 或注解都能做。如果查詢不復(fù)雜MyBatis 注解其實(shí)夠用比如Select(SELECT COUNT(*) FROM rental_order WHERE car_id #{carId} AND status NOT IN (5) AND start_date #{endDate} AND end_date #{startDate}) int countOverlap(Param(carId) Long carId, Param(startDate) LocalDate startDate, Param(endDate) LocalDate endDate);3.3 JWT 登錄鑒權(quán)與全局異常處理用戶和管理員登錄成功后后端生成一個(gè) JWT 返回給前端。JWT 里只放 userId 和 role 兩個(gè)關(guān)鍵信息過(guò)期時(shí)間設(shè)成 24 小時(shí)。用 java-jwt 生成Algorithm algorithm Algorithm.HMAC256(your-secret-key); String token JWT.create() .withClaim(userId, user.getId()) .withClaim(role, user.getRole()) .withExpiresAt(new Date(System.currentTimeMillis() 86400000)) .sign(algorithm);再寫(xiě)一個(gè)攔截器攔截所有 /api/** 請(qǐng)求但放行登錄、注冊(cè)、車輛列表這些公開(kāi)接口。注意攔截器里拿到 token 后要解析 userId 放回 request 的 attribute 里方便 Controller 直接取避免每個(gè)接口都重復(fù)解析一次。全局異常處理用一個(gè) RestControllerAdvice 兜底業(yè)務(wù)異常返回 Result 里的錯(cuò)誤碼和提示其他異常返回“系統(tǒng)繁忙”的通用信息。這一步很值得做因?yàn)榍岸四玫浇y(tǒng)一結(jié)構(gòu)的錯(cuò)誤提示才能直接彈消息框給用戶看。不做的后果就是接口一報(bào)錯(cuò)前端控制臺(tái)一堆英文堆棧演示當(dāng)場(chǎng)翻車。4. 前端實(shí)現(xiàn)Vue3 頁(yè)面搭建與聯(lián)調(diào)部署4.1 Vue3 前端工程結(jié)構(gòu)設(shè)計(jì)前端用 Vite 創(chuàng)建工程npm create vuelatest模板選擇時(shí)把 Vue Router、Pinia 選上工具鏈部分選 ESlint 和 Prettier 按個(gè)人習(xí)慣即可。依賴安裝npm install element-plus axios頁(yè)面結(jié)構(gòu)按角色分開(kāi)。普通用戶能看到首頁(yè)、車輛列表、車輛詳情、創(chuàng)建訂單、我的訂單、登錄注冊(cè)這些頁(yè)面管理員進(jìn)入后臺(tái)路由包括車輛管理、訂單管理、用戶管理。路由用懶加載方式引入組件避免首屏一次性加載太多內(nèi)容。Element Plus 在這個(gè)項(xiàng)目里非常好用。車輛列表直接用 el-card 做卡片式展示后臺(tái)表單用 el-form 加校驗(yàn)訂單表格用 el-table 配 el-tag 顯示狀態(tài)整體出效果很快不需要寫(xiě)太多手搓樣式。4.2 Axios 封裝與跨域代理的兩種姿勢(shì)前端所有接口請(qǐng)求統(tǒng)一走 axios 實(shí)例封裝時(shí)最重要的兩件事是請(qǐng)求攔截器注入 token響應(yīng)攔截器統(tǒng)一處理 401 和業(yè)務(wù)錯(cuò)誤。const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization token } return config }) request.interceptors.response.use( response { if (response.data.code ! 200) { ElMessage.error(response.data.message) return Promise.reject(new Error(response.data.message)) } return response.data }, error { if (error.response error.response.status 401) { ElMessage.error(登錄已過(guò)期請(qǐng)重新登錄) localStorage.removeItem(token) router.push(/login) } return Promise.reject(error) } )開(kāi)發(fā)環(huán)境跨域問(wèn)題我強(qiáng)烈推薦用 Vite 代理而不是后端開(kāi) CORS。后端開(kāi) CORS 容易在攔截器順序、預(yù)檢請(qǐng)求上踩坑而代理是在開(kāi)發(fā)服務(wù)器層面轉(zhuǎn)發(fā)請(qǐng)求瀏覽器看到的是同源請(qǐng)求干凈利落// vite.config.js server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }后端接口統(tǒng)一以 /api 開(kāi)頭Controller 層直接寫(xiě) RequestMapping(/api/car) 也行或者用 server.servlet.context-path/api 統(tǒng)一加前綴。兩種方式選一個(gè)就行不要同時(shí)用否則路徑變兩遍。4.3 打包把 Vue 構(gòu)建產(chǎn)物裝進(jìn) Spring Boot開(kāi)發(fā)完成后前端要打包給后端統(tǒng)一部署。最省事的方式是執(zhí)行 npm run build把生成的 dist 目錄下的文件復(fù)制到 Spring Boot 項(xiàng)目的 src/main/resources/static 里然后重新打包后端 jar。啟動(dòng)后瀏覽器直接訪問(wèn) http://localhost:8080 就能看到前端頁(yè)面接口也走同源不存在跨域問(wèn)題。如果路由用了 history 模式要注意一個(gè)問(wèn)題直接訪問(wèn) /car/1 刷新頁(yè)面會(huì)返回 404因?yàn)?Spring Boot 找不到對(duì)應(yīng)的 Controller。解決辦法有兩個(gè)。一個(gè)是圖省事把 Vue Router 改成 hash 模式URL 會(huì)帶上 # 號(hào)不美觀但絕對(duì)穩(wěn)定。另一個(gè)是在后端加一個(gè)轉(zhuǎn)發(fā) Controller把非 /api 開(kāi)頭的路徑都轉(zhuǎn)發(fā)到 index.htmlController public class PageForwardController { RequestMapping(value {/, /car/**, /order/**, /admin/**}) public String forward() { return forward:/index.html; } }我個(gè)人建議課設(shè)階段直接用 hash 模式省一個(gè)類也少一個(gè)配置點(diǎn)。如果追求最終效果好看再用 history 模式加轉(zhuǎn)發(fā)。5. 常見(jiàn)問(wèn)題與排查實(shí)錄5.1 開(kāi)發(fā)環(huán)境與依賴版本類問(wèn)題這類問(wèn)題出現(xiàn)的頻率最高而且很多并不是代碼邏輯錯(cuò)了純粹是環(huán)境不一致導(dǎo)致。問(wèn)題現(xiàn)象大概率原因解決建議Spring Boot 3 項(xiàng)目啟動(dòng)報(bào) class not found jakarta.* 相關(guān)錯(cuò)誤依賴引入了 javax 老版本庫(kù)低版本 JDK 起步或者統(tǒng)一換 Spring Boot 3 兼容版本MyBatis 查詢結(jié)果全是 null沒(méi)開(kāi)啟駝峰映射application.yml 配置 map-underscore-to-camel-case: true前端請(qǐng)求接口報(bào)跨域前后端端口不同且未代理優(yōu)先用 Vite proxy少用后端 CrossOriginMaven 依賴下載慢或失敗網(wǎng)絡(luò)問(wèn)題配置阿里云鏡像倉(cāng)庫(kù)Vue 頁(yè)面組件能顯示但接口 404請(qǐng)求路徑和后端 Controller 不一致前后端統(tǒng)一約定 /api 前綴檢查 RequestMapping熱詞里有一條“springboot版本太高”說(shuō)的就是這個(gè)老老實(shí)實(shí)先選 2.7.x不要盲目追最新。5.2 業(yè)務(wù)邏輯和數(shù)據(jù)類問(wèn)題訂單時(shí)間沖突判斷是我見(jiàn)過(guò)最容易被忽略的地方。很多人只判斷車輛當(dāng)前是否空閑卻不管用戶選的時(shí)間段和已有訂單是否重疊結(jié)果就是兩個(gè)人訂了同一輛車。上面給出的 countOverlap SQL 就是解決這個(gè)問(wèn)題的標(biāo)準(zhǔn)寫(xiě)法。日期參數(shù)另一個(gè)坑是前后端格式不一致。前端傳 2026-05-01后端用 LocalDate 接收時(shí)Spring 默認(rèn)可能解析不了。在 application.yml 里加一段配置spring: jackson: date-format: yyyy-MM-dd time-zone: GMT8如果接口還接收時(shí)間字段可以給實(shí)體字段加 JsonFormat(pattern yyyy-MM-dd)雙保險(xiǎn)。密碼存儲(chǔ)也要注意。明文存密碼是答辯時(shí)最容易被老師挑刺的點(diǎn)用 Hutool 的 BCrypt 加密注冊(cè)時(shí)加密登錄時(shí)校驗(yàn)代碼只有兩行String hashed BCrypt.hashpw(rawPassword); boolean check BCrypt.checkpw(rawPassword, hashed);5.3 部署演示階段的順手技巧圖片上傳和展示是最常見(jiàn)的“平時(shí)好好的演示時(shí)掛了”類問(wèn)題。車輛圖片如果保存在本地磁盤(pán)目錄Spring Boot 默認(rèn)不會(huì)把它映射成可訪問(wèn)的靜態(tài)資源。需要在配置類里加一個(gè)資源映射Configuration public class WebConfig implements WebMvcConfigurer { Value(${upload.base-dir}) private String baseDir; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceHandler(file: baseDir /); } }路徑分隔符記得不要寫(xiě)死成 Windows 的反斜杠用 Paths.get 或者直接配置里寫(xiě)相對(duì)路徑否則部署到云服務(wù)器就炸。還有一個(gè)小技巧給的數(shù)據(jù)庫(kù)腳本里一定要預(yù)置測(cè)試數(shù)據(jù)。建議至少放一個(gè) admin 管理員賬號(hào)、五個(gè)不同品牌車輛、一個(gè)已完成的歷史訂單和一個(gè)進(jìn)行中的訂單。這樣打開(kāi)系統(tǒng)就能直接演示前后臺(tái)全部流程不用現(xiàn)場(chǎng)注冊(cè)、現(xiàn)場(chǎng)造數(shù)據(jù)。6. 我的一些實(shí)際操作心得把整條鏈路從頭到尾跑過(guò)一遍之后我的感受是這個(gè)選題真正的難點(diǎn)不在某個(gè)技術(shù)本身而在于你要把“租賃業(yè)務(wù)的時(shí)間沖突校驗(yàn)”“訂單狀態(tài)流轉(zhuǎn)的閉環(huán)”“前端操作與后端狀態(tài)的聯(lián)動(dòng)”這三件事同時(shí)想清楚。很多同學(xué)進(jìn)度卡住不是因?yàn)椴粫?huì)寫(xiě) CRUD而是沒(méi)想明白車輛憑什么不能被重復(fù)預(yù)訂、狀態(tài)按鈕憑什么在不同階段顯示不同內(nèi)容這一類業(yè)務(wù)規(guī)則導(dǎo)致代碼寫(xiě)了一半推倒重來(lái)。另一個(gè)值得養(yǎng)成的習(xí)慣是從一開(kāi)始就用統(tǒng)一返回結(jié)構(gòu)。Result{ code, message, data } 這套東西所有接口都按這個(gè)格式返回前端 axios 響應(yīng)攔截器統(tǒng)一處理后面聯(lián)調(diào)時(shí)你會(huì)省下大量時(shí)間。千萬(wàn)別這個(gè)接口返回對(duì)象、那個(gè)接口直接返回字符串聯(lián)調(diào)時(shí)前端光寫(xiě)類型判斷就夠煩了。如果后續(xù)想再擴(kuò)展還可以給這個(gè)系統(tǒng)加一個(gè)“超時(shí)還車自動(dòng)計(jì)費(fèi)”的邏輯還車日期超過(guò) endDate 時(shí)按日租金的 1.5 倍補(bǔ)收超期費(fèi)用。資金有限又不能出錯(cuò)的話我的建議就是照著本文這個(gè)順序走先數(shù)據(jù)庫(kù)后代碼先后端后前端先主流程后細(xì)節(jié)。主流程跑通之后其他都是錦上添花。