卷調(diào)查系統(tǒng)設(shè)計(jì)與實(shí)現(xiàn))
簡(jiǎn)介一份基于Spring Boot與Vue的在線問(wèn)卷調(diào)查系統(tǒng)畢業(yè)設(shè)計(jì)論文文檔面向計(jì)算機(jī)相關(guān)專業(yè)畢業(yè)生及需要完成類似課題的開(kāi)發(fā)者提供從需求分析、系統(tǒng)設(shè)計(jì)到編碼實(shí)現(xiàn)、測(cè)試評(píng)估的完整論述。論文核心解決問(wèn)卷調(diào)查信息管理不規(guī)范、效率低等問(wèn)題涵蓋問(wèn)卷管理、用戶管理、題目管理、問(wèn)卷調(diào)查管理、新聞資訊管理等模塊并使用MySQL數(shù)據(jù)庫(kù)、Java語(yǔ)言及Spring Boot框架完成技術(shù)落地。資源為1個(gè)doc文檔容量約4.04MB包含完整論文正文方便查閱、修改與二次編輯。目前已有198人瀏覽學(xué)習(xí)適合用于畢業(yè)設(shè)計(jì)參考、論文結(jié)構(gòu)梳理或技術(shù)方案借鑒。文檔內(nèi)含系統(tǒng)架構(gòu)說(shuō)明、數(shù)據(jù)庫(kù)設(shè)計(jì)思路、后端API與前端頁(yè)面開(kāi)發(fā)要點(diǎn)還能了解系統(tǒng)測(cè)試與穩(wěn)定性評(píng)估方法為獨(dú)立完成類似項(xiàng)目提供有力支撐。1. 在線問(wèn)卷調(diào)查系統(tǒng)從畢業(yè)設(shè)計(jì)到可落地的 Spring Boot Vue 全棧項(xiàng)目問(wèn)卷系統(tǒng)的難點(diǎn)從來(lái)不在「能填」而在「怎么設(shè)計(jì)題目、怎么控制發(fā)布狀態(tài)、怎么保證一次作答不重復(fù)提交」。這套基于 Spring Boot Vue 的在線問(wèn)卷調(diào)查系統(tǒng)覆蓋了問(wèn)卷管理、題目管理、用戶管理、問(wèn)卷調(diào)查記錄和新聞資訊模塊是一個(gè)完整的前后端分離案例。后端用 Spring Boot 提供 REST API前端用 Vue 做頁(yè)面交互數(shù)據(jù)落在 MySQL適合正在做 Java 方向畢業(yè)設(shè)計(jì)的人也適合想快速搭一套內(nèi)部調(diào)研工具的開(kāi)發(fā)者。它的表結(jié)構(gòu)設(shè)計(jì)——問(wèn)卷表、題目表、答卷記錄表——足夠簡(jiǎn)潔稍加擴(kuò)展就能接進(jìn)真實(shí)業(yè)務(wù)。下面從數(shù)據(jù)模型開(kāi)始拆解這套系統(tǒng)的實(shí)現(xiàn)路徑。2. 數(shù)據(jù)模型先行問(wèn)卷、題目與答卷記錄的表結(jié)構(gòu)設(shè)計(jì)在線問(wèn)卷調(diào)查系統(tǒng)的核心不在頁(yè)面而在數(shù)據(jù)庫(kù)里那幾張表的關(guān)系。問(wèn)卷與題目是一對(duì)多問(wèn)卷與答卷記錄是一對(duì)多用戶與答卷記錄也是一對(duì)多。這三層關(guān)系理順了后面寫(xiě)接口和頁(yè)面都會(huì)順暢很多。2.1 問(wèn)卷表狀態(tài)、時(shí)長(zhǎng)與結(jié)束語(yǔ)的設(shè)計(jì)問(wèn)卷表對(duì)應(yīng)實(shí)體是「問(wèn)卷」字段在論文中給出了明確設(shè)計(jì)建表 SQL 如下CREATE TABLE exampaper ( id int(20) NOT NULL AUTO_INCREMENT COMMENT 主鍵, exampaper_name varchar(200) NOT NULL COMMENT 問(wèn)卷名稱, exampaper_date int(11) NOT NULL COMMENT 時(shí)長(zhǎng)分鐘, exampaper_jieshuyu varchar(255) DEFAULT NULL COMMENT 結(jié)束語(yǔ), exampaper_state tinyint(1) DEFAULT 0 COMMENT 問(wèn)卷狀態(tài)0-草稿 1-發(fā)布 2-關(guān)閉, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 創(chuàng)建時(shí)間, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT問(wèn)卷表;字段里值得注意的有兩個(gè)。exampaper_state用 tinyint 而不是 varchar 存狀態(tài)是為了方便做狀態(tài)流轉(zhuǎn)判斷后端只比對(duì)數(shù)值即可exampaper_jieshuyu允許為空因?yàn)閱?wèn)卷創(chuàng)建初期不需要立刻填寫(xiě)結(jié)束語(yǔ)可以發(fā)布前再補(bǔ)。exampaper_date設(shè)定的時(shí)長(zhǎng)不是計(jì)時(shí)上限而是提醒閾值——用戶進(jìn)入問(wèn)卷后前端根據(jù)這個(gè)值啟動(dòng)倒計(jì)時(shí)提醒但不強(qiáng)制交卷避免數(shù)據(jù)丟失。create_time用數(shù)據(jù)庫(kù)默認(rèn)值生成后端插入時(shí)不需要手動(dòng)賦值。2.2 題目表與問(wèn)卷調(diào)查記錄表一對(duì)多關(guān)系落到字段上題目表在設(shè)計(jì)上必須攜帶所屬問(wèn)卷的 ID否則無(wú)法建立關(guān)聯(lián)。問(wèn)卷調(diào)查記錄表則是每次作答的「流水賬」記錄誰(shuí)在什么時(shí)間答了哪份問(wèn)卷。兩張表的設(shè)計(jì)如下CREATE TABLE exampaper_question ( id int(20) NOT NULL AUTO_INCREMENT COMMENT 題目ID, exampaper_id int(20) NOT NULL COMMENT 所屬問(wèn)卷ID, question_name varchar(255) NOT NULL COMMENT 試題名稱, question_type tinyint(1) DEFAULT 1 COMMENT 試題類型1-單選 2-多選 3-填空, question_option text COMMENT 選項(xiàng)內(nèi)容用JSON數(shù)組存儲(chǔ), create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_exampaper_id (exampaper_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT題目表; CREATE TABLE exampaper_record ( id int(20) NOT NULL AUTO_INCREMENT COMMENT 問(wèn)卷調(diào)查記錄ID, record_no varchar(32) DEFAULT NULL COMMENT 問(wèn)卷調(diào)查編號(hào), user_id int(20) NOT NULL COMMENT 問(wèn)卷調(diào)查用戶ID, exampaper_id int(20) NOT NULL COMMENT 所屬問(wèn)卷ID, answer_content text COMMENT 答卷內(nèi)容JSON格式存儲(chǔ), create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT問(wèn)卷調(diào)查記錄表;題目的question_option用 text 存 JSON 數(shù)組比單獨(dú)建一張選項(xiàng)表更省事。問(wèn)卷系統(tǒng)的題目選項(xiàng)一般是固定幾個(gè)文本不需要對(duì)選項(xiàng)做單獨(dú)檢索或統(tǒng)計(jì)JSON 存儲(chǔ)能減少一次關(guān)聯(lián)查詢。答題記錄表的record_no是業(yè)務(wù)編號(hào)格式可以定為「問(wèn)卷ID 用戶ID 時(shí)間戳」用于前端展示查詢answer_content存放用戶提交的答案映射格式為{題目ID: 答答案}這樣無(wú)論題目怎么增刪記錄結(jié)構(gòu)都不用變。2.3 用戶表最小集認(rèn)證所需字段與擴(kuò)展邊界用戶表在論文實(shí)體屬性中給出了較多字段——賬戶、密碼、姓名、性別、手機(jī)號(hào)、照片、身份證號(hào)。實(shí)際做系統(tǒng)時(shí)身份證號(hào)這類敏感字段能不放就不放尤其畢設(shè)階段沒(méi)必要引入額外合規(guī)負(fù)擔(dān)。建議保留核心字段CREATE TABLE sys_user ( id int(20) NOT NULL AUTO_INCREMENT COMMENT 用戶ID, username varchar(50) NOT NULL COMMENT 賬戶, password varchar(100) NOT NULL COMMENT 密碼BCrypt加密, real_name varchar(50) DEFAULT NULL COMMENT 用戶姓名, sex varchar(10) DEFAULT NULL COMMENT 性別, phone varchar(20) DEFAULT NULL COMMENT 手機(jī)號(hào), avatar varchar(255) DEFAULT NULL COMMENT 照片, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用戶表;密碼字段長(zhǎng)度設(shè)為 100是因?yàn)?Spring Security 的 BCrypt 加密結(jié)果長(zhǎng)度約為 60 字符如果按直覺(jué)設(shè)成 varchar(50)加密后寫(xiě)入會(huì)報(bào)數(shù)據(jù)過(guò)長(zhǎng)錯(cuò)誤。username加唯一索引這是登錄查詢的命脈字段創(chuàng)建時(shí)就要預(yù)防重復(fù)注冊(cè)。3. Spring Boot 后端實(shí)現(xiàn)Controller、Service 與 MyBatis 的工程落地后端部分的核心不是代碼量而是三層結(jié)構(gòu)的清晰度。Controller 只接收參數(shù)和返回結(jié)果Service 處理業(yè)務(wù)邏輯Mapper 層負(fù)責(zé) SQL 操作。以問(wèn)卷管理模塊為例把這套結(jié)構(gòu)完整走一遍。3.1 問(wèn)卷管理接口分頁(yè)查詢與狀態(tài)過(guò)濾問(wèn)卷列表頁(yè)最常見(jiàn)的需求是分頁(yè)展示并支持按狀態(tài)篩選。Controller 層代碼RestController RequestMapping(/api/exampaper) public class ExampaperController { Autowired private ExampaperService exampaperService; GetMapping(/page) public Result page(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) String exampaperName, RequestParam(required false) Integer exampaperState) { PageInfoExampaper pageInfo exampaperService.findPage(pageNum, pageSize, exampaperName, exampaperState); return Result.success(pageInfo); } }Service 層對(duì)應(yīng)實(shí)現(xiàn)Service public class ExampaperServiceImpl implements ExampaperService { Autowired private ExampaperMapper exampaperMapper; Override public PageInfoExampaper findPage(Integer pageNum, Integer pageSize, String exampaperName, Integer exampaperState) { PageHelper.startPage(pageNum, pageSize); ExampaperQuery query new ExampaperQuery(); query.setExampaperName(exampaperName); query.setExampaperState(exampaperState); ListExampaper list exampaperMapper.selectByCondition(query); return new PageInfo(list); } }PageHelper.startPage(pageNum, pageSize)是 MyBatis 分頁(yè)插件的入口寫(xiě)在這行之后的第一個(gè)查詢會(huì)自動(dòng)追加 limit 語(yǔ)句。這里有個(gè)常見(jiàn)的坑startPage 必須緊跟查詢語(yǔ)句中間不能插入其他查詢操作否則分頁(yè)會(huì)作用到錯(cuò)誤的 SQL 上。exampaperName和exampaperState都允許為空查詢條件里要做動(dòng)態(tài) SQL 判斷避免空值參與拼接導(dǎo)致查不到數(shù)據(jù)。3.2 新增與編輯參數(shù)校驗(yàn)和狀態(tài)判斷創(chuàng)建問(wèn)卷時(shí)只保存名稱、時(shí)長(zhǎng)和狀態(tài)字段題目先不處理。這里關(guān)鍵在于狀態(tài)機(jī)的約束——只有處于草稿狀態(tài)的問(wèn)卷才能編輯題目和基礎(chǔ)信息已發(fā)布的問(wèn)卷只允許關(guān)閉不允許再改結(jié)構(gòu)。PostMapping(/save) public Result save(RequestBody Exampaper exampaper) { if (exampaper.getId() null) { exampaper.setExampaperState(0); exampaperMapper.insert(exampaper); } else { Exampaper dbPaper exampaperMapper.selectById(exampaper.getId()); if (dbPaper.getExampaperState() ! 0) { return Result.error(當(dāng)前狀態(tài)不允許編輯); } exampaperMapper.updateById(exampaper); } return Result.success(); }新增和編輯共用一個(gè)接口通過(guò)id是否為空來(lái)區(qū)分。如果前端傳入的 id 和數(shù)據(jù)庫(kù)中的狀態(tài)不一致——比如數(shù)據(jù)庫(kù)里已經(jīng)是發(fā)布狀態(tài)但請(qǐng)求試圖修改問(wèn)卷名稱——后端必須攔截只靠前端按鈕隱藏沒(méi)有任何意義。3.3 題目批量保存事務(wù)與級(jí)聯(lián)刪除編輯問(wèn)卷題目時(shí)常見(jiàn)的做法是前端一次提交全部題目后端先刪后插。先按問(wèn)卷 ID 刪除舊題目再批量插入新題目。這個(gè)操作必須加Transactional否則刪除成功但插入失敗時(shí)題庫(kù)會(huì)變成空表對(duì)用戶來(lái)說(shuō)等于問(wèn)卷的題全丟了。Transactional(rollbackFor Exception.class) public void saveQuestions(Long exampaperId, ListQuestion questions) { exampaperQuestionMapper.deleteByExampaperId(exampaperId); for (Question q : questions) { q.setExampaperId(exampaperId); exampaperQuestionMapper.insert(q); } }rollbackFor Exception.class指定了異常即回滾不加這個(gè)參數(shù)的話Spring 默認(rèn)只在運(yùn)行時(shí)異常時(shí)回滾檢查異常不會(huì)觸發(fā)回滾數(shù)據(jù)就可能處在不一致?tīng)顟B(tài)。批量插入也可以在 Mapper 里用 foreach 實(shí)現(xiàn)減少數(shù)據(jù)庫(kù)連接往返次數(shù)。3.4 登錄與權(quán)限攔截器判斷管理員與普通用戶系統(tǒng)分了管理員和普通用戶兩個(gè)角色最簡(jiǎn)單的實(shí)現(xiàn)方式是攔截器 角色標(biāo)記位不需要把 Spring Security 全套引入畢設(shè)或小型系統(tǒng)這樣足夠清晰Component public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (request.getMethod().equals(OPTIONS)) { return true; } HttpSession session request.getSession(); User loginUser (User) session.getAttribute(loginUser); if (loginUser null) { response.setStatus(401); return false; } return true; } }Controller 中通過(guò)request.getSession().getAttribute(loginUser)判斷當(dāng)前用戶角色管理員接口再校驗(yàn)user.getRole()是否為管理員。這種方案的取舍是非登錄用戶會(huì)被統(tǒng)一攔截到 401前端 axios 響應(yīng)攔截器里捕獲 401 后跳轉(zhuǎn)登錄頁(yè)但登錄頁(yè)本身要放行否則會(huì)死循環(huán)。4. Vue 前端實(shí)現(xiàn)問(wèn)卷列表到答題頁(yè)的路由與狀態(tài)管理前端的核心頁(yè)面是問(wèn)卷列表、答題頁(yè)面和答卷記錄查看。Vue 2 的 Options API 寫(xiě)法在這類項(xiàng)目里最常用代碼結(jié)構(gòu)直觀生態(tài)資料也多網(wǎng)上搜到的組件和插件基本都兼容。4.1 問(wèn)卷列表頁(yè)axios 請(qǐng)求與分頁(yè)渲染問(wèn)卷列表頁(yè)根據(jù)登錄角色的不同展示不同功能——管理員看到的是「編輯」「發(fā)布」「關(guān)閉」操作按鈕普通用戶看到的是「參與答題」。頁(yè)面初始化時(shí)調(diào)用分頁(yè)接口export default { data() { return { pageNum: 1, pageSize: 10, total: 0, exampaperList: [], exampaperName: , exampaperState: undefined }; }, created() { this.loadPage(); }, methods: { async loadPage() { const res await axios.get(/api/exampaper/page, { params: { pageNum: this.pageNum, pageSize: this.pageSize, exampaperName: this.exampaperName, exampaperState: this.exampaperState } }); this.exampaperList res.data.data.list; this.total res.data.data.total; } } };params傳參會(huì)自動(dòng)進(jìn)行 URL 編碼不需要手動(dòng)拼接查詢字符串。created鉤子里調(diào)用加載方法確保頁(yè)面渲染前數(shù)據(jù)已經(jīng)就緒。分頁(yè)組件切換頁(yè)碼時(shí)把pageNum改成目標(biāo)值并再次調(diào)用loadPage()。注意不要在data里把exampaperState定義為空字符串否則請(qǐng)求參數(shù)會(huì)帶上exampaperState后端接收時(shí)可能因類型轉(zhuǎn)換報(bào)錯(cuò)。4.2 答題頁(yè)動(dòng)態(tài)渲染根據(jù)題目類型加載組件答題頁(yè)拿到問(wèn)卷 ID 后調(diào)用題目查詢接口獲取題目列表。題目類型是單選、多選、填空三種前端使用v-if判斷組件類型template div v-for(question, index) in questionList :keyquestion.id div classquestion-title{{ index 1 }}. {{ question.questionName }}/div el-radio-group v-ifquestion.questionType 1 v-modelanswers[question.id] el-radio v-foropt in question.optionList :keyopt :labelopt{{ opt }}/el-radio /el-radio-group el-checkbox-group v-else-ifquestion.questionType 2 v-modelanswers[question.id] el-checkbox v-foropt in question.optionList :keyopt :labelopt{{ opt }}/el-checkbox /el-checkbox-group el-input v-else-ifquestion.questionType 3 v-modelanswers[question.id] typetextarea placeholder請(qǐng)輸入你的回答/el-input /div /templateanswers是用題目 id 做 key 的對(duì)象answers {}初始化運(yùn)行時(shí)根據(jù)題目 id 動(dòng)態(tài)添加屬性。這樣設(shè)計(jì)的好處是提交時(shí)直接把整個(gè)answers對(duì)象序列化成 JSON后端存進(jìn)answer_content字段不需要前端額外組裝。4.3 答題時(shí)長(zhǎng)控制與提交倒計(jì)時(shí)與防重復(fù)時(shí)長(zhǎng)提醒放在問(wèn)卷頁(yè)的路由守衛(wèi)里beforeRouteLeave(to, from, next) { if (this.timer) { clearInterval(this.timer); this.timer null; } next(); }進(jìn)入問(wèn)卷時(shí)讀取exampaperDate字段啟動(dòng)一個(gè) setInterval 倒計(jì)時(shí)頁(yè)面離開(kāi)時(shí)清除定時(shí)器避免內(nèi)存泄漏。提交時(shí)把問(wèn)卷 ID 和答案對(duì)象一起傳給后端后端檢查該用戶是否有這條問(wèn)卷的已提交記錄如果存在則拒絕保存并提示「請(qǐng)勿重復(fù)提交」。5. 發(fā)布前驗(yàn)證用測(cè)試用例和服務(wù)狀態(tài)機(jī)守住問(wèn)卷的可用性系統(tǒng)寫(xiě)完到交付之間最有價(jià)值的驗(yàn)證方式不是對(duì)著頁(yè)面點(diǎn)點(diǎn)點(diǎn)而是把關(guān)鍵流程整理成可執(zhí)行的測(cè)試用例同時(shí)把問(wèn)卷的狀態(tài)流轉(zhuǎn)邏輯明確到代碼里防止后面接手的人把狀態(tài)改亂。5.1 測(cè)試場(chǎng)景設(shè)計(jì)登錄、答題、結(jié)果校驗(yàn)下面是一組覆蓋率較高的測(cè)試用例表按照這個(gè)順序去做基本能跑通日常路徑用例編號(hào)測(cè)試場(chǎng)景操作步驟預(yù)期結(jié)果TC-01管理員登錄輸入正確賬號(hào)密碼跳轉(zhuǎn)后臺(tái)首頁(yè)顯示問(wèn)卷管理菜單TC-02管理員創(chuàng)建問(wèn)卷填寫(xiě)名稱及時(shí)長(zhǎng)點(diǎn)擊保存列表出現(xiàn)「草稿」?fàn)顟B(tài)的新問(wèn)卷TC-03管理員編輯題目進(jìn)入草稿問(wèn)卷添加單選/多選/填空各一題題目數(shù)顯示為 3重復(fù)進(jìn)入后數(shù)據(jù)仍在TC-04管理員發(fā)布問(wèn)卷點(diǎn)擊發(fā)布按鈕用戶端問(wèn)卷列表可見(jiàn)該問(wèn)卷TC-05用戶提交問(wèn)卷填寫(xiě)全部題目后點(diǎn)提交跳轉(zhuǎn)完成頁(yè)顯示結(jié)束語(yǔ)「感謝參與」TC-06用戶重復(fù)提交再次進(jìn)入該問(wèn)卷提交答案后端返回「請(qǐng)勿重復(fù)提交」不寫(xiě)入新記錄TC-07用戶查看記錄進(jìn)入「問(wèn)卷調(diào)查記錄」菜單展示歷史提交時(shí)間及問(wèn)卷名稱測(cè)試中要注意「編輯題目后發(fā)布」的順序問(wèn)題。發(fā)布完成后再去修改題目用戶端正在作答的問(wèn)卷就會(huì)出現(xiàn)題目錯(cuò)位——用戶已經(jīng)答了第 1 題后端數(shù)據(jù)庫(kù)中第 1 題的名稱變了但答案 ID 對(duì)應(yīng)的還是舊題目。這就是為什么要在后端鎖死編輯權(quán)限只允許修改草稿狀態(tài)的問(wèn)卷。壓測(cè)場(chǎng)景下問(wèn)卷通常不要求極高的并發(fā)如果預(yù)計(jì)參與人數(shù)破千數(shù)據(jù)庫(kù)索引的重點(diǎn)放在exampaper_record表的exampaper_id和user_id聯(lián)合索引上防止重復(fù)提交校驗(yàn)的查詢拖慢寫(xiě)入。5.2 狀態(tài)機(jī)的邊界處理草稿、發(fā)布到關(guān)閉的生命周期問(wèn)卷狀態(tài)不能只是「改個(gè)數(shù)字」要設(shè)計(jì)成不可逆的狀態(tài)流轉(zhuǎn)。從草稿到發(fā)布允許撤回草稿但保留原答案從發(fā)布到關(guān)閉拒絕編輯和刪除只保留查看統(tǒng)計(jì)的權(quán)限。用枚舉管理狀態(tài)字段的語(yǔ)義public enum ExampaperState { DRAFT(0, 草稿), PUBLISHED(1, 發(fā)布), CLOSED(2, 關(guān)閉); private final int code; private final String desc; ExampaperState(int code, String desc) { this.code code; this.desc desc; } }實(shí)際開(kāi)發(fā)時(shí)把枚舉替換成「狀態(tài)常量類」也能運(yùn)行但枚舉的好處是編譯期就能發(fā)現(xiàn)狀態(tài)值寫(xiě)錯(cuò)的問(wèn)題。關(guān)閉問(wèn)卷后用戶端列表不再展示「參與答題」入口已提交的答卷保留在記錄表中管理員仍然能查看結(jié)果。這套生命周期設(shè)計(jì)解決了在線問(wèn)卷最容易出現(xiàn)的「已經(jīng)收集了數(shù)據(jù)卻不小心被改壞」的故障。寫(xiě)完后用 TC-04 到 TC-07 跑一遍再手動(dòng)驗(yàn)證一下「發(fā)布后試圖編輯」的攔截是否正確這份系統(tǒng)就可以從畢設(shè)演示平滑過(guò)渡到小范圍的正式使用了。本文還有配套的精品資源點(diǎn)擊獲取