上點(diǎn)餐系統(tǒng)畢業(yè)設(shè)計全攻略:從數(shù)據(jù)庫設(shè)計到前后端分離實(shí)戰(zhàn))
作為一個常年混跡各類項(xiàng)目、也幫不少人救過火的開發(fā)者我看到“網(wǎng)上點(diǎn)餐系統(tǒng)”這類題目還是挺有感觸的。它幾乎是畢業(yè)設(shè)計里最典型的“麻雀雖小五臟俱全”涉及用戶端、商家端、訂單流程、支付模擬還得把前后端分離、權(quán)限控制、數(shù)據(jù)庫設(shè)計這些點(diǎn)都串起來。如果你正打算做這個題目或者已經(jīng)被它折磨了一陣子這篇文章應(yīng)該能幫你把整個技術(shù)路線徹底理順。我會從選題拆解、技術(shù)選型、數(shù)據(jù)庫設(shè)計、后端實(shí)現(xiàn)、前端實(shí)現(xiàn)、聯(lián)調(diào)部署一直到答辯避坑按我實(shí)際做項(xiàng)目時的順序完整講一遍。先說結(jié)論這個題目非常適合作為前后端分離的練手項(xiàng)目因?yàn)樗炔粫窦兒笈_管理系統(tǒng)那樣單調(diào)又不會像電商平臺那樣復(fù)雜到失控。一個點(diǎn)餐系統(tǒng)的核心閉環(huán)是“瀏覽菜品—加入購物車—下單—接單—結(jié)算”圍繞這個閉環(huán)把一對多、多對多關(guān)系理清楚你的畢業(yè)設(shè)計就已經(jīng)贏了一大半。下面直接進(jìn)入干活模式。1. 選題定位與核心需求拆解1.1 這個題目到底在考什么很多同學(xué)誤以為畢業(yè)設(shè)計就是要做得“高大上”于是加一堆用不上的功能。實(shí)際上評審老師關(guān)注的永遠(yuǎn)是三件事數(shù)據(jù)模型合不合理、業(yè)務(wù)流程通不通、技術(shù)點(diǎn)有沒有真正掌握。網(wǎng)上點(diǎn)餐系統(tǒng)恰好能把這三件事一次性覆蓋?!熬W(wǎng)上點(diǎn)餐”的業(yè)務(wù)核心是訂單。訂單離不開菜品、用戶、門店這幾個基本實(shí)體。訂單本身又要拆成主表和明細(xì)表因?yàn)橐粋€訂單包含多個菜品每個菜品又有下單時的快照價格。你把這幾張表建清楚CURD怎么擴(kuò)展都只是體力活。反過來如果一上來就堆功能、堆頁面最后數(shù)據(jù)庫字段亂成一團(tuán)聯(lián)調(diào)時就會出現(xiàn)“購物車數(shù)量同步不了”“訂單狀態(tài)改不動”這種抓瞎問題。我建議你把題目拆成兩個層面來理解一是面向消費(fèi)者的前臺微信風(fēng)格的長列表、菜品分類、購物車角標(biāo)、下單流程二是面向管理員/商家的后臺菜品上下架、訂單處理、銷量統(tǒng)計。這兩個層面共用一套后端接口但權(quán)限不同這正好是前后端分離和權(quán)限控制的絕佳應(yīng)用場景。1.2 前后端分離到底是不是必須的放在幾年前用 Thymeleaf 寫服務(wù)端渲染也能交差但放在現(xiàn)在如果你為了省事放棄前后端分離答辯時基本會被問倒。原因不是“現(xiàn)在流行”而是業(yè)務(wù)場景確實(shí)需要分離用戶端和管理端是兩套完全不同的界面節(jié)奏用戶端要輕快、交互多管理端要表格密集、操作批量。前后端分離的開發(fā)模式會讓你把“接口設(shè)計”當(dāng)成一件正經(jīng)事來對待。比如下訂單接口是 POST /api/order更新菜品庫存是 PUT /api/dish/status我不需要在后端方法里返回一堆 HTML 片段只需要定義清晰的 JSON 結(jié)構(gòu)。前端拿到數(shù)據(jù)后自己渲染調(diào)樣式、改組件都不影響后端邏輯。從部署角度講分離還意味著后端可以被多個前端復(fù)用。將來你想加一個小程序端只要復(fù)用同一套接口前端重新開發(fā)就行。這本身也是畢業(yè)設(shè)計里一個值得寫進(jìn)“項(xiàng)目展望”的亮點(diǎn)。1.3 目標(biāo)用戶與功能清單做任何項(xiàng)目之前先定義清楚誰在用。我推薦把用戶分成三類普通用戶、商家管理員、超級管理員可選。普通用戶能注冊登錄、瀏覽菜品、加購物車、下單、查看訂單狀態(tài)商家管理員能維護(hù)菜品分類和菜品信息、處理訂單接單、完成超級管理員可以管理用戶和基礎(chǔ)數(shù)據(jù)。你不需要做支付網(wǎng)關(guān)對接那樣會把復(fù)雜度和安全要求提得太高。做個模擬支付即可下單后訂單狀態(tài)變?yōu)椤按Ц丁鼻岸藦棾鲆粋€模擬收銀臺點(diǎn)擊“確認(rèn)支付”就把狀態(tài)置為“已支付待商家接單”。這里只需要一個事務(wù)操作既講得清楚又符合安全邊界。核心功能清單如下用戶注冊登錄用戶名密碼、JWT 令牌。菜品分類與菜品展示分類樹、分頁、關(guān)鍵字搜索。購物車單選加購、數(shù)量增減、角標(biāo)聯(lián)動。訂單流程購物車生成訂單、訂單狀態(tài)流轉(zhuǎn)、訂單明細(xì)。商家后臺菜品管理、分類管理、訂單處理。數(shù)據(jù)統(tǒng)計訂單數(shù)量趨勢、銷量排行。這些功能全部做完不會超過兩周卻能覆蓋一個完整信息系統(tǒng)的所有核心面。后續(xù)如果你想加積分、優(yōu)惠券、配送地址都是在訂單主表上延伸字段和關(guān)聯(lián)表不會傷筋動骨。2. 技術(shù)選型與開發(fā)環(huán)境搭建2.1 后端技術(shù)棧版本怎么選后端我建議用 Spring Boot 2.7.x 而不是 3.x。原因很現(xiàn)實(shí)2.7 的生態(tài)最成熟網(wǎng)上資料最多遇到的依賴兼容問題最少。嗯畢業(yè)設(shè)計的重點(diǎn)是跑通邏輯不是當(dāng)新版本小白鼠。Spring Boot 3 必須用 Java 17而很多學(xué)校機(jī)器上的 JDK 還是 8一旦版本對不上環(huán)境問題就能耗掉你一周。持久層我推薦 MyBatis-Plus。它比原生 MyBatis 少寫大量 XML單表 CRUD 直接用 BaseMapper 就能搞定多表查詢用 LambdaQueryWrapper 也夠。另一個選擇是 Spring Data JPA但對于新手JPA 的懶加載和級聯(lián)操作很容易把人繞暈不如 MyBatis-Plus 來得直觀。數(shù)據(jù)庫用 MySQL 8.0默認(rèn)字符集設(shè)置為 utf8mb4這樣才能存 emoji 以及生僻字。如果你不想在本地安裝 MySQL用 Docker 拉一個 mysql:8.0 鏡像也很方便但要注意掛載數(shù)據(jù)目錄別把數(shù)據(jù)存在容器里否則容器一刪全沒了。2.2 前端技術(shù)棧的取舍前端基礎(chǔ)框架我用 Vue 3 Vite。很多教程還在講 Vue 2 Vue CLI但新項(xiàng)目沒必要給自己埋坑。Vue 3 的組合式 API 寫業(yè)務(wù)代碼更順手Vite 的開發(fā)服務(wù)器冷啟動快得多整體體驗(yàn)比 Webpack 時代舒服好幾個檔次。你還需要這三樣?xùn)|西路由用 Vue Router 4狀態(tài)管理用 PiniaUI 組件庫用 Element Plus。如果目標(biāo)是做一個更像真實(shí)點(diǎn)餐 App 的界面可以再用 UnoCSS 或 Tailwind 自定義樣式但核心業(yè)務(wù)部分用 Element Plus 快速搭框架是最可靠的。移動端適配問題我后面會單獨(dú)講這里先記住Element Plus 的柵格布局能省你很多事。構(gòu)建工具上Vite 默認(rèn)配置已經(jīng)足夠。開發(fā)環(huán)境下用 proxy 把 /api 請求轉(zhuǎn)發(fā)到 localhost:8080避免前后端端口不一致帶來的跨域問題。生產(chǎn)環(huán)境則用 Nginx 反向代理把 /api 路徑轉(zhuǎn)發(fā)到后端服務(wù)同時把前端靜態(tài)文件托管在 /usr/share/nginx/html這一套方案在畢業(yè)設(shè)計答辯現(xiàn)場非常常見也最容易講清楚。2.3 開發(fā)環(huán)境逐步配置清單我提供一個可以直接照抄的初始化步驟省去你到處翻坑的時間。安裝 JDK 8 或 11配置 JAVA_HOME 環(huán)境變量。安裝 Maven 3.8修改 settings.xml 里的鏡像源。你懂的國外源經(jīng)常抽風(fēng)用阿里云的鏡像倉能讓你不至于卡在下載依賴這一步。安裝 MySQL 8.0創(chuàng)建數(shù)據(jù)庫點(diǎn)餐系統(tǒng)庫比如 takeout_db字符集選 utf8mb4。安裝 Node.js 16 或 18npm 源可以設(shè)置為國內(nèi)鏡像否則 npm install 可能慢到懷疑人生。使用 Spring Initializr 生成后端骨架依賴選 Spring Web、MySQL Driver、Lombok后面手動加入 MyBatis-Plus 和 JJWT。使用 Vite 創(chuàng)建前端骨架npm create vitelatest takeout-web模板選 vue。安裝前端依賴vue-router、pinia、axios、element-plus。這里有幾個特別容易踩的細(xì)節(jié)。第一Maven 鏡像和 npm 鏡像別弄混云效、阿里云認(rèn)準(zhǔn)一個就行。第二MySQL 8 的驅(qū)動名是 com.mysql.cj.jdbc.Driver不是舊版的 com.mysql.jdbc.DriverURL 里別忘了加 serverTimezoneAsia/Shanghai不然時間全偏。第三Lombok 和你 IDE 的版本要匹配新版 IDEA 一般沒事但老版本社區(qū)版如果沒裝 Lombok 插件編譯能過但運(yùn)行報找不到方法很玄學(xué)。3. 數(shù)據(jù)庫設(shè)計與權(quán)限模型詳解3.1 核心表結(jié)構(gòu)設(shè)計網(wǎng)上點(diǎn)餐系統(tǒng)的核心表我建議六張用戶表、菜品分類表、菜品表、購物車表、訂單表、訂單明細(xì)表。再加一張可選的輪播圖表或地址表但核心永遠(yuǎn)是那六張設(shè)計原則是避免字段冗余。用戶表字段id, username, password, nickname, phone, avatar, role, status, create_time。密碼存放的是 BCrypt 哈希值不是明文。role 用字符串存 USER 和 ADMIN 就好不需要建用戶角色關(guān)聯(lián)表因?yàn)橄到y(tǒng)只有兩種角色加一張表反而顯得過度設(shè)計。status 是狀態(tài)開關(guān)可以用來做賬號禁用。菜品分類表字段id, name, sort。sort 用于排序比如飲品放前面還是后面。菜品表字段id, category_id, name, description, image_url, price, stock, status, create_time。注意 price 我用 decimal(10,2)不要用 double涉及金額的類型必須精確。購物車表字段id, user_id, dish_id, quantity, selected。selected 字段表示是否勾選前端購物車?yán)锏膹?fù)選框就是對應(yīng)這個字段。如果只支持整單提交這個字段可以去掉但如果你想讓用戶勾選部分菜品結(jié)算就必須有這個字段。訂單表字段id, order_no, user_id, total_amount, status, remark, create_time, pay_time。重點(diǎn)是這個 status需要定義一組狀態(tài)碼0 待支付、1 待接單已支付、2 已接單/制作中、3 已完成、4 已取消。訂單明細(xì)表字段id, order_id, dish_id, dish_name, dish_image, price, quantity其中 dish_name 和 price 是從下單時刻的商品數(shù)據(jù)快照而來防止商家后來改價導(dǎo)致歷史訂單金額變掉。3.2 為什么我不用數(shù)據(jù)庫外鍵這是很多數(shù)據(jù)庫課程里的“標(biāo)準(zhǔn)答案”和真實(shí)開發(fā)之間的差異點(diǎn)。課程里講外鍵約束但真實(shí)項(xiàng)目里我?guī)缀醪挥梦锢硗怄I。外鍵會帶來什么問題第一刪除菜品時如果被訂單明細(xì)引用會被約束擋住你得先處理關(guān)聯(lián)數(shù)據(jù)很麻煩第二高并發(fā)插入訂單時外鍵校驗(yàn)會帶來不必要的鎖競爭第三一旦需要做分庫分表物理外鍵完全不可用。我推薦用邏輯外鍵在訂單明細(xì)表里保存 dish_id通過應(yīng)用層保證數(shù)據(jù)的引用完整性。比如刪除菜品前先查一下訂單明細(xì)表有沒有引用如果有就做假刪除把菜品的 status 置為下架而不是 DELETE。這樣既保留了數(shù)據(jù)可追溯性又不會因?yàn)榧s束把業(yè)務(wù)卡死。答辯時可以主動向老師解釋“為什么不用外鍵”這往往是一個加分項(xiàng)因?yàn)檎f明你思考過真實(shí)場景。3.3 登錄態(tài)與權(quán)限控制設(shè)計登錄態(tài)我用 JWT而不是 Session。原因很簡單前后端分離之后后端不能依賴 Cookie 里的 Session ID 來識別用戶因?yàn)榭缬蚯闆r下 Cookie 要么被瀏覽器攔截要么需要復(fù)雜的跨域配置。JWT 本身是無狀態(tài)的后端簽發(fā) Token 給前端前端后續(xù)請求時在 HTTP 頭里帶上 Authorization: Bearer 。JWT 里我存三個信息userId、username、role。不存密碼不存敏感信息因?yàn)?JWT 只是 Base64Url 編碼并不是加密任何人都能解碼看到 payload。簽名由后端密鑰掌控防的是篡改不是防窺視。后端拿到 Token 后解析出 role用攔截器做兩級校驗(yàn)。第一級校驗(yàn) Token 是否有效第二級校驗(yàn)接口需要的角色。對于管理端接口我會在 Controller 方法上寫一個自定義注解 RequireAdmin攔截器發(fā)現(xiàn)當(dāng)前角色的不是 ADMIN 就返回 403。這樣權(quán)限邏輯集中在一處而不是散落在每個業(yè)務(wù)方法里代碼可維護(hù)性高很多。4. 后端核心模塊實(shí)現(xiàn)Spring Boot 實(shí)操4.1 統(tǒng)一響應(yīng)體與全局異常處理后端接口如果每個方法各返各的格式前端解析起來就是災(zāi)難。我定義一個統(tǒng)一的 Result Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.setCode(200); r.setMessage(success); r.setData(data); return r; } public static T ResultT error(String message) { ResultT r new Result(); r.setCode(500); r.setMessage(message); return r; } }這個類看起來簡單但它決定了整個前后端協(xié)作的基礎(chǔ)。前端 axios 攔截器看到 code 為 200 就直接用 data否則彈 message。別小看這個機(jī)制它能讓你在聯(lián)調(diào)時迅速區(qū)分“后端報錯”和“前端邏輯問題”。全局異常處理用 RestControllerAdvice。我把業(yè)務(wù)異常和系統(tǒng)異常分開處理業(yè)務(wù)異常返回 code 比如 400系統(tǒng)異常記錄到日志文件里返回給前端的 message 則是“系統(tǒng)繁忙請稍后重試”。日志打印用 Lombok 的 Slf4j關(guān)鍵請求參數(shù)也要打日志否則出問題你連復(fù)現(xiàn)都無從下手。4.2 JWT 登錄與攔截器寫法登錄接口的流程根據(jù)用戶名查用戶比對 BCrypt 密碼比對成功生成 JWT返回給前端。生成方式和校驗(yàn)方式我都單獨(dú)封裝成 JwtUtil 工具類不要在 Controller 里寫 JWT 構(gòu)建邏輯。Component public class JwtUtil { Value(${jwt.secret}) private String secret; Value(${jwt.expire}) private Long expire; public String generateToken(Long userId, String username, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(username, username) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() expire)) .signWith(SignatureAlgorithm.HS256, secret.getBytes(StandardCharsets.UTF_8)) .compact(); } public Claims parseToken(String token) { return Jwts.parser() .setSigningKey(secret.getBytes(StandardCharsets.UTF_8)) .parseClaimsJws(token) .getBody(); } }這里有一個重要坑點(diǎn)。JWT 的 signWith 方法在高版本 jjwt 庫里的簽名要求傳入 Key 對象而不是字符串。如果你用 0.9.x 版本那按字符串傳沒問題如果你用 0.11.x必須用 Keys.hmacShaKeyFor(secret.getBytes())。這個版本差異導(dǎo)致了大量“能生成不能解析”的古怪報錯我在本地就踩過一次查了好久才發(fā)現(xiàn)是 API 變更。攔截器繼承 HandlerInterceptorAdapter 重寫 preHandlepublic class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } String token request.getHeader(Authorization); if (StringUtils.hasText(token) token.startsWith(Bearer )) { token token.substring(7); try { Claims claims jwtUtil.parseToken(token); request.setAttribute(userId, Long.valueOf(claims.getSubject())); request.setAttribute(userRole, claims.get(role)); return true; } catch (Exception e) { // 解析失敗說明 token 過期或偽造 } } response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write(JSON.toJSONString(Result.error(未登錄或登錄過期))); return false; } }注冊攔截器時要注意靜態(tài)資源放行。后端一般沒有靜態(tài)資源但如果接入了文件上傳圖片預(yù)覽要把 /upload/** 這類路徑放行否則圖片都加載不出來。4.3 點(diǎn)餐下單的核心事務(wù)邏輯下單是系統(tǒng)里最核心的方法也是評委會專門盯著問的地方。絕不能寫一個 INSERT 訂單、再循環(huán)插入明細(xì)就完事必須考慮完整的事務(wù)一致性。我推薦把下單和模擬支付拆成兩個方法下單時生成成功即凍結(jié)庫存支付時才扣減。下單的核心邏輯校驗(yàn)購物車選中項(xiàng)取出菜品 ID 和數(shù)量。用 FOR UPDATE 鎖住商品行或者樂觀鎖控制庫存。畢業(yè)設(shè)計用樂觀鎖就可以UPDATE dish SET stock stock - #{quantity} WHERE id #{dishId} AND stock #{quantity}返回受影響行數(shù)來判斷是否超賣。生成訂單主表狀態(tài)為 0 待支付。批量插入訂單明細(xì)。清空對應(yīng)購物車項(xiàng)。整個方法上用 Transactional(rollbackFor Exception.class)確保任何一個環(huán)節(jié)拋異常前面插入的訂單和扣減的庫存全部回滾。注意 rollbackFor 必須寫成 rollbackFor Exception.class只寫 Transactional 時默認(rèn)只回滾 RuntimeException普通異常拋出來不會回滾這是新手最容易犯的錯誤。模擬支付接口更簡單把訂單狀態(tài)從 0 改成 1同時記錄支付時間。這里同樣要校驗(yàn)訂單歸屬防止用戶把別人的訂單支付了。接口里必須帶上 userId下單接口保存訂單表時要記錄 user_id支付時先查訂單的 user_id 是否等于當(dāng)前登錄用戶。4.4 統(tǒng)計報表的 SQL 優(yōu)化思路后臺管理端要展示銷量排行和訂單數(shù)趨勢。很多同學(xué)直接用 groupBy 把全表查出來數(shù)據(jù)量少時還好量一大數(shù)據(jù)庫立刻卡頓。其實(shí)這類報表查詢的核心不是寫復(fù)雜 SQL而是要讓索引命中。訂單表按 create_time 建索引統(tǒng)計某天訂單數(shù)時SQL 寫成SELECT DATE_FORMAT(create_time, %Y-%m-%d) AS day, COUNT(*) AS cnt FROM orders WHERE create_time 2024-01-01 AND create_time 2024-02-01 GROUP BY DATE_FORMAT(create_time, %Y-%m-%d);如果不加時間范圍限制MySQL 會掃描全表。你可以在開發(fā)時用 EXPLAIN 看一眼理論上應(yīng)該走 range 而不是 ALL這一點(diǎn)要能主動在答辯時說清楚。銷量排行則會 join 菜品表按菜品分組求和數(shù)據(jù)量大時同樣要先限定時間范圍。統(tǒng)計查詢一般不加載到內(nèi)存超過幾百行因?yàn)榫S度只有天和菜品不會膨脹。如果你想做得更漂亮可以引入定時任務(wù)每天凌晨統(tǒng)計一次把結(jié)果寫入統(tǒng)計表查詢時直接查結(jié)果表。這種方式叫做“空間換時間”在功能演示時性能提升非常明顯也是答辯里很好講的優(yōu)化點(diǎn)。5. 前端核心模塊實(shí)現(xiàn)Vue 實(shí)操5.1 Axios 封裝與請求攔截前端如果每個頁面里直接寫 axios.get將來改接口前綴或者加 Token 就非常痛苦。我習(xí)慣在 src/utils/request.js 里封裝一個 axios 實(shí)例import axios from axios import { ElMessage } from element-plus import router from /router const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message) return Promise.reject(new Error(res.message)) } return res.data }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } ElMessage.error(網(wǎng)絡(luò)請求失敗) return Promise.reject(error) } ) export default request注意我返回的是 res.data 而不是 res這樣調(diào)用方不需要每次解一層包裝業(yè)務(wù)代碼清爽很多。401 時自動跳登錄頁也算把之前后端攔截器設(shè)計了一次閉環(huán)。5.2 購物車與訂單確認(rèn)的交互設(shè)計前端“加購”這個動作看上去簡單但實(shí)際上要處理好三個地方購物車圖標(biāo)角標(biāo)、商品卡片按鈕狀態(tài)、購物車頁面的數(shù)量修改。我推薦用 Pinia 管理購物車列表和總數(shù)量組件之間不做 event bus 傳遞避免數(shù)據(jù)不同步。購物車 state 里維護(hù)一個 items 數(shù)組每個元素包含 dishId、name、price、quantity、selected。加購操作去重有兩種選擇后端把購物車表當(dāng)持久化數(shù)據(jù)或者前端純本地存儲。我建議采用后端持久化因?yàn)檫@樣才能做到“換個設(shè)備購物車還在”這也讓購物車表的設(shè)計有了真實(shí)意義。前端加購時調(diào)用后端接口返回最新數(shù)量后更新 Pinia。購物車頁面的加減按鈕不能每點(diǎn)一下都等后端響應(yīng)再做本地更新那樣體驗(yàn)會很卡。正確做法是樂觀更新點(diǎn)擊加號立刻更新本地數(shù)量和總價同時向后臺發(fā)請求。如果請求失敗再回滾本地狀態(tài)并提示錯誤。這種交互細(xì)節(jié)寫進(jìn)“項(xiàng)目心得體會”里老師會覺得你真懂前端。下單確認(rèn)頁要展示商品列表、總金額和用戶備注。備注字段要限制長度防止惡意用戶塞超長字符串。提交訂單時把 quantity 和 dishId 做成數(shù)組傳給后端后端用訂單明細(xì)表接收。前端收到訂單號后跳轉(zhuǎn)到“訂單列表/訂單詳情頁”而不是停留在購物車頁這樣用戶的認(rèn)知路徑是完整的。5.3 路由守衛(wèi)與不同角色的首頁區(qū)分前端路由需要區(qū)分用戶端和管理端。用戶端的頁面包括首頁、菜品列表、購物車、我的訂單管理端的頁面包括后臺布局、菜品管理、分類管理、訂單管理、數(shù)據(jù)統(tǒng)計。兩者不能互相串訪。我配置路由時給管理端加一個 meta.requiresAdmin。全局前置守衛(wèi)里同時判斷登錄態(tài)和角色router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) return } const role localStorage.getItem(role) if (to.meta.requiresAdmin role ! ADMIN) { next(/) return } next() })這里有一個小細(xì)節(jié)用戶登錄后把基本信息存到 localStorage但不要存密碼等敏感信息。只存 userId、username、role、token。頁面刷新時 Pinia 里的用戶信息會清空這時候需要從 localStorage 恢復(fù)。我封裝了一個 init 方法在 App.vue 的 created 里調(diào)用保證刷新頁面不丟登錄狀態(tài)。管理端的側(cè)邊菜單我也根據(jù)角色動態(tài)生成不是寫死。游客看不到管理入口管理員進(jìn)系統(tǒng)后默認(rèn)落在數(shù)據(jù)統(tǒng)計頁。這個“動態(tài)菜單”功能用 Vue Router 的 addRoute 實(shí)現(xiàn)比較高級但如果你時間緊張暫時固定菜單再配合路由守衛(wèi)攔截也能交差功能上并無硬傷。6. 聯(lián)調(diào)部署與源碼組織建議6.1 前后端聯(lián)調(diào)最容易崩的三個點(diǎn)前后端各自跑得通合在一起就掛這事太常見了。我聯(lián)調(diào)時遇到過三個高頻問題先列出來你遇到的時候能少走彎路。第一個是跨域配置。如果你用 Vite 的 proxy前端頁面代碼里的請求路徑必須以 /api 開頭后端 Context Path 不要重復(fù)配置成 /api。也就是說前端請求 /api/auth/login后端 Controller 的 RequestMapping 是 /auth/login。如果后端又配了 context-path/api那么實(shí)際接口路徑會變成 /api/auth/login而前端又加了一層 /api最后變成 /api/api/auth/login404 沒跑。第二個是時間格式串臺。后端返回 LocalDateTime 時默認(rèn)序列化格式是數(shù)組或帶 T 的 ISO 字符串前端顯示出來很奇怪。統(tǒng)一在 application.yml 里配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8第三個是字段命名不一致。后端數(shù)據(jù)庫字段用下劃線 user_id實(shí)體類用駝峰 userId如果 MyBatis-Plus 沒開啟 map-underscore-to-camel-case 全局配置查詢結(jié)果里 user_id 就映射不到 userId返回的 JSON 缺失字段前端數(shù)據(jù)全是 undefined。這個配置通常默認(rèn)開啟但最好主動確認(rèn)一次。6.2 生產(chǎn)環(huán)境構(gòu)建與 Nginx 配置演示的時候你大概率不會用生產(chǎn)模式Vite dev server 跑起來就夠了。但畢業(yè)設(shè)計說明書里大概率要寫“部署方案”而且答辯現(xiàn)場如果網(wǎng)絡(luò)抽風(fēng)本地開發(fā)服務(wù)器掛了會很尷尬。所以我還是建議把生產(chǎn)構(gòu)建流程跑通一遍。前端執(zhí)行 npm run build生成 dist 目錄。后端正用 Maven 打包mvn clean package -DskipTests生成 jar 包。本地測試可以直接 java -jar target/takeout-backend.jar前提是 MySQL 已啟動。Nginx 配置模板我給出最小可用的版本server { listen 80; server_name localhost; root /opt/takeout-web/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }這里最容易出錯的是 proxy_pass 的結(jié)尾斜杠。location /api/ 配上 proxy_pass http://127.0.0.1:8080; 沒有斜杠等價于把 /api 前綴保留放在一起轉(zhuǎn)發(fā)如果有斜杠路徑會被替換為 /api 之后的部分。兩種寫法對應(yīng)不同后端接口設(shè)計我建議統(tǒng)一用沒有斜杠的形式然后后端接口路徑不要帶 /api 前綴讓 Nginx 統(tǒng)一處理前綴結(jié)構(gòu)最清晰。6.3 源碼目錄結(jié)構(gòu)與說明書章節(jié)對應(yīng)源碼結(jié)構(gòu)清晰與否直接影響評閱老師對你的第一印象。后端我用標(biāo)準(zhǔn)的包結(jié)構(gòu)controller、service、mapper、entity、dto、config、common。前端按功能模塊分目錄views/user、views/admin、api、utils、stores、router、components。所有配置項(xiàng)都要放進(jìn)配置文件變量禁止在代碼里寫死。數(shù)據(jù)庫密碼尤其不能寫在業(yè)務(wù)代碼里要寫在 application.yml 里然后通過 Value 注入到數(shù)據(jù)源配置。就算項(xiàng)目是本地 demo也要養(yǎng)成這種習(xí)慣答辯時提起“配置與代碼分離”是一個能加印象分的點(diǎn)。畢業(yè)設(shè)計說明書的大綱可以直接對照源碼結(jié)構(gòu)來寫第一章緒論對應(yīng)項(xiàng)目背景和意義第二章技術(shù)選型對應(yīng)項(xiàng)目開發(fā)環(huán)境第三章系統(tǒng)設(shè)計對應(yīng)數(shù)據(jù)庫表和接口設(shè)計第四章系統(tǒng)實(shí)現(xiàn)對應(yīng)前后端核心代碼講解第五章測試對應(yīng)功能測試和聯(lián)調(diào)結(jié)果。你的源碼里每個 service 類的方法名盡量對應(yīng)說明書里的小節(jié)標(biāo)題這樣寫論文時不用來回映射。7. 常見問題排查與答辯避坑實(shí)錄7.1 高頻故障速查表現(xiàn)象可能原因解決辦法前端控制臺 404代理路徑或后端 Context Path 重復(fù)檢查 /api 前綴是否只加了一次登錄接口報 500數(shù)據(jù)庫密碼或 URL 配錯了檢查 application.yml 的 url/username/password查詢數(shù)據(jù)中文亂碼MySQL 字符集不是 utf8mb4建庫時指定 utf8mb4連接串加 characterEncodingutf8提交訂單后庫存沒變事務(wù)沒生效或方法間調(diào)用被代理攔截確認(rèn) Controller 調(diào)用的是 Service 代理而非同類內(nèi)部方法Token 生成成功但解析失敗jjwt 版本 API 變更統(tǒng)一用 Keys.hmacShaKeyFor 構(gòu)建 Key前端訪問后端接口跨域未配置代理或跨域過濾器Vite proxy 優(yōu)先CORS 配置只是后端兜底圖片上傳無法訪問靜態(tài)資源路徑未放行映射本地 upload 目錄并放行對應(yīng)路徑打包后前端刷新 404History 路由未配置回退Nginx 里配置 try_files 到 index.html以上每一條我都實(shí)際踩過不是從文檔里順手抄的。尤其是中間兩條事務(wù)不生效和 Token 解析失敗卡了我最長的時間。你如果開發(fā)到一半突然遇到這些魔幻問題先按表里檢查別急著懷疑人生。7.2 答辯時老師常抓的三個軟肋很多同學(xué)的代碼能跑但被老師一問就漏氣。我見過的高頻追問有三個。第一個是“購物車為什么不直接存 Redis”。老師想考察的是你對緩存的理解。你可以回答Redis 適合臨時性高并發(fā)場景但這個項(xiàng)目為了降低架構(gòu)復(fù)雜度、保證數(shù)據(jù)不丟失把購物車持久化到 MySQL后續(xù)如果要做大規(guī)模并發(fā)再引入 Redis 做緩存層。這樣回答既表達(dá)了你了解 Redis又說明你的方案是有針對性的不是不會。第二個是“訂單并發(fā)超賣怎么辦”。這個問得最多。你可以把扣庫存的 SQL 寫法展示出來UPDATE dish SET stock stock - #{quantity} WHERE id #{id} AND stock #{quantity}并解釋為什么這條語句天然保證不會扣成負(fù)數(shù)。如果他想聽鎖機(jī)制你再繼續(xù)說悲觀鎖和樂觀鎖的區(qū)別以及在這個量級里樂觀鎖足夠。第三個是“JWT 和 Session 有什么區(qū)別”。標(biāo)準(zhǔn)答法是Session 存儲在服務(wù)端需要共享存儲才能支撐多實(shí)例部署JWT 自包含服務(wù)端無狀態(tài)直接靠簽名驗(yàn)簽天然適合前后端分離和分布式環(huán)境。但要承認(rèn) JWT 的缺點(diǎn)是難以主動注銷所以通常配合短期過期和前端登出清 Token。這種“既講優(yōu)點(diǎn)也講局限”的回答比單純吹捧 JWT 可信得多。7.3 我在實(shí)際動手中的幾個獨(dú)家體會第一別等項(xiàng)目做完再寫文檔。我建議每做一個模塊就在說明文檔里同步貼一張截圖和一段核心代碼否則臨到答辯前三天熬夜補(bǔ)文檔質(zhì)量差到離譜。我的習(xí)慣是每天收工前花十五分鐘記錄“今天完成了什么、踩了什么坑”答辯時直接把這些一手資料加工成“項(xiàng)目難點(diǎn)”章節(jié)真實(shí)又具體。第二演示數(shù)據(jù)要提前準(zhǔn)備好而且要有邏輯。不要用“菜品1”“菜品2”這種傻瓜數(shù)據(jù)菜品名稱、價格、分類要按真實(shí)場景配好比如“招牌紅燒肉 28 元”“冰鎮(zhèn)酸梅湯 6 元”。訂單數(shù)據(jù)里要有不同狀態(tài)和不同日期的多條記錄這樣打開數(shù)據(jù)統(tǒng)計頁時趨勢圖才有起伏演示效果完全不一樣。第三做一點(diǎn)“超出預(yù)期”的小功能但不要多。比如菜品搜索的關(guān)鍵字高亮、訂單列表的分頁、圖片上傳后實(shí)時預(yù)覽。選擇一個跟你核心業(yè)務(wù)有結(jié)合的小亮點(diǎn)做精比堆砌五個半成品功能有用得多。我當(dāng)時做了一個“訂單狀態(tài)時間線”用戶在訂單詳情頁能看到從下單到完成的每一步時間戳老師當(dāng)場就問了這個數(shù)據(jù)是怎么組織和展示的成了整場答辯互動最多的一個點(diǎn)。最后再分享一個小技巧。這個項(xiàng)目做完之后不要急著刪開發(fā)記錄。把數(shù)據(jù)庫初始化腳本、啟動說明、常見問題說明寫進(jìn)項(xiàng)目的 README并且把 README 掛在倉庫首頁。不僅是自己以后回看方便而且如果指導(dǎo)老師或者評審想跑一下你的項(xiàng)目照著 README 就能十分鐘內(nèi)啟動成功這會大大降低溝通成本也側(cè)面證明你的工程化意識。不要小看這一個小動作很多同學(xué)的項(xiàng)目在老師手里根本跑不起來就是差一份像樣的啟動說明。你可以按我的經(jīng)驗(yàn)在項(xiàng)目根目錄建一個 docs 文件夾把 SQL 腳本、環(huán)境變量示例、接口文檔表格全放進(jìn)去答辯的時候直接把這份文檔展示出來效果不比你半小時的代碼演示差。