尘哥早上在技术群里被问到一个挺常见的问题毕业设计选了“在线问卷调查系统”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 那种全自动 ORMMyBatis 半自动的模式在复杂 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT问卷表;几个容易忽略的设计细节status字段用 tinyint 而不是 varchar因为状态是一个有穷集合用数字存储更省空间也更好做索引时间字段统一用 datetime别用 timestamptimestamp 到 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT答案明细表;这样设计的好处很直接统计单选题时一条 SQL 就能按answer_content分组计数统计问卷回收量时对survey_id计数即可。联合索引idx_survey_question保证多题查询时能走覆盖索引不需要回表。这里有一个很实际的教训答案表不要做外键约束。题目删除、问卷删除这类操作会导致外键校验失败而且高并发提交时外键检查会拖慢写入性能。数据一致性完全可以通过应用层保证在问卷发布后禁止删除已收集答案的题目同时定期做数据补偿检查。这是我踩过坑之后得出的经验——数据库表越简单后续扩展越从容。3. 后端核心实现SpringBoot MyBatis3.1 项目骨架与统一接口返回后端工程采用标准的 Maven 目录结构controller、service、mapper、entity、dto、common分包。common包里我放了一个统一的返回结果类Result所有接口的返回值都包装成它这个设计看似不起眼但前后端联调时会省掉大量“你返回的格式到底是什么”的沟通成本。public class ResultT { private Integer code; // 200 成功500 失败 private String message; // 提示信息 private T data; // 业务数据 public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } public static T ResultT error(String message) { ResultT 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); ListSurveyVO list surveyMapper.selectSurveyList(condition); PageInfoSurveyVO pageInfo new PageInfo(list);PageHelper.startPage之后紧接着的那条 MyBatis 查询会被自动拦截并改写成分页 SQL在末尾追加LIMIT子句。这里有个非常容易踩的坑startPage设置后如果下一句执行的不是期望分页的查询插件就会把它错误地分页。所以分页参数必须紧贴查询语句中间不能夹杂其他 Mapper 调用。3.3 MyBatis XML 与关联查询实操MyBatis 的 XML 配置是后端的重头戏。以“查询问卷详情含题目和选项”为例如果用一次循环查询就需要 N1 次数据库访问数据量一大会特别慢。我用的方法是先查出问卷再一次性查出该问卷下所有题目和选项在 Java 代码里手动组装成树结构select idselectQuestionsBySurveyId resultTypecom.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 idselectOptionsByQuestionIds resultTypecom.example.entity.Option SELECT id, question_id, option_text, sort_order FROM option WHERE question_id IN foreach collectionquestionIds itemitem open( separator, close) #{item} /foreach ORDER BY sort_order ASC /selectforeach动态处理IN条件时collection 参数名与传入参数要严格对应传 List 时写collectionlist或collectioncollection传数组时写array把这个搞错 MyBatis 会直接报参数绑定异常。另外大数量级时IN条件的参数尽量控制在 1000 个以内超出后部分数据库会有限制。关于 MyBatis 的一个重要面试点——#{}和${}的区别。我从这个项目里总结出来的经验就是能不用${}就不用。#{}会被编译成预编译语句的占位符?由 JDBC 驱动做参数绑定天然免疫 SQL 注入${}是字符串拼接直接把值拼进 SQL一旦传入值被恶意构造就可能被注入攻击。这个项目的排序字段排序号因为需要动态指定列名只能勉强用${}但接入值必须经过严格白名单校验。4. 前端核心实现Vue4.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 :keyquestion.id div classquestion-title span{{ index 1 }}. {{ question.title }}/span span v-ifquestion.required classrequired*必答/span /div div v-ifquestion.type 1 el-radio-group v-modelanswers[question.id] el-radio v-foropt in question.options :keyopt.id :labelopt.id {{ opt.optionText }} /el-radio /el-radio-group /div div v-else-ifquestion.type 2 el-checkbox-group v-modelanswerList[question.id] el-checkbox v-foropt in question.options :keyopt.id :labelopt.id {{ opt.optionText }} /el-checkbox /el-checkbox-group /div div v-else-ifquestion.type 3 el-input typetextarea v-modelanswers[question.id] / /div /div /template选择单选题和多选题的 value 存到数组里提交时把多选题组内的数组用逗号 join 成字符串加上对应题目的 ID 组合成后端需要的提交格式。前端验证逻辑也很直接遍历题目列表对于required 1的题目检查对应的 answers 对象里是否有值没有值就阻断提交并给出提示。这类验证逻辑放在前端只是为了提升用户体验真正可靠的验证必须在后端再做一遍——用户完全可以绕过浏览器构造请求直接提交空数据。题目数据的存储和管理在编辑页里用到了一个技巧questionList数组里维护一个临时 id用负数区分创建问卷时后端看到负数 id 就知道是新增题目而不是已有题目的修改。这样做的好处是编辑已有问卷和创建新问卷可以完全共用一套编辑组件后端接口只关心最终提交的题目列表。4.3 问卷统计页与 ECharts 可视化问卷回收后的统计展示是这个项目里最出效果也最受关注的功能。统计页的思路是这样后端提供一个聚合统计接口接收问卷 ID返回每个题目各选项的被选次数以及有效回收总数。前端拿到数据后用 ECharts 渲染柱状图或饼图。后端统计代码的核心也就是一条 SQLSELECT 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.8SpringBoot 2.x 最稳妥的选择不要一上来就装 JDK 17很多老配置会不兼容MySQL 5.7 或 8.0统一用 utf8mb4 字符集Maven 3.6本地用于打包后端 jarNginx 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?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/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.port8080 app.log 21 用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和一堆静态资源。把这些目录上传到服务器再配置 Nginxserver { 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 DataSourceapplication.yml数据库配置错误或没被读取检查 URL、用户名、密码是否匹配重点检查密码里是否有特殊字符需要转义Access denied for userMySQL 账号权限不足用 root 执行 GRANT 授权或者检查 MySQL 8.0 认证插件问题CORS 跨域报错前端地址和后端地址不同源使用 Nginx 反代或后端加 CORS 过滤器生产环境推荐前者前端刷新 404Vue Router history 模式缺少 Nginx 配置在 location / 里添加try_files $uri $uri/ /index.html数据库中文乱码连接串缺少characterEncodingutf8检查 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建库脚本和完整源码跑通一遍再按需改造比重新造轮子高效多了。
