尘哥早上在技术群里被问到一个挺常见的问题:毕业设计选了“在线问卷调查系统”,SpringBoot + Vue 前后端分离,代码看明白了但不知道从哪下手部署,更不知道整个项目的前后端是怎么串联起来的。我干脆写一篇完整的实操笔记,把整个项目从架构设计、数据库建模、后端接口实现、前端页面渲染到最终部署上线,全流程拆开讲透。这篇博客基于一个完整可跑的 SpringBoot + Vue + MyBatis + MySQL 前后端分离问卷系统源码,适合正在做课程设计、毕业设计或想入门前后端分离开发实战的读者。看懂这篇文章,你不仅能跑起来一套完整系统,还能搞清楚“请求从浏览器发出到页面渲染”这条完整链路到底经历了什么。
1. 项目整体设计与思路拆解
1.1 为什么选前后端分离架构
在线问卷调查系统这种业务场景,天然适合前后端分离。问卷的创建端(管理端)和填写端(用户端)交互差异很大:管理端有复杂的题目编辑、逻辑跳转、数据统计,用户端则是简洁的表单填写页面。如果混在一起开发,后端模板引擎渲染的负担会越来越大,前端改个样式也可能要重启服务。
前后端分离的核心思路,是把后端从“渲染页面”这件事里彻底解放出来。后端只提供 JSON 格式的数据接口,通过 RESTful API 向外界暴露能力;前端单独部署,通过 axios 发请求拿数据,再用 Vue 的双向绑定把数据渲染成页面。这样做最直观的好处是:后端可以专注处理业务逻辑,前端专注交互体验,两端各自独立开发、独立测试、独立部署,互不阻塞。
从实际开发的角度看,还有一个非常现实的原因——现在的后端面试和项目考核里,前后端分离已经成了默认标配。如果你还停留在写 JSP + Servlet 或者用 Thymeleaf 渲染页面,很多团队连简历初筛都过不了。做一个结构清晰的前后端分离项目,能同时锻炼接口设计能力、跨域处理能力、前端组件化能力和数据库建模能力,这些正好是初中级工程师日常工作里最常碰到的技能点。
1.2 技术栈选型背后的取舍
这个项目用的四个核心组件——SpringBoot、Vue、MyBatis、MySQL,都是当前国内中小型项目里使用频率最高的组合,选它们不是偶然。
SpringBoot 负责后端应用骨架。它最大的价值在于“约定优于配置”,内嵌 Tomcat,不用再打 war 包丢进外部容器,一个java -jar就能启动。对问卷系统这种业务不算复杂的项目来说,SpringBoot 的自动配置已经足够覆盖大部分场景,没必要引入 SpringCloud 那套微服务全家桶,过度设计反而是负担。
Vue 负责前端页面开发。它的响应式数据绑定让问卷题目的动态渲染变得极其自然:用户往题目数组里 push 一个新对象,页面会自动多出对应的一组编辑控件,不需要手动操作 DOM,这在传统的 jQuery 时代是不可想象的。Vue 的组件化机制也适合问卷这种“题目类型不同但结构相似”的界面,把单选题、多选题、简答题分别封装成不同组件,维护成本会低很多。
MyBatis 负责数据库访问层。相比 JPA 那种全自动 ORM,MyBatis 半自动的模式在复杂 SQL 场景下更可控。问卷系统的题目、选项、答案表之间关联关系比较多,在 MyBatis 的 XML 里写关联查询,SQL 的每一步执行逻辑一目了然,出了问题也好排查。分页插件 PageHelper 的引入,则让列表查询分页这件事变得几乎零成本。
MySQL 负责数据持久化。问卷系统本质上就是一个读写比例比较均衡的应用,题目和答案的写入量不小,但单表数据量通常远没到需要分库分表的级别。MySQL 5.7 或 8.0 在这类负载下表现稳定,而且安装、备份、迁移的资料极其丰富,遇到问题基本都能搜到答案。
提示:如果你完全没接触过这套组合,建议先按顺序补三个基础点:SpringBoot 的自动配置原理、Vue 的生命周期钩子、MyBatis 的
#{}与${}区别。这三个是后续理解代码的关键,也是面试高频题。
2. 数据库设计与核心表结构
2.1 调查问卷主表与题目表设计
数据库设计是整个问卷系统的地基,基础打不好,后面的接口写起来会处处别扭。问卷业务的核心实体有三个:问卷(survey)、题目(question)、选项(option),外加一个答案记录表。我实际建库时用的是 utf8mb4 字符集,必须用这个,因为它支持完整的 UTF-8 字符,能存下表情符号,避免用户填写时输入一些特殊字符导致入库报错。
问卷主表survey的字段设计如下:
CREATE TABLE `survey` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '问卷ID', `title` varchar(200) NOT NULL COMMENT '问卷标题', `description` varchar(500) DEFAULT NULL COMMENT '问卷说明', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态:0草稿 1发布 2结束', `start_time` datetime DEFAULT NULL COMMENT '开始时间', `end_time` datetime DEFAULT NULL COMMENT '结束时间', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `creator_id` bigint(20) DEFAULT NULL COMMENT '创建人ID', PRIMARY KEY (`id`), KEY `idx_status` (`status`), KEY `idx_create_time` (`create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='问卷表';几个容易忽略的设计细节:status字段用 tinyint 而不是 varchar,因为状态是一个有穷集合,用数字存储更省空间也更好做索引;时间字段统一用 datetime,别用 timestamp,timestamp 到 2038 年会有溢出问题;idx_status索引很重要,后台列表页最常见的查询条件就是按状态筛选,没索引的话数据量稍大就会全表扫描。
题目表question是问卷系统的核心,它需要表达“一道题属于哪个问卷、是什么类型、是否必答、选项是什么”。我设计了这样的表结构:
CREATE TABLE `question` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '题目ID', `survey_id` bigint(20) NOT NULL COMMENT '所属问卷ID', `title` varchar(500) NOT NULL COMMENT '题目内容', `type` tinyint(4) NOT NULL COMMENT '题型:1单选 2多选 3简答', `required` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否必答:0否 1是', `sort_order` int(11) NOT NULL DEFAULT '0' COMMENT '排序号', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), KEY `idx_survey_id` (`survey_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='题目表';type字段就是问卷系统的灵魂。前端要依据这个字段决定渲染什么控件:值是 1 渲染单选按钮组,值是 2 渲染复选框组,值是 3 渲染文本框。我把这个约定植入了整个系统的数据结构,前后端围绕它协作,这也是快速开发类项目最常用的手段——用约定的枚举值驱动 UI 渲染,而不是为每种题型单独建表。
选项表option相对简单,核心字段是question_id和option_text,再配一个排序号。单选和多选题目需要从这张表读取选项列表,简答题没有选项,该表不产生记录。
2.2 答案与统计相关表设计
答案表answer的设计需要斟酌一下。最简单的做法是每次提交存一条记录,把答案 JSON 序列化后丢进一个 text 字段。这种方案开发最快,但统计的时候就麻烦了——你想知道“第一题选 A 的有多少人”,得先把所有 JSON 解析出来再在内存里聚合。我最后采用的是按题目组织答案的方式:
CREATE TABLE `answer` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '答案ID', `survey_id` bigint(20) NOT NULL COMMENT '问卷ID', `question_id` bigint(20) NOT NULL COMMENT '题目ID', `answer_content` varchar(1000) DEFAULT NULL COMMENT '回答内容', `submit_user` varchar(100) DEFAULT NULL COMMENT '提交人标识', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '提交时间', PRIMARY KEY (`id`), KEY `idx_survey_question` (`survey_id`, `question_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='答案明细表';这样设计的好处很直接:统计单选题时,一条 SQL 就能按answer_content分组计数;统计问卷回收量时,对survey_id计数即可。联合索引idx_survey_question保证多题查询时能走覆盖索引,不需要回表。
这里有一个很实际的教训:答案表不要做外键约束。题目删除、问卷删除这类操作会导致外键校验失败,而且高并发提交时外键检查会拖慢写入性能。数据一致性完全可以通过应用层保证,在问卷发布后禁止删除已收集答案的题目,同时定期做数据补偿检查。这是我踩过坑之后得出的经验——数据库表越简单,后续扩展越从容。
3. 后端核心实现:SpringBoot + MyBatis
3.1 项目骨架与统一接口返回
后端工程采用标准的 Maven 目录结构:controller、service、mapper、entity、dto、common分包。common包里我放了一个统一的返回结果类Result,所有接口的返回值都包装成它,这个设计看似不起眼,但前后端联调时会省掉大量“你返回的格式到底是什么”的沟通成本。
public class Result<T> { private Integer code; // 200 成功,500 失败 private String message; // 提示信息 private T data; // 业务数据 public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("操作成功"); result.setData(data); return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.setCode(500); result.setMessage(message); return result; } }控制层只做参数接收和结果包装,不写业务逻辑。以问卷列表接口为例,Controller 里做的事情就是:接收当前页码和条数,调用 Service 层查询分页数据,然后包一层 Result 返回。真正复杂的动态 SQL 查询、数据组装逻辑都下沉到 Service 和 Mapper 层。这样分层的意义在于:当后续要加权限控制或参数校验时,只需要在 Controller 或者 Service 入口统一处理,不需要改动底层 SQL。
统一异常处理也是这个项目里必须做的一步。SpringBoot 的@RestControllerAdvice配合@ExceptionHandler,可以拦截所有未捕获异常,把堆栈信息记录到日志,同时向前端返回一个友好的错误提示。否则一旦 SQL 报错,SpringBoot 默认会返回一长串内部错误信息,既暴露数据库细节,前端也难解析。
3.2 问卷管理接口实现与动态 SQL
问卷管理接口是后端工作量最大的部分,核心是“创建问卷”和“问卷列表分页查询”。
创建问卷接口的实现思路是:接收一个包含问卷基本信息和题目列表的大对象,在同一事务里先插入survey主表,再遍历题目列表逐个插入question表,每个题目再遍历其选项插入option表。@Transactional注解必须加在这个方法上,保证任一环节失败时全部回滚——否则会出现问卷主表有记录、题目表只有一半数据的脏数据,这是问卷系统最怕出现的情况之一。
@Transactional(rollbackFor = Exception.class) public Long createSurvey(SurveyDTO dto) { // 1. 插入问卷主表 Survey survey = new Survey(); survey.setTitle(dto.getTitle()); survey.setDescription(dto.getDescription()); surveyMapper.insert(survey); // 2. 插入题目及选项 if (dto.getQuestions() != null) { for (QuestionDTO q : dto.getQuestions()) { Question question = new Question(); question.setSurveyId(survey.getId()); question.setTitle(q.getTitle()); question.setType(q.getType()); questionMapper.insert(question); if (q.getOptions() != null) { for (OptionDTO o : q.getOptions()) { Option option = new Option(); option.setQuestionId(question.getId()); option.setOptionText(o.getOptionText()); optionMapper.insert(option); } } } } return survey.getId(); }注意rollbackFor = Exception.class这个参数,Spring 的@Transactional默认只在运行时异常时回滚,如果方法里 catch 住了异常但没抛出去,事务不会回滚。写代码时尽量让异常自然抛出,或者在 catch 里手动TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。这个细节网上教程很少提,但生产环境里真能救命。
分页查询列表用了 PageHelper 插件。在 pom.xml 引入依赖后,业务代码里只需要这么写:
PageHelper.startPage(pageNum, pageSize); List<SurveyVO> list = surveyMapper.selectSurveyList(condition); PageInfo<SurveyVO> pageInfo = new PageInfo<>(list);PageHelper.startPage之后紧接着的那条 MyBatis 查询会被自动拦截并改写成分页 SQL,在末尾追加LIMIT子句。这里有个非常容易踩的坑:startPage设置后,如果下一句执行的不是期望分页的查询,插件就会把它错误地分页。所以分页参数必须紧贴查询语句,中间不能夹杂其他 Mapper 调用。
3.3 MyBatis XML 与关联查询实操
MyBatis 的 XML 配置是后端的重头戏。以“查询问卷详情(含题目和选项)”为例,如果用一次循环查询就需要 N+1 次数据库访问,数据量一大会特别慢。我用的方法是先查出问卷,再一次性查出该问卷下所有题目和选项,在 Java 代码里手动组装成树结构:
<select id="selectQuestionsBySurveyId" resultType="com.example.entity.Question"> SELECT id, survey_id, title, type, required, sort_order FROM question WHERE survey_id = #{surveyId} ORDER BY sort_order ASC </select> <select id="selectOptionsByQuestionIds" resultType="com.example.entity.Option"> SELECT id, question_id, option_text, sort_order FROM option WHERE question_id IN <foreach collection="questionIds" item="item" open="(" separator="," close=")"> #{item} </foreach> ORDER BY sort_order ASC </select><foreach>动态处理IN条件时,collection 参数名与传入参数要严格对应,传 List 时写collection="list"或collection="collection",传数组时写array,把这个搞错 MyBatis 会直接报参数绑定异常。另外,大数量级时IN条件的参数尽量控制在 1000 个以内,超出后部分数据库会有限制。
关于 MyBatis 的一个重要面试点——#{}和${}的区别。我从这个项目里总结出来的经验就是:能不用${}就不用。#{}会被编译成预编译语句的占位符?,由 JDBC 驱动做参数绑定,天然免疫 SQL 注入;${}是字符串拼接,直接把值拼进 SQL,一旦传入值被恶意构造,就可能被注入攻击。这个项目的排序字段排序号因为需要动态指定列名,只能勉强用${},但接入值必须经过严格白名单校验。
4. 前端核心实现:Vue
4.1 前端项目结构与路由设计
前端工程用 Vue CLI 初始化,目录结构如下:src/api统一放 axios 请求封装,src/router放路由,src/views放页面组件,src/components放公共组件。关键的是src/api/request.js这个公共请求模块,所有接口调用前必须经过它。
常用封装的 axios 实例有这么几个要点:设置baseURL指向后端接口地址根路径;配置请求拦截器,在每次请求头部加上 token(如果项目有登录功能);配置响应拦截器,统一处理后端返回的code——如果code不是 200 就直接弹错误提示,而 401 这种需要跳转登录页的则做特殊处理。这样业务代码里每处都不再重复 writing 错误判断逻辑,代码能省一半。
路由设计上,问卷管理后台的页面分两大块:SurveyList是问卷列表页,SurveyEdit是创建/编辑问卷页,SurveyFill是用户填写页,SurveyResult是统计结果页。SurveyEdit和SurveyFill共用了同一份数据模型——前者生成题目配置,后者消费题目配置,这正是前后端分离架构里“一份接口、多种消费端”的典型场景。
4.2 动态渲染问卷题目的实现要点
用户填写问卷页是前端体验的核心。后端传过来的数据结构是这样的:一个问卷对象里包含一个题目数组,每个题目对象含有type字段。Vue 里我用v-if配合题型枚举值完成动态渲染。
<template> <div v-for="(question, index) in questionList" :key="question.id"> <div class="question-title"> <span>{{ index + 1 }}. {{ question.title }}</span> <span v-if="question.required" class="required">*必答</span> </div> <div v-if="question.type === 1"> <el-radio-group v-model="answers[question.id]"> <el-radio v-for="opt in question.options" :key="opt.id" :label="opt.id"> {{ opt.optionText }} </el-radio> </el-radio-group> </div> <div v-else-if="question.type === 2"> <el-checkbox-group v-model="answerList[question.id]"> <el-checkbox v-for="opt in question.options" :key="opt.id" :label="opt.id"> {{ opt.optionText }} </el-checkbox> </el-checkbox-group> </div> <div v-else-if="question.type === 3"> <el-input type="textarea" v-model="answers[question.id]" /> </div> </div> </template>选择单选题和多选题的 value 存到数组里,提交时把多选题组内的数组用逗号 join 成字符串,加上对应题目的 ID 组合成后端需要的提交格式。前端验证逻辑也很直接:遍历题目列表,对于required === 1的题目,检查对应的 answers 对象里是否有值,没有值就阻断提交并给出提示。这类验证逻辑放在前端只是为了提升用户体验,真正可靠的验证必须在后端再做一遍——用户完全可以绕过浏览器构造请求直接提交空数据。
题目数据的存储和管理在编辑页里用到了一个技巧:questionList数组里维护一个临时 id(用负数区分),创建问卷时后端看到负数 id 就知道是新增题目,而不是已有题目的修改。这样做的好处是,编辑已有问卷和创建新问卷可以完全共用一套编辑组件,后端接口只关心最终提交的题目列表。
4.3 问卷统计页与 ECharts 可视化
问卷回收后的统计展示,是这个项目里最出效果也最受关注的功能。统计页的思路是这样:后端提供一个聚合统计接口,接收问卷 ID,返回每个题目各选项的被选次数以及有效回收总数。前端拿到数据后,用 ECharts 渲染柱状图或饼图。
后端统计代码的核心也就是一条 SQL:
SELECT question_id, answer_content, COUNT(*) AS count FROM answer WHERE survey_id = #{surveyId} GROUP BY question_id, answer_content拿到分组结果后,在 Service 层按question_id重新组织成 Map,前端按题目的选项顺序把统计值一一对应填入图表。需要注意的一点是:单选题的answer_content存的是选项 id,统计时把 id 解析成选项文本再展示;简答题因为答案无法聚合,直接拉列表展示前 N 条内容即可。
Vue 里使用 ECharts 也有一点心得:不要在组件销毁时忘记把图表实例销毁或调用dispose,否则在 SPA 页面切换过程中很容易出现“图表加载空白或报错 Cannot read property 'dispose' of undefined”这类问题。更稳妥的做法是在nextTick回调里初始化图表,确保 DOM 节点已经渲染完成,再setOption。
5. 部署实战:从代码到线上环境
5.1 环境准备与后端打包
部署环节才是前后端分离项目的真正分水岭。很多人在本地跑得顺风顺水,一到服务器就抓瞎,多数是因为环境版本坑。我这里直接给出经过验证的组合参数:
- JDK 1.8(SpringBoot 2.x 最稳妥的选择,不要一上来就装 JDK 17,很多老配置会不兼容)
- MySQL 5.7 或 8.0(统一用 utf8mb4 字符集)
- Maven 3.6+(本地用于打包后端 jar)
- Nginx 1.18+(托管前端静态文件并做接口反向代理)
- Node.js 14+(构建前端工程)
后端打包前先检查application.yml里的数据库连接配置:
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/survey?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: yourpassword注意serverTimezone参数,不配置的话连接 MySQL 8.x 会报时区错误。打包命令如下:
mvn clean package -DskipTests-DskipTests跳过测试是为了避免测试类里没有配置测试环境数据源时报错。打包完成后在target目录下会生成survey-system.jar,这个 jar 自带内嵌 Tomcat,扔到服务器上直接运行:
nohup java -jar survey-system.jar --server.port=8080 > app.log 2>&1 &用nohup和&让进程在后台运行,日志输出到app.log。启动后可以用tail -f app.log观察启动日志,看到Started Application in x seconds字样就说明后端已经起来了。
5.2 前端构建与 Nginx 配置
前端在本地开发时,Vue CLI 会启动一个 Dev Server 监听 8080 端口,通过代理转发请求到后端,解决跨域问题。但生产环境不能这么干,必须把前端代码真正构建成静态文件,交给 Nginx 托管。
npm install npm run build构建完成后在dist目录下生成index.html和一堆静态资源。把这些目录上传到服务器,再配置 Nginx:
server { listen 80; server_name your-domain.com; root /data/www/survey/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这段配置里最有含金量的是两条:try_files $uri $uri/ /index.html是为了支持 Vue Router 的 history 模式——因为前端路由切换不会真正改变 URL 对应的文件路径,刷新页面时 Nginx 找不到对应物理文件,必须把请求全部重写到index.html,由前端路由接管;location /api/的代理转发则是为了解决生产环境的跨域问题——浏览器同源策略限制了不同端口之间的请求,而通过 Nginx 反向代理,前端请求/api路径时由 Nginx 转发给后端的 8080,从浏览器视角看没有跨域。
提示:如果后端接口路径不是以
/api开头,记得同步修改 axios 封装的baseURL,我建议从一开始前后端约定好,所有接口统一加/api前缀,这样部署层的代理配置就能保持稳定不变。
5.3 数据库初始化与常见部署细节
数据库初始化不要靠人肉手敲 SQL,直接把项目里的sql/survey.sql脚本传给 MySQL 执行。如果服务器上的 MySQL 是远程连接,注意设置用户访问权限:MySQL 8.0 默认认证插件是caching_sha2_password,一些老版本的客户端或驱动会连不上,需要执行:
ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'yourpassword'; FLUSH PRIVILEGES;另外还要检查安全组和防火墙。云服务器默认只会对 80 和 443 端口开放,如果你想让 8080 端口直接被外部访问,就必须在控制台安全组里放行,但更推荐的做法是保持 8080 只在内部局域网可见,外部流量全部走 Nginx 的 80 端口进入。这是一个对外开放流量最小化的原则——后端服务没必要直接暴露公网。
这里还要提一个实战中很常见的坑:服务器内存不够。SpringBoot 默认 JVM 内存参数会随着 heap 增长占满系统内存,如果服务器只有 1G 内存,建议启动时限制堆内存:
java -Xms256m -Xmx512m -jar survey-system.jar-Xms256m设置初始堆大小,-Xmx512m设置最大堆大小,这样能有效避免内存溢出导致的进程被杀。
6. 常见问题与排查技巧实录
6.1 联调和部署阶段的高频报错速查表
我把实际运行这套系统时最容易遇到的几个问题整理成了表格,每个都附带排查思路。这些报错信息你在搜索引擎里搜能搜到一堆结果,但如果没有一个清晰的排查路径,很容易绕圈子。
| 报错/问题 | 常见原因 | 排查思路 |
|---|---|---|
| Failed to configure a DataSource | application.yml数据库配置错误或没被读取 | 检查 URL、用户名、密码是否匹配,重点检查密码里是否有特殊字符需要转义 |
| Access denied for user | MySQL 账号权限不足 | 用 root 执行 GRANT 授权,或者检查 MySQL 8.0 认证插件问题 |
| CORS 跨域报错 | 前端地址和后端地址不同源 | 使用 Nginx 反代或后端加 CORS 过滤器,生产环境推荐前者 |
| 前端刷新 404 | Vue Router history 模式缺少 Nginx 配置 | 在 location / 里添加try_files $uri $uri/ /index.html |
| 数据库中文乱码 | 连接串缺少characterEncoding=utf8 | 检查 URL 参数,并确认表结构字符集是 utf8mb4 |
#{}参数被误拼进 SQL 报语法错误 | ${}使用不当或 SQL 拼接错误 | 打印 MyBatis 日志定位问题 SQL,优先改用#{} |
| ECharts 图形不显示 | 图表初始化时 DOM 未渲染完成 | 在this.$nextTick内初始化图表 |
做前后端分离联调时,强烈建议把后端日志的 SQL 打印打开,在application.yml里配置:
mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl打开后,每次执行的 SQL 都会在控制台输出,包括参数值、查询结果条数。排查问题时能直接看到实际执行的 SQL 长什么样,很多逻辑错误一眼就能定位——比如分页没有生效、WHERE 条件没带上等,通过输出 SQL 立刻就能发现。
6.2 环境相关的三个隐藏坑
第一个坑是 Node 版本过高导致前端构建失败。Vue CLI 5 和 webpack 4 在 Node 17 以上版本构建时,经常报error:0308010C:digital envelope routines::unsupported。这个问题的根因是 OpenSSL 的 MD4 算法在新版本中被移除了。解决方案要么用 Node 14 或 16 这类 LTS 版本,要么在构建命令前加上:
NODE_OPTIONS=--openssl-legacy-provider npm run build第二个坑是 MySQL 8.0 驱动类名变化。SpringBoot 2.x 默认用的是com.mysql.cj.jdbc.Driver,而网上很多老教程还在写com.mysql.jdbc.Driver。如果按旧写法,启动会报ClassNotFoundException。直接用新驱动类名即可,这也提示了你尽量不要整段复制网上年代久远的配置。
第三个坑是前后端分离环境下的 Session 会话管理。如果你的问卷系统需要登录功能,使用 Session 方案时要注意:浏览器跨域请求默认不带 Cookie,前端 axios 需要设置withCredentials: true,后端 CORS 也要设置allowCredentials(true)。如果没处理好,登录接口能返回数据但后续请求都提示未登录,这是前后端分离项目里非常经典、也非常隐蔽的一个会话问题。
最后分享一个我实际操作中的体会:做这类全栈项目,最大的瓶颈往往不是单个技术点难学,而是调试链路太长——前端报错可能根因在后端,后端逻辑错了可能是数据库脏数据。我建议你把“日志”当成这个项目最重要的伙伴:后端打开 SQL 日志、前端用 Vue Devtools 和浏览器 Network 面板观察请求响应。看到数据在哪一环断掉,问题就已经解决了一半。
如果你要把这个项目后续扩展成自己的毕设或者作品集,方向可以从这几个地方发力:加上登录注册和角色权限(管理员/普通用户)、把问卷发布改成定时任务自动更改状态、统计页导出 Excel 报告、用 Redis 缓存高频访问的问卷详情。功能不在多,有一条主线能讲清楚、能演示完整就是好项目。前面说到的那套survey.sql建库脚本和完整源码,跑通一遍再按需改造,比重新造轮子高效多了。