搭建數(shù)學(xué)競(jìng)賽網(wǎng)站全流程解析)
數(shù)學(xué)競(jìng)賽類網(wǎng)站這套題目在畢業(yè)設(shè)計(jì)里真的算常青樹(shù)了。每年都有大量學(xué)生做類似選題但大多數(shù)版本都停留在“能跑就行”的狀態(tài)代碼結(jié)構(gòu)、業(yè)務(wù)邏輯、部署方式都存在不少隱患。這篇文章我結(jié)合自己帶過(guò)的項(xiàng)目經(jīng)驗(yàn)把基于JavaSSMFlask混合架構(gòu)的數(shù)學(xué)競(jìng)賽網(wǎng)站從設(shè)計(jì)思路、核心代碼、部署調(diào)試到排坑過(guò)程完整拆開(kāi)講一遍適合正在做畢設(shè)、課設(shè)或者想自己搭一個(gè)數(shù)學(xué)練習(xí)與競(jìng)賽平臺(tái)的開(kāi)發(fā)者參考。1. 項(xiàng)目整體設(shè)計(jì)與技術(shù)選型思路1.1 為什么用JavaSSMFlask這種混合架構(gòu)拿到“數(shù)學(xué)競(jìng)賽網(wǎng)站”這個(gè)題目第一步要做的不是急著寫(xiě)代碼而是先把技術(shù)架構(gòu)定下來(lái)。目前市面上的方案大致分三類純JavaSSM或SpringBoot、純PythonDjango或Flask、前后端分離VueSpringBoot。而標(biāo)題里這個(gè)組合——JavaSSMFlask看起來(lái)有點(diǎn)“混搭”實(shí)際卻是很多高校畢業(yè)設(shè)計(jì)里非常實(shí)用的方案背后其實(shí)有很現(xiàn)實(shí)的考慮。SSMSpringSpringMVCMyBatis是Java后端里非常經(jīng)典的組合它穩(wěn)、資料多、面試也常問(wèn)。用SSM來(lái)做網(wǎng)站的主體業(yè)務(wù)比如用戶登錄、試題管理、競(jìng)賽發(fā)布、成績(jī)記錄這些核心模塊是非常穩(wěn)妥的。而Flask作為Python端的輕量框架非常適合做兩件事一是實(shí)現(xiàn)一些需要快速開(kāi)發(fā)的輔助接口比如智能組卷、成績(jī)統(tǒng)計(jì)分析、爬蟲(chóng)抓取競(jìng)賽資訊等二是為Java端提供一個(gè)可調(diào)用的Python服務(wù)方便展示“多種技術(shù)棧協(xié)作”的能力這在畢業(yè)設(shè)計(jì)的答辯中是一個(gè)很加分的亮點(diǎn)。當(dāng)然也有很多人會(huì)問(wèn)為什么不直接用SpringBoot代替SSM說(shuō)實(shí)話如果是從零開(kāi)始自己做項(xiàng)目SpringBoot確實(shí)更省事。但很多高校的課程體系和畢設(shè)大綱還停留在SSM階段題目要求里明確寫(xiě)了“基于SSM”所以標(biāo)題里保留SSM是有原因的。再說(shuō)了SSM雖然配置繁瑣但恰恰是這種繁瑣能逼著你把Spring的IOC、AOP、MyBatis的映射機(jī)制這些東西真正弄明白而不是像用SpringBoot那樣把一切都自動(dòng)屏蔽掉。1.2 前端與數(shù)據(jù)交互的設(shè)計(jì)考量前端部分大多數(shù)數(shù)學(xué)競(jìng)賽網(wǎng)站用的都是服務(wù)端渲染加少量Ajax的方式。用Thymeleaf或者JSP來(lái)渲染頁(yè)面配合jQuery和Bootstrap做交互。這種方案的好處是開(kāi)發(fā)速度快、對(duì)SEO友好、調(diào)試也直觀特別適合課設(shè)畢設(shè)這種時(shí)間緊、任務(wù)重的場(chǎng)景。數(shù)據(jù)交互方面Java端和Flask端的通信我推薦兩種方式方式一Java端通過(guò)HTTP Client比如RestTemplate或OkHttp調(diào)用Flask的RESTful接口這種方式邏輯清晰兩端完全解耦。方式二共用同一個(gè)MySQL數(shù)據(jù)庫(kù)Java負(fù)責(zé)寫(xiě)Flask負(fù)責(zé)讀然后通過(guò)Redis做緩存同步。這種方式少了一層HTTP調(diào)用性能更好一點(diǎn)但要注意數(shù)據(jù)一致性問(wèn)題。我自己在做類似項(xiàng)目時(shí)更喜歡方式一。原因很簡(jiǎn)單Java和Flask各自維護(hù)獨(dú)立的業(yè)務(wù)邊界接口文檔寫(xiě)清楚聯(lián)調(diào)的時(shí)候不容易出“這個(gè)字段誰(shuí)改的”這種扯皮問(wèn)題。而且答辯時(shí)展示兩端獨(dú)立部署、通過(guò)接口通信比“共用一個(gè)庫(kù)”聽(tīng)起來(lái)更有技術(shù)含量。1.3 核心功能模塊拆解一個(gè)完整的數(shù)學(xué)競(jìng)賽網(wǎng)站至少要包含以下模塊這也是評(píng)閱老師重點(diǎn)看的部分。我列一個(gè)表格方便對(duì)照模塊名稱核心功能技術(shù)要點(diǎn)用戶模塊注冊(cè)、登錄、個(gè)人信息維護(hù)Session管理、密碼加密、驗(yàn)證碼試題模塊試題錄入、分類、檢索、在線答題批量導(dǎo)入、分頁(yè)查詢、題目類型區(qū)分競(jìng)賽模塊發(fā)布競(jìng)賽、在線報(bào)名、模擬競(jìng)賽時(shí)間控制、競(jìng)賽狀態(tài)機(jī)、防作弊成績(jī)模塊自動(dòng)判分、成績(jī)排名、統(tǒng)計(jì)分析判定規(guī)則、成績(jī)可視化資訊模塊競(jìng)賽公告、數(shù)學(xué)文章展示富文本發(fā)布、分類展示管理后臺(tái)用戶管理、試題審核、數(shù)據(jù)統(tǒng)計(jì)權(quán)限控制、圖表展示這六大模塊基本覆蓋了“數(shù)學(xué)競(jìng)賽網(wǎng)站”“數(shù)學(xué)練習(xí)網(wǎng)站”“數(shù)學(xué)學(xué)習(xí)資源”這些熱詞的全部需求。后面我會(huì)重點(diǎn)挑幾個(gè)容易出問(wèn)題的模塊詳細(xì)講代碼實(shí)現(xiàn)和踩坑過(guò)程。2. 核心業(yè)務(wù)模塊詳解與代碼實(shí)現(xiàn)2.1 用戶登錄與權(quán)限控制的正確姿勢(shì)用戶模塊看起來(lái)簡(jiǎn)單但登錄態(tài)管理和權(quán)限控制是做網(wǎng)站最容易出安全隱患的地方。很多同學(xué)的代碼里登錄就是查一下用戶名密碼對(duì)不對(duì)然后把用戶ID存在session里就完事了。這種做法在課設(shè)答辯中勉強(qiáng)能過(guò)但實(shí)際上漏洞挺多的。我建議的做法是分三層第一層表單提交時(shí)用JS做一次基礎(chǔ)校驗(yàn)比如用戶名非空、密碼長(zhǎng)度限制這層是為了用戶體驗(yàn)。第二層后端Controller里再做一次嚴(yán)格的參數(shù)校驗(yàn)防SQL注入和XSS攻擊。比如MyBatis里一定要用#{}而不是${}因?yàn)?{}是字符串拼接很容易被注入。第三層密碼不能明文存數(shù)據(jù)庫(kù)。至少要用MD5加鹽或者直接用BCrypt。我通常用Spring自帶的BCryptPasswordEncoder每次校驗(yàn)時(shí)把用戶輸入的密碼和數(shù)據(jù)庫(kù)里的哈希值做比對(duì)。// 用戶登錄的Service層核心邏輯 public User login(String username, String password) { User user userMapper.findByUsername(username); if (user null) { throw new BusinessException(用戶名不存在); } // BCrypt校驗(yàn)password是用戶輸入的明文user.getPassword()是庫(kù)里的哈希值 if (!bcryptPasswordEncoder.matches(password, user.getPassword())) { throw new BusinessException(密碼錯(cuò)誤); } // 更新最后登錄時(shí)間 userMapper.updateLastLoginTime(user.getId()); return user; }權(quán)限控制方面用一個(gè)攔截器就夠了。定義一個(gè)LoginInterceptor重寫(xiě)preHandle方法檢查session里有沒(méi)有user信息。沒(méi)有就重定向到登錄頁(yè)。然后Admin相關(guān)的請(qǐng)求再檢查這個(gè)用戶的角色字段是不是“admin”。注意角色判斷不能只在前端隱藏按鈕后端接口必須也校驗(yàn)否則別人直接構(gòu)造請(qǐng)求就能刪數(shù)據(jù)了。2.2 試題管理從批量錄入到智能組卷試題管理是數(shù)學(xué)競(jìng)賽網(wǎng)站的核心也是工作量最大的部分。一道題要有題干、選項(xiàng)如果是選擇題、正確答案、解析、難度等級(jí)、知識(shí)點(diǎn)標(biāo)簽。在數(shù)據(jù)庫(kù)設(shè)計(jì)中我建議把題目基礎(chǔ)信息放一張表題目和知識(shí)點(diǎn)的關(guān)聯(lián)關(guān)系放另一張表采用多對(duì)多的設(shè)計(jì)。這樣后面做篩選、分組、統(tǒng)計(jì)都非常方便。批量錄入這塊很多同學(xué)是一個(gè)個(gè)在后臺(tái)手輸太累了。比較實(shí)用的做法是支持Excel導(dǎo)入。我用的是Apache POI前端上傳xlsx文件后端解析每一行校驗(yàn)字段完整性后逐條插入數(shù)據(jù)庫(kù)。// 使用POI讀取Excel的簡(jiǎn)化代碼 Workbook workbook WorkbookFactory.create(inputStream); Sheet sheet workbook.getSheetAt(0); for (Row row : sheet) { if (row.getRowNum() 0) { continue; // 跳過(guò)表頭 } Question question new Question(); question.setContent(row.getCell(0).getStringCellValue()); question.setType(row.getCell(1).getStringCellValue()); question.setAnswer(row.getCell(2).getStringCellValue()); question.setDifficulty(row.getCell(3).getNumericCellValue()); questionService.addQuestion(question); }這里有個(gè)坑POI讀取單元格時(shí)數(shù)字類型的cell要用getNumericCellValue字符串類型要用getStringCellValue如果不提前判斷getCellType()很容易拋異常。我在這上面吃過(guò)虧后來(lái)都寫(xiě)了統(tǒng)一的解析工具類把所有cell值都先轉(zhuǎn)成字符串再處理。智能組卷是一個(gè)加分功能。簡(jiǎn)單版本的做法是根據(jù)用戶選擇的知識(shí)點(diǎn)、難度分布和題量先從數(shù)據(jù)庫(kù)里把符合條件的題目全部查出來(lái)然后按比例隨機(jī)抽取。稍微智能一點(diǎn)可以給每道題設(shè)置一個(gè)權(quán)重值比如最近被抽中次數(shù)多的題目權(quán)重降低實(shí)現(xiàn)“不重復(fù)刷題”的學(xué)習(xí)體驗(yàn)。這個(gè)邏輯用SQL加Java配合很容易實(shí)現(xiàn)。2.3 競(jìng)賽流程設(shè)計(jì)報(bào)名、開(kāi)賽、交卷、判分?jǐn)?shù)學(xué)競(jìng)賽網(wǎng)站里的“競(jìng)賽”功能核心難點(diǎn)在于時(shí)間控制和狀態(tài)流轉(zhuǎn)。一場(chǎng)競(jìng)賽的狀態(tài)我一般設(shè)計(jì)為四個(gè)未開(kāi)始、進(jìn)行中、已結(jié)束、已發(fā)布成績(jī)。整個(gè)流程是這樣的管理員創(chuàng)建競(jìng)賽設(shè)置開(kāi)始時(shí)間、結(jié)束時(shí)間、參賽限制。用戶瀏覽競(jìng)賽列表在“未開(kāi)始”狀態(tài)下可以報(bào)名。到了開(kāi)始時(shí)間競(jìng)賽狀態(tài)自動(dòng)變?yōu)椤斑M(jìn)行中”用戶可以進(jìn)入答題頁(yè)面。用戶交卷或時(shí)間截止自動(dòng)交卷進(jìn)入判分環(huán)節(jié)。所有參賽者成績(jī)統(tǒng)計(jì)完成后管理員發(fā)布成績(jī)用戶可以看到排名。狀態(tài)判斷有個(gè)細(xì)節(jié)不能只靠前端按鈕控制顯示后端每次請(qǐng)求競(jìng)賽詳情時(shí)都要根據(jù)當(dāng)前時(shí)間動(dòng)態(tài)判斷狀態(tài)。因?yàn)橛脩敉耆梢岳@過(guò)前端直接調(diào)接口獲取題目。所以我的做法是在Service層寫(xiě)一個(gè)getCompetitionStatus(Competition competition)方法每次請(qǐng)求都實(shí)時(shí)計(jì)算保證業(yè)務(wù)邏輯安全。交卷判分這塊如果題目全是選擇題和判斷題那判分就很簡(jiǎn)單——遍歷用戶的答案跟正確答案比對(duì)就行。但數(shù)學(xué)競(jìng)賽往往有填空題和解答題。填空題要注意答案格式的統(tǒng)一比如“3/4”和“0.75”本質(zhì)等價(jià)但字符串比對(duì)認(rèn)為是不同的。我的處理辦法是填空題設(shè)置多個(gè)可接受的答案或者用正則做歸一化比較。解答題則需要人工閱卷我的做法是系統(tǒng)先自動(dòng)判選擇和填空把得分記錄下來(lái)然后管理員在后臺(tái)對(duì)解答題進(jìn)行評(píng)分最終分?jǐn)?shù)等于兩部分之和。2.4 Flask端輔助功能成績(jī)分析與資訊接口Flask在整個(gè)項(xiàng)目里的角色是一個(gè)Python輔助服務(wù)。我主要讓它干三件事成績(jī)數(shù)據(jù)分析、競(jìng)賽資訊采集、模擬成績(jī)預(yù)測(cè)。成績(jī)分析這塊Flask通過(guò)HTTP接口接收J(rèn)ava端傳過(guò)來(lái)的成績(jī)數(shù)據(jù)JSON格式然后用Pandas做統(tǒng)計(jì)計(jì)算比如平均分、最高分、分?jǐn)?shù)段分布再配合Matplotlib生成一張圖片返回給Java端Java端直接在前端頁(yè)面上展示。這樣圖表繪制完全不用Java代碼省了很多麻煩。from flask import Flask, request, jsonify import pandas as pd import matplotlib.pyplot as plt import io import base64 app Flask(__name__) app.route(/api/analyze, methods[POST]) def analyze(): data request.get_json() # Java端傳過(guò)來(lái)的成績(jī)列表 df pd.DataFrame(data) avg_score df[score].mean() max_score df[score].max() # 生成分?jǐn)?shù)分布圖轉(zhuǎn)base64返回 plt.hist(df[score], bins10) buf io.BytesIO() plt.savefig(buf, formatpng) buf.seek(0) img_base64 base64.b64encode(buf.read()).decode(utf-8) return jsonify({ avg: avg_score, max: max_score, img: img_base64, total: len(df) }) if __name__ __main__: app.run(host0.0.0.0, port5001)Java端用RestTemplate調(diào)用這個(gè)接口的代碼也很簡(jiǎn)單構(gòu)造一個(gè)MultiValueMap或JSON對(duì)象POST過(guò)去拿到返回的JSON解析后把圖片的base64字符串直接放進(jìn)前端的img標(biāo)簽里。實(shí)測(cè)下來(lái)這個(gè)鏈路很穩(wěn)定數(shù)據(jù)量不大的情況下響應(yīng)速度也很快。3. 數(shù)據(jù)庫(kù)設(shè)計(jì)與關(guān)鍵配置文件解讀3.1 表結(jié)構(gòu)設(shè)計(jì)哪些表是必須的怎么建最合理數(shù)據(jù)庫(kù)設(shè)計(jì)是整個(gè)項(xiàng)目的地基。很多人的表結(jié)構(gòu)是隨用隨建到后期發(fā)現(xiàn)缺字段就加字段類型不統(tǒng)一關(guān)聯(lián)查詢巨慢。我這里直接給出一個(gè)比較完整的設(shè)計(jì)方案建表的SQL你可以直接參考。核心表有七張用戶表、角色表、題目表、知識(shí)點(diǎn)表、題目知識(shí)點(diǎn)關(guān)聯(lián)表、競(jìng)賽表、競(jìng)賽成績(jī)表。如果要做新聞資訊再加一張文章表。用戶表字段id, username, password, salt, email, avatar, role, status, create_time題目表字段id, content, options, answer, analysis, type, difficulty, subject, create_time競(jìng)賽表字段id, title, description, start_time, end_time, duration, question_ids, status, create_time這里特別說(shuō)一下question_ids字段。競(jìng)賽和題目是多對(duì)多的關(guān)系理論上應(yīng)該建一張關(guān)聯(lián)表但競(jìng)賽場(chǎng)景比較特殊——管理員組卷后該場(chǎng)競(jìng)賽的題目集合就固定了直接用逗號(hào)分隔的ID字符串存起來(lái)取題的時(shí)候split(,)一下就行。好處是查詢快、理解簡(jiǎn)單壞處是不太好做統(tǒng)計(jì)比如“哪道題被哪些競(jìng)賽用過(guò)”??错?xiàng)目復(fù)雜度自行取舍課設(shè)層面我建議直接用字符串存夠用且省事。競(jìng)賽成績(jī)表字段id, competition_id, user_id, score, total_score, answer_detail, submit_time, rank, statusanswer_detail是個(gè)JSON字符串存的是用戶每道題的作答詳情比如[{question_id: 1, user_answer: C, is_correct: true}]。這樣做的好處是成績(jī)復(fù)核、人工批改的時(shí)候都能還原當(dāng)時(shí)用戶的作答情況不用再去聯(lián)查原始表。3.2 SSM框架配置Spring、SpringMVC、MyBatis的整合細(xì)節(jié)SSM的配置主要有三個(gè)文件spring-context.xml、spring-mvc.xml、mybatis-config.xml或直接通過(guò)application.properties。這里我不貼完整配置重點(diǎn)說(shuō)幾個(gè)容易被忽視但影響巨大的細(xì)節(jié)。第一Spring容器和SpringMVC容器要分清。spring-context.xml負(fù)責(zé)掃描Service、Daospring-mvc.xml只掃描Controller。如果掃描范圍重疊會(huì)出現(xiàn)事務(wù)失效的問(wèn)題。具體表現(xiàn)就是Service層沒(méi)加Transactional都正常加了反而報(bào)錯(cuò)或者不生效。第二MyBatis的駝峰命名映射一定要開(kāi)啟。數(shù)據(jù)庫(kù)字段都是下劃線風(fēng)格比如create_timeJava實(shí)體類都是駝峰風(fēng)格createTime如果不開(kāi)啟map-underscore-to-camel-case: true你查出來(lái)的結(jié)果全是null這個(gè)問(wèn)題能卡死人。第三數(shù)據(jù)庫(kù)連接池用Druid別用默認(rèn)的。Druid自帶監(jiān)控頁(yè)面在答辯的時(shí)候打開(kāi)監(jiān)控頁(yè)展示SQL執(zhí)行次數(shù)和耗時(shí)是很直觀的加分項(xiàng)。配置上注意設(shè)置initialSize和maxActive別全部用默認(rèn)值。!-- Druid數(shù)據(jù)源配置關(guān)鍵參數(shù) -- bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName valuecom.mysql.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/math_contest?useSSLfalseamp;characterEncodingutf8/ property nameusername valueroot/ property namepassword valueroot/ property nameinitialSize value5/ property namemaxActive value20/ property nameminIdle value5/ property namemaxWait value60000/ /beanJDBC連接串里的characterEncodingutf8非常重要不加大概率會(huì)中文亂碼。你查出來(lái)的題目?jī)?nèi)容全是問(wèn)號(hào)排查一圈發(fā)現(xiàn)是連接串沒(méi)加編碼參數(shù)這種低級(jí)錯(cuò)誤太常見(jiàn)了。3.3 Flask端配置和跨域問(wèn)題處理Flask端如果要被Java端調(diào)用要注意兩個(gè)問(wèn)題一個(gè)是端口別跟Java的Tomcat沖突Java一般在8080Flask我習(xí)慣用5001另一個(gè)就是跨域問(wèn)題。雖然Java后端調(diào)用Flask屬于服務(wù)端到服務(wù)端的HTTP通信不存在瀏覽器跨域限制但如果哪天你的前端頁(yè)面直接調(diào)Flask接口就會(huì)遇到跨域。提前把跨域配置寫(xiě)上去避免后面臨時(shí)抱佛腳。from flask_cors import CORS CORS(app, resources{r/api/*: {origins: *}})還有一點(diǎn)Flask端的接口返回值統(tǒng)一用JSON格式并且在業(yè)務(wù)異常時(shí)也要返回JSON不要讓Flask默認(rèn)返回HTML錯(cuò)誤頁(yè)。給Flask寫(xiě)一個(gè)全局異常處理器捕獲所有異常后統(tǒng)一返回{code: 500, msg: 服務(wù)器內(nèi)部錯(cuò)誤}。4. 項(xiàng)目部署與調(diào)試過(guò)程中的經(jīng)驗(yàn)教訓(xùn)4.1 本地環(huán)境搭建JDK、Maven、Tomcat、MySQL一步步來(lái)環(huán)境搭建是很多新手第一個(gè)攔路虎。我見(jiàn)過(guò)太多人在裝環(huán)境這一步卡了三天。實(shí)際上按順序操作沒(méi)那么復(fù)雜。JDK裝1.8就行畢設(shè)項(xiàng)目不需要太高版本太高版本反而可能跟老項(xiàng)目的編譯配置不兼容。環(huán)境變量配JAVA_HOME和PATH裝完在命令行輸入java -version驗(yàn)證。Maven解壓后配置MAVEN_HOME關(guān)鍵是要換阿里云鏡像倉(cāng)庫(kù)否則下載依賴能等到天荒地老。Tomcat解壓版就行用IDE自帶的也沒(méi)問(wèn)題。MySQL安裝時(shí)要注意設(shè)置root密碼后默認(rèn)的認(rèn)證方式如果是caching_sha2_password老版本的JDBC驅(qū)動(dòng)連接會(huì)報(bào)錯(cuò)。解決辦法是在MySQL命令行里執(zhí)行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密碼; FLUSH PRIVILEGES;這一步不做的話你會(huì)在Tomcat啟動(dòng)時(shí)報(bào)Public Key Retrieval is not allowed錯(cuò)誤或者干脆連不上數(shù)據(jù)庫(kù)。4.2 數(shù)據(jù)庫(kù)初始化腳本的編寫(xiě)規(guī)范項(xiàng)目交付時(shí)數(shù)據(jù)庫(kù)腳本是必須的。很多人直接Navicat可視化建庫(kù)建表最后導(dǎo)出一份SQL交差。這沒(méi)問(wèn)題但有兩個(gè)地方要檢查。一是SQL里不要帶DROP DATABASE這種破壞性語(yǔ)句評(píng)閱老師或者你自己后面想演示的時(shí)候誤執(zhí)行一下數(shù)據(jù)全沒(méi)了。二是建表語(yǔ)句最好加上IF NOT EXISTS。另外強(qiáng)烈建議寫(xiě)一份data.sql初始化數(shù)據(jù)腳本里面放幾道例題、一個(gè)測(cè)試用戶、一場(chǎng)模擬競(jìng)賽。這樣別人拿到項(xiàng)目導(dǎo)入后不用自己錄入就能直接看到效果。數(shù)據(jù)的初始化和演示效果直接相關(guān)這一份腳本的價(jià)值比想象中大。4.3 修改配置文件的小技巧和注意點(diǎn)項(xiàng)目里配置文件包括jdbc.properties數(shù)據(jù)庫(kù)連接、log4j.properties日志、spring-mvc.xml靜態(tài)資源放行、視圖解析器等。改配置最容易出的問(wèn)題有三個(gè)屬性文件里的符號(hào)要轉(zhuǎn)義成amp;上面提過(guò)。IP地址或端口改了但前端JS里的Ajax請(qǐng)求地址還是localhost。文件編碼問(wèn)題properties文件默認(rèn)ISO-8859-1直接寫(xiě)中文注釋會(huì)導(dǎo)致亂碼。盡管不影響運(yùn)行但看著難受建議把IDE的properties文件默認(rèn)編碼改成UTF-8。日志這塊建議把log4j.properties里com.xxx.mapper這一級(jí)的日志級(jí)別設(shè)成DEBUG這樣MyBatis的執(zhí)行日志會(huì)打出來(lái)你能看到SQL是怎么拼的排查問(wèn)題非常有用。等項(xiàng)目上線了再調(diào)回INFO。4.4 前后端聯(lián)調(diào)我用過(guò)的幾個(gè)快速調(diào)試技巧聯(lián)調(diào)階段我積累了幾個(gè)特別實(shí)用的技巧分享給你們。第一Chrome開(kāi)發(fā)者工具的Network面板是上帝視角所有請(qǐng)求的狀態(tài)碼、耗時(shí)、參數(shù)一目了然。看到一個(gè)請(qǐng)求404先別急著改代碼看看URL是不是打錯(cuò)了、Controller里的RequestMapping路徑是不是沒(méi)配對(duì)。第二用Postman單獨(dú)測(cè)試后端接口。在寫(xiě)頁(yè)面之前先把所有接口在Postman里跑通確認(rèn)返回的數(shù)據(jù)結(jié)構(gòu)再讓前端對(duì)接。這樣出了問(wèn)題能快速定位是后端還是前端。第三Java端調(diào)用Flask接口如果報(bào)連接超時(shí)優(yōu)先檢查Flask服務(wù)是否啟動(dòng)、端口是否正確、防火墻是否攔截。命令行curl -X POST http://localhost:5001/api/analyze -d {}測(cè)一下就知道問(wèn)題在哪。5. 常見(jiàn)問(wèn)題與排查技巧實(shí)錄根據(jù)我實(shí)際看過(guò)和經(jīng)歷過(guò)的情況整理了一份高頻問(wèn)題速查表每一個(gè)都是真實(shí)發(fā)生過(guò)的。問(wèn)題現(xiàn)象根本原因解決方案登錄時(shí)輸入正確密碼提示“密碼錯(cuò)誤”密碼加密方式不一致比如注冊(cè)用了BCrypt、登錄用MD5統(tǒng)一加密算法保證注冊(cè)和登錄走同一套用戶頭像圖片上傳后無(wú)法顯示上傳路徑是相對(duì)路徑前端訪問(wèn)的基礎(chǔ)路徑不對(duì)將上傳目錄映射成虛擬路徑或用絕對(duì)路徑拼接URL競(jìng)賽到了開(kāi)始時(shí)間仍顯示未開(kāi)始狀態(tài)判斷用的是管理員設(shè)置的靜態(tài)狀態(tài)沒(méi)有按當(dāng)前時(shí)間實(shí)時(shí)計(jì)算每次請(qǐng)求時(shí)用LocalDateTime.now()對(duì)比開(kāi)始時(shí)間批量導(dǎo)入Excel時(shí)報(bào)IllegalStateExceptionPOI讀取cell類型沒(méi)有做判斷統(tǒng)一寫(xiě)單元格值轉(zhuǎn)換方法先判類型再取值前端Ajax獲取數(shù)據(jù)后頁(yè)面空白后端返回的JSON字段名和前端取的字段名不一致統(tǒng)一使用駝峰命名且前后端約定好字段名Flask端接口偶發(fā)CORS報(bào)錯(cuò)前端跨域調(diào)Flask接口且未配置CORS用flask-cors全局開(kāi)啟指定允許的來(lái)源部署后MyBatis查詢字段全是null忘了開(kāi)啟下劃線轉(zhuǎn)駝峰映射mybatis配置中開(kāi)啟map-underscore-to-camel-case中文亂碼頁(yè)面全是問(wèn)號(hào)連接串沒(méi)加characterEncodingutf8或數(shù)據(jù)庫(kù)編碼不對(duì)檢查JDBC URL、數(shù)據(jù)庫(kù)建庫(kù)字符集、頁(yè)面編碼統(tǒng)一UTF-8Tomcat部署war包后訪問(wèn)報(bào)404項(xiàng)目訪問(wèn)路徑不對(duì)沒(méi)加項(xiàng)目名URL訪問(wèn)前加/項(xiàng)目名或者把war包名設(shè)為ROOT除了表格里的我再補(bǔ)充幾個(gè)比較隱蔽的坑。第一個(gè)是并發(fā)訪問(wèn)session的問(wèn)題。有些同學(xué)在Controller里直接request.getSession().setAttribute(user, user)然后在攔截器里取。如果同時(shí)開(kāi)兩個(gè)瀏覽器標(biāo)簽頁(yè)登錄不同賬號(hào)后登錄的會(huì)把先登錄的頂?shù)?。解決辦法是Session里存用戶ID而不是整個(gè)用戶對(duì)象需要用戶信息時(shí)再查庫(kù)。這樣即使被頂?shù)粢仓皇嵌嗖橐淮螖?shù)據(jù)庫(kù)邏輯上不會(huì)串。第二個(gè)是競(jìng)賽答題頁(yè)面刷新時(shí)倒計(jì)時(shí)重置的問(wèn)題。這個(gè)特別容易出問(wèn)題。用戶進(jìn)入答題頁(yè)面后如果按F5刷新頁(yè)面重新加載如果倒計(jì)時(shí)是JS的setInterval從零開(kāi)始算就相當(dāng)于時(shí)間重置了用戶可以無(wú)限刷新延長(zhǎng)答題時(shí)間。正確做法是后端在用戶進(jìn)入答題狀態(tài)時(shí)記錄一個(gè)開(kāi)始時(shí)間戳前端每次獲取時(shí)間都用“截止時(shí)間減去當(dāng)前服務(wù)器時(shí)間”來(lái)計(jì)算剩余時(shí)間。刷新頁(yè)面只是重新拿一下剩余時(shí)間而已不會(huì)占便宜。第三個(gè)是文件上傳的安全問(wèn)題。數(shù)學(xué)競(jìng)賽網(wǎng)站里用戶頭像上傳功能看起來(lái)很普通但如果沒(méi)做文件類型校驗(yàn)惡意用戶上傳一個(gè)JSP或PHP文件然后拼一個(gè)路徑去訪問(wèn)就能getshell。哪怕只是做個(gè)課設(shè)也應(yīng)該對(duì)上傳文件后綴做白名單校驗(yàn)別用黑名單。同時(shí)重命名文件為隨機(jī)字符串不要保留用戶原始文件名。6. 從課設(shè)到答辯項(xiàng)目打磨與展示的經(jīng)驗(yàn)分享項(xiàng)目做完代碼部分離交付還有兩個(gè)重要的收尾工作文檔撰寫(xiě)和答辯準(zhǔn)備。這部分往往被低估但它直接影響最終成績(jī)。LW也就是畢業(yè)論文或設(shè)計(jì)說(shuō)明書(shū)的結(jié)構(gòu)一般是緒論、相關(guān)技術(shù)介紹、需求分析、系統(tǒng)設(shè)計(jì)、系統(tǒng)實(shí)現(xiàn)、系統(tǒng)測(cè)試、總結(jié)。寫(xiě)的時(shí)候技術(shù)介紹不要大段抄書(shū)要結(jié)合你項(xiàng)目里的場(chǎng)景寫(xiě)。需求分析要用用例圖系統(tǒng)設(shè)計(jì)要有E-R圖這些我建議用ProcessOn先梳理思路再放到文檔里。代碼的核心邏輯截圖加簡(jiǎn)短說(shuō)明就可以了關(guān)鍵是要展示你的思考過(guò)程比如“為什么選MySQL不選Oracle”“為什么組卷用權(quán)重隨機(jī)算法”。調(diào)試文檔DD我習(xí)慣寫(xiě)成帶問(wèn)題痕跡的記錄而不是一個(gè)純命令清單。比如某一天我遇到了什么報(bào)錯(cuò)、排查思路是什么、最后怎么解決的。這種文檔面試或評(píng)閱時(shí)特別好用因?yàn)榭吹贸瞿阏娴氖窃贒ebug而不是照著教程敲。答辯前把項(xiàng)目的核心亮點(diǎn)提煉成三句話。比如“我的系統(tǒng)基于SSM框架構(gòu)建主站為Java實(shí)現(xiàn)同時(shí)引入Python Flask作為數(shù)據(jù)分析微服務(wù)實(shí)現(xiàn)了跨語(yǔ)言架構(gòu)的協(xié)作”。這句話能背下來(lái)就贏了。最后再分享一個(gè)實(shí)操中的小技巧在做完項(xiàng)目后把整套環(huán)境的啟動(dòng)步驟寫(xiě)成一個(gè)README.md文檔放在項(xiàng)目根目錄。里面寫(xiě)清楚從數(shù)據(jù)庫(kù)初始化到Tomcat啟動(dòng)、從Flask服務(wù)啟動(dòng)到瀏覽器訪問(wèn)的每一個(gè)步驟。這個(gè)文檔既是給別人看的也是給以后的你自己看的。等過(guò)了一個(gè)月再回頭看項(xiàng)目你會(huì)發(fā)現(xiàn)沒(méi)有這份文檔連你自己都忘了該怎么啟動(dòng)。數(shù)學(xué)競(jìng)賽網(wǎng)站這類項(xiàng)目技術(shù)點(diǎn)不算深但勝在業(yè)務(wù)清晰、模塊完整是對(duì)SSM框架、數(shù)據(jù)庫(kù)設(shè)計(jì)、前后端交互能力的一次全面檢驗(yàn)。只要把用戶、試題、競(jìng)賽、成績(jī)這四個(gè)核心模塊做扎實(shí)再點(diǎn)綴一點(diǎn)Flask輔助功能和優(yōu)秀的文檔這個(gè)項(xiàng)目就能成為一份非常拿得出手的畢設(shè)作品。做項(xiàng)目的過(guò)程中踩的那些坑調(diào)整的那些細(xì)節(jié)其實(shí)比最后的分?jǐn)?shù)更有價(jià)值工作后面試聊起這些經(jīng)歷才是真正能打動(dòng)人的東西。