:開(kāi)發(fā)部署與避坑指南)
做后臺(tái)開(kāi)發(fā)這些年考試系統(tǒng)這類項(xiàng)目我前后接手過(guò)不少版本有學(xué)校拿來(lái)期末測(cè)評(píng)的有企業(yè)內(nèi)部做培訓(xùn)認(rèn)證的還有金融機(jī)構(gòu)拿來(lái)做上崗考核的。需求大同小異但源碼質(zhì)量真的天差地別。最近花時(shí)間把一套基于 SpringBoot Vue MyBatis MySQL 的在線考試系統(tǒng)完整跑了一遍順手做了二次開(kāi)發(fā)和部署調(diào)優(yōu)今天把項(xiàng)目骨架、核心實(shí)現(xiàn)和那些容易踩的坑一次性說(shuō)清楚。不管你是剛?cè)肼毜?Java 后端還是想拿考試系統(tǒng)做畢業(yè)設(shè)計(jì)、接外包項(xiàng)目的開(kāi)發(fā)者這套東西都值得仔細(xì)看一遍。它沒(méi)有堆砌花哨技術(shù)而是把在線考試最核心的“組卷、答題、閱卷、統(tǒng)計(jì)”閉環(huán)串得比較完整屬于那種能直接落到生產(chǎn)環(huán)境再改改就能用的項(xiàng)目。1. 這套系統(tǒng)實(shí)際解決什么問(wèn)題角色、閉環(huán)與核心模塊1.1 企業(yè)級(jí)考試系統(tǒng)的典型痛點(diǎn)很多中小企業(yè)的培訓(xùn)考試還在用紙質(zhì)問(wèn)卷或者 Excel 統(tǒng)計(jì)組織一場(chǎng)考試要發(fā)卷、收卷、判分、錄成績(jī)?nèi)肆Τ杀痉浅8?。在線考試系統(tǒng)要解決的不只是“把題目放到網(wǎng)頁(yè)上”而是要把題庫(kù)管理、隨機(jī)組卷、限時(shí)作答、自動(dòng)閱卷、成績(jī)報(bào)表這一整條鏈路打通。這套系統(tǒng)最值得看的地方就是它沒(méi)有把業(yè)務(wù)邏輯堆在 Controller 里而是按模塊拆開(kāi)了。后端管理端負(fù)責(zé)用戶管理、題庫(kù)管理、考試管理、成績(jī)管理學(xué)生端負(fù)責(zé)報(bào)名考試、在線打開(kāi)試卷、提交答案、查看成績(jī)。前后端通過(guò) RESTful API 通信接口邊界清楚二次開(kāi)發(fā)時(shí)不容易改一處崩一片。1.2 角色權(quán)限模型系統(tǒng)涉及三類核心角色分別是管理員、教師和考生。權(quán)限設(shè)計(jì)直接決定了很多頁(yè)面和接口是否要展示。角色核心操作管理員用戶管理、學(xué)科/題庫(kù)維護(hù)、考試發(fā)布與撤回、全局公告、數(shù)據(jù)統(tǒng)計(jì)教師題目錄入與批量導(dǎo)入、手動(dòng)組卷/隨機(jī)組卷、手動(dòng)閱卷、導(dǎo)出成績(jī)單考生查看可參加的考試、在線答題、自動(dòng)提交、查看歷史成績(jī)與錯(cuò)題權(quán)限這塊用 JWT 攔截器配合前端 Vue Router 導(dǎo)航守衛(wèi)做后端每個(gè)接口按角色校驗(yàn)前端則根據(jù)登錄用戶角色動(dòng)態(tài)生成可訪問(wèn)的路由菜單。具體實(shí)現(xiàn)我在第 4 節(jié)、第 5 節(jié)會(huì)細(xì)說(shuō)。1.3 一次完整考試的業(yè)務(wù)流理解這個(gè)系統(tǒng)的關(guān)鍵不是看單個(gè)接口而是盯住考試狀態(tài)機(jī)草稿、已發(fā)布、進(jìn)行中、已結(jié)束。一個(gè)管理員創(chuàng)建考試后要先指定考試名稱、總時(shí)長(zhǎng)、總分、及格分然后選擇科目并從題庫(kù)中抽題或手動(dòng)選題組成試卷。發(fā)布后考生才能看到并報(bào)名。考生點(diǎn)擊開(kāi)始考試后系統(tǒng)記錄開(kāi)考時(shí)間到點(diǎn)自動(dòng)交卷后端按預(yù)設(shè)規(guī)則判分最后生成成績(jī)單和匯總統(tǒng)計(jì)。這套閉環(huán)跑通之后再往上加防作弊、視頻監(jiān)考、錯(cuò)題本這些功能就會(huì)容易很多因?yàn)檫吔绾芮宄?. 為什么是這套技術(shù)組合SpringBoot Vue MyBatis MySQL 的選型復(fù)盤(pán)2.1 MyBatis 比 JPA 更適合考試系統(tǒng)這種復(fù)雜查詢場(chǎng)景很多人會(huì)問(wèn)為什么不用 Spring Data JPA我的實(shí)際體會(huì)是考試系統(tǒng)的查詢場(chǎng)景很偏“報(bào)表型”。比如統(tǒng)計(jì)某個(gè)科目的及格率、導(dǎo)出某場(chǎng)考試所有考生的明細(xì)分?jǐn)?shù)SQL 里動(dòng)不動(dòng)就是多表 join、子查詢、條件動(dòng)態(tài)拼接。JPA 在這種場(chǎng)景下寫(xiě)起來(lái)非常憋屈要么寫(xiě) JPQL要么回退到原生 SQL。而 MyBatis 的 Mapper XML 天然支持動(dòng)態(tài) SQL一張考試記錄明細(xì)表十幾個(gè)可選查詢條件拼在一起用where、if就能輕松搞定SQL 還能拿出去在 Navicat 里直接驗(yàn)證。另外這類“企業(yè)級(jí)源碼”如果要用起來(lái)交付給客戶之后實(shí)施人員大概率是能看懂 SQL 的但未必熟悉 JPA 的關(guān)聯(lián)映射。MyBatis 的 XML 文件夠直白出問(wèn)題也容易定位。2.2 Vue 負(fù)責(zé)交互SpringBoot 只管業(yè)務(wù)前端這塊Vue 做單頁(yè)應(yīng)用的優(yōu)勢(shì)在于頁(yè)面狀態(tài)好管理尤其是答題過(guò)程中大量數(shù)據(jù)需要在前端暫存。項(xiàng)目里 Vue 通過(guò) Axios 請(qǐng)求后端接口登錄后把 token 存到 localStorage每次請(qǐng)求通過(guò)請(qǐng)求攔截器自動(dòng)攜帶 Authorization 頭。這里要提醒一點(diǎn)既然選擇了前后端分離后端就不要再在模板里寫(xiě)頁(yè)面跳轉(zhuǎn)了。我見(jiàn)過(guò)不少考試系統(tǒng)源碼寫(xiě)著 Vue 前端結(jié)果后端 Controller 還返回 ModelAndView前端框架等于白引入。這套項(xiàng)目里前后端的邊界是清晰的接口只返回 JSON路由只由前端控制這樣部署時(shí)前端可以丟 Nginx也可以打包進(jìn) SpringBoot 的 static 目錄。2.3 MySQL 版本選型和表字符集問(wèn)題數(shù)據(jù)庫(kù)用的是 MySQL 5.7 或 8.0 都能跑。如果你本地裝的是 MySQL 8.0連接驅(qū)動(dòng)必須用com.mysql.cj.jdbc.DriverJDBC URL 里建議顯式加上時(shí)區(qū)和 SSL 參數(shù)。建庫(kù)時(shí)一定要用 utf8mb4不然考生姓名里帶個(gè)生僻字或者表情符號(hào)插入時(shí)直接報(bào)錯(cuò)。CREATE DATABASE IF NOT EXISTS exam_system DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;字符集這種事看似不起眼但往往是在正式上線后往題庫(kù)里導(dǎo)入真實(shí)題目時(shí)才炸出來(lái)。3. 數(shù)據(jù)模型是考試系統(tǒng)的地基核心表設(shè)計(jì)思路3.1 六張核心表如何各司其職看源碼第一件事不是讀代碼而是打開(kāi)數(shù)據(jù)庫(kù)腳本看表結(jié)構(gòu)。這套系統(tǒng)的表設(shè)計(jì)很典型核心就是下面這些表名作用t_user用戶表存登錄賬號(hào)、密碼、姓名、角色、狀態(tài)t_subject學(xué)科表比如 Java、數(shù)據(jù)庫(kù)、市場(chǎng)營(yíng)銷(xiāo)t_question題目表單選題、多選題、判斷題t_exam考試表存考試基本信息、時(shí)長(zhǎng)、及格分、組卷方式t_paper試卷表記錄每次考試實(shí)際生成的題目集合t_exam_record考試記錄表記錄每個(gè)考生參與考試的狀態(tài)和得分t_answer_record答題明細(xì)表記錄每道題考生填寫(xiě)的答案用戶、題目、考試屬于基礎(chǔ)數(shù)據(jù)真正把業(yè)務(wù)串起來(lái)的是t_paper和t_exam_record這兩張關(guān)系表。3.2 試卷與答題記錄的字段設(shè)計(jì)試卷表設(shè)計(jì)上有一個(gè)容易踩坑的點(diǎn)它到底要存“題目的完整內(nèi)容”還是只存“題目 ID 的關(guān)聯(lián)關(guān)系”我的建議是只存關(guān)聯(lián)題目?jī)?nèi)容始終以t_question表為準(zhǔn)。這樣題目如果被老師更新過(guò)所有歷史試卷會(huì)自然同步。缺點(diǎn)是組卷后如果題目被刪了歷史試卷會(huì)關(guān)聯(lián)不上所以源碼里通常對(duì)已有考試引用的題目做了刪除保護(hù)刪除前先查一遍引用數(shù)量。答題明細(xì)表字段大致這樣CREATE TABLE t_answer_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, record_id BIGINT NOT NULL COMMENT 考試記錄ID, question_id BIGINT NOT NULL COMMENT 題目ID, user_answer VARCHAR(500) COMMENT 考生答案如A,B,D, is_correct TINYINT DEFAULT 0 COMMENT 是否正確, score DECIMAL(5,1) DEFAULT 0 COMMENT 本題得分, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_record (record_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;這里把用戶答案和得分冗余在明細(xì)表里查詢成績(jī)明細(xì)時(shí)不用反復(fù) join 題目表做邏輯判斷。數(shù)據(jù)庫(kù)設(shè)計(jì)的時(shí)候適當(dāng)冗余是省性能最直接的方式。3.3 索引怎么設(shè)計(jì)才不會(huì)被查詢拖死考試系統(tǒng)一旦上線最容易膨脹的是t_exam_record每個(gè)考生每場(chǎng)考試一條記錄配合明細(xì)表就是幾十萬(wàn)甚至上百萬(wàn)行。索引必須提前規(guī)劃好t_exam_record至少要有(user_id, exam_id)的聯(lián)合唯一索引防止同一考生重復(fù)提交。t_answer_record要對(duì)record_id建立普通索引查詢成績(jī)明細(xì)走索引非???。t_question要按(subject_id, question_type)建聯(lián)合索引因?yàn)殡S機(jī)抽題和列表篩選都依賴這兩個(gè)字段。很多源碼在表結(jié)構(gòu)里連索引都不加本地跑著沒(méi)問(wèn)題一旦考試人數(shù)上來(lái)成績(jī)統(tǒng)計(jì)接口會(huì)把數(shù)據(jù)庫(kù)拖到 CPU 百分百。索引這塊必須自己動(dòng)手補(bǔ)。4. SpringBoot 后端實(shí)現(xiàn)重點(diǎn)從登錄鑒權(quán)到自動(dòng)閱卷的完整鏈路4.1 JWT 登錄與接口鑒權(quán)登錄接口校驗(yàn)用戶名密碼后生成 JWTtoken 里包含用戶 ID、角色、昵稱這些非敏感信息。后端寫(xiě)一個(gè) HandlerInterceptor 或 Filter統(tǒng)一攔截除登錄、公告等白名單之外的所有接口校驗(yàn) Authorization 頭里面的 token。public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String auth request.getHeader(Authorization); if (auth null || !auth.startsWith(Bearer )) { throw new AuthException(401, 未登錄或token過(guò)期); } Claims claims JwtUtil.parseToken(auth.substring(7)); request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } }這里的關(guān)鍵點(diǎn)在于攔截器里解析完 token 后把用戶信息放進(jìn) request objectController 方法里直接用RequestAttribute取避免每次請(qǐng)求都重新查數(shù)據(jù)庫(kù)。雖然 jjwt 版本之間 API 差異有點(diǎn)大但整體思路是通用的。4.2 隨機(jī)組卷別迷信 ORDER BY RAND()源碼里隨機(jī)組卷最直觀的實(shí)現(xiàn)是ORDER BY RAND() LIMIT n這個(gè)寫(xiě)法在題庫(kù)幾百條數(shù)據(jù)時(shí)很爽但題庫(kù)上萬(wàn)后MySQL 會(huì)對(duì)全表每一行生成隨機(jī)數(shù)再排序性能非常拉胯。select idrandomSelect resultTypeQuestion SELECT * FROM t_question WHERE subject_id #{subjectId} AND question_type #{type} ORDER BY RAND() LIMIT #{count} /select生產(chǎn)環(huán)境下我一般會(huì)改成基于主鍵隨機(jī)區(qū)間的方式select idrandomSelectByOffset resultTypeQuestion SELECT * FROM t_question WHERE subject_id #{subjectId} AND question_type #{type} AND id ( SELECT FLOOR(RAND() * (SELECT MAX(id) FROM t_question)) 1 ) ORDER BY id LIMIT #{count} /select這種方案也有個(gè)隱患如果題目 ID 中間有斷層可能一次取不夠數(shù)量。穩(wěn)妥做法是先查出符合條件的id列表放進(jìn)內(nèi)存再用程序隨機(jī)打亂后取前 N 個(gè)通過(guò)IN查詢題目詳情。題目量不大的情況下性能差別不大但思路要清楚。4.3 自動(dòng)閱卷的判分邏輯設(shè)計(jì)自動(dòng)閱卷是整個(gè)系統(tǒng)最不能出錯(cuò)的模塊。單選和判斷題好辦直接比對(duì)標(biāo)準(zhǔn)答案和考生答案。多選題要特別注意順序問(wèn)題考生選 A,B 和 B,A 應(yīng)該算同一答案拼接前必須排序后再比較。至于多選題給分策略不同企業(yè)要求不一樣。有的全對(duì)才給分有的選對(duì)一個(gè)給一半分。源碼里默認(rèn)是全對(duì)才得分但我在改造時(shí)會(huì)把策略放到考試配置里用score_strategy字段區(qū)分“全對(duì)得分”和“部分給分”。這樣不同科目可以有不同的給分規(guī)則。提交答案之后閱卷和成績(jī)匯總必須放在同一個(gè)事務(wù)里Transactional public void submitExam(SubmitRequest request) { ExamRecord record saveAnswerRecords(request); calculateScore(record); updateExamRecord(record); }不加事務(wù)的話一旦閱卷邏輯報(bào)錯(cuò)考生答案存進(jìn)去了但成績(jī)沒(méi)算出來(lái)復(fù)查時(shí)會(huì)非常痛苦。5. Vue 前端關(guān)鍵實(shí)現(xiàn)路由、狀態(tài)管理和答題體驗(yàn)5.1 動(dòng)態(tài)路由和權(quán)限菜單的坑前端首次登錄后拿到用戶角色然后按角色把對(duì)應(yīng)的路由動(dòng)態(tài)加進(jìn)去。Vue Router 4 里用router.addRoute可以逐條添加。const roleRouteMap { admin: adminRoutes, teacher: teacherRoutes, student: studentRoutes } router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token) return next(/login) const role localStorage.getItem(role) if (!hasAddedRoutes) { roleRouteMap[role].forEach(r router.addRoute(r)) hasAddedRoutes true return next({ ...to, replace: true }) } next() })很多人用動(dòng)態(tài)路由時(shí)遇到刷新后菜單丟失原因是 Vuex state 在頁(yè)面刷新后會(huì)重置動(dòng)態(tài)添加的路由是沒(méi)了但代碼里沒(méi)有重新加載用戶信息的邏輯。解決方式很簡(jiǎn)單刷新后從 token 里解析角色或者向后端請(qǐng)求一次用戶信息再重新 addRoute。另外hasAddedRoutes這個(gè)標(biāo)記必須放在模塊級(jí)變量而不是 Vuex 里否則刷新后還是會(huì)丟。5.2 答題狀態(tài)管理和本地草稿備份考試頁(yè)面最怕的是什么用戶答了半小時(shí)題一個(gè)誤刷新或者網(wǎng)絡(luò)抖動(dòng)全部答案清空。所以答題記錄不能只存在組件內(nèi)部要么放 Vuex 里要么配合 localStorage 持久化。我的做法是用戶每選一道題就把答案寫(xiě)入 Vuex 對(duì)象answers同時(shí)用 localStorage 保存一份備份。開(kāi)始考試時(shí)先檢查 localStorage 里是否有未提交的答案快照有的話提示用戶恢復(fù)。提交成功后立刻清空快照。這樣即使考試中途瀏覽器崩潰重新進(jìn)入頁(yè)面也能續(xù)答考生的體驗(yàn)會(huì)好很多。倒計(jì)時(shí)部分用setInterval每秒更新剩余時(shí)間到 0 秒自動(dòng)觸發(fā)提交。這里要處理一個(gè)關(guān)鍵問(wèn)題前端倒計(jì)時(shí)不準(zhǔn)所以后端在開(kāi)考時(shí)會(huì)返回服務(wù)端時(shí)間和考試截止時(shí)間前端用時(shí)間戳差值計(jì)算剩余時(shí)長(zhǎng)而不是本地時(shí)間計(jì)算避免考生手動(dòng)改系統(tǒng)時(shí)間作弊。5.3 打包放進(jìn) SpringBoot 的兩種常見(jiàn)做法開(kāi)發(fā)調(diào)試時(shí)前端用 Vite / Webpack 的 devServer訪問(wèn)后端接口用代理配置把/api轉(zhuǎn)發(fā)到http://localhost:8080。正式部署時(shí)前端執(zhí)行構(gòu)建生成dist目錄。第一種做法是把dist整體復(fù)制到后端src/main/resources/static隨 Maven 打包進(jìn) jar訪問(wèn)同一個(gè)端口。這種方案適合內(nèi)部系統(tǒng)部署簡(jiǎn)單。第二種是前端單獨(dú)部署到 Nginx后端單獨(dú)起 jar通過(guò)反向代理轉(zhuǎn)發(fā)請(qǐng)求。更推薦第二種因?yàn)榍岸遂o態(tài)資源更新不需要重新打后端包。如果選第一種記得在vue.config.js里設(shè)置module.exports { publicPath: ./, outputDir: dist }不然靜態(tài)資源路徑會(huì)按/根路徑加載放到 jar 里打開(kāi)頁(yè)面全是白屏。6. 從零跑通這套源碼部署全流程與高頻問(wèn)題排查6.1 環(huán)境準(zhǔn)備和啟動(dòng)順序先把整套環(huán)境列出來(lái)缺哪個(gè)補(bǔ)哪個(gè)。依賴推薦版本說(shuō)明JDK8 或 11SpringBoot 2.x 都能跑Maven3.6后臺(tái)依賴管理Node.js14/16/18Vue 前端構(gòu)建MySQL5.7 或 8.0數(shù)據(jù)庫(kù)啟動(dòng)順序不要亂。第一步導(dǎo)入數(shù)據(jù)庫(kù)腳本第二步改application.yml里的數(shù)據(jù)庫(kù)用戶名密碼第三步啟動(dòng)后端第四步起來(lái)前端。后端起來(lái)后先訪問(wèn) Swagger 接口文檔或者直接登錄接口確認(rèn)數(shù)據(jù)庫(kù)連接沒(méi)問(wèn)題再折騰前端。# 后端打包啟動(dòng) mvn clean package -DskipTests java -jar target/exam-system.jar # 前端構(gòu)建 cd frontend npm install npm run build6.2 MySQL 連接報(bào)錯(cuò)集中排查鏈路數(shù)據(jù)庫(kù)連接是這套源碼報(bào)錯(cuò)最高發(fā)的位置。我踩過(guò)的典型問(wèn)題有這幾個(gè)第一個(gè)是 SSL 連接錯(cuò)誤控制臺(tái)出現(xiàn)SSL connection error。這不是數(shù)據(jù)庫(kù)壞了而是 MySQL 8 默認(rèn)要求 SSL 握手本地開(kāi)發(fā)環(huán)境不需要。JDBC URL 里加兩個(gè)參數(shù)useSSLfalseallowPublicKeyRetrievaltrue。后者尤其重要因?yàn)?MySQL 8 默認(rèn)的caching_sha2_password認(rèn)證插件在非 SSL 連接下拉取公鑰時(shí)需要顯式允許。第二個(gè)是 Windows 下 MySQL Installer 報(bào)E0434352。這基本不是 MySQL 本身的問(wèn)題而是 Windows 環(huán)境里缺少對(duì)應(yīng)版本的 .NET 桌面運(yùn)行時(shí)。處理辦法是檢查并安裝 .NET Framework 4.7.2 或更高版本再重新跑安裝程序。第三個(gè)是時(shí)區(qū)問(wèn)題日志里出現(xiàn)The server time zone value ?D1ú±ê×?ê±?? is unrecognized。URL 里加上serverTimezoneAsia/Shanghai就解決了。6.3 MyBatis 配置“文件找不到”和“駝峰不映射”的坑把源碼下載下來(lái)后最容易出現(xiàn)的問(wèn)題就是啟動(dòng)時(shí)報(bào)Invalid bound statement (not found)。這通常是 Mapper 接口和 XML 文件沒(méi)有建立對(duì)應(yīng)關(guān)系。檢查兩處Mapper 接口的包路徑要和 XML 的namespace完全一致application.yml里mybatis.mapper-locations要寫(xiě)成classpath*:mapper/*.xml注意是classpath*而不是classpath。另一個(gè)隱性坑是數(shù)據(jù)庫(kù)字段real_name映射不到realName。如果 XML 里resultType用實(shí)體類而沒(méi)有寫(xiě)resultMap一定要在 MyBatis 全局配置里開(kāi)啟mybatis: configuration: map-underscore-to-camel-case: true6.4 Vue 構(gòu)建報(bào)錯(cuò)和接口跨域問(wèn)題前端npm install如果因?yàn)榫W(wǎng)絡(luò)問(wèn)題失敗可以把 registry 換成國(guó)內(nèi)鏡像然后再裝。構(gòu)建報(bào) Node 版本不兼容時(shí)先看項(xiàng)目里用的 Vue CLI 還是 ViteVue CLI 老項(xiàng)目對(duì) Node 新版經(jīng)常會(huì)有 OpenSSL 的 hash 報(bào)錯(cuò)加一句N(xiāo)ODE_OPTIONS--openssl-legacy-provider能臨時(shí)繞過(guò)去。前端聯(lián)調(diào)接口最容易碰到跨域。開(kāi)發(fā)環(huán)境用 devServer 的 proxy 代理生產(chǎn)環(huán)境用 Nginx 的反向代理。Nginx 配置時(shí)注意把/api/的請(qǐng)求轉(zhuǎn)發(fā)到后端端口location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }如果 Vue Router 用的是 history 模式刷新頁(yè)面時(shí) Nginx 還要配try_files $uri $uri/ /index.html;否則會(huì)出現(xiàn)刷新后 404 的詭異問(wèn)題。7. 從“能跑”到“能用”這套源碼值得優(yōu)先擴(kuò)展的方向源碼拿到手能啟動(dòng)只是第一步。真正要落地到企業(yè)場(chǎng)景我建議優(yōu)先做這幾件事。第一把題庫(kù)隨機(jī)組卷從ORDER BY RAND()改成內(nèi)存打散或者加一張題號(hào)索引表做隨機(jī)偏移。這是最影響考試創(chuàng)建時(shí)性能的一個(gè)點(diǎn)。第二給答題明細(xì)和考試成績(jī)?cè)黾赢惒綄?dǎo)出。成績(jī)單導(dǎo)出很容易做成同步接口數(shù)據(jù)量一大前端等超時(shí)很常見(jiàn)。簡(jiǎn)單做法是導(dǎo)出任務(wù)落到表里后臺(tái)線程生成 Excel考生端點(diǎn)導(dǎo)出時(shí)返回“正在生成”的狀態(tài)再輪詢獲取下載鏈接。第三如果要支持聽(tīng)力題、視頻題這類資源型題目可以把音視頻文件放到 MinIO 這樣的對(duì)象存儲(chǔ)里SpringBoot 只管生成臨時(shí)訪問(wèn)憑證前端用播放器拉流。題目表里增加一個(gè)resource_url字段考試時(shí)考生端根據(jù)資源類型渲染音頻或視頻播放器和純文字題共用一套答題記錄邏輯擴(kuò)展成本很低。第四并發(fā)量上來(lái)以后緩存是繞不開(kāi)的。考試基礎(chǔ)信息、已發(fā)布試卷這類讀多寫(xiě)少的數(shù)據(jù)可以放 Redis但考生答案和成績(jī)絕不能只寫(xiě) Redis 不回寫(xiě) MySQL必須以數(shù)據(jù)庫(kù)為準(zhǔn)。這套源碼最適合的用法不是我說(shuō)的這些功能它都有而是它把考試系統(tǒng)里最穩(wěn)定的那套骨架搭好了你這個(gè)拿到手的人可以在上面很清晰地做取舍和替換。我實(shí)際部署中最常遇到的情況反而是很多人先急著改頁(yè)面結(jié)果后端連數(shù)據(jù)庫(kù)都沒(méi)連上。所以最后一句話還是那句老話先跑通再架構(gòu)先看表再改碼。把數(shù)據(jù)庫(kù)腳本和最小閉環(huán)跑起來(lái)比什么都重要。