站畢業(yè)設(shè)計(jì)完整實(shí)戰(zhàn)指南)
又到了一年里被畢設(shè)題目反復(fù)“折磨”的時(shí)候最近來(lái)問(wèn)我的學(xué)弟學(xué)妹里有好幾個(gè)拿的都是“SSMVue旅游資源網(wǎng)站”這類(lèi)題目。實(shí)話(huà)實(shí)說(shuō)這個(gè)題目在計(jì)算機(jī)類(lèi)本科畢設(shè)里屬于典型的“老牌選手”技術(shù)上不算激進(jìn)但需要做的東西一點(diǎn)也不少。如果能把程序結(jié)構(gòu)和論文思路都打通它既可以當(dāng)一份完成度很高的畢設(shè)也可以在答辯時(shí)讓老師清楚地看到你的工程能力。這篇文章我就圍繞“ssmvue旅游資源網(wǎng)站”這個(gè)項(xiàng)目從選題邏輯、數(shù)據(jù)庫(kù)設(shè)計(jì)、前后端分工、核心代碼實(shí)現(xiàn)、論文寫(xiě)作和答辯準(zhǔn)備幾個(gè)維度完整拆一遍。適合正在做同類(lèi)題目、或者打算拿旅游類(lèi)網(wǎng)站當(dāng)畢設(shè)項(xiàng)目的人參考哪怕你目前只寫(xiě)過(guò)增刪改查跟著這套思路也能把一個(gè)合格的項(xiàng)目搭起來(lái)。1. 項(xiàng)目定位與整體設(shè)計(jì)思路1.1 為什么SSMVue這套組合至今還是畢設(shè)的穩(wěn)妥選擇很多學(xué)生看到現(xiàn)在到處都是Spring Boot、微服務(wù)、前后端分離就會(huì)疑惑為什么還要做一個(gè)SSM的項(xiàng)目這里有個(gè)非常實(shí)際的背景不少高校的選題庫(kù)和答辯組仍然保留了SSM體系題目寫(xiě)什么就按什么來(lái)這是最穩(wěn)妥的路線(xiàn)。如果提綱上明確寫(xiě)著“基于SSM框架的旅游資源網(wǎng)站”那你就不要在畢設(shè)里私自換成Spring Boot除非指導(dǎo)老師明確表示可以調(diào)整。否則答辯時(shí)會(huì)面臨“你的題目和實(shí)現(xiàn)不一致”的尷尬局面。從技術(shù)本身來(lái)說(shuō)SSM不是過(guò)時(shí)的東西。Spring管對(duì)象和事務(wù)SpringMVC管請(qǐng)求分發(fā)MyBatis管數(shù)據(jù)庫(kù)操作這三者配合能讓一個(gè)本科生把“分層思想”講得很清楚。相比Spring Boot那種“自動(dòng)配置一把梭”的用法SSM反而更能逼迫你去理解Bean、IoC和過(guò)濾器這些基礎(chǔ)概念。前端這塊配Vue是加分項(xiàng)。老式畢設(shè)喜歡用JSPjQuery做出來(lái)的頁(yè)面一眼就回到2015年。用Vue做單頁(yè)應(yīng)用配上Element UI或Element Plus頁(yè)面的觀感會(huì)明顯好于傳統(tǒng)模板渲染。旅游資源這種偏展示型的網(wǎng)站前端體驗(yàn)恰恰是用戶(hù)第一眼關(guān)注的東西所以Vue在這里不是炫技而是解決實(shí)際問(wèn)題。1.2 業(yè)務(wù)功能邊界把“資源展示”變成可驗(yàn)收項(xiàng)目很多同學(xué)拿到“旅游資源網(wǎng)站”這個(gè)題目后第一反應(yīng)是拼命加功能支付、搶票、線(xiàn)路規(guī)劃、智能推薦……我見(jiàn)過(guò)一位同學(xué)想做一個(gè)完整版“在線(xiàn)旅游電商平臺(tái)”結(jié)果寫(xiě)了三個(gè)月連核心流程都沒(méi)跑通最后只能熬夜砍功能。正確的思路是先把項(xiàng)目定性成“資源展示輕交互”。游客可以瀏覽景點(diǎn)和旅游線(xiàn)路注冊(cè)用戶(hù)可以收藏景點(diǎn)、發(fā)表評(píng)論管理員可以在后臺(tái)管理景點(diǎn)、線(xiàn)路、公告、用戶(hù)和評(píng)論。這個(gè)范圍的拿捏是有講究的有完整的用戶(hù)體系注冊(cè)、登錄、權(quán)限區(qū)分這是每一個(gè)答辯老師都會(huì)關(guān)注的點(diǎn)。有核心業(yè)務(wù)實(shí)體景點(diǎn)、線(xiàn)路、資訊做的是旅游資源領(lǐng)域的事。有典型的系統(tǒng)行為瀏覽、搜索、詳情、收藏、評(píng)論能體現(xiàn)數(shù)據(jù)庫(kù)設(shè)計(jì)和業(yè)務(wù)邏輯。有后臺(tái)管理端管理員的增刪改查能體現(xiàn)完整的閉環(huán)。把邊界定在這個(gè)程度既能在一兩個(gè)月內(nèi)做出來(lái)又足夠支撐一篇結(jié)構(gòu)完整的論文。支付、地圖、推薦這些可以寫(xiě)進(jìn)“未來(lái)展望”但不要真花時(shí)間去開(kāi)發(fā)。角色劃分也很清晰我用下表把它結(jié)構(gòu)化角色能做什么對(duì)應(yīng)主體游客瀏覽景點(diǎn)、線(xiàn)路、公告搜索景點(diǎn)前臺(tái)公開(kāi)頁(yè)面注冊(cè)用戶(hù)游客功能 登錄、收藏、評(píng)論、個(gè)人中心前臺(tái)登錄區(qū)管理員用戶(hù)管理、景點(diǎn)管理、線(xiàn)路管理、評(píng)論管理、公告管理后臺(tái)管理端1.3 程序與論文的配合節(jié)奏我后來(lái)帶項(xiàng)目時(shí)一直強(qiáng)調(diào)一件事不要等程序全部寫(xiě)完再開(kāi)始寫(xiě)論文也不要先把論文編出來(lái)再補(bǔ)程序。正確的節(jié)奏是用論文大綱反推程序模塊用程序截圖填充論文。具體操作上我建議先花兩天列出論文的章節(jié)框架摘要寫(xiě)什么、需求分析分哪幾節(jié)、數(shù)據(jù)庫(kù)表畫(huà)哪些、系統(tǒng)實(shí)現(xiàn)要哪些截圖、測(cè)試寫(xiě)哪些用例。然后按照這個(gè)框架去開(kāi)發(fā)程序每完成一個(gè)模塊馬上截圖并寫(xiě)好功能說(shuō)明。比如你做完了景點(diǎn)管理就立刻把列表頁(yè)截圖、新增頁(yè)截圖、修改頁(yè)截圖存到一個(gè)“論文素材”文件夾里順手寫(xiě)下這段功能是怎么實(shí)現(xiàn)的。這樣到論文階段你不需要回憶“當(dāng)時(shí)那個(gè)功能是怎么做的”素材都在手邊寫(xiě)作速度會(huì)快很多而且截圖和程序永遠(yuǎn)對(duì)得上。很多同學(xué)最后要花大量時(shí)間返工就是因?yàn)槌绦蚪缑娓陌婧笳撐睦锏呐f截圖和新代碼對(duì)不上答辯時(shí)被老師一眼看出來(lái)。2. 核心細(xì)節(jié)解析與實(shí)操要點(diǎn)2.1 數(shù)據(jù)表設(shè)計(jì)少踩“字段拍腦袋”的坑旅游資源網(wǎng)站的數(shù)據(jù)表不算復(fù)雜但設(shè)計(jì)得規(guī)不規(guī)范答辯老師一眼就能看出來(lái)。我通常建議最少建六張表用戶(hù)表id、用戶(hù)名、密碼、昵稱(chēng)、手機(jī)號(hào)、頭像、角色、注冊(cè)時(shí)間。景點(diǎn)表id、景點(diǎn)名稱(chēng)、所屬地區(qū)、封面圖片、簡(jiǎn)介、詳細(xì)內(nèi)容、門(mén)票價(jià)格、開(kāi)放時(shí)間、創(chuàng)建時(shí)間、狀態(tài)。線(xiàn)路表id、線(xiàn)路名稱(chēng)、出發(fā)地、目的地、天數(shù)、價(jià)格、線(xiàn)路圖片、詳情、創(chuàng)建時(shí)間。評(píng)論表id、用戶(hù)id、景點(diǎn)id、評(píng)論內(nèi)容、評(píng)論時(shí)間。收藏表id、用戶(hù)id、景點(diǎn)id、創(chuàng)建時(shí)間。公告表id、標(biāo)題、內(nèi)容、發(fā)布時(shí)間。這套表里有一個(gè)比較常見(jiàn)的坑圖片字段。景點(diǎn)封面圖很可能不止一張但畢設(shè)項(xiàng)目沒(méi)必要做獨(dú)立圖片表。最省事也最穩(wěn)妥的做法是直接在景點(diǎn)表里用一個(gè)字符串字段比如cover_image存圖片的相對(duì)路徑或完整URL。前端拿到這個(gè)字符串直接渲染后面想要多圖用逗號(hào)分隔字符也足夠應(yīng)付。另一個(gè)容易出問(wèn)題的地方是價(jià)格字段。做旅游類(lèi)項(xiàng)目時(shí)門(mén)票價(jià)格、線(xiàn)路價(jià)格一定用decimal類(lèi)型不能圖省事用float或double。很多同學(xué)在真實(shí)項(xiàng)目中吃過(guò)浮點(diǎn)數(shù)計(jì)算的虧這里用decimal(10, 2)就能避免價(jià)格顯示成99.999999這種奇怪情況。評(píng)論表和收藏表都要記得加索引。尤其是收藏表最好對(duì)user_id和scenic_id建立唯一索引這樣同一個(gè)人重復(fù)收藏同一個(gè)景點(diǎn)時(shí)數(shù)據(jù)庫(kù)會(huì)直接攔截應(yīng)用層不用寫(xiě)一堆判斷邏輯。我當(dāng)時(shí)做的時(shí)候后端收藏接口先查一遍是否存在再配合數(shù)據(jù)庫(kù)唯一索引雙保險(xiǎn)既直觀又防臟數(shù)據(jù)。外鍵策略也值得提前定好。刪除景點(diǎn)時(shí)如果該景點(diǎn)下已經(jīng)有評(píng)論和收藏直接物理刪除會(huì)導(dǎo)致孤兒數(shù)據(jù)。一種做法是刪景點(diǎn)前先刪相關(guān)的評(píng)論和收藏另一種是給景點(diǎn)表加一個(gè)status字段做邏輯刪除。畢設(shè)層面我推薦做邏輯刪除答辯時(shí)還能解釋“保留歷史數(shù)據(jù)”的業(yè)務(wù)含義。2.2 后端分層別把Controller寫(xiě)成萬(wàn)能類(lèi)接手過(guò)幾個(gè)學(xué)生的半成品項(xiàng)目發(fā)現(xiàn)最典型的問(wèn)題就是Controller里塞滿(mǎn)了業(yè)務(wù)代碼查數(shù)據(jù)庫(kù)、拼數(shù)據(jù)、寫(xiě)返回全塞在一個(gè)方法里。比如這樣的代碼Controller public class ScenicController { Autowired private JdbcTemplate jdbcTemplate; RequestMapping(/list) ResponseBody public ListMapString, Object list(String keyword) { // 直接在這里寫(xiě)SQL語(yǔ)句拼條件 String sql select * from t_scenic where name like %keyword%; return jdbcTemplate.queryForList(sql); } }這種寫(xiě)法功能倒是能做出來(lái)但答辯時(shí)只要老師追問(wèn)一句“你的項(xiàng)目怎么分層”就會(huì)露出破綻。規(guī)范的SSM項(xiàng)目至少要有Controller、Service、Mapper三層Controller層接收參數(shù)、校驗(yàn)參數(shù)、調(diào)用Service、返回結(jié)果。Service層處理業(yè)務(wù)邏輯比如判斷評(píng)論人是否登錄、收藏是否已存在。Mapper層只負(fù)責(zé)和數(shù)據(jù)庫(kù)交互。為了讓前端處理返回值更方便還需要一個(gè)統(tǒng)一的返回類(lèi)型。比如定義Result類(lèi)包含code、msg、data三個(gè)字段。成功就返回new Result(200, 操作成功, data)失敗就返回new Result(500, 操作失敗, null)。前端拿到這個(gè)結(jié)構(gòu)體在axios的響應(yīng)攔截器里統(tǒng)一判斷code彈出提示信息代碼會(huì)干凈很多。還有一個(gè)小細(xì)節(jié)查詢(xún)景點(diǎn)列表時(shí)全文搜索和分頁(yè)要一起做。最簡(jiǎn)單的實(shí)現(xiàn)是用PageHelper分頁(yè)插件在Service層查詢(xún)之前調(diào)用PageHelper.startPage(pageNum, pageSize)緊接著執(zhí)行的Mapper查詢(xún)就會(huì)被自動(dòng)拼接limit。這個(gè)方法用起來(lái)很爽但一定要注意startPage和查詢(xún)之間不能夾其他數(shù)據(jù)庫(kù)操作否則分頁(yè)會(huì)失效。這個(gè)坑我在第4節(jié)還會(huì)專(zhuān)門(mén)講。2.3 Vue前端的狀態(tài)與路由設(shè)計(jì)前端部分是整個(gè)項(xiàng)目最容易出彩、也最容易翻車(chē)的地方。如果你是從零開(kāi)始又暫時(shí)沒(méi)有明確要求Vue版本我建議直接用Vue3 Element Plus因?yàn)檫@是現(xiàn)在主流的組合資料多遇到問(wèn)題也好搜。當(dāng)然如果學(xué)校模板指定的是Vue2 Element UI那也沒(méi)必要硬改功能上差異不大。頁(yè)面結(jié)構(gòu)上我習(xí)慣把前臺(tái)和后臺(tái)分成兩套路由區(qū)域前臺(tái) / 首頁(yè) /scenic/list 景點(diǎn)列表 /scenic/detail/:id 景點(diǎn)詳情 /route/list 旅游線(xiàn)路 /notice/list 公告列表 /login 登錄 /register 注冊(cè) /user/center 個(gè)人中心 后臺(tái) /admin/dashboard 數(shù)據(jù)概況 /admin/scenic 景點(diǎn)管理 /admin/route 線(xiàn)路管理 /admin/user 用戶(hù)管理 /admin/comment 評(píng)論管理 /admin/notice 公告管理這背后要做三件關(guān)鍵的事第一是axios封裝。新建一個(gè)request.js配置baseURL指向后端地址請(qǐng)求攔截器從localStorage里取token并放到請(qǐng)求頭響應(yīng)攔截器統(tǒng)一處理業(yè)務(wù)碼。這樣每個(gè)頁(yè)面調(diào)接口都只需要傳URL和參數(shù)不用重復(fù)處理錯(cuò)誤提示。第二是路由守衛(wèi)。在router/index.js里給后臺(tái)路由加上meta: { requiresAuth: true }然后在全局前置守衛(wèi)里判斷如果用戶(hù)信息里沒(méi)有管理員標(biāo)記就跳回登錄頁(yè)。游客可以訪(fǎng)問(wèn)的頁(yè)面則不需要守衛(wèi)。第三是狀態(tài)管理。Vue3項(xiàng)目用PiniaVue2項(xiàng)目用Vuex把登錄用戶(hù)的昵稱(chēng)、角色、頭像存到store里同時(shí)同步到localStorage刷新頁(yè)面后從localStorage恢復(fù)。很多同學(xué)登錄后一刷新就回到未登錄狀態(tài)就是因?yàn)橹话褦?shù)據(jù)存在內(nèi)存里沒(méi)有做本地持久化。前臺(tái)頁(yè)面我建議關(guān)注一個(gè)體驗(yàn)點(diǎn)景點(diǎn)列表的卡片布局。每個(gè)景點(diǎn)卡展示封面圖、名稱(chēng)、地區(qū)、價(jià)格和一個(gè)“去看看”按鈕圖片高度保持一致價(jià)格用醒目的顏色標(biāo)出來(lái)整體觀感會(huì)比純表格列表好得多。這種頁(yè)面細(xì)節(jié)花不了太多時(shí)間但對(duì)答辯印象分提升非常明顯。3. 實(shí)操過(guò)程與核心環(huán)節(jié)實(shí)現(xiàn)3.1 環(huán)境準(zhǔn)備與項(xiàng)目骨架搭建先把開(kāi)發(fā)環(huán)境說(shuō)清楚推薦組合是JDK 1.8或11MySQL 5.7或8.0Maven 3.6Node.js 16開(kāi)發(fā)工具IDEA VS Code工作目錄里建議創(chuàng)建前后端兩個(gè)文件夾比如tourism-backend和tourism-frontend長(zhǎng)期維護(hù)起來(lái)一眼就懂。后端用Maven管理依賴(lài)核心依賴(lài)需要這些dependencies !-- Spring核心 -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version5.x.x/version /dependency !-- SpringMVC -- dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.x.x/version /dependency !-- MyBatis -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.x/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.x.x/version /dependency !-- MySQL驅(qū)動(dòng) -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.x/version /dependency !-- 數(shù)據(jù)庫(kù)連接池 -- dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.x/version /dependency !-- JSON處理 -- dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.13.x/version /dependency !-- 分頁(yè)插件 -- dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper/artifactId version5.3.x/version /dependency /dependencies這里的版本號(hào)我個(gè)人建議用穩(wěn)定版本不要一上來(lái)就追最新Spring 5.x和MyBatis 3.5.x這種組合已經(jīng)很多項(xiàng)目驗(yàn)證過(guò)了能避免因?yàn)榘姹炯嫒輪?wèn)題浪費(fèi)時(shí)間。后端配置文件至少有四份jdbc.properties存放數(shù)據(jù)庫(kù)連接信息。spring.xml配置包掃描、數(shù)據(jù)源、SqlSessionFactory、事務(wù)管理器。spring-mvc.xml配置注解驅(qū)動(dòng)、靜態(tài)資源放行、視圖解析器如果前后端分離不需要JSP配置。web.xml配置Spring監(jiān)聽(tīng)器和SpringMVC的DispatcherServlet。前端用Vue CLI創(chuàng)建項(xiàng)目vue create tourism-frontend選擇Vue3預(yù)設(shè)安裝vue-router、pinia或axios。然后執(zhí)行npm install element-plus element-plus/icons-vueElement Plus在main.js里做完整引入雖然首屏包體積會(huì)大一些但畢設(shè)項(xiàng)目無(wú)所謂開(kāi)發(fā)效率最重要import { createApp } from vue import ElementPlus from element-plus import element-plus/dist/index.css import App from ./App.vue import router from ./router import { createPinia } from pinia const app createApp(App) app.use(ElementPlus) app.use(router) app.use(createPinia()) app.mount(#app)3.2 用戶(hù)登錄、角色權(quán)限與攔截器登錄接口是畢設(shè)項(xiàng)目的“交通樞紐”幾乎所有頁(yè)面交互都跟用戶(hù)身份掛鉤。我建議登錄做成這樣用戶(hù)提交用戶(hù)名和密碼Controller接收后調(diào)用ServiceService根據(jù)用戶(hù)名查出用戶(hù)記錄比對(duì)密碼。密碼在數(shù)據(jù)庫(kù)里不要存明文注冊(cè)時(shí)用MD5或BCrypt加密一次登錄時(shí)把用戶(hù)輸入的密碼做同樣的加密再比對(duì)。如果一致生成一個(gè)代表登錄狀態(tài)的token返回給前端。token的實(shí)現(xiàn)有兩種檔次。最簡(jiǎn)單的是Session方案登錄成功把用戶(hù)對(duì)象放到session里后端攔截器判斷session里有沒(méi)有用戶(hù)。這種方案代碼少答辯也能講清楚但前后端分離部署時(shí)跨域和Session失效問(wèn)題比較煩人。我推薦用JWT風(fēng)格的token不一定要引入完整jwt庫(kù)模擬一個(gè)也可以。登錄成功后后端把用戶(hù)id和角色拼成一個(gè)帶簽名的字符串返回前端存到localStorage每次請(qǐng)求放請(qǐng)求頭。后端做一個(gè)攔截器解析請(qǐng)求頭如果解析失敗就返回401。后端攔截器的核心邏輯public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 如果是跨域預(yù)檢請(qǐng)求直接放行 if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token null || .equals(token)) { response.setStatus(401); return false; } // 解析token判斷是否有效、是否過(guò)期 // 解析出的userId放入request屬性方便Controller使用 return true; } }這個(gè)攔截器要注冊(cè)到SpringMVC中同時(shí)配置放行路徑比如/api/user/login、/api/user/register、/api/scenic/**這些公開(kāi)接口不需要token而/api/user/center/**、/api/favorite/**則需要登錄。管理員接口比如/api/admin/**還要額外校驗(yàn)用戶(hù)角色是否為1。前端路由守衛(wèi)配合起來(lái)也很簡(jiǎn)單router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) } else { next() } })3.3 景點(diǎn)檢索、詳情與評(píng)論收藏實(shí)現(xiàn)景點(diǎn)列表頁(yè)是訪(fǎng)問(wèn)量最高的頁(yè)面它的核心是“條件查詢(xún)分頁(yè)”。哪些條件適合用來(lái)篩選最常用的是景點(diǎn)名稱(chēng)關(guān)鍵字、所屬地區(qū)、價(jià)格范圍。對(duì)應(yīng)的MyBatis XML我一般這么寫(xiě)select idselectScenicPage resultMapScenicResultMap SELECT s.id, s.name, s.region, s.cover_image, s.price, s.open_time FROM t_scenic s where if testname ! null and name ! AND s.name LIKE CONCAT(%, #{name}, %) /if if testregion ! null and region ! AND s.region #{region} /if if testminPrice ! null AND s.price gt; #{minPrice} /if if testmaxPrice ! null AND s.price lt; #{maxPrice} /if /where ORDER BY s.create_time DESC /selectService層再配合PageHelperpublic PageInfoScenic getScenicPage(int pageNum, int pageSize, ScenicQuery query) { PageHelper.startPage(pageNum, pageSize); ListScenic list scenicMapper.selectScenicPage(query); return new PageInfo(list); }這樣前端每次只傳pageNum、pageSize和幾個(gè)篩選項(xiàng)就能拿到分頁(yè)數(shù)據(jù)。景點(diǎn)詳情頁(yè)除了顯示景點(diǎn)信息還要顯示評(píng)論列表。評(píng)論接口按景點(diǎn)id分頁(yè)查詢(xún)每條評(píng)論要帶上發(fā)表人的昵稱(chēng)和頭像所以SQL要關(guān)聯(lián)用戶(hù)表。這里用resultMap映射會(huì)比resultType更直觀select idselectCommentsByScenicId resultMapCommentResultMap SELECT c.id, c.content, c.create_time, u.nickname, u.avatar FROM t_comment c LEFT JOIN t_user u ON c.user_id u.id WHERE c.scenic_id #{scenicId} ORDER BY c.create_time DESC /select收藏模塊是“用戶(hù)與景點(diǎn)”關(guān)系的核心實(shí)現(xiàn)。接口設(shè)計(jì)就兩個(gè)一個(gè)添加收藏一個(gè)取消收藏。添加時(shí)先查詢(xún)是否已收藏如果已收藏就返回提示如果沒(méi)有就插入一條記錄。取消收藏直接按userId和scenicId刪除記錄。個(gè)人中心里查詢(xún)當(dāng)前用戶(hù)的收藏列表時(shí)關(guān)聯(lián)景點(diǎn)表把景點(diǎn)的基本信息和封面圖帶出來(lái)方便做一個(gè)“我收藏的景點(diǎn)”卡片列表。后臺(tái)管理模塊基本都是增刪改查這里不展開(kāi)每個(gè)接口但有一個(gè)思路值得分享后臺(tái)列表頁(yè)沿用前臺(tái)的分頁(yè)組件每個(gè)管理頁(yè)用Element Plus的el-table el-pagination el-dialog組合。彈窗里放表單提交時(shí)調(diào)用新增或更新接口成功后刷新表格。這套模式寫(xiě)熟手之后一張后臺(tái)頁(yè)面大概一晚上就能做完六張管理頁(yè)面也花不了太久。3.4 論文撰寫(xiě)的章節(jié)組織與查重應(yīng)對(duì)程序做完后論文才是重頭戲。SSMVue旅游資源網(wǎng)站的論文目錄基本是固定的套路我建議按這六章走第一章緒論。寫(xiě)選題背景和研究意義重點(diǎn)說(shuō)明“為什么需要旅游資源網(wǎng)站為什么要用JavaVue技術(shù)?!弊詈蠓乓粡垏?guó)內(nèi)外研究現(xiàn)狀的綜述。第二章需求分析。先寫(xiě)業(yè)務(wù)流程再寫(xiě)功能需求用用例圖展示游客、用戶(hù)、管理員各自能做什么。非功能性需求要提性能、易用性、安全性。這一章對(duì)應(yīng)程序里的“功能邊界”寫(xiě)得越具體后面的系統(tǒng)設(shè)計(jì)越好寫(xiě)。第三章系統(tǒng)設(shè)計(jì)。包括系統(tǒng)總體架構(gòu)圖、功能模塊圖、數(shù)據(jù)庫(kù)E-R圖和表結(jié)構(gòu)設(shè)計(jì)。實(shí)際上E-R圖和表結(jié)構(gòu)直接抄自己建的表就行逆向梳理一遍改造成可視化圖形。第四章系統(tǒng)實(shí)現(xiàn)。這是論文最占篇幅的一章按功能模塊逐個(gè)寫(xiě)每個(gè)模塊放核心代碼片段和運(yùn)行截圖。注意代碼不用貼全部只貼關(guān)鍵方法并配一段文字說(shuō)明“這是做什么的為什么這么做”。第五章系統(tǒng)測(cè)試。寫(xiě)測(cè)試環(huán)境、測(cè)試方法然后列測(cè)試用例表測(cè)試編號(hào)、測(cè)試項(xiàng)、輸入數(shù)據(jù)、預(yù)期結(jié)果、實(shí)際結(jié)果。別全寫(xiě)“通過(guò)”針對(duì)邊界條件寫(xiě)一兩個(gè)失敗用例反而顯得真實(shí)。第六章總結(jié)與展望。總結(jié)做了一個(gè)什么系統(tǒng)、用了什么技術(shù)、有什么不足后續(xù)可以增加什么功能。關(guān)于查重我必須提醒一句參考文獻(xiàn)里的概念介紹千萬(wàn)不要大段復(fù)制教科書(shū)原文。寫(xiě)技術(shù)介紹時(shí)用自己的話(huà)把“Spring是什么、MyBatis是什么”重新表述一遍。尤其要小心網(wǎng)上的博客模板很多畢業(yè)論文模板章節(jié)相似度極高直接照抄會(huì)被查重系統(tǒng)標(biāo)紅。正確做法是定義概念后立刻結(jié)合自己項(xiàng)目里的具體場(chǎng)景說(shuō)一句比如“在本系統(tǒng)中Spring負(fù)責(zé)管理Service和Mapper對(duì)象的創(chuàng)建和依賴(lài)注入”這樣既不容易重復(fù)又能體現(xiàn)你對(duì)技術(shù)的掌握。4. 常見(jiàn)問(wèn)題與排查技巧實(shí)錄4.1 跨域報(bào)錯(cuò)與預(yù)檢請(qǐng)求處理前后端分離最常遇到的就是跨域問(wèn)題?,F(xiàn)象很明確前端控制臺(tái)報(bào)No Access-Control-Allow-Origin header is present on the requested resource。原因簡(jiǎn)單說(shuō)就是前端跑在http://localhost:8080后端跑在http://localhost:8088瀏覽器認(rèn)為這不是同一個(gè)源。解決辦法一般是在后端加一個(gè)跨域過(guò)濾器。Component public class CorsFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletResponse response (HttpServletResponse) res; response.setHeader(Access-Control-Allow-Origin, http://localhost:8080); response.setHeader(Access-Control-Allow-Credentials, true); response.setHeader(Access-Control-Allow-Methods, GET, POST, PUT, DELETE, OPTIONS); response.setHeader(Access-Control-Allow-Headers, Content-Type, Authorization); chain.doFilter(req, res); } }一個(gè)隱藏很深的坑是預(yù)檢請(qǐng)求。前端請(qǐng)求帶了Authorization這樣的自定義頭或者請(qǐng)求方法是PUT、DELETE時(shí)瀏覽器會(huì)先發(fā)一個(gè)OPTIONS請(qǐng)求確認(rèn)服務(wù)器是否允許。如果你的后端攔截器把所有OPTIONS請(qǐng)求都攔下來(lái)要求登錄跨域就永遠(yuǎn)過(guò)不了。所以在攔截器的preHandle里一定要放行OPTIONS請(qǐng)求我前面的代碼示例里已經(jīng)寫(xiě)到了這一步。如果你不想在后端處理跨域也可以在前端利用開(kāi)發(fā)服務(wù)器做代理。在Vue項(xiàng)目的vue.config.js里配置module.exports { devServer: { proxy: { /api: { target: http://localhost:8088, changeOrigin: true } } } }這樣所有/api開(kāi)頭的請(qǐng)求都轉(zhuǎn)發(fā)到后端瀏覽器看到的是同源地址。但這個(gè)方法只對(duì)開(kāi)發(fā)環(huán)境有效正式打包部署后還是要用Nginx或其他方式統(tǒng)一入口。4.2 登錄狀態(tài)反復(fù)失效的兩種解決辦法這是我見(jiàn)過(guò)學(xué)生踩得最多的坑之一表現(xiàn)形式是登錄成功后頁(yè)面跳轉(zhuǎn)正常但刷新一下或者點(diǎn)幾個(gè)頁(yè)面后就提示“未登錄”。如果你用的是Session方案多半問(wèn)題出在跨域配置沒(méi)有帶上Cookie。瀏覽器默認(rèn)在同源請(qǐng)求時(shí)才會(huì)攜帶Cookie跨域請(qǐng)求需要后端在響應(yīng)頭設(shè)置Access-Control-Allow-Credentials: true同時(shí)前端axios要設(shè)置withCredentials: true。但個(gè)人建議直接切換成token方案能避開(kāi)上面這一整攤事。登錄接口返回一個(gè)token字符串前端存localStorage每次請(qǐng)求在攔截器里加上service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] token } return config })后端攔截器從請(qǐng)求頭拿到token驗(yàn)證通過(guò)就把用戶(hù)信息放到request屬性里后面的Controller直接取。這套方案不受Cookie跨域限制部署到Nginx時(shí)也好處理。token還有一個(gè)好處是答辯時(shí)容易講。你可以說(shuō)“因?yàn)橄到y(tǒng)采用無(wú)狀態(tài)的會(huì)話(huà)保持方案服務(wù)器不需要保存用戶(hù)的登錄狀態(tài)減輕了內(nèi)存壓力”這句話(huà)翻譯成大白話(huà)就是“每個(gè)請(qǐng)求都帶著自己的身份證明來(lái)過(guò)檢”。4.3 MyBatis常見(jiàn)報(bào)錯(cuò)與駝峰映射寫(xiě)MyBatis遇到報(bào)錯(cuò)是常態(tài)我把高頻問(wèn)題整理成一張速查表報(bào)錯(cuò)信息本質(zhì)原因解決辦法Invalid bound statement (not found)Mapper接口和XML文件沒(méi)有對(duì)應(yīng)上檢查namespace是否等于接口全限定名以及mybatis配置里mapper-locations是否指向xml目錄BindingException: Parameter xxx not found接口方法參數(shù)沒(méi)加Param多參數(shù)時(shí)必須寫(xiě)Param(name)查詢(xún)結(jié)果全是null列名和實(shí)體屬性名不一致開(kāi)啟mapUnderscoreToCamelCase或?qū)憆esultMapCause: java.sql.SQLSyntaxErrorExceptionSQL語(yǔ)句語(yǔ)法錯(cuò)誤檢查表名、列名特別是MySQL關(guān)鍵字例如表名不要用order、descPageHelper分頁(yè)不生效startPage和查詢(xún)之間隔了其他數(shù)據(jù)庫(kù)操作讓startPage緊貼Mapper查詢(xún)執(zhí)行駝峰命名這個(gè)問(wèn)題在SSM項(xiàng)目里特別容易反復(fù)出現(xiàn)。Java實(shí)體類(lèi)習(xí)慣用createTime數(shù)據(jù)庫(kù)列名習(xí)慣用create_time如果不在全局配置開(kāi)啟駝峰映射查出來(lái)的createTime就是null。配置方式是在mybatis-config.xml里settings setting namemapUnderscoreToCamelCase valuetrue/ /settings注意只開(kāi)啟駝峰映射還不夠數(shù)據(jù)庫(kù)列名必須真的是create_time這種下劃線(xiàn)格式如果開(kāi)發(fā)時(shí)有人圖省事把列名直接命名為create_time但實(shí)體類(lèi)是create_time那映射出來(lái)又會(huì)錯(cuò)所以命名規(guī)范要從建表開(kāi)始就統(tǒng)一。我個(gè)人的習(xí)慣是數(shù)據(jù)庫(kù)全部用lower_case_with_underscoreJava實(shí)體全部用駝峰中間靠MyBatis的映射自動(dòng)轉(zhuǎn)換。4.4 答辯前最后三天的檢查清單最后說(shuō)點(diǎn)跟答辯直接相關(guān)的實(shí)操經(jīng)驗(yàn)。答辯內(nèi)容、截圖、提問(wèn)環(huán)節(jié)都有對(duì)應(yīng)的準(zhǔn)備技巧建議最后三天按下面的清單過(guò)一遍檢查項(xiàng)具體內(nèi)容主流程閉環(huán)從注冊(cè)、登錄、瀏覽景點(diǎn)、搜索、收藏、發(fā)表評(píng)論到后臺(tái)登錄、管理景點(diǎn)全程走一遍任何一步報(bào)錯(cuò)都要當(dāng)場(chǎng)解決數(shù)據(jù)自洽刪除景點(diǎn)時(shí)它下面的評(píng)論和收藏怎么處理外鍵約束會(huì)不會(huì)導(dǎo)致刪除報(bào)錯(cuò)后臺(tái)刪除數(shù)據(jù)后前臺(tái)會(huì)不會(huì)出現(xiàn)NullPointerException數(shù)據(jù)庫(kù)導(dǎo)出用mysqldump導(dǎo)出一份完整的SQL腳本或者至少在開(kāi)發(fā)工具里準(zhǔn)備好一鍵重建數(shù)據(jù)庫(kù)的命令代碼結(jié)構(gòu)可講隨便挑一個(gè)Controller你能不看IDE說(shuō)出它的Service和Mapper分別做了什么嗎論文與截圖一致論文里出現(xiàn)的每一個(gè)頁(yè)面截圖程序里都必須能找到對(duì)應(yīng)頁(yè)面且數(shù)據(jù)最好相同演示環(huán)境穩(wěn)定如果現(xiàn)場(chǎng)沒(méi)有網(wǎng)絡(luò)數(shù)據(jù)庫(kù)和依賴(lài)要提前裝好如果用IDEA啟動(dòng)提前把依賴(lài)下載完整避免現(xiàn)場(chǎng)卡在下載還有一個(gè)小技巧答辯演示時(shí)建議用授課千萬(wàn)注意不要念代碼。講登錄功能時(shí)就說(shuō)“用戶(hù)輸入用戶(hù)名密碼后端校驗(yàn)成功后返回token前端保存到本地之后請(qǐng)求自動(dòng)攜帶實(shí)現(xiàn)會(huì)話(huà)保持”然后用演示操作補(bǔ)充細(xì)節(jié)老師看到的是“論述清晰系統(tǒng)可用”這兩點(diǎn)都滿(mǎn)足分?jǐn)?shù)一定不會(huì)差。這個(gè)項(xiàng)目做完之后我個(gè)人實(shí)際帶項(xiàng)目的體會(huì)是畢設(shè)最重要的不是技術(shù)多炫而是“完整”兩個(gè)字。功能鏈路完整文檔和代碼對(duì)得上答辯時(shí)講得清楚這三點(diǎn)做到就已經(jīng)是一份非常合格的畢業(yè)設(shè)計(jì)。如果你時(shí)間充裕再往里面加一點(diǎn)小創(chuàng)新比如用Redis緩存熱門(mén)景點(diǎn)數(shù)據(jù)或者把后臺(tái)改造成帶統(tǒng)計(jì)圖表的Dashboard就是錦上添花。最后再分享一個(gè)小技巧每天開(kāi)發(fā)結(jié)束后花十分鐘在本地Gitee或GitHub倉(cāng)庫(kù)提交一次代碼。別嫌麻煩那些最后做不下去、程序跑不起來(lái)、論文寫(xiě)不出來(lái)的同學(xué)大多數(shù)不是能力問(wèn)題而是改著改著把自己改亂了沒(méi)有可以回退的干凈版本。用好了版本控制你的畢設(shè)進(jìn)度就會(huì)穩(wěn)很多。