站畢業(yè)設(shè)計(jì)完整實(shí)戰(zhàn)指南)
又到了一年里被畢設(shè)題目反復(fù)“折磨”的時(shí)候最近來問我的學(xué)弟學(xué)妹里有好幾個(gè)拿的都是“SSMVue旅游資源網(wǎng)站”這類題目。實(shí)話實(shí)說這個(gè)題目在計(jì)算機(jī)類本科畢設(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)、論文寫作和答辯準(zhǔn)備幾個(gè)維度完整拆一遍。適合正在做同類題目、或者打算拿旅游類網(wǎng)站當(dāng)畢設(shè)項(xiàng)目的人參考哪怕你目前只寫過增刪改查跟著這套思路也能把一個(gè)合格的項(xiàng)目搭起來。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體系題目寫什么就按什么來這是最穩(wěn)妥的路線。如果提綱上明確寫著“基于SSM框架的旅游資源網(wǎng)站”那你就不要在畢設(shè)里私自換成Spring Boot除非指導(dǎo)老師明確表示可以調(diào)整。否則答辯時(shí)會(huì)面臨“你的題目和實(shí)現(xiàn)不一致”的尷尬局面。從技術(shù)本身來說SSM不是過時(shí)的東西。Spring管對(duì)象和事務(wù)SpringMVC管請(qǐng)求分發(fā)MyBatis管數(shù)據(jù)庫(kù)操作這三者配合能讓一個(gè)本科生把“分層思想”講得很清楚。相比Spring Boot那種“自動(dòng)配置一把梭”的用法SSM反而更能逼迫你去理解Bean、IoC和過濾器這些基礎(chǔ)概念。前端這塊配Vue是加分項(xiàng)。老式畢設(shè)喜歡用JSPjQuery做出來的頁面一眼就回到2015年。用Vue做單頁應(yīng)用配上Element UI或Element Plus頁面的觀感會(huì)明顯好于傳統(tǒng)模板渲染。旅游資源這種偏展示型的網(wǎng)站前端體驗(yàn)恰恰是用戶第一眼關(guān)注的東西所以Vue在這里不是炫技而是解決實(shí)際問題。1.2 業(yè)務(wù)功能邊界把“資源展示”變成可驗(yàn)收項(xiàng)目很多同學(xué)拿到“旅游資源網(wǎng)站”這個(gè)題目后第一反應(yīng)是拼命加功能支付、搶票、線路規(guī)劃、智能推薦……我見過一位同學(xué)想做一個(gè)完整版“在線旅游電商平臺(tái)”結(jié)果寫了三個(gè)月連核心流程都沒跑通最后只能熬夜砍功能。正確的思路是先把項(xiàng)目定性成“資源展示輕交互”。游客可以瀏覽景點(diǎn)和旅游線路注冊(cè)用戶可以收藏景點(diǎn)、發(fā)表評(píng)論管理員可以在后臺(tái)管理景點(diǎn)、線路、公告、用戶和評(píng)論。這個(gè)范圍的拿捏是有講究的有完整的用戶體系注冊(cè)、登錄、權(quán)限區(qū)分這是每一個(gè)答辯老師都會(huì)關(guān)注的點(diǎn)。有核心業(yè)務(wù)實(shí)體景點(diǎ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)做出來又足夠支撐一篇結(jié)構(gòu)完整的論文。支付、地圖、推薦這些可以寫進(jìn)“未來展望”但不要真花時(shí)間去開發(fā)。角色劃分也很清晰我用下表把它結(jié)構(gòu)化角色能做什么對(duì)應(yīng)主體游客瀏覽景點(diǎn)、線路、公告搜索景點(diǎn)前臺(tái)公開頁面注冊(cè)用戶游客功能 登錄、收藏、評(píng)論、個(gè)人中心前臺(tái)登錄區(qū)管理員用戶管理、景點(diǎn)管理、線路管理、評(píng)論管理、公告管理后臺(tái)管理端1.3 程序與論文的配合節(jié)奏我后來帶項(xiàng)目時(shí)一直強(qiáng)調(diào)一件事不要等程序全部寫完再開始寫論文也不要先把論文編出來再補(bǔ)程序。正確的節(jié)奏是用論文大綱反推程序模塊用程序截圖填充論文。具體操作上我建議先花兩天列出論文的章節(jié)框架摘要寫什么、需求分析分哪幾節(jié)、數(shù)據(jù)庫(kù)表畫哪些、系統(tǒng)實(shí)現(xiàn)要哪些截圖、測(cè)試寫哪些用例。然后按照這個(gè)框架去開發(fā)程序每完成一個(gè)模塊馬上截圖并寫好功能說明。比如你做完了景點(diǎn)管理就立刻把列表頁截圖、新增頁截圖、修改頁截圖存到一個(gè)“論文素材”文件夾里順手寫下這段功能是怎么實(shí)現(xiàn)的。這樣到論文階段你不需要回憶“當(dāng)時(shí)那個(gè)功能是怎么做的”素材都在手邊寫作速度會(huì)快很多而且截圖和程序永遠(yuǎn)對(duì)得上。很多同學(xué)最后要花大量時(shí)間返工就是因?yàn)槌绦蚪缑娓陌婧笳撐睦锏呐f截圖和新代碼對(duì)不上答辯時(shí)被老師一眼看出來。2. 核心細(xì)節(jié)解析與實(shí)操要點(diǎn)2.1 數(shù)據(jù)表設(shè)計(jì)少踩“字段拍腦袋”的坑旅游資源網(wǎng)站的數(shù)據(jù)表不算復(fù)雜但設(shè)計(jì)得規(guī)不規(guī)范答辯老師一眼就能看出來。我通常建議最少建六張表用戶表id、用戶名、密碼、昵稱、手機(jī)號(hào)、頭像、角色、注冊(cè)時(shí)間。景點(diǎn)表id、景點(diǎn)名稱、所屬地區(qū)、封面圖片、簡(jiǎn)介、詳細(xì)內(nèi)容、門票價(jià)格、開放時(shí)間、創(chuàng)建時(shí)間、狀態(tài)。線路表id、線路名稱、出發(fā)地、目的地、天數(shù)、價(jià)格、線路圖片、詳情、創(chuàng)建時(shí)間。評(píng)論表id、用戶id、景點(diǎn)id、評(píng)論內(nèi)容、評(píng)論時(shí)間。收藏表id、用戶id、景點(diǎn)id、創(chuàng)建時(shí)間。公告表id、標(biāo)題、內(nèi)容、發(fā)布時(shí)間。這套表里有一個(gè)比較常見的坑圖片字段。景點(diǎn)封面圖很可能不止一張但畢設(shè)項(xiàng)目沒必要做獨(dú)立圖片表。最省事也最穩(wěn)妥的做法是直接在景點(diǎn)表里用一個(gè)字符串字段比如cover_image存圖片的相對(duì)路徑或完整URL。前端拿到這個(gè)字符串直接渲染后面想要多圖用逗號(hào)分隔字符也足夠應(yīng)付。另一個(gè)容易出問題的地方是價(jià)格字段。做旅游類項(xiàng)目時(shí)門票價(jià)格、線路價(jià)格一定用decimal類型不能圖省事用float或double。很多同學(xué)在真實(shí)項(xiàng)目中吃過浮點(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)用層不用寫一堆判斷邏輯。我當(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寫成萬能類接手過幾個(gè)學(xué)生的半成品項(xiàng)目發(fā)現(xiàn)最典型的問題就是Controller里塞滿了業(yè)務(wù)代碼查數(shù)據(jù)庫(kù)、拼數(shù)據(jù)、寫返回全塞在一個(gè)方法里。比如這樣的代碼Controller public class ScenicController { Autowired private JdbcTemplate jdbcTemplate; RequestMapping(/list) ResponseBody public ListMapString, Object list(String keyword) { // 直接在這里寫SQL語句拼條件 String sql select * from t_scenic where name like %keyword%; return jdbcTemplate.queryForList(sql); } }這種寫法功能倒是能做出來但答辯時(shí)只要老師追問一句“你的項(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)一的返回類型。比如定義Result類包含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é)查詢景點(diǎn)列表時(shí)全文搜索和分頁要一起做。最簡(jiǎn)單的實(shí)現(xiàn)是用PageHelper分頁插件在Service層查詢之前調(diào)用PageHelper.startPage(pageNum, pageSize)緊接著執(zhí)行的Mapper查詢就會(huì)被自動(dòng)拼接limit。這個(gè)方法用起來很爽但一定要注意startPage和查詢之間不能夾其他數(shù)據(jù)庫(kù)操作否則分頁會(huì)失效。這個(gè)坑我在第4節(jié)還會(huì)專門講。2.3 Vue前端的狀態(tài)與路由設(shè)計(jì)前端部分是整個(gè)項(xiàng)目最容易出彩、也最容易翻車的地方。如果你是從零開始又暫時(shí)沒有明確要求Vue版本我建議直接用Vue3 Element Plus因?yàn)檫@是現(xiàn)在主流的組合資料多遇到問題也好搜。當(dāng)然如果學(xué)校模板指定的是Vue2 Element UI那也沒必要硬改功能上差異不大。頁面結(jié)構(gòu)上我習(xí)慣把前臺(tái)和后臺(tái)分成兩套路由區(qū)域前臺(tái) / 首頁 /scenic/list 景點(diǎn)列表 /scenic/detail/:id 景點(diǎn)詳情 /route/list 旅游線路 /notice/list 公告列表 /login 登錄 /register 注冊(cè) /user/center 個(gè)人中心 后臺(tái) /admin/dashboard 數(shù)據(jù)概況 /admin/scenic 景點(diǎn)管理 /admin/route 線路管理 /admin/user 用戶管理 /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è)頁面調(diào)接口都只需要傳URL和參數(shù)不用重復(fù)處理錯(cuò)誤提示。第二是路由守衛(wèi)。在router/index.js里給后臺(tái)路由加上meta: { requiresAuth: true }然后在全局前置守衛(wèi)里判斷如果用戶信息里沒有管理員標(biāo)記就跳回登錄頁。游客可以訪問的頁面則不需要守衛(wèi)。第三是狀態(tài)管理。Vue3項(xiàng)目用PiniaVue2項(xiàng)目用Vuex把登錄用戶的昵稱、角色、頭像存到store里同時(shí)同步到localStorage刷新頁面后從localStorage恢復(fù)。很多同學(xué)登錄后一刷新就回到未登錄狀態(tài)就是因?yàn)橹话褦?shù)據(jù)存在內(nèi)存里沒有做本地持久化。前臺(tái)頁面我建議關(guān)注一個(gè)體驗(yàn)點(diǎn)景點(diǎn)列表的卡片布局。每個(gè)景點(diǎn)卡展示封面圖、名稱、地區(qū)、價(jià)格和一個(gè)“去看看”按鈕圖片高度保持一致價(jià)格用醒目的顏色標(biāo)出來整體觀感會(huì)比純表格列表好得多。這種頁面細(xì)節(jié)花不了太多時(shí)間但對(duì)答辯印象分提升非常明顯。3. 實(shí)操過程與核心環(huán)節(jié)實(shí)現(xiàn)3.1 環(huán)境準(zhǔn)備與項(xiàng)目骨架搭建先把開發(fā)環(huán)境說清楚推薦組合是JDK 1.8或11MySQL 5.7或8.0Maven 3.6Node.js 16開發(fā)工具IDEA VS Code工作目錄里建議創(chuàng)建前后端兩個(gè)文件夾比如tourism-backend和tourism-frontend長(zhǎng)期維護(hù)起來一眼就懂。后端用Maven管理依賴核心依賴需要這些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 !-- 分頁插件 -- dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper/artifactId version5.3.x/version /dependency /dependencies這里的版本號(hào)我個(gè)人建議用穩(wěn)定版本不要一上來就追最新Spring 5.x和MyBatis 3.5.x這種組合已經(jīng)很多項(xiàng)目驗(yàn)證過了能避免因?yà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)聽器和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)目無所謂開發(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 用戶登錄、角色權(quán)限與攔截器登錄接口是畢設(shè)項(xiàng)目的“交通樞紐”幾乎所有頁面交互都跟用戶身份掛鉤。我建議登錄做成這樣用戶提交用戶名和密碼Controller接收后調(diào)用ServiceService根據(jù)用戶名查出用戶記錄比對(duì)密碼。密碼在數(shù)據(jù)庫(kù)里不要存明文注冊(cè)時(shí)用MD5或BCrypt加密一次登錄時(shí)把用戶輸入的密碼做同樣的加密再比對(duì)。如果一致生成一個(gè)代表登錄狀態(tài)的token返回給前端。token的實(shí)現(xiàn)有兩種檔次。最簡(jiǎn)單的是Session方案登錄成功把用戶對(duì)象放到session里后端攔截器判斷session里有沒有用戶。這種方案代碼少答辯也能講清楚但前后端分離部署時(shí)跨域和Session失效問題比較煩人。我推薦用JWT風(fēng)格的token不一定要引入完整jwt庫(kù)模擬一個(gè)也可以。登錄成功后后端把用戶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判斷是否有效、是否過期 // 解析出的userId放入request屬性方便Controller使用 return true; } }這個(gè)攔截器要注冊(cè)到SpringMVC中同時(shí)配置放行路徑比如/api/user/login、/api/user/register、/api/scenic/**這些公開接口不需要token而/api/user/center/**、/api/favorite/**則需要登錄。管理員接口比如/api/admin/**還要額外校驗(yàn)用戶角色是否為1。前端路由守衛(wè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)列表頁是訪問量最高的頁面它的核心是“條件查詢分頁”。哪些條件適合用來篩選最常用的是景點(diǎn)名稱關(guān)鍵字、所屬地區(qū)、價(jià)格范圍。對(duì)應(yīng)的MyBatis XML我一般這么寫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)就能拿到分頁數(shù)據(jù)。景點(diǎn)詳情頁除了顯示景點(diǎn)信息還要顯示評(píng)論列表。評(píng)論接口按景點(diǎn)id分頁查詢每條評(píng)論要帶上發(fā)表人的昵稱和頭像所以SQL要關(guān)聯(lián)用戶表。這里用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收藏模塊是“用戶與景點(diǎn)”關(guān)系的核心實(shí)現(xiàn)。接口設(shè)計(jì)就兩個(gè)一個(gè)添加收藏一個(gè)取消收藏。添加時(shí)先查詢是否已收藏如果已收藏就返回提示如果沒有就插入一條記錄。取消收藏直接按userId和scenicId刪除記錄。個(gè)人中心里查詢當(dāng)前用戶的收藏列表時(shí)關(guān)聯(lián)景點(diǎn)表把景點(diǎn)的基本信息和封面圖帶出來方便做一個(gè)“我收藏的景點(diǎn)”卡片列表。后臺(tái)管理模塊基本都是增刪改查這里不展開每個(gè)接口但有一個(gè)思路值得分享后臺(tái)列表頁沿用前臺(tái)的分頁組件每個(gè)管理頁用Element Plus的el-table el-pagination el-dialog組合。彈窗里放表單提交時(shí)調(diào)用新增或更新接口成功后刷新表格。這套模式寫熟手之后一張后臺(tái)頁面大概一晚上就能做完六張管理頁面也花不了太久。3.4 論文撰寫的章節(jié)組織與查重應(yīng)對(duì)程序做完后論文才是重頭戲。SSMVue旅游資源網(wǎng)站的論文目錄基本是固定的套路我建議按這六章走第一章緒論。寫選題背景和研究意義重點(diǎn)說明“為什么需要旅游資源網(wǎng)站為什么要用JavaVue技術(shù)?!弊詈蠓乓粡垏?guó)內(nèi)外研究現(xiàn)狀的綜述。第二章需求分析。先寫業(yè)務(wù)流程再寫功能需求用用例圖展示游客、用戶、管理員各自能做什么。非功能性需求要提性能、易用性、安全性。這一章對(duì)應(yīng)程序里的“功能邊界”寫得越具體后面的系統(tǒng)設(shè)計(jì)越好寫。第三章系統(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è)寫每個(gè)模塊放核心代碼片段和運(yùn)行截圖。注意代碼不用貼全部只貼關(guān)鍵方法并配一段文字說明“這是做什么的為什么這么做”。第五章系統(tǒng)測(cè)試。寫測(cè)試環(huán)境、測(cè)試方法然后列測(cè)試用例表測(cè)試編號(hào)、測(cè)試項(xiàng)、輸入數(shù)據(jù)、預(yù)期結(jié)果、實(shí)際結(jié)果。別全寫“通過”針對(duì)邊界條件寫一兩個(gè)失敗用例反而顯得真實(shí)。第六章總結(jié)與展望??偨Y(jié)做了一個(gè)什么系統(tǒng)、用了什么技術(shù)、有什么不足后續(xù)可以增加什么功能。關(guān)于查重我必須提醒一句參考文獻(xiàn)里的概念介紹千萬不要大段復(fù)制教科書原文。寫技術(shù)介紹時(shí)用自己的話把“Spring是什么、MyBatis是什么”重新表述一遍。尤其要小心網(wǎng)上的博客模板很多畢業(yè)論文模板章節(jié)相似度極高直接照抄會(huì)被查重系統(tǒng)標(biāo)紅。正確做法是定義概念后立刻結(jié)合自己項(xiàng)目里的具體場(chǎng)景說一句比如“在本系統(tǒng)中Spring負(fù)責(zé)管理Service和Mapper對(duì)象的創(chuàng)建和依賴注入”這樣既不容易重復(fù)又能體現(xiàn)你對(duì)技術(shù)的掌握。4. 常見問題與排查技巧實(shí)錄4.1 跨域報(bào)錯(cuò)與預(yù)檢請(qǐng)求處理前后端分離最常遇到的就是跨域問題?,F(xiàn)象很明確前端控制臺(tái)報(bào)No Access-Control-Allow-Origin header is present on the requested resource。原因簡(jiǎn)單說就是前端跑在http://localhost:8080后端跑在http://localhost:8088瀏覽器認(rèn)為這不是同一個(gè)源。解決辦法一般是在后端加一個(gè)跨域過濾器。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)求都攔下來要求登錄跨域就永遠(yuǎn)過不了。所以在攔截器的preHandle里一定要放行OPTIONS請(qǐng)求我前面的代碼示例里已經(jīng)寫到了這一步。如果你不想在后端處理跨域也可以在前端利用開發(fā)服務(wù)器做代理。在Vue項(xiàng)目的vue.config.js里配置module.exports { devServer: { proxy: { /api: { target: http://localhost:8088, changeOrigin: true } } } }這樣所有/api開頭的請(qǐng)求都轉(zhuǎn)發(fā)到后端瀏覽器看到的是同源地址。但這個(gè)方法只對(duì)開發(fā)環(huán)境有效正式打包部署后還是要用Nginx或其他方式統(tǒng)一入口。4.2 登錄狀態(tài)反復(fù)失效的兩種解決辦法這是我見過學(xué)生踩得最多的坑之一表現(xiàn)形式是登錄成功后頁面跳轉(zhuǎn)正常但刷新一下或者點(diǎn)幾個(gè)頁面后就提示“未登錄”。如果你用的是Session方案多半問題出在跨域配置沒有帶上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方案能避開上面這一整攤事。登錄接口返回一個(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)證通過就把用戶信息放到request屬性里后面的Controller直接取。這套方案不受Cookie跨域限制部署到Nginx時(shí)也好處理。token還有一個(gè)好處是答辯時(shí)容易講。你可以說“因?yàn)橄到y(tǒng)采用無狀態(tài)的會(huì)話保持方案服務(wù)器不需要保存用戶的登錄狀態(tài)減輕了內(nèi)存壓力”這句話翻譯成大白話就是“每個(gè)請(qǐng)求都帶著自己的身份證明來過檢”。4.3 MyBatis常見報(bào)錯(cuò)與駝峰映射寫MyBatis遇到報(bào)錯(cuò)是常態(tài)我把高頻問題整理成一張速查表報(bào)錯(cuò)信息本質(zhì)原因解決辦法Invalid bound statement (not found)Mapper接口和XML文件沒有對(duì)應(yīng)上檢查namespace是否等于接口全限定名以及mybatis配置里mapper-locations是否指向xml目錄BindingException: Parameter xxx not found接口方法參數(shù)沒加Param多參數(shù)時(shí)必須寫Param(name)查詢結(jié)果全是null列名和實(shí)體屬性名不一致開啟mapUnderscoreToCamelCase或?qū)憆esultMapCause: java.sql.SQLSyntaxErrorExceptionSQL語句語法錯(cuò)誤檢查表名、列名特別是MySQL關(guān)鍵字例如表名不要用order、descPageHelper分頁不生效startPage和查詢之間隔了其他數(shù)據(jù)庫(kù)操作讓startPage緊貼Mapper查詢執(zhí)行駝峰命名這個(gè)問題在SSM項(xiàng)目里特別容易反復(fù)出現(xiàn)。Java實(shí)體類習(xí)慣用createTime數(shù)據(jù)庫(kù)列名習(xí)慣用create_time如果不在全局配置開啟駝峰映射查出來的createTime就是null。配置方式是在mybatis-config.xml里settings setting namemapUnderscoreToCamelCase valuetrue/ /settings注意只開啟駝峰映射還不夠數(shù)據(jù)庫(kù)列名必須真的是create_time這種下劃線格式如果開發(fā)時(shí)有人圖省事把列名直接命名為create_time但實(shí)體類是create_time那映射出來又會(huì)錯(cuò)所以命名規(guī)范要從建表開始就統(tǒng)一。我個(gè)人的習(xí)慣是數(shù)據(jù)庫(kù)全部用lower_case_with_underscoreJava實(shí)體全部用駝峰中間靠MyBatis的映射自動(dòng)轉(zhuǎn)換。4.4 答辯前最后三天的檢查清單最后說點(diǎn)跟答辯直接相關(guān)的實(shí)操經(jīng)驗(yàn)。答辯內(nèi)容、截圖、提問環(huán)節(jié)都有對(duì)應(yīng)的準(zhǔn)備技巧建議最后三天按下面的清單過一遍檢查項(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腳本或者至少在開發(fā)工具里準(zhǔn)備好一鍵重建數(shù)據(jù)庫(kù)的命令代碼結(jié)構(gòu)可講隨便挑一個(gè)Controller你能不看IDE說出它的Service和Mapper分別做了什么嗎論文與截圖一致論文里出現(xiàn)的每一個(gè)頁面截圖程序里都必須能找到對(duì)應(yīng)頁面且數(shù)據(jù)最好相同演示環(huán)境穩(wěn)定如果現(xiàn)場(chǎng)沒有網(wǎng)絡(luò)數(shù)據(jù)庫(kù)和依賴要提前裝好如果用IDEA啟動(dòng)提前把依賴下載完整避免現(xiàn)場(chǎng)卡在下載還有一個(gè)小技巧答辯演示時(shí)建議用授課千萬注意不要念代碼。講登錄功能時(shí)就說“用戶輸入用戶名密碼后端校驗(yàn)成功后返回token前端保存到本地之后請(qǐng)求自動(dòng)攜帶實(shí)現(xiàn)會(huì)話保持”然后用演示操作補(bǔ)充細(xì)節(jié)老師看到的是“論述清晰系統(tǒng)可用”這兩點(diǎ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緩存熱門景點(diǎn)數(shù)據(jù)或者把后臺(tái)改造成帶統(tǒng)計(jì)圖表的Dashboard就是錦上添花。最后再分享一個(gè)小技巧每天開發(fā)結(jié)束后花十分鐘在本地Gitee或GitHub倉(cāng)庫(kù)提交一次代碼。別嫌麻煩那些最后做不下去、程序跑不起來、論文寫不出來的同學(xué)大多數(shù)不是能力問題而是改著改著把自己改亂了沒有可以回退的干凈版本。用好了版本控制你的畢設(shè)進(jìn)度就會(huì)穩(wěn)很多。