批閱系統(tǒng)設(shè)計(jì)與大屏可視化實(shí)戰(zhàn)解析)
每年到畢業(yè)季Java Web方向的畢設(shè)選題總有那么幾個(gè)“常青樹(shù)”作業(yè)批閱系統(tǒng)就是其中之一。市面上這類(lèi)系統(tǒng)的源碼確實(shí)不少標(biāo)題里動(dòng)不動(dòng)就是“免費(fèi)領(lǐng)源碼”、“上萬(wàn)套實(shí)戰(zhàn)教程”但真拿到手能順利跑起來(lái)、講清楚、答得上答辯老師提問(wèn)的其實(shí)沒(méi)幾個(gè)。我前前后后幫人調(diào)試過(guò)好幾套這類(lèi)項(xiàng)目也自己完整從零寫(xiě)過(guò)一版今天就把這套基于Web的作業(yè)批閱系統(tǒng)的拆解、設(shè)計(jì)和實(shí)操經(jīng)驗(yàn)一次性說(shuō)清楚重點(diǎn)聊聊大屏數(shù)據(jù)可視化這個(gè)加分項(xiàng)是怎么落地的以及那些源碼里不會(huì)告訴你的坑。這篇文章適合正在做畢設(shè)、或者想快速上手一個(gè)完整Web項(xiàng)目的同學(xué)參考內(nèi)容偏實(shí)戰(zhàn)爭(zhēng)取讓你看完就能動(dòng)手。1. 項(xiàng)目整體設(shè)計(jì)與思路拆解1.1 為什么作業(yè)批閱系統(tǒng)適合做畢設(shè)很多人選畢設(shè)題目時(shí)會(huì)糾結(jié)覺(jué)得作業(yè)批閱系統(tǒng)“太普通”、“沒(méi)技術(shù)含量”。但我的看法恰恰相反——這類(lèi)系統(tǒng)是最適合用來(lái)拿高分的選題之一。原因很簡(jiǎn)單它的業(yè)務(wù)鏈路足夠完整從用戶(hù)登錄、角色權(quán)限到作業(yè)發(fā)布、文件上傳、在線(xiàn)批閱、成績(jī)統(tǒng)計(jì)再到數(shù)據(jù)大屏展示幾乎覆蓋了一個(gè)真實(shí)Web項(xiàng)目的所有核心環(huán)節(jié)。對(duì)于計(jì)算機(jī)專(zhuān)業(yè)的畢業(yè)生來(lái)說(shuō)這套鏈路剛好能把你學(xué)過(guò)的Java基礎(chǔ)、數(shù)據(jù)庫(kù)、前端、甚至可視化知識(shí)全部串起來(lái)答辯時(shí)也容易講出東西。另外從老師打分角度來(lái)說(shuō)一個(gè)有完整業(yè)務(wù)閉環(huán)、界面干凈、還能跑通數(shù)據(jù)大屏的系統(tǒng)已經(jīng)超出了“普通CRUD”的預(yù)期。大多數(shù)畢業(yè)設(shè)計(jì)的分?jǐn)?shù)差距恰恰就體現(xiàn)在這些“超出預(yù)期”的地方。1.2 技術(shù)選型為什么主流方案是Java Spring Boot MySQL市面上的作業(yè)批閱系統(tǒng)源碼絕大多數(shù)是Java技術(shù)棧這背后是有原因的。Java Web經(jīng)過(guò)這么多年的發(fā)展生態(tài)成熟度確實(shí)最高。Spring Boot框架讓配置量大幅下降一個(gè)業(yè)務(wù)系統(tǒng)從零搭起來(lái)只需要幾分鐘。而且高校里 Java 課程覆蓋面廣答辯時(shí)老師不管用不用 Java都能看懂你的代碼邏輯溝通成本低。具體到我這套系統(tǒng)的技術(shù)棧選型如下后端Spring Boot 2.x MyBatis Plus數(shù)據(jù)庫(kù)用MySQL 5.7或者8.0都可以前端Vue 2 Element UI管理后臺(tái)用現(xiàn)成模板大屏部分用 ECharts權(quán)限認(rèn)證JWTJSON Web Token做無(wú)狀態(tài)登錄比傳統(tǒng)的Session方案更適合前后端分離文件存儲(chǔ)本地磁盤(pán)存儲(chǔ)數(shù)據(jù)庫(kù)只記錄文件路徑元數(shù)據(jù)這套組合的好處是每一樣都是主流且經(jīng)過(guò)大量項(xiàng)目驗(yàn)證的出了問(wèn)題網(wǎng)上一搜就有解決方案。更重要的是這套技術(shù)棧學(xué)習(xí)資料極其豐富對(duì)于時(shí)間緊迫的畢設(shè)黨來(lái)說(shuō)是保命的選擇。有些同學(xué)可能會(huì)糾結(jié)要不要用微服務(wù)、要不要上Redis緩存——我的建議是除非你基礎(chǔ)非常好否則不要給自己挖坑。畢設(shè)項(xiàng)目的評(píng)分核心是“完整可用 邏輯清晰”而不是技術(shù)堆得多高。1.3 功能模塊劃分一個(gè)完整的作業(yè)閉環(huán)需要哪些東西一個(gè)能正常交差的作業(yè)批閱系統(tǒng)至少需要三個(gè)角色學(xué)生、教師、管理員。圍繞這三個(gè)角色功能點(diǎn)可以拆成下面這張表來(lái)說(shuō)明角色核心功能說(shuō)明學(xué)生查看作業(yè)、提交作業(yè)、查看批閱結(jié)果支持作業(yè)附件上傳提交后能看到教師評(píng)語(yǔ)和分?jǐn)?shù)教師發(fā)布作業(yè)、批閱作業(yè)、導(dǎo)出成績(jī)、查看統(tǒng)計(jì)支持在線(xiàn)打分和評(píng)語(yǔ)可按班級(jí)篩選可導(dǎo)出Excel管理員用戶(hù)管理、班級(jí)管理、課程管理、系統(tǒng)監(jiān)控維護(hù)基礎(chǔ)數(shù)據(jù)查看全局大屏數(shù)據(jù)這個(gè)功能拆解的邏輯是學(xué)生和教師是業(yè)務(wù)主體管理員負(fù)責(zé)兜底數(shù)據(jù)維護(hù)。很多源碼會(huì)把管理員功能做得很單薄只有登錄和用戶(hù)列表這種系統(tǒng)在答辯時(shí)很容易被問(wèn)住——老師會(huì)追問(wèn)“你怎么管理基礎(chǔ)數(shù)據(jù)沒(méi)有管理員班級(jí)和課程信息從哪來(lái)”所以哪怕代碼量多一點(diǎn)也建議把管理員的角色補(bǔ)完整。大屏監(jiān)控模塊是我覺(jué)得整套系統(tǒng)最出彩的地方。它不參與具體的業(yè)務(wù)操作而是把作業(yè)提交率、批閱進(jìn)度、成績(jī)分布等數(shù)據(jù)用圖表形式展示出來(lái)適合投在教室大屏或答辯展示環(huán)節(jié)。很多同學(xué)會(huì)忽略這個(gè)模塊但它恰恰是你和同組同學(xué)拉開(kāi)差距的關(guān)鍵。2. 數(shù)據(jù)庫(kù)設(shè)計(jì)與核心流程實(shí)現(xiàn)2.1 數(shù)據(jù)表結(jié)構(gòu)設(shè)計(jì)五張核心表與它們的關(guān)聯(lián)關(guān)系寫(xiě)Web系統(tǒng)第一步永遠(yuǎn)是設(shè)計(jì)數(shù)據(jù)庫(kù)。好的表結(jié)構(gòu)是系統(tǒng)健壯運(yùn)行的基礎(chǔ)而作業(yè)批閱系統(tǒng)核心表其實(shí)并不多關(guān)鍵是理清它們之間的關(guān)聯(lián)。我設(shè)計(jì)的核心表包括用戶(hù)表(user)、班級(jí)表(class)、課程表(course)、作業(yè)表(homework)、提交記錄表(submission)。其中比較難處理的是用戶(hù)表與角色之間的關(guān)系——我采用的方案是用戶(hù)表里直接加一個(gè)role字段用整數(shù)區(qū)分學(xué)生和教師避免引入額外的關(guān)聯(lián)表。作業(yè)表homework的字段設(shè)計(jì)很關(guān)鍵我把一些容易忽略但實(shí)際非常有用的字段列出來(lái)id主鍵自增title作業(yè)標(biāo)題content作業(yè)要求描述course_id關(guān)聯(lián)的課程teacher_id發(fā)布教師deadline截止時(shí)間status作業(yè)狀態(tài)0為草稿1為已發(fā)布提交記錄表submission是整個(gè)系統(tǒng)的核心。它的字段決定了下游所有統(tǒng)計(jì)能否順暢實(shí)現(xiàn)homework_id關(guān)聯(lián)作業(yè)student_id提交學(xué)生file_url附件存儲(chǔ)路徑score分?jǐn)?shù)NULL表示未批閱comment教師評(píng)語(yǔ)submit_time提交時(shí)間status提交狀態(tài)用來(lái)標(biāo)識(shí)是否在截止日期前提交特別要注意的是submission表里score字段設(shè)計(jì)成可空而不是默認(rèn)0——這樣在統(tǒng)計(jì)“待批閱數(shù)量”時(shí)直接查score is null就能篩出來(lái)不用額外維護(hù)狀態(tài)字段。這是一個(gè)很小的設(shè)計(jì)細(xì)節(jié)但寫(xiě)多了查詢(xún)語(yǔ)句的人應(yīng)該能體會(huì)它的好處。2.2 核心流程一從發(fā)布作業(yè)到學(xué)生提交的完整鏈路作業(yè)批閱系統(tǒng)的核心其實(shí)就是一整個(gè)業(yè)務(wù)流轉(zhuǎn)過(guò)程。拆開(kāi)來(lái)看它由下面幾個(gè)環(huán)節(jié)組成。教師端先創(chuàng)建作業(yè)填標(biāo)題、寫(xiě)要求、設(shè)置截止時(shí)間關(guān)聯(lián)到指定課程保存后作業(yè)狀態(tài)為草稿學(xué)生還不可見(jiàn)。教師確認(rèn)無(wú)誤后點(diǎn)擊發(fā)布狀態(tài)變?yōu)橐寻l(fā)布。這個(gè)設(shè)計(jì)的好處是教師可以提前編輯作業(yè)信息不用一上來(lái)就得填完整。學(xué)生端登錄后首頁(yè)能看到自己課程下的所有已發(fā)布作業(yè)未提交的顯示“去提交”已提交的顯示“已提交”。學(xué)生上傳作業(yè)文件后系統(tǒng)把摘要信息寫(xiě)入submission表這時(shí)狀態(tài)為已提交但未批閱學(xué)生端顯示“待批閱”。教師端查看到待批閱列表逐個(gè)打分、寫(xiě)評(píng)語(yǔ)保存后score字段被賦值系統(tǒng)狀態(tài)變?yōu)橐雅?。學(xué)生端刷新后就能看到自己的分?jǐn)?shù)和評(píng)語(yǔ)。如果超過(guò)截止時(shí)間仍未提交系統(tǒng)在作業(yè)列表和統(tǒng)計(jì)中將其標(biāo)記為“未提交”。這條鏈路看起來(lái)簡(jiǎn)單但實(shí)現(xiàn)時(shí)有兩個(gè)點(diǎn)比較容易出錯(cuò)。一個(gè)是文件上傳——很多源碼用MultipartFile接收后直接存數(shù)據(jù)庫(kù)BLOB字段這種做法在小規(guī)模演示時(shí)沒(méi)問(wèn)題但文件稍微大一點(diǎn)就會(huì)出現(xiàn)性能問(wèn)題。比較穩(wěn)妥的做法是存到服務(wù)器磁盤(pán)指定目錄數(shù)據(jù)庫(kù)只存文件的相對(duì)路徑取文件時(shí)用http://你的IP:端口/static/文件名的方式映射。另一個(gè)是截止時(shí)間判斷——不要在前端做死判斷后端接口里也必須要校驗(yàn)否則學(xué)生改一下本地時(shí)間就能繞過(guò)這在答辯演示時(shí)一旦被老師挖出來(lái)會(huì)很難看。2.3 核心流程二批閱打分的狀態(tài)機(jī)邏輯作業(yè)批閱模塊在技術(shù)上看起來(lái)很簡(jiǎn)單無(wú)非是更新一條記錄的score和comment字段。但用“狀態(tài)機(jī)”的角度去思考它的邏輯其實(shí)更優(yōu)雅也更健壯。我把一次作業(yè)提交的生命周期劃分為三個(gè)狀態(tài)SUBMITTED已提交待批閱、GRADED已批閱、OVERDUE逾期未交。初始創(chuàng)建時(shí)是SUBMITTED教師批閱后變?yōu)镚RADED超過(guò)截止時(shí)間且沒(méi)有提交記錄的則為OVERDUE。狀態(tài)流轉(zhuǎn)用代碼寫(xiě)死不允許從GRADED直接跳回SUBMITTED確保批閱數(shù)據(jù)的嚴(yán)肅性。實(shí)際操作時(shí)我在后端Service層單獨(dú)寫(xiě)了一個(gè)submitHomework方法和一個(gè)gradeHomework方法分別處理學(xué)生提交和教師批閱。gradeHomework里先判斷當(dāng)前提交記錄狀態(tài)是否為SUBMITTED不是就拋出業(yè)務(wù)異常前端捕獲后給出提示。這樣做的好處是即使前端頁(yè)面被繞過(guò)直接調(diào)接口也無(wú)法破壞狀態(tài)流轉(zhuǎn)的一致性。這個(gè)思路聽(tīng)起來(lái)有點(diǎn)學(xué)院派但寫(xiě)起來(lái)并不復(fù)雜而且答辯時(shí)你拋出“狀態(tài)機(jī)”這個(gè)概念老師會(huì)認(rèn)為你有軟件工程思維這是一個(gè)很容易拿到的印象分。2.4 大屏可視化模塊接口聚合與ECharts渲染大屏模塊算是我最想重點(diǎn)分享的部分因?yàn)檫@是本項(xiàng)目里“看起來(lái)最有技術(shù)含量”、實(shí)際寫(xiě)起來(lái)也不難的模塊性?xún)r(jià)比極高。大屏展示的數(shù)據(jù)主要來(lái)自三個(gè)統(tǒng)計(jì)維度作業(yè)提交統(tǒng)計(jì)總作業(yè)數(shù)、已提交數(shù)、未提交數(shù)、提交率批閱統(tǒng)計(jì)總需批閱數(shù)、已批閱數(shù)、待批閱數(shù)、批閱率成績(jī)分布各分?jǐn)?shù)段人數(shù)占比、平均分、最高分、最低分這些數(shù)據(jù)如果一條條按傳統(tǒng)接口去查前端要發(fā)好幾個(gè)請(qǐng)求而且大屏刷新時(shí)會(huì)閃動(dòng)。我的做法是寫(xiě)了一個(gè)獨(dú)立的DashboardController用一個(gè)大聚合接口把三類(lèi)統(tǒng)計(jì)數(shù)據(jù)一次性組裝成JSON返回前端拿到后一次性渲染。比如提交率就是先SELECT COUNT(*) FROM homework得到總作業(yè)數(shù)再SELECT COUNT(DISTINCT homework_id) FROM submission得到已提交作業(yè)數(shù)兩個(gè)數(shù)一除就得到百分比。性能上對(duì)于畢設(shè)級(jí)的數(shù)據(jù)量完全夠用不需要上復(fù)雜的緩存方案。前端大屏我用的方案是ECharts 柵格布局分成左上、右上、左下、右下四個(gè)區(qū)域中間放一個(gè)核心指標(biāo)數(shù)字區(qū)。用grid和flex布局讓它在16:9的屏幕上能自適應(yīng)鋪滿(mǎn)。配色方面選了深色背景亮色圖表這樣投影出來(lái)的效果最好。ECharts的折線(xiàn)圖展示提交量趨勢(shì)、餅圖展示成績(jī)分布、柱狀圖對(duì)比不同班級(jí)的作業(yè)提交率三張圖加上幾個(gè)核心數(shù)字卡片視覺(jué)效果就很專(zhuān)業(yè)了。有一個(gè)實(shí)際踩過(guò)的坑要提醒大家ECharts的容器在初始化時(shí)如果沒(méi)有設(shè)置寬高或者所在div一開(kāi)始是隱藏的圖表就渲染不出來(lái)。大屏頁(yè)面經(jīng)常有頁(yè)面初始化時(shí)異步獲取數(shù)據(jù)的過(guò)程如果此時(shí)容器尺寸還沒(méi)算好圖表就會(huì)變成空白。解決方案是在mounted鉤子里先this.$nextTick()再初始化圖表必要時(shí)監(jiān)聽(tīng)窗口resize事件調(diào)用chart.resize()。3. 實(shí)操過(guò)程與核心環(huán)節(jié)實(shí)現(xiàn)3.1 從源碼到本地跑起來(lái)的第一步環(huán)境準(zhǔn)備不少同學(xué)下載了源碼結(jié)果卡在第一步——跑不起來(lái)。這套系統(tǒng)的運(yùn)行環(huán)境其實(shí)非常標(biāo)準(zhǔn)但也正因如此很多默認(rèn)配置會(huì)導(dǎo)致“水土不服”。我建議的環(huán)境清單如下JDK 1.8注意Spring Boot 2.x和JDK 8的兼容性最好Maven 3.6以上用于依賴(lài)管理MySQL 5.7或8.0注意8.0的驅(qū)動(dòng)配置和5.7不同Node.js 14以上前端項(xiàng)目需要IDEA 2020以上版本社區(qū)版也夠用源碼拿到手先不要急著點(diǎn)啟動(dòng)。第一步要改的是數(shù)據(jù)庫(kù)連接配置。在application.yml或application.properties里把數(shù)據(jù)庫(kù)名、用戶(hù)名、密碼改成你自己的。這里最常見(jiàn)的坑是時(shí)區(qū)問(wèn)題——MySQL連接串建議加上serverTimezoneAsia/Shanghai和useUnicodetruecharacterEncodingutf8否則高版本MySQL會(huì)報(bào)時(shí)區(qū)錯(cuò)誤或者中文亂碼。如果拿到的是前后端分離的項(xiàng)目還要確認(rèn)前端請(qǐng)求后端接口的地址。一般前端項(xiàng)目里有一個(gè)request.js或api.js里面配了baseURL如果你只在本地跑改成http://localhost:8080即可。如果是部署在服務(wù)器上則需要改成服務(wù)器的公網(wǎng)IP加端口。這個(gè)配置不搞清楚前端頁(yè)面能打開(kāi)但所有數(shù)據(jù)請(qǐng)求都會(huì)404。3.2 核心接口實(shí)現(xiàn)提交作業(yè)與批閱打分的代碼示例大部分源碼的核心接口邏輯其實(shí)差不多真正拉開(kāi)代碼質(zhì)量差距的是細(xì)節(jié)處理。下面我給出提交作業(yè)和批閱打分兩個(gè)接口的核心代碼片段大家可以對(duì)著自己的源碼比對(duì)。學(xué)生提交作業(yè)的Controller層接口PostMapping(/api/student/submit) public Result submitHomework(RequestParam(homeworkId) Integer homeworkId, RequestParam(studentId) Integer studentId, RequestParam(value file, required false) MultipartFile file) { if (file null || file.isEmpty()) { return Result.error(請(qǐng)上傳作業(yè)文件); } // 校驗(yàn)截止時(shí)間 Homework homework homeworkService.getById(homeworkId); if (homework.getDeadline().before(new Date())) { return Result.error(已過(guò)截止時(shí)間無(wú)法提交); } // 保存文件到本地目錄 String fileName UUID.randomUUID().toString().replace(-, ) getExt(file.getOriginalFilename()); file.transferTo(new File(UPLOAD_DIR fileName)); // 寫(xiě)入提交記錄 Submission submission new Submission(); submission.setHomeworkId(homeworkId); submission.setStudentId(studentId); submission.setFileUrl(/files/ fileName); submission.setSubmitTime(new Date()); submission.setStatus(0); // 0-待批閱 submissionService.save(submission); return Result.success(提交成功, submission); }教師批閱的Service實(shí)現(xiàn)Transactional public void gradeSubmission(Integer submissionId, Integer score, String comment) { Submission submission submissionService.getById(submissionId); if (submission null) { throw new BusinessException(提交記錄不存在); } if (submission.getStatus() ! 0) { throw new BusinessException(該作業(yè)已被批閱不能重復(fù)操作); } if (score 0 || score 100) { throw new BusinessException(分?jǐn)?shù)必須在0到100之間); } submission.setScore(score); submission.setComment(comment); submission.setStatus(1); // 1-已批閱 submissionService.updateById(submission); }這兩段代碼看起來(lái)平淡無(wú)奇但包含了幾個(gè)“防御性編程”的點(diǎn)文件為空判斷、截止時(shí)間后端校驗(yàn)、文件重命名防覆蓋、分?jǐn)?shù)范圍校驗(yàn)、重復(fù)批閱攔截。這些細(xì)節(jié)在正常演示時(shí)看不到作用但一旦老師較真去測(cè)你就會(huì)發(fā)現(xiàn)它們能擋住幾乎所有常見(jiàn)的“不合理操作”。3.3 大屏數(shù)據(jù)聚合接口一個(gè)接口搞定所有統(tǒng)計(jì)指標(biāo)大屏模塊的重點(diǎn)在后端聚合接口的設(shè)計(jì)。我把所有統(tǒng)計(jì)邏輯收斂到一個(gè)Controller里返回統(tǒng)一結(jié)構(gòu)的JSON前端只負(fù)責(zé)渲染。GetMapping(/api/dashboard/statistics) public Result getDashboardStatistics() { MapString, Object data new HashMap(); // 作業(yè)總數(shù) data.put(totalHomework, homeworkService.count()); // 已提交人數(shù)去重 data.put(submittedCount, submissionService.countDistinctStudent()); // 待批閱數(shù)量 data.put(pendingCount, submissionService.countPending()); // 批閱完成率 long gradedCount submissionService.countGraded(); long totalSubmission submissionService.count(); data.put(gradeRate, totalSubmission 0 ? 0 : Math.round(gradedCount * 100.0 / totalSubmission)); // 成績(jī)分布 data.put(scoreDist, submissionService.getScoreDistribution()); return Result.success(data); }其中g(shù)etScoreDistribution的SQL可以用一行按分?jǐn)?shù)段統(tǒng)計(jì)的語(yǔ)句實(shí)現(xiàn)比如用CASE WHEN把0-59、60-69、70-79、80-89、90-100分成五個(gè)段位然后GROUP BY統(tǒng)計(jì)人數(shù)。這個(gè)SQL寫(xiě)法很經(jīng)典很多源碼里也會(huì)這么干但有的實(shí)現(xiàn)得比較繞——用五個(gè)獨(dú)立查詢(xún)分別去查每個(gè)分?jǐn)?shù)段的人數(shù)。相比之下用CASE WHEN一條SQL搞定既快又清晰而且答辯時(shí)這段代碼可以直接拿出來(lái)講。前端大屏核心初始化部分可以參考下面的思路mounted() { this.$nextTick(() { this.initChart(); this.fetchData(); }); }, methods: { initChart() { this.chart echarts.init(document.getElementById(scoreChart)); }, fetchData() { axios.get(/api/dashboard/statistics).then(res { const data res.data.data; this.scoreChart.setOption({ xAxis: { data: [0-59, 60-69, 70-79, 80-89, 90-100] }, yAxis: {}, series: [{ type: bar, data: data.scoreDist }] }); }); } }整個(gè)模塊的核心代碼量其實(shí)不大但它把后端查詢(xún)、接口設(shè)計(jì)、前端可視化完整串了起來(lái)屬于典型的“低成本高展示度”功能。3.4 部署與演示環(huán)節(jié)的準(zhǔn)備工作畢設(shè)答辯或者項(xiàng)目驗(yàn)收之前一定要做一次完整的演示彩排。我見(jiàn)過(guò)太多同學(xué)當(dāng)場(chǎng)出狀況基本都是下面這幾個(gè)原因。第一數(shù)據(jù)庫(kù)里沒(méi)有演示數(shù)據(jù)??諗?shù)據(jù)庫(kù)打開(kāi)大屏頁(yè)面全是0圖表空蕩蕩視覺(jué)效果大打折扣。建議提前往庫(kù)里插入至少30條學(xué)生數(shù)據(jù)、5個(gè)作業(yè)、每條作業(yè)有對(duì)應(yīng)的提交和批閱記錄。數(shù)據(jù)量不用大但要把“待批閱”“已批閱”“未提交”三種狀態(tài)都覆蓋到。第二端口沖突。Spring Boot默認(rèn)8080端口如果你本機(jī)其他服務(wù)占用了會(huì)啟動(dòng)失敗。建議在配置里顯式改成server.port: 8090或者一個(gè)不常用的端口然后前端baseURL同步修改。第三文件上傳目錄權(quán)限。如果用Linux服務(wù)器部署文件上傳路徑要確保應(yīng)用進(jìn)程有寫(xiě)權(quán)限否則上傳報(bào)錯(cuò)。我習(xí)慣把上傳目錄設(shè)為項(xiàng)目根目錄下的upload/文件夾并在啟動(dòng)前手動(dòng)創(chuàng)建好避免第一次上傳時(shí)因目錄不存在失敗。4. 常見(jiàn)問(wèn)題與排查技巧實(shí)錄4.1 啟動(dòng)失敗、頁(yè)面404、數(shù)據(jù)加載不出來(lái)我把調(diào)試這套系統(tǒng)時(shí)最常見(jiàn)的幾個(gè)問(wèn)題整理成表方便大家對(duì)照排查癥狀大概率原因排查方式Spring Boot啟動(dòng)失敗端口被占用 / MySQL未啟動(dòng)看啟動(dòng)日志最后一段報(bào)錯(cuò)改端口或啟動(dòng)MySQL前端頁(yè)面打開(kāi)但驗(yàn)證碼不顯示后端接口沒(méi)通 / 跨域問(wèn)題F12看Network請(qǐng)求是否報(bào)錯(cuò)檢查跨域配置登錄成功但列表數(shù)據(jù)為空數(shù)據(jù)庫(kù)表里沒(méi)數(shù)據(jù)先用Navicat連庫(kù)查表確認(rèn)數(shù)據(jù)存在上傳文件報(bào)錯(cuò)上傳目錄不存在 / 大小超限檢查application.yml里的上傳大小配置大屏圖表空白容器初始化為0 / 數(shù)據(jù)格式不對(duì)在nextTick里初始化打印接口返回?cái)?shù)據(jù)有一個(gè)很多源碼都會(huì)犯的經(jīng)典問(wèn)題就是數(shù)據(jù)庫(kù)初始化腳本里沒(méi)寫(xiě)SET NAMES utf8mb4導(dǎo)致建表時(shí)中文注釋或數(shù)據(jù)寫(xiě)入變亂碼。如果遇到中文亂碼問(wèn)題優(yōu)先檢查數(shù)據(jù)庫(kù)連接的字符集配置其次才是代碼層面。4.2 前端跨域問(wèn)題一次配置解決前后端聯(lián)調(diào)前后端分離項(xiàng)目遇到最多的攔路虎就是跨域也就是前端地址比如localhost:3000和后端地址比如localhost:8090不一致瀏覽器攔截了請(qǐng)求。最簡(jiǎn)單的解決方式是后端加一個(gè)CORS全局配置類(lèi)幾行代碼解決問(wèn)題Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOrigins(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .maxAge(3600); } }需要注意的是如果引入了Spring Security或者自定義攔截器做JWT校驗(yàn)預(yù)檢請(qǐng)求OPTIONS必須放行否則前端會(huì)提示“跨域請(qǐng)求失敗”。這個(gè)坑非常隱蔽報(bào)錯(cuò)信息又特別模糊很多人卡在這里半天排查不出來(lái)。我在寫(xiě)這個(gè)系統(tǒng)時(shí)遇到過(guò)一次最后是在攔截器里專(zhuān)門(mén)加了一個(gè)判斷如果請(qǐng)求方法是OPTIONS直接放行。這個(gè)細(xì)節(jié)值得記下來(lái)。4.3 文件上傳大小限制與路徑映射作業(yè)附件通常是PDF、Word或者壓縮包幾MB到幾十MB都有。Spring Boot默認(rèn)的上傳大小限制是1MB所以學(xué)生一上傳稍大一點(diǎn)的作業(yè)就會(huì)報(bào)錯(cuò)。解決的姿勢(shì)是在配置文件里把限制調(diào)大spring: servlet: multipart: max-file-size: 50MB max-request-size: 50MB同時(shí)還要配置靜態(tài)資源映射讓上傳的文件可以通過(guò)URL訪(fǎng)問(wèn)。如果你把文件存到了本地的upload/目錄需要這樣映射Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/files/**) .addResourceLocations(file: System.getProperty(user.dir) /upload/); } }這里最容易犯的錯(cuò)是路徑分隔符。Windows和Linux的差異會(huì)導(dǎo)致路徑拼接出錯(cuò)所以我習(xí)慣用System.getProperty(user.dir)獲取項(xiàng)目所在目錄再拼上相對(duì)路徑這樣跨平臺(tái)跑都不會(huì)出問(wèn)題。4.4 防重復(fù)提交與異常處理機(jī)制最后補(bǔ)充一個(gè)不太顯眼但很影響體驗(yàn)的問(wèn)題——重復(fù)提交。很多學(xué)生在頁(yè)面卡頓時(shí)會(huì)習(xí)慣性多點(diǎn)幾次提交按鈕如果沒(méi)有對(duì)應(yīng)的防重復(fù)機(jī)制數(shù)據(jù)庫(kù)里就會(huì)出現(xiàn)好幾條相同作業(yè)的提交記錄導(dǎo)致后續(xù)統(tǒng)計(jì)錯(cuò)亂。我的處理方案是雙保險(xiǎn)。前端的做法是在提交按鈕點(diǎn)擊后立刻禁用按鈕同時(shí)顯示“正在上傳…”的加載狀態(tài)后端的做法是在Submission表里給homework_id和student_id加上聯(lián)合唯一索引從數(shù)據(jù)庫(kù)層面保證同一個(gè)學(xué)生只能提交一次作業(yè)。如果學(xué)生想要重新提交應(yīng)該是更新已有的記錄而不是新插入一條。這個(gè)設(shè)計(jì)在數(shù)據(jù)完整性上非常重要也是代碼評(píng)審時(shí)老師比較關(guān)注的點(diǎn)。5. 從實(shí)用到答辯項(xiàng)目亮點(diǎn)打磨與經(jīng)驗(yàn)總結(jié)5.1 三個(gè)讓系統(tǒng)顯高級(jí)的加分設(shè)計(jì)一套能拿高分的畢設(shè)系統(tǒng)除了功能完備之外還要有一些讓人眼前一亮的“小心思”。我梳理了三個(gè)性?xún)r(jià)比極高的設(shè)計(jì)你可以對(duì)照自己的系統(tǒng)看看有沒(méi)有。第一個(gè)是操作日志。用一個(gè)簡(jiǎn)單的AOP切面或者自定義注解把關(guān)鍵操作發(fā)布作業(yè)、批閱打分、刪除用戶(hù)記錄下來(lái)。這個(gè)功能的代碼量不大但會(huì)讓系統(tǒng)的完整性上一個(gè)檔次。答辯時(shí)你可以說(shuō)“我們系統(tǒng)里通過(guò)操作日志模塊能追蹤教師的批閱操作為教學(xué)質(zhì)量檢查提供數(shù)據(jù)支持?!币痪湓?huà)就讓系統(tǒng)從一個(gè)工具變成了一個(gè)平臺(tái)。第二個(gè)是敏感詞過(guò)濾或者內(nèi)容校驗(yàn)。比如教師發(fā)布作業(yè)時(shí)對(duì)標(biāo)題和內(nèi)容做長(zhǎng)度校驗(yàn)和空值校驗(yàn)批閱評(píng)語(yǔ)限制字?jǐn)?shù)。功能雖小但體現(xiàn)了你對(duì)系統(tǒng)健壯性的思考。第三個(gè)是成績(jī)數(shù)據(jù)分析。除了大屏展示的總體統(tǒng)計(jì)之外可以給教師端增加一個(gè)“成績(jī)趨勢(shì)分析”頁(yè)面展示某門(mén)課程歷次作業(yè)的平均分變化曲線(xiàn)。這個(gè)功能在數(shù)據(jù)層面完全基于已有的submission表不需要改數(shù)據(jù)庫(kù)結(jié)構(gòu)但你把它做成一個(gè)獨(dú)立模塊就顯得系統(tǒng)不是模板拼湊而是有自己的業(yè)務(wù)洞察。5.2 代碼組織的經(jīng)驗(yàn)別讓畢設(shè)變成屎山很多同學(xué)拿到源碼后喜歡直接上手改需求改著改著發(fā)現(xiàn)代碼越改越亂。我分享一個(gè)深有體會(huì)的教訓(xùn)拿到一套源碼第一步不是改功能而是先跑通、再梳理、最后動(dòng)刀。跑通之后用IDEA的結(jié)構(gòu)面板快速瀏覽一遍包結(jié)構(gòu)。好的源碼通常有清晰的controller/service/mapper/entity分層。如果沒(méi)有分層那建議你自己重新建包整理否則后面調(diào)試會(huì)非常痛苦。為了數(shù)據(jù)安全修改任何功能前先備份整個(gè)項(xiàng)目目錄和數(shù)據(jù)庫(kù)這花不了幾分鐘但能避免很多返工。如果源碼里的方法名命名混亂比如getList、getData這種毫無(wú)辨識(shí)度的名稱(chēng)我建議你花幾分鐘統(tǒng)一命名。這個(gè)工作對(duì)于一個(gè)三千行代碼的項(xiàng)目來(lái)說(shuō)用時(shí)不多但對(duì)你熟悉整個(gè)系統(tǒng)邏輯的幫助極大。答辯時(shí)老師問(wèn)你某個(gè)功能在哪實(shí)現(xiàn)你如果回答得支支吾吾印象分會(huì)大打折扣。5.3 答辯準(zhǔn)備中的實(shí)戰(zhàn)套路最后說(shuō)說(shuō)答辯。不少學(xué)校答辯時(shí)老師會(huì)直接打開(kāi)你的項(xiàng)目當(dāng)場(chǎng)點(diǎn)幾個(gè)功能讓你演示同時(shí)追問(wèn)一些問(wèn)題。根據(jù)我觀察的經(jīng)驗(yàn)作業(yè)批閱系統(tǒng)經(jīng)常被問(wèn)到的問(wèn)題就這么幾個(gè)提前準(zhǔn)備好回答思路現(xiàn)場(chǎng)就不會(huì)慌亂。第一個(gè)必問(wèn)的是“你這個(gè)系統(tǒng)的角色權(quán)限是怎么控制的”回答時(shí)聚焦在登錄后返回JWT令牌、前端根據(jù)用戶(hù)角色渲染不同菜單、后端接口做了對(duì)應(yīng)的攔截判斷即可。第二個(gè)常問(wèn)的是“如果兩個(gè)老師同時(shí)批閱同一個(gè)學(xué)生的作業(yè)會(huì)怎樣”回答思路是在狀態(tài)機(jī)邏輯里攔截重復(fù)批閱并解釋這樣處理保證了業(yè)務(wù)一致性。第三個(gè)刁鉆一點(diǎn)的是“作業(yè)提交高峰時(shí)系統(tǒng)會(huì)不會(huì)卡怎么優(yōu)化”如果你還沒(méi)做優(yōu)化就坦誠(chéng)說(shuō)當(dāng)前對(duì)于課程班級(jí)規(guī)模來(lái)說(shuō)性能足夠的如果后續(xù)擴(kuò)展可以引入消息隊(duì)列異步處理文件上傳或者用Redis緩存熱點(diǎn)數(shù)據(jù)。不要把答辯想成考試它更像一次技術(shù)交流。你對(duì)系統(tǒng)每一行代碼的理解比代碼本身更能打動(dòng)老師。我在實(shí)際做這類(lèi)項(xiàng)目過(guò)程中的一個(gè)感受是很多人執(zhí)著于找“完美”源碼但真正把一套普通源碼完全吃透、能講清楚每個(gè)模塊為什么這樣設(shè)計(jì)最后的收獲遠(yuǎn)比換十套源碼要大。這套作業(yè)批閱系統(tǒng)的代碼量并不算大但它覆蓋的Web開(kāi)發(fā)知識(shí)點(diǎn)足夠全面從數(shù)據(jù)庫(kù)外鍵關(guān)系到JWT無(wú)狀態(tài)認(rèn)證、從文件上傳到可視化大屏足夠你完整走一遍真實(shí)項(xiàng)目的開(kāi)發(fā)流程。如果你打算在這個(gè)系統(tǒng)上做二次開(kāi)發(fā)我建議下一個(gè)可以動(dòng)手的小目標(biāo)是給大屏模塊加上實(shí)時(shí)刷新用定時(shí)器每30秒輪詢(xún)一次統(tǒng)計(jì)接口讓“批閱進(jìn)度”在演示時(shí)動(dòng)起來(lái)。這個(gè)小改動(dòng)技術(shù)上幾乎沒(méi)有難度但演示時(shí)那種數(shù)據(jù)自己增長(zhǎng)的效果遠(yuǎn)比靜態(tài)截圖來(lái)得震撼。祝你順利拿下畢設(shè)少熬夜多拿分。