SpringBoot问卷调查系统全流程复盘:从设计到落地
2026/9/17 7:23:46 网站建设 项目流程

SpringBoot网上问卷调查系统,从设计到落地全流程复盘

做Java后端的朋友应该都有这种感觉:问卷调查系统听起来简单,真要动手写的时候才发现里面藏了不少细节。它不是一个纯粹的CRUD增删改查,而是涉及动态表单、题目类型抽象、答卷数据收集、统计图表呈现这一整套链路。我最近刚完成了一个基于SpringBoot的网上问卷调查系统,代码已经整理好附带在文中,这篇文章把这套系统的完整设计思路、核心实现和我在实际开发中踩过的坑都捋一遍,给正在做毕设、做课程设计或者想练手SpringBoot项目的朋友一个参考。

这套系统解决的核心问题是:让管理员快速创建问卷、灵活配置题型、发布回收答卷,同时自动完成统计汇总。你可以把它理解成一个简化版的问卷星,适用于校园调研、企业满意度收集、活动报名登记这类场景。适合对SpringBoot已有基础、想通过一个完整的实际项目把框架知识串起来的人,也适合需要现成源码做毕设二次开发的同学。

1. 项目整体设计与技术选型

1.1 为什么用SpringBoot而不是其他框架

先说选型。这个项目的主技术栈是SpringBoot,理由很直接:SpringBoot把Spring生态里最常用的东西全部自动装配好了,我和别人协作的时候,不需要花大量时间写XML配置、配数据源、配事务管理器,集中精力在业务逻辑上才是正事。

对比老式的SpringMVC工程,SpringBoot带来的最大改进是约定优于配置。同样一个问卷调查系统,用SpringMVC搭建环境至少需要一天,用SpringBoot的话,通过start.spring.io生成一个基础工程,IDEA打开就能直接跑。再加上内嵌Tomcat,打完包一个java -jar就上线了,部署成本低很多。

配套选型上,我做了以下搭配:

组件选型选择理由
持久层MyBatis-Plus单表CRUD不用写SQL,问卷和题目这种关系映射也方便
数据库MySQL 8.x社区活跃、资料多,存储问卷数据完全够用
前端Thymeleaf + Bootstrap服务端渲染,适合没有单独前端人员的项目,部署简单
认证Sa-Token / JWT轻量级登录方案,不需要引入Spring Security那么重的框架
图表ECharts问卷统计结果可视化,开箱即用

1.2 功能模块需求拆解

这套问卷系统分为两个端:管理员端用户填写端

管理员(或者说问卷创建者)要能干这些事:

  • 创建问卷,填写问卷标题、副标题、欢迎语、结束语
  • 添加题目,支持单选、多选、填空、下拉选择这四种基础题型
  • 题目可以设置必答、排序,编辑完之后发布问卷
  • 查看发布后的问卷数据,包括回收的答卷数量和每道题的统计结果
  • 支持导出答卷原始数据和统计结果

用户端只需要一件事:打开问卷链接,答题,提交。提交之后如果有需要,可以设置提交后跳转页面或者显示感谢语。

这样一个简单的需求拆解下来,后端核心功能点其实就四个字:建卷、发卷、答卷、统卷

1.3 表结构设计思路

数据库设计是整个项目的基石,我踩过一个教训:一开始图省事,把选项直接拼在题目表的一个字段里,用逗号分隔。后来做统计的时候发现,这种设计导致每个选项都无法单独索引,统计还得先把字符串拆开,代码写起来非常难受。

后来我规规矩矩地建了四张核心表:

-- 问卷表 CREATE TABLE survey ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL COMMENT '问卷标题', description VARCHAR(500) COMMENT '问卷描述', status TINYINT DEFAULT 0 COMMENT '状态 0草稿 1发布 2关闭', start_time DATETIME COMMENT '开始时间', end_time DATETIME COMMENT '结束时间', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); -- 题目表 CREATE TABLE question ( id BIGINT PRIMARY KEY AUTO_INCREMENT, survey_id BIGINT NOT NULL COMMENT '所属问卷ID', title VARCHAR(500) NOT NULL COMMENT '题目内容', type TINYINT NOT NULL COMMENT '题目类型 1单选 2多选 3填空 4下拉', required TINYINT DEFAULT 0 COMMENT '是否必答', sort_order INT DEFAULT 0 COMMENT '排序', options TEXT COMMENT '选项JSON数组' ); -- 答卷主表 CREATE TABLE answer_sheet ( id BIGINT PRIMARY KEY AUTO_INCREMENT, survey_id BIGINT NOT NULL, submit_time DATETIME DEFAULT CURRENT_TIMESTAMP, use_time INT COMMENT '填写用时(秒)', ip_address VARCHAR(64) COMMENT '提交IP' ); -- 答卷明细表 CREATE TABLE answer_detail ( id BIGINT PRIMARY KEY AUTO_INCREMENT, sheet_id BIGINT NOT NULL COMMENT '答卷ID', question_id BIGINT NOT NULL COMMENT '题目ID', answer_content TEXT COMMENT '答案内容' );

这里的核心设计决策有两个:

第一,题目选项用JSON数组存储。很多人纠结要不要单独建一张选项表。我的观点是:对于问卷这种场景,选项的生命周期完全从属于题目,不会单独被查询、被关联。建独立选项表反而让代码复杂度翻倍,查询一道题还要JOIN另一张表。用JSON数组存储,配合fastjson2或者Jackson做序列化反序列化,简单又高效。

第二,答卷明细表用纵表结构。每一道题的答案存一行,而不是把整份问卷的答案存成一整条JSON。这样查询"某道题的某个选项被选了多少次"时,可以直接在answer_detail里按question_id分组统计,配合索引效率很高。如果存成一条JSON,统计时要把所有答卷数据加载到内存里解析,学生问卷几千份的时候还能忍,上万份就直接卡死。

1.4 项目实际结构

后端代码按功能模块分包,不是按技术层分包:

com.example.survey ├── controller // 接口层 │ ├── admin // 管理端接口 │ └── user // 用户填写端接口 ├── service // 业务逻辑层 ├── mapper // 数据访问层 ├── entity // 实体类 ├── dto // 数据传输对象 ├── vo // 视图对象 ├── common // 通用结果分页等 └── config // 拦截器配置等

我见过分controller/service/mapper三层再各自放一个包的写法,说不上错,但项目一旦业务多了,找起来很痛苦。按业务功能分包,小组协作的时候每个人负责一块,Git冲突的概率也会小很多。

2. 核心功能实现与关键代码解析

2.1 动态问卷渲染——前端怎么知道显示什么题

问卷和普通表单最大的区别是题目是动态的。用户打开问卷之前,我们不知道这张卷子里有几道题、每道题是什么类型、选项是什么。所以后端提供一个接口返回完整的问卷JSON结构,前端拿到后再动态渲染表单:

@GetMapping("/survey/{surveyId}") public Result<SurveyVO> getSurvey(@PathVariable Long surveyId) { Survey survey = surveyService.getById(surveyId); if (survey == null || survey.getStatus() != 1) { return Result.error("问卷不存在或未发布"); } List<Question> questions = questionMapper.selectList( new LambdaQueryWrapper<Question>() .eq(Question::getSurveyId, surveyId) .orderByAsc(Question::getSortOrder) ); SurveyVO vo = new SurveyVO(); BeanUtils.copyProperties(survey, vo); vo.setQuestions(questions); return Result.success(vo); }

前端拿到题目的type字段后,用th:switch或者JavaScript判断渲染哪种类型的输入控件:

<div th:each="q : ${survey.questions}"> <div th:switch="${q.type}"> <!-- 单选题 --> <div th:case="1"> <label th:each="opt : ${q.optionList}"> <input type="radio" th:name="'question_' + ${q.id}" th:value="${opt}"> <span th:text="${opt}"></span> </label> </div> <!-- 多选题 --> <div th:case="2"> <label th:each="opt : ${q.optionList}"> <input type="checkbox" th:name="'question_' + ${q.id}" th:value="${opt}"> <span th:text="${opt}"></span> </label> </div> <!-- 填空题 --> <div th:case="3"> <input type="text" th:name="'question_' + ${q.id}"> </div> <!-- 下拉题 --> <div th:case="4"> <select th:name="'question_' + ${q.id}"> <option th:each="opt : ${q.optionList}" th:value="${opt}" th:text="${opt}"></option> </select> </div> </div> </div>

这里需要注意一个问题:从数据库读出来的options字段是JSON字符串,需要先转成List<String>才能直接遍历。我在Question实体里加了一个@TableField(exist = false)的临时字段:

@Data public class Question { private Long id; private Long surveyId; private String title; private Integer type; private Integer required; private Integer sortOrder; private String options; // 以下字段不参与数据库映射 @TableField(exist = false) private List<String> optionList; // 从字符串转换为列表 public List<String> getOptionList() { return JSON.parseArray(this.options, String.class); } }

这种写法虽然不符合"严格分层"的理念,但对这种视图展示型的小项目来说,是最方便快捷的。不要为了代码洁癖增加不必要的复杂度,能跑、好维护才是关键。

2.2 答卷提交——批量插入与校验逻辑

用户提交答卷时,前端会把试卷ID和所有题目的答案汇总成一个JSON传到后端。这里最关键的步骤是校验

  • 试卷是否存在且在发布状态
  • 必答题是否都填了
  • 单选题是否只选了一个
  • 多选题是否有超出选限
@PostMapping("/submit") public Result<?> submit(@RequestBody AnswerSubmitDTO dto) { Survey survey = surveyService.getById(dto.getSurveyId()); if (survey == null || survey.getStatus() != 1) { return Result.error("问卷不存在或已停止收集"); } // 校验必答题 List<Question> questions = questionService.listBySurveyId(dto.getSurveyId()); Map<Long, String> answerMap = dto.getAnswers().stream() .collect(Collectors.toMap(AnswerItem::getQuestionId, AnswerItem::getContent)); for (Question q : questions) { if (q.getRequired() == 1) { String content = answerMap.get(q.getId()); if (StringUtils.isBlank(content)) { return Result.error("题目【" + q.getTitle() + "】为必答题"); } } } // 插入答卷 AnswerSheet sheet = new AnswerSheet(); sheet.setSurveyId(dto.getSurveyId()); answerSheetMapper.insert(sheet); // 批量插入明细 List<AnswerDetail> details = new ArrayList<>(); for (AnswerItem item : dto.getAnswers()) { AnswerDetail detail = new AnswerDetail(); detail.setSheetId(sheet.getId()); detail.setQuestionId(item.getQuestionId()); detail.setAnswerContent(item.getContent()); details.add(detail); } answerDetailService.saveBatch(details); return Result.success("提交成功"); }

这里有一个小小的性能优化点:问卷题目一般就十道左右,答案批量插入时用的是MyBatis-Plus的saveBatch,这个方法默认会拼接多条INSERT语句成一条执行,能减少一半的网络往返时间。如果你用的是原始的MyBatis,记得在Mapper里写<foreach>标签自己拼批量插入SQL,不要用for循环逐条插入。

2.3 统计功能——分组查询与多选拆分

问卷统计是第二个核心点。对于单选、下拉这类题型,直接按answer_content分组统计就出来了:

// 单选统计 SELECT question_id, answer_content, COUNT(*) AS cnt FROM answer_detail WHERE question_id = #{questionId} GROUP BY answer_content

对于多选题,由于用户在提交时多选答案通常以逗号拼接,统计先要把字符串拆开再聚合,SQL可以这样写:

SELECT answer_content FROM answer_detail WHERE question_id = #{questionId}

然后Java代码里处理:

public Map<String, Integer> statMulti(List<String> contents) { Map<String, Integer> result = new HashMap<>(); for (String content : contents) { String[] items = content.split(","); for (String item : items) { String trimmed = item.trim(); result.put(trimmed, result.getOrDefault(trimmed, 0) + 1); } } return result; }

这个拆分逻辑看起来简单,实际开发中很容易出问题的一个细节是:选项内容本身包含逗号怎么办

比如我有一个选项是"找工作时,优先考虑薪资、发展前景",用户选了这个选项,后端存的就是"找工作时,优先考虑薪资、发展前景",按逗号拆分后统计就错乱了。

所以我后来调整了存储格式,多选用JSON数组存,而不是逗号拼接:

["找工作时,优先考虑薪资、发展前景", "公司氛围好"]

拆的时候用JSON解析,不要用split(",")。这是我在实际调试中踩过的坑,也是现在看源码项目时最容易发现的低级错误。

2.4 问卷状态机与权限控制

问卷有草稿、发布、关闭三种状态。发布之后用户才能看到,草稿状态只能编辑预览。关闭后不能再提交答卷。这个状态流转需要做一个统一校验。

我把状态判断封装成一个独立的校验方法:

public void checkSurveyStatus(Long surveyId, SurveyStatus expected) { Survey survey = surveyService.getById(surveyId); if (survey == null) { throw new BizException("问卷不存在"); } // 草稿 -> 发布:仅创建者可操作 if (expected == SurveyStatus.PUBLISHED && survey.getStatus() != SurveyStatus.DRAFT.getCode()) { throw new BizException("只有草稿状态的问卷才能发布"); } // 发布 -> 关闭:开始时间判断 if (expected == SurveyStatus.CLOSED && survey.getStatus() != SurveyStatus.PUBLISHED.getCode()) { throw new BizException("只有发布状态的问卷才能关闭"); } }

管理端接口统一加了登录拦截器,用户填写端接口则不做任何认证。这样设计的原因是问卷填写端要保证足够的开放度,用户不需要注册、不需要登录,点开链接就能填,填写率才会高。管理端则必须要有权限控制,否则谁都能删问卷就完蛋了。

3. 实操过程:从立项到跑通全流程

3.1 开发环境准备与项目初始化

我用的开发环境是:

  • JDK 8(因为在个人电脑上测过JDK 17,某些老依赖会有兼容问题,做项目选JDK 8是最稳的
  • Maven 3.6.3
  • IDEA 2023.2
  • MySQL 8.0.33

初始化SpringBoot工程的时候,我直接在IDEA里通过Spring Initializr创建,选择以下依赖:

  • Spring Web
  • MyBatis-Plus
  • MySQL Driver
  • Lombok
  • Thymeleaf
  • Validation

有人问要不要选Spring Data JPA。我的观点是,JPA对动态条件查询的支持比较繁琐,那些"根据状态筛选""根据创建时间排序"的查询,JPA的Specification写起来不如MyBatis的LambdaQueryWrapper直接。对于国内中小型项目,MyBatis-Plus的受众更广,遇到问题也更容易搜到答案。

3.2 问卷创建与编辑功能完整实现

问卷编辑是这个系统里交互最重的一个功能。我的实现思路是:问卷基本信息(标题、说明、时间段)+ 动态题目列表一起提交。

前端层面,题目列表用JavaScript维护一个数组,每个题目对象包含:

{ "id": null, // 编辑时才有 "title": "你的年龄段?", "type": 1, // 1单选 2多选 3填空 4下拉 "required": true, "sortOrder": 1, "options": ["18岁以下", "18-30岁", "30-45岁", "45岁以上"] }

保存的时候一次性把所有题目JSON串传到后端。后端先删除这张问卷的所有旧题目,再重新插入新题目。这种先删后插的方式最简单,不容易出错。

@PostMapping("/admin/survey/save") @Transactional(rollbackFor = Exception.class) public Result<?> saveSurvey(@RequestBody SurveySaveDTO dto) { // 1.保存问卷基本信息 Survey survey = new Survey(); BeanUtils.copyProperties(dto, survey); survey.setStatus(dto.getStatus() == null ? 0 : dto.getStatus()); surveyService.saveOrUpdate(survey); // 2.删除已有题目 if (survey.getId() != null) { questionService.remove(new LambdaQueryWrapper<Question>() .eq(Question::getSurveyId, survey.getId())); } // 3.重新插入题目 List<Question> questions = dto.getQuestions(); for (int i = 0; i < questions.size(); i++) { Question q = questions.get(i); q.setSurveyId(survey.getId()); q.setSortOrder(i); // 选项数组转JSON字符串存储 if (q.getOptionList() != null) { q.setOptions(JSON.toJSONString(q.getOptionList())); } questionService.save(q); } return Result.success(survey.getId()); }

这里用了@Transactional,确保问卷信息保存和题目重新插入是一个原子操作,不会出现问卷保存成功但题目保存失败导致的数据不一致。事务在涉及多表写操作时是底线,不要省。

3.3 发布、分享与答卷回收

问卷创建完成后,点击"发布"按钮,系统把状态改为1(发布),生成一个访问链接:

http://localhost:8080/survey/fill/{surveyId}

这个链接可以直接发给用户,也可以生成二维码让用户扫码填写。二维码我用的是ZXing库,一个BufferedImage就搞定了,不需要额外的服务。

回收答卷的关键点是防重复提交。用户提交成功后,后端返回成功标记。前端用JavaScript控制提交按钮置灰,防止用户手滑连点两次导致产生两条答卷记录。更保险的做法是根据IP加一层去重:

public void checkDuplicateSubmit(Long surveyId, String ip) { Long count = answerSheetMapper.selectCount( new LambdaQueryWrapper<AnswerSheet>() .eq(AnswerSheet::getSurveyId, surveyId) .eq(AnswerSheet::getIpAddress, ip) .last("AND submit_time > DATE_SUB(NOW(), INTERVAL 5 MINUTE)") ); if (count > 0) { throw new BizException("您刚刚提交过问卷,请勿重复提交"); } }

这个策略解决不了高水平攻击,但能防得住普通用户的误触。如果要做更严格的限制,可以为每张答卷生成一个一次性token,提交时校验token,提交后立即失效。

3.4 统计图表展示

统计页面我用了ECharts做可视化。单选、下拉题用饼图展示占比,多选题用柱状图展示选项被选次数。

后端把统计结果返回前端,前端动态渲染图表:

// 饼图配置 function renderPie(containerId, data) { var chart = echarts.init(document.getElementById(containerId)); chart.setOption({ tooltip: { trigger: 'item' }, series: [{ type: 'pie', radius: '60%', data: data // [{name: '选项A', value: 23}, ...] }] }); }

实测下来,ECharts确实够用,但有一个坑:图表容器必须在DOM渲染完成后初始化。如果页面是Thymeleaf服务端渲染,直接在<script>里初始化没问题;如果用了Ajax局部加载内容,得确保容器已经插入到DOM里再调用init,否则图表宽度会变成0。

3.5 源码获取与本地运行方式

代码里已经包含了完整的SQL初始化脚本、后端Java代码和前端页面模板。本地运行只需要三步:

  1. 创建一个名为survey_system的数据库,执行项目根目录下的sql/init.sql脚本
  2. 修改application.yml里的数据库账号密码
  3. 运行SurveyApplicationmain方法,访问http://localhost:8080/admin/login登录管理后台

默认管理员账号密码在sys_user表里,初始密码带了个简单的MD5加盐处理,实际部署时记得改掉。

4. 系统性能优化与常见问题排查

4.1 数据库索引设计

问卷系统一旦投入真实使用,数据量会上来得比较快。一张问卷如果有5000份答卷、20道题,answer_detail表就有10万条记录。如果不加索引,统计接口的查询会非常慢。

我给这三张表加了核心索引:

ALTER TABLE question ADD INDEX idx_survey_id (survey_id); ALTER TABLE answer_detail ADD INDEX idx_question_id (question_id); ALTER TABLE answer_detail ADD INDEX idx_sheet_id (sheet_id); ALTER TABLE answer_sheet ADD INDEX idx_survey_id (survey_id);

经过实测,在10万条明细数据量下,按question_id分组统计单题结果,索引命中后查询时间从800ms降到了50ms左右。做统计类的功能,索引依赖程度极高,千万不要等卡了再补。

4.2 高并发提交场景的应对

假设一份满意度问卷上线后,短时间内有200个用户同时提交。系统会不会出问题?

如果不做任何处理,answer_sheet表的auto_increment主键不会有问题,MySQL的InnoDB在插入时会自动处理并发自增。真正容易出问题的点是问卷发布操作的同时有用户正在填写,这种读写并发在MySQL的事务隔离级别下默认是REPEATABLE READ,不会产生脏读,所以其实没有太大问题。

真正需要关注的是缓存层面。如果问卷详情需要频繁访问数据库,可以加一层Spring Cache:

@Cacheable(value = "survey", key = "#surveyId") public SurveyVO getSurveyDetail(Long surveyId) { // 查询数据库... }

但要注意,如果问卷题目被管理员重新编辑,缓存必须及时清掉,否则用户会看到一份过期的问卷。我在实际开发中吃过这个亏,后来加了@CacheEvict注解解决:

@CacheEvict(value = "survey", key = "#dto.id") @PostMapping("/admin/survey/save") public Result<?> saveSurvey(@RequestBody SurveySaveDTO dto) { // 保存逻辑... }

4.3 前端页面兼容性处理

问卷填写页面的坑主要在手机端适配。问卷星之所以在手机端体验好,是因为移动端做了响应式布局。我的系统用的是Bootstrap 4,默认对手机屏幕友好度尚可,但还是有几个细节要注意:

  • 多选题的复选框点击区域要大,手指才能准确点中
  • 填空题的输入框在手机上要触发合适的键盘(数字题触发数字键盘)
  • 文字大小不小于14px,否则在手机上看很吃力
<meta name="viewport" content="width=device-width, initial-scale=1.0">

这行标签必须写在head里,少了它手机端会自动用PC的宽度来渲染,体验直接崩溃。

4.4 问卷状态混乱的处理

开发过程中最容易遇到的异常是问卷ID传错。比如用户A创建了一份草稿问卷,直接把链接分享给别人,别人打开的时候因为没有做状态校验,看到的是空白页面。

我在getSurvey接口里加了状态判断就不存在这个问题了:

if (survey.getStatus() != 1) { return Result.error("问卷不存在或已停止收集"); }

还有一种是管理员忘记填开始和结束时间。我在前端做了必填校验,后端也做了兜底——如果两个时间都为null,表示问卷长期有效,不需要时间过滤;如果只有其中一个有值,按照单边校验处理。这种边界情况如果不处理,后面统计时间段的答卷数量时会出大问题。

4.5 常见问题速查表

现象可能原因排查思路
启动报Failed to configure a DataSource数据库连接没配好检查application.yml里的URL账号密码,确认MySQL服务已启动
访问管理页面404静态资源没走对Thymeleaf模板要放在templates目录下,静态资源放static目录
提交答卷后统计为0明细数据没插入成功检查answer_detail表是否为空,确认saveBatch是否被执行
图表显示不完整容器宽高为0确认init调用时DOM元素已渲染,给容器设置明确的宽高
编辑问卷后题目重复先删后插逻辑执行了两次检查前端是否点击了两次保存按钮,后端加幂等性处理
中文乱码数据库连接参数未指定编码JDBC URL中加上characterEncoding=utf8

5. 我还做了哪些好用的加分设计

5.1 问卷回收Excel导出

除了在线看统计图表,我还加了一个导出功能:把答卷原始数据导成Excel文件。后端用Ali EasyExcel做这件事。

@GetMapping("/admin/survey/{surveyId}/export") public void export(@PathVariable Long surveyId, HttpServletResponse response) throws IOException { List<AnswerExportVO> dataList = answerService.listAnswersBySurvey(surveyId); response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setCharacterEncoding("utf-8"); response.setHeader("Content-Disposition", "attachment;filename=survey_" + surveyId + ".xlsx"); EasyExcel.write(response.getOutputStream(), AnswerExportVO.class) .sheet("答卷数据") .doWrite(dataList); }

这里有一个必须说明的坑:导出Excel时列名最好不要用中文实体类字段名映射,因为EasyExcel默认用反射取字段名或@ExcelProperty注解的值,如果字段名不规范,导出来的表头会很难看。我的做法是在VO上明确定义列名:

@Data public class AnswerExportVO { @ExcelProperty("提交时间") private String submitTime; @ExcelProperty("IP地址") private String ipAddress; @ExcelProperty("答卷内容") private String answerSummary; }

5.2 密码加密存储

管理员登录的密码不能明文存储。我用的是SHA-256 + 盐的方案:

public String encryptPassword(String rawPassword, String salt) { String combined = rawPassword + salt; return DigestUtils.sha256Hex(combined); }

盐值在用户创建时随机生成,存在sys_user表里。登录时把输入密码加上盐再加密,和数据库里的密文比对。

注意一点:MD5/SHA系列加密并不安全,容易被彩虹表反查。如果项目用于生产环境,建议引入BCrypt等加盐哈希算法。我这套系统因为主攻教学和毕设演示,使用SHA-256配合随机盐足以说明加密思路。

5.3 题目逻辑跳转

原本计划做"选择某个选项就跳转特定题目"的逻辑跳转功能,后来在实际开发中砍掉了。原因很简单:问卷的逻辑跳转会大幅增加数据结构和前端渲染的复杂度,一个题目可能要关联N个后续题目,前端需要实时判断、动态隐藏显示。对于毕设或课程设计来说,这个功能属于加分项,有了确实漂亮,但代价很大,我当时权衡后决定不做,把精力放在统计和导出这些更实用的功能上。

如果你一定要实现这个功能,我的建议是:在题目表增加jump_rule字段,存JSON结构,如{"选项值":"跳转题目ID"},前端每题渲染完成后检查跳转规则,根据选中的选项控制后续题目DOM的显隐。后端提交时只保存当前可见题目的答案即可。

6. 部署上线与后续扩展建议

6.1 打包部署流程

SpringBoot项目部署很简单,在IDEA右侧的Maven工具面板里执行clean package,然后在target目录下找到survey-0.0.1-SNAPSHOT.jar

java -Xms256m -Xmx512m -jar survey-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod

生产环境我建议单独建一份application-prod.yml,把数据库地址、服务器地址这些都作为环境变量注入,不要把生产库密码写在代码里提交到Git。比如:

spring: datasource: url: jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_NAME}?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: ${DB_USERNAME} password: ${DB_PASSWORD}

启动时通过--DB_HOST=xxx或者操作系统的环境变量传入值。

6.2 容器化部署参考

如果服务器上有Docker环境,可以直接用Dockerfile打包镜像:

FROM openjdk:8-jre-alpine COPY target/survey-0.0.1-SNAPSHOT.jar /app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "/app.jar"]

MySQL如果用Docker跑,记得把数据目录挂载到宿主机,否则容器一删数据全没了。

6.3 功能扩展路线

这套问卷系统目前跑通了核心链路,但真要往生产级靠拢,下面几个方向值得继续做:

  • 问卷模板库:预设常用问卷模板,管理员一键复制修改
  • 答卷分页/分端统计:按提交时间段、按IP归属地筛选数据
  • 问卷回收率:把问卷发给指定用户列表,统计已答/未答比例
  • 定时发布:到期自动关闭问卷,不需要管理员手动操作
  • 暗号验证:管理员可以设置问卷访问密码,只有输对密码才能填写

这些功能的实现思路在这个核心架构上扩展都不算复杂,表结构也预留了扩展空间。

最后分享两个小技巧

第一,开发问卷类系统时,先想清楚数据统计怎么算,再设计表结构。我见过太多人先建表、后写代码,最后做到统计功能时发现表结构设计不合理,不得不返工。题目选项是存JSON还是独立表、答案是纵表还是横表,这些决策在做ER图的那一刻就应该想清楚。

第二,前端把题目JSON结构定义好后,先写死一组数据测试,不要等到后端接口通了再联调。我用的是Thymeleaf,写了个临时接口直接返回Mock数据,前端的题目渲染、必填校验、选项样式全都提前调通了,后端工作完成后对接几乎零成本。

这套系统前前后后从动手到跑通花了大概一周时间,现在代码已经整理干净,数据库脚本、接口文档都在里面。做毕设的话直接改改前端的Logo和文案就能用,想深入学习SpringBoot底层原理的话,逐行读一遍源码也是个很不错的项目样本。遇到问题欢迎在评论区和大家一起讨论。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询