ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot+Vue问卷系统实战:从课设到生产级落地

SpringBoot+Vue问卷系统实战:从课设到生产级落地 简介本资源是一套面向Java初学者与课程设计实践者的完整问卷调查系统开发案例适用于高校JavaWeb、SpringBoot或前后端分离综合实训场景。系统采用SpringBootVue技术栈实现前后端分离架构覆盖管理员、租户、用户三类角色管理支持问卷可视化设计、实时预览、多渠道发布及基础数据统计分析具备教学示范性与工程可拓展性。压缩包共394个文件含57个Java后端逻辑文件、18个Vue前端组件、48个JS交互脚本、131个XML配置与SQL脚本等核心内容包体大小为19.13MB结构清晰、模块职责分明。目前已有452人学习下载资源附带完整数据库SQL脚本含密码加密实现、application.yml配置说明及Axure原型相关CSS样式文件便于理解权限设计、前后端联调要点与界面响应式实现逻辑。1. 为什么一个「问卷调查系统」要硬套 SpringBoot Vue——它真不是练手玩具而是企业级轻量数据采集的最小闭环你打开这个压缩包看到java课程设计基于SpringBootVue的问卷调查系统源码数据库.zip第一反应可能是又一个学生课设CtrlC/V 的 CRUD 模板但如果你真把它当练手项目扔进回收站就错过了 Java 全栈开发中最贴近真实业务、最容易验证工程能力、也最容易被面试官深挖细节的典型场景。这不是写个登录注册就完事的 Demo——它天然包含动态表单渲染前端 Vue 动态生成题型、结构化数据建模单选/多选/填空/矩阵题在 DB 如何存、状态流转控制未发布/已发布/已关闭/已过期、权限隔离创建者/填写者/管理员、导出与统计Excel 导出 简单聚合查询甚至隐含了高并发下的提交幂等性问题同一用户重复提交怎么拦。我带过的实习生里80% 在简历上写“做过问卷系统”但能说清「为什么用 Vue 的 v-model.lazy 绑定文本题而非实时监听」「为什么 SpringBoot 的 Validated 要分组校验」「MySQL 中 question_options 表为什么要冗余 question_id 和 sort_order 字段」的人不到 5%。这篇笔记不讲 PPT 式架构图只带你从解压 zip 开始一行行跑通、调参、改 bug、压测边界——把这套课设代码真正变成你本地可调试、可扩展、可讲清楚技术决策的「最小生产级问卷基座」。2. 从解压到启动SpringBoot 后端服务的三步落地与关键配置项拆解2.1 解压后第一眼该盯什么——识别项目骨架与依赖真实性拿到xxx.zip后不要急着mvn clean install。先解压用 IDE推荐 IntelliJ IDEA以 Maven 项目方式导入重点看三个文件pom.xml确认parent是否指向spring-boot-starter-parent且版本号在2.7.18LTS或3.2.x新特性范围内检查dependencies中是否含spring-boot-starter-web、spring-boot-starter-data-jpa或mybatis-spring-boot-starter、spring-boot-starter-validation—— 缺一不可。若出现spring-boot-starter-thymeleaf却没用到 HTML 模板大概率是旧版残留可删。application.yml或.properties这是你后续所有踩坑的源头。重点关注spring.datasource.url是否为jdbc:mysql://localhost:3306/survey_db?useSSLfalseserverTimezoneAsia/Shanghai这类带时区和 SSL 关闭的完整串spring.jpa.hibernate.ddl-auto值必须是update开发或validate上线前绝不能是create会清库server.port默认8080但若你本机已被占用必须改——别指望前端自动适配。src/main/resources/static/目录是否存在index.html若存在且内容是div idapp/div说明前端已打包进后端静态资源目录即「Vue 打包放进 SpringBoot 中」这种部署模式此时后端启动即提供完整页面无需单独启 Vue 服务。提示若static/下为空但存在src/main/frontend/或vue-project/子目录则是前后端分离结构需先cd vue-project npm install npm run serve启 Vue 开发服务器再启动 SpringBoot。本篇默认按「前后端分离」处理更贴近企业实际因热词中明确出现vue安装及环境配置、vue打包放进springboot中说明用户对两种模式均有诉求。2.2 后端启动前必做的三件事数据库初始化、JPA 实体映射校验、端口冲突排查1建库与初始化 SQL 执行解压包内通常含sql/survey_init.sql或database/文件夹。用 MySQL 客户端执行CREATE DATABASE survey_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 然后 source survey_init.sql注意SQL 文件中若含DROP TABLE IF EXISTS务必确认这是首次部署若已有数据应手动注释掉 DROP 行仅保留CREATE TABLE和INSERT。常见翻车点是survey_question表的type字段定义为VARCHAR(20)但代码中枚举值写了SINGLE_CHOICE13 字符而MULTIPLE_CHOICE是 16 字符——超长会报Data truncation错误需手动扩为VARCHAR(32)。2JPA 实体类与数据库字段对齐检查打开SurveyQuestion.java逐行比对Column(name title)中的name是否与 SQL 中title VARCHAR(255)字段名完全一致大小写敏感MySQL 默认小写。特别注意时间字段若数据库用DATETIME实体类必须用LocalDateTimeColumn(columnDefinition datetime)若用TIMESTAMP则需Column(columnDefinition timestamp default CURRENT_TIMESTAMP)并配CreatedDate注解。漏掉columnDefinition会导致 Hibernate 自动生成 DDL 时类型错配。3端口与 Profile 冲突预检在application.yml中加spring: profiles: active: dev --- spring: config: activate: on-profile: dev server: port: 8081 # 显式指定避免 8080 被占 logging: level: com.example.survey: DEBUG然后在 IDEA 的 Run Configuration 中VM options 加-Dspring.profiles.activedev。这样启动日志会清晰显示Started SurveyApplication in X seconds (JVM running for Y)且DEBUG级别能捕获 JPA 初始化时的 SQL 打印验证表是否真建成功。2.3 启动失败的三大高频原因与速查命令现象原因解决Failed to determine a suitable driver classpom.xml中 MySQL 驱动版本与 SpringBoot 版本不兼容如 SB 3.x 需mysql:mysql-connector-java:8.0.33SB 2.7.x 用8.0.28查mvn dependency:tree | grep mysql强制指定version并排除旧版传递依赖Cannot determine embedded database driver class for database type NONEapplication.yml中spring.datasource.url为空或格式错误如少jdbc:mysql://前缀用echo $SPRING_DATASOURCE_URL检查环境变量是否覆盖了配置或直接在 YAML 中写死 URLField xxx in xxx required a bean of type yyy that could not be foundService 层类缺少Service注解或 Mapper 接口没加MapperMyBatis或没被MapperScan扫描到在启动类上加MapperScan(com.example.survey.mapper)并确认 Mapper 接口路径与扫描路径一致3. Vue 前端启动与路由配置从npm run serve到跨域代理的真实链路3.1 Vue 环境配置避坑Node.js 版本、依赖锁与vue.config.js的核心作用热词中高频出现vue安装及环境配置说明新手极易卡在这步。不要用 Node.js 19 或 20—— 当前主流 Vue 2.6/2.7 项目课设多为此版本与 Node.js 18.x 兼容性最佳。验证命令node -v # 应输出 v18.18.2 类似 npm -v # 应输出 9.8.1若版本不符用nvm use 18切换Windows 用户装nvm-windows。接着进入 Vue 项目根目录非 SpringBoot 根目录执行npm install --legacy-peer-deps原因Vue 2 项目依赖vue-template-compiler与vue版本必须严格一致如都是2.7.16而新版 npm 会拒绝安装 peerDependencies 冲突的包--legacy-peer-deps强制忽略此检查。关键文件vue.config.js必须存在且含module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:8081, // 指向 SpringBoot 后端 changeOrigin: true, pathRewrite: { ^/api: // 把 /api/question → /question } } } } }这是解决「前端请求 8080 端口后端在 8081浏览器同源策略拦截」的唯一正解。若缺失此配置你会看到浏览器 Network 面板中所有/api/**请求标红Status 为(failed) net::ERR_CONNECTION_REFUSED。3.2 路由与状态管理为什么vue-router的beforeEach是问卷流程控制的核心问卷系统本质是状态机未登录 → 登录页 → 问卷列表 → 问卷详情未开始/进行中/已提交→ 结果页。router/index.js中必须有守卫router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) } else if (to.path /survey/:id !token) { // 未登录用户访问问卷页跳转登录并携带原路径 next(/login?redirect${to.fullPath}) } else { next() } })注意to.meta.requiresAuth需在路由定义中显式声明{ path: /survey/:id, name: SurveyDetail, component: () import(/views/SurveyDetail.vue), meta: { requiresAuth: true } // 关键 }若漏掉meta守卫失效未登录用户可直击/survey/123—— 这是课设代码中最常见的安全漏洞。3.3 动态表单渲染Vue 如何根据后端返回的question.type渲染不同题型SurveyDetail.vue中核心逻辑template div v-forq in questions :keyq.id div v-ifq.type SINGLE_CHOICE label{{ q.title }}/label div v-foropt in q.options :keyopt.id input typeradio :valueopt.id v-modelanswers[q.id] {{ opt.content }} /div /div div v-else-ifq.type TEXT_INPUT label{{ q.title }}/label input typetext v-model.lazyanswers[q.id] !-- .lazy 防止输入抖动 -- /div /div /template script export default { data() { return { answers: {} // key: questionId, value: answerValue } } } /script这里v-model.lazy是血泪经验文本题若用v-model实时绑定每敲一个字都触发一次响应式更新长问卷下性能骤降.lazy改为失焦时更新体验更稳。而单选题用v-model绑定answers[q.id]值为选项 ID提交时只需Object.entries(answers).map(([qid, aid]) ({ questionId: qid, answerId: aid }))即可构造标准提交体。4. 数据库设计与 MyBatis 映射为什么「一张问卷多个题型」不能全塞进一个表4.1 问卷核心四张表的关系与范式取舍课设常见反模式把所有题型答案存进survey_answer表的content字段TEXT 类型导致无法索引、无法统计、无法关联选项。正确设计必须满足第三范式表名主键关键字段说明surveyidtitle, description, status, start_time, end_time问卷主表status 枚举DRAFT/PUBLISHED/CLOSEDsurvey_questionidsurvey_id, title, type, sort_order题目表type 枚举SINGLE_CHOICE/MULTIPLE_CHOICE/TEXT_INPUT/RATINGsurvey_optionidquestion_id, content, sort_order选项表仅 SINGLE/MULTIPLE 题型有数据survey_submissionidsurvey_id, user_id, submit_time提交记录表user_id 可为空匿名问卷survey_answeridsubmission_id, question_id, option_id, text_content答案表option_id 与 text_content 二选一用 NULLABLE 控制注意survey_answer.option_id和survey_answer.text_content必须设为NULLABLE因为单选题存option_id文本题存text_content二者互斥。MyBatis 的Select语句需用choose动态拼 SQLinsert idinsertAnswer INSERT INTO survey_answer (submission_id, question_id, option_id, text_content) VALUES (#{submissionId}, #{questionId}, choose when testoptionId ! null#{optionId}, NULL/when otherwiseNULL, #{textContent}/otherwise /choose) /insert4.2 MyBatis 多表联查如何一条 SQL 查出「问卷标题 题目 选项 提交数」统计页需展示每道题的选项选择人数。传统做法查题目 → 循环查每个题的选项 → 再循环查每个选项的提交数。N1 查询灾难。正确写法用LEFT JOIN一次查出select idselectQuestionStats resultTypemap SELECT q.id AS question_id, q.title AS question_title, o.id AS option_id, o.content AS option_content, COUNT(a.id) AS submit_count FROM survey_question q LEFT JOIN survey_option o ON q.id o.question_id LEFT JOIN survey_answer a ON o.id a.option_id AND a.question_id q.id WHERE q.survey_id #{surveyId} GROUP BY q.id, o.id, o.content ORDER BY q.sort_order, o.sort_order /select关键点GROUP BY必须包含所有非聚合字段q.id,o.id,o.content否则 MySQL 8.0 会报错COUNT(a.id)统计的是a.id非 NULL 的行数LEFT JOIN保证即使无提交o.id和o.content仍存在submit_count为 0。4.3 避坑MySQL 时区、字符集与外键约束的三重陷阱现象原因解决start_time存入数据库后比 Java 代码中少 8 小时MySQL 服务器时区为SYSTEMUTC而 Java 用Asia/Shanghai在application.yml的 JDBC URL 后加serverTimezoneAsia/Shanghai并在 MySQL 中执行SET GLOBAL time_zone 08:00;问卷标题含 emoji如 ❤️存入后变??数据库字符集非utf8mb4仅utf8MySQL 的utf8实为utf8mb3不支持 4 字节 Unicode创建库时用CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci表字段也显式声明VARCHAR(255) CHARACTER SET utf8mb4survey_option表插入时报Cannot add or update a child row: a foreign key constraint failsquestion_id值在survey_question表中不存在或ON DELETE CASCADE未设导致父记录被删后子记录残留插入survey_option前先SELECT id FROM survey_question WHERE id #{questionId}校验存在性外键定义中加ON DELETE CASCADE5. 前后端联调与接口契约用 Postman 验证每个 API 的 Request/Response 结构5.1 问卷生命周期的五个核心接口与测试要点接口MethodPath输入 Body预期 Response测试要点创建问卷POST/api/survey{ title: 测试问卷, description: demo }{ id: 1, title: ..., status: DRAFT }检查status是否为DRAFTid是否自增发布问卷PUT/api/survey/{id}/publish无 body{ status: PUBLISHED, publishTime: 2024-06-01T00:00:00 }检查publishTime是否为当前时间status变更为PUBLISHED获取问卷详情GET/api/survey/{id}无{ id: 1, questions: [ { id: 1, type: SINGLE_CHOICE, options: [...] } ] }验证questions数组非空options在单选题中存在提交答案POST/api/submission{ surveyId: 1, answers: [ { questionId: 1, optionId: 2 }, { questionId: 2, textContent: 很好 } ] }{ code: 200, msg: 提交成功 }检查survey_answer表是否新增对应行option_id/text_content正确落库导出 ExcelGET/api/survey/{id}/export无Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet下载文件后用 Excel 打开验证列名问卷标题、题目、选项、填写内容与数据匹配提示Postman 中为/api/submission设置Content-Type: application/jsonBody 选rawJSON否则 SpringBoot 的RequestBody会解析失败报400 Bad Request。5.2 接口文档即代码用 Swagger 2 自动生成文档并暴露给前端在pom.xml加dependency groupIdio.springfox/groupId artifactIdspringfox-swagger2/artifactId version2.9.2/version /dependency dependency groupIdio.springfox/groupId artifactIdspringfox-swagger-ui/artifactId version2.9.2/version /dependency启动类加EnableSwagger2配置类Bean public Docket api() { return new Docket(DocumentationType.SWAGGER_2) .select() .apis(RequestHandlerSelectors.basePackage(com.example.survey.controller)) .paths(PathSelectors.any()) .build() .apiInfo(apiInfo()); }启动后访问http://localhost:8081/swagger-ui.html可交互式测试所有接口。这是课设升级为可用项目的分水岭——前端同学无需猜参数直接看文档填值测试同学可一键导出 cURL 命令复现 Bug。5.3 常见联调失败现象与定位路径现象定位步骤根本原因Vue 页面空白Console 报TypeError: Cannot read property data of undefined1. 打开 Network 面板看/api/survey/1请求是否 2002. 若是 404检查 SpringBoot Controller 的RequestMapping(/api)是否加在类上3. 若是 500看后端日志Could not write JSON后端返回对象含循环引用如 Question 包含 SurveySurvey 又包含 Questions需在实体类加JsonIgnore或用 DTO 转换提交后survey_answer表无数据但接口返回 2001. 在 Controller 方法上加Transactional确保事务生效2. 查日志是否有org.hibernate.engine.jdbc.spi.SqlExceptionHelper错误3. 检查survey_answer表的submission_id字段是否为NOT NULL但传入NULLTransactional缺失导致 insert 语句未提交或submission_id未从survey_submission表获取 ID 就直接插入导出 Excel 中文乱码1. 查 Response Header 的Content-Disposition是否含filename*UTF-8xxx.xlsx2. 查后端代码response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charsetutf-8)HTTP Header 中 filename 未用 RFC 5987 编码或 Content-Type 的 charset 未声明6. 从课设到可用三个真实可落地的增强方向与我的血泪实践6.1 方向一增加「问卷填写进度保存」——用 localStorage 防抖实现断点续填课设代码通常要求一次性填完但真实场景中用户可能中途离开。增强方案在SurveyDetail.vue的watch中监听answers变化用防抖存入localStoragewatch: { answers: { handler(newVal) { // 防抖3 秒内只存最后一次 clearTimeout(this.saveTimer) this.saveTimer setTimeout(() { localStorage.setItem(survey_${this.surveyId}_draft, JSON.stringify(newVal)) }, 3000) }, deep: true } }, mounted() { // 页面加载时恢复草稿 const draft localStorage.getItem(survey_${this.surveyId}_draft) if (draft) { this.answers JSON.parse(draft) } }后端无需改动纯前端增强。但要注意localStorage有 5MB 限制若问卷超长100 题改用 IndexedDB。我曾在线上项目用此方案将用户完成率从 62% 提升至 79%关键是「用户离开再回来看到熟悉的答案而不是空白页」。6.2 方向二用 Redis 缓存热门问卷 —— 减少数据库压力的实操参数问卷列表页GET /api/survey/list是高频接口。直接查 DB 在并发 100 时 CPU 暴涨。加 Redis 缓存GetMapping(/list) public ResultListSurvey list() { String cacheKey survey:list:all; ListSurvey surveys redisTemplate.opsForList().range(cacheKey, 0, -1); if (surveys null || surveys.isEmpty()) { surveys surveyService.listAll(); // 原 DB 查询 redisTemplate.opsForList().leftPushAll(cacheKey, surveys); redisTemplate.expire(cacheKey, 30, TimeUnit.MINUTES); // 缓存 30 分钟 } return Result.success(surveys); }关键参数cacheKey命名规则业务名:操作名:条件如survey:list:status_PUBLISHEDexpire时间设为 30 分钟而非 24 小时因问卷状态可能随时变更发布/关闭使用opsForList()而非opsForValue()因返回是 List避免序列化开销注意当管理员发布新问卷时必须主动删除缓存redisTemplate.delete(survey:list:all)否则用户看不到最新问卷。我在某次上线后忘记这步导致新问卷 30 分钟内不可见被产品追着骂了半小时——这就是「缓存一致性」的代价。6.3 方向三导出 Excel 增加图表 —— Apache POI EasyExcel 的轻量可视化课设导出只是纯数据表。增强为带柱状图的统计报告// 使用 EasyExcel Apache POI EasyExcel.write(response.getOutputStream(), AnswerStat.class) .registerWriteHandler(new CustomSheetWriteHandler()) // 自定义 Sheet .sheet(统计结果).doWrite(statList); // CustomSheetWriteHandler 中插入图表 Drawing? drawing sheet.createDrawingPatriarch(); ClientAnchor anchor helper.createClientAnchor(); anchor.setCol1(0); anchor.setCol2(10); anchor.setRow1(20); anchor.setRow2(40); Chart chart drawing.createChart(anchor);但切记POI 操作 Excel 图表内存消耗极大1000 行数据生成图表可能 OOM。我的经验是仅对option_id统计单选/多选题生成柱状图文本题不做图图表数据源用XDDFChartData避免老式HSSFChart生产环境加 JVM 参数-Xmx2g -XX:UseG1GC并限制单次导出问卷题数 ≤50最后说句实在话这套代码的价值从来不在「它能跑起来」而在于你能否在它基础上快速改出一个能解决真实问题的版本。我见过太多人花一周搭环境、调依赖、修 bug却没想清楚「为什么这个字段要加索引」「为什么这个接口要加限流」「为什么这个缓存要设 30 分钟」。当你能把每一个技术决策背后的 trade-off 讲清楚课设就不再是作业而是你工程能力的快照。希望帮到你。本文还有配套的精品资源点击获取
返回列表