Spring Boot 在线问卷系统源码实战:从跑通到二次开发

发布时间:2026/10/23 14:20:05
Spring Boot 在线问卷系统源码实战:从跑通到二次开发
简介这份资源是基于Spring Boot的在线问卷调查系统完整源码项目名为questionnaire-master面向具备Java与Spring基础、希望积累完整Web项目实战经验的学习者与开发者。系统覆盖用户登录注册与权限控制、问卷模板创建编辑、社交网络分享、结果统计与图表展示、个人中心等核心模块并涉及Spring Security、Spring Data JPA、Spring MVC、RESTful API设计及MySQL数据库表结构设计等知识点可作为课程设计或毕业设计的参考方案。压缩包共294个文件以100个java源码、26个html页面、26个js脚本、8个css样式及4个sql建表脚本为主另含properties配置、dockerfile与yml部署文件整体约37.17MB目录结构清晰便于按模块阅读与二次开发。目前已有269人学习下载适合通过源码理解前后端交互流程、数据库设计与接口分层快速掌握Spring Boot项目从搭建到部署的完整开发思路。1. 从一份问卷源码说起为什么我建议你从“跑通”开始前阵子帮一个做市场调研的朋友看需求他们团队想搭一套内部用的问卷收集工具预算有限又不想用第三方平台数据得留在自己服务器上。我翻了一圈开源方案最后把目光落在一份基于 Spring Boot 的在线问卷调查系统源码上。这东西说白了就是一套能自己部署的问卷工具管理员建问卷、发链接、收答卷、看统计用户打开浏览器就能填后端用 Spring Boot 把接口和页面串起来。它适合两类人一类是刚学完 Spring Boot 想找个完整项目练手的开发者另一类是需要私有化问卷系统、又不想从零造轮子的小团队。我拿到源码的第一件事不是读代码而是先把它跑起来——跑不通读再多也是纸上谈兵。2. 技术栈与目录结构先看清骨架再动手2.1 技术选型拆解为什么是 Spring Boot 这套组合这份源码的技术栈是典型的 Spring Boot 全家桶路线。后端核心是 Spring Boot配合 Spring MVC 处理请求路由Spring Data JPA 或 MyBatis 做持久层具体用哪个得看 pom.xml 里的依赖。前端大概率是 Thymeleaf 模板引擎因为问卷系统需要服务端渲染页面Thymeleaf 和 Spring Boot 集成最顺。数据库方面常见做法是 MySQL也有用 H2 做演示环境的。构建工具是 Maven打包方式 jar内嵌 Tomcat所以启动就是java -jar一条命令的事。为什么这套组合适合问卷系统问卷的业务逻辑不复杂核心就是增删改查加统计Spring Boot 的自动配置能省掉大量 XML 配置让开发者把精力放在问卷逻辑上。JPA 的实体映射对问卷、题目、选项、答卷这些关联关系处理起来很自然一对多、多对多的注解一贴就行。Thymeleaf 虽然现在前后端分离是大趋势但问卷系统页面交互不重服务端渲染反而部署简单不用单独起前端工程。我一般拿到一个 Spring Boot 项目先看三个地方pom.xml 确认依赖版本和组件application.properties 或 application.yml 确认数据库连接和端口然后看启动类的位置。这三处看完项目能不能跑起来心里就有七成底了。2.2 目录结构逐层拆解每个包负责什么源码的目录结构遵循 Maven 标准布局我按包名逐个说。根目录下 src/main/java 放 Java 代码src/main/resources 放配置和静态资源src/test 放测试代码。Java 代码按分层拆成几个包controller 包处理 HTTP 请求接收前端参数调用 service返回视图或 JSON。问卷的创建、发布、提交、统计接口都在这里。service 包业务逻辑层问卷的组装、答案的校验、统计数据的计算。这一层是核心代码量最大。repository 或 dao 包数据访问层继承 JpaRepository 或写 MyBatis Mapper 接口负责和数据库打交道。entity 包实体类对应数据库表。问卷、题目、选项、答卷、答案这些表各有对应的实体。dto 或 vo 包数据传输对象用于前端和后端之间的数据封装避免直接暴露实体。config 包配置类比如 WebMvcConfig 做拦截器注册SecurityConfig 做权限控制。resources 目录下templates 放 Thymeleaf 模板static 放 CSS、JS、图片application.yml 是主配置。这种分层结构清晰改起来也好定位——要改问卷创建逻辑就去 service要改页面就去 templates。2.3 数据库表设计问卷系统的核心关系问卷系统的数据库设计有几个关键表我按常见做法推演一下。问卷表存问卷标题、描述、状态、创建时间题目表存题目内容、类型单选、多选、填空、排序、所属问卷 ID选项表存选项内容、排序、所属题目 ID答卷表存答卷人信息、提交时间、所属问卷 ID答案表存具体答案内容、所属答卷 ID、所属题目 ID。这几张表的关系是问卷一对多题目题目一对多选项问卷一对多答卷答卷一对多答案。用 JPA 映射的话问卷实体里用OneToMany关联题目列表题目实体里用ManyToOne指回问卷。这种双向关联在保存和查询时要注意级联配置不然容易出问题。Entity Table(name survey) public class Survey { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; private String title; private String description; private Integer status; // 0-草稿 1-已发布 2-已关闭 OneToMany(mappedBy survey, cascade CascadeType.ALL, orphanRemoval true) OrderBy(sortOrder ASC) private ListQuestion questions new ArrayList(); // getter/setter 省略 }这段代码里cascade CascadeType.ALL表示保存问卷时级联保存题目orphanRemoval true表示从列表移除的题目会被删除。OrderBy保证题目按排序字段返回。参数上status 用整数表示状态比布尔值灵活能扩展更多状态。注意级联删除要谨慎问卷删了题目和答案都没了生产环境建议用软删除。3. 本地运行全流程从 clone 到浏览器访问3.1 环境准备与依赖安装跑这个项目需要几个基础环境。JDK 版本看 pom.xml 里的 java.version常见是 1.8 或 11我建议用 11兼容性好。Maven 3.6 以上用来拉依赖和打包。MySQL 5.7 或 8.0建一个空数据库字符集用 utf8mb4不然中文和 emoji 会乱码。IDE 用 IntelliJ IDEA 或 Eclipse 都行IDEA 对 Spring Boot 支持更好。安装完环境先确认版本java -version mvn -version mysql --version三条命令都能输出版本号说明环境没问题。如果 mvn 命令找不到检查 MAVEN_HOME 和 PATH 配置。MySQL 装好后用mysql -u root -p登录建库CREATE DATABASE survey_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;数据库名要和配置文件里的一致不一致启动时会报连接失败。3.2 配置文件修改与数据库初始化打开 src/main/resources/application.yml 或 application.properties改数据库连接信息。常见配置项spring: datasource: url: jdbc:mysql://localhost:3306/survey_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update show-sql: true server: port: 8080ddl-auto: update表示启动时自动根据实体建表或更新表结构第一次跑很方便生产环境建议改成 none 或 validate。show-sql: true会在控制台打印 SQL调试时有用上线关掉。serverTimezone必须配不然 MySQL 8 会报时区错误。如果源码里带了 schema.sql 或 data.sqlSpring Boot 会自动执行初始化表结构和初始数据。3.3 启动与访问第一次跑通的验证步骤配置改完在项目根目录执行mvn clean package -DskipTests java -jar target/*.jar或者直接在 IDE 里右键启动类 Run。启动日志里看到Started Application in X seconds就成功了。浏览器访问http://localhost:8080应该能看到登录页或首页。如果源码带了默认账号一般在 data.sql 里常见是 admin/admin 或 admin/123456。第一次跑通后我习惯做三件事验证登录进去建一个问卷加两道题发布然后用另一个浏览器无痕模式填一遍最后看统计页面有没有数据。这三步走完说明核心链路是通的。如果哪一步卡住看控制台报错八成是数据库字段没对上或者外键约束问题。4. 核心模块代码拆解问卷创建与提交的完整链路4.1 问卷创建接口从请求到入库问卷创建是系统的入口功能Controller 层接收前端表单Service 层组装实体Repository 层保存。先看 ControllerController RequestMapping(/survey) public class SurveyController { Autowired private SurveyService surveyService; PostMapping(/create) public String createSurvey(ModelAttribute SurveyCreateDTO dto, RedirectAttributes redirectAttributes) { try { Long surveyId surveyService.createSurvey(dto); redirectAttributes.addFlashAttribute(msg, 创建成功); return redirect:/survey/edit/ surveyId; } catch (Exception e) { redirectAttributes.addFlashAttribute(error, e.getMessage()); return redirect:/survey/create; } } }ModelAttribute把表单参数绑定到 DTO 对象字段名要和表单 name 一致。返回redirect:是重定向避免刷新重复提交。RedirectAttributes用来传一次性提示消息。这里用 try-catch 包住失败时回创建页并带错误信息比直接抛异常体验好。Service 层的组装逻辑Service Transactional public class SurveyService { Autowired private SurveyRepository surveyRepository; public Long createSurvey(SurveyCreateDTO dto) { Survey survey new Survey(); survey.setTitle(dto.getTitle()); survey.setDescription(dto.getDescription()); survey.setStatus(0); // 草稿状态 survey.setCreateTime(new Date()); // 组装题目 if (dto.getQuestions() ! null) { for (QuestionDTO qDto : dto.getQuestions()) { Question question new Question(); question.setContent(qDto.getContent()); question.setType(qDto.getType()); question.setSortOrder(qDto.getSortOrder()); question.setSurvey(survey); // 组装选项 if (qDto.getOptions() ! null) { for (OptionDTO oDto : qDto.getOptions()) { Option option new Option(); option.setContent(oDto.getContent()); option.setSortOrder(oDto.getSortOrder()); option.setQuestion(question); question.getOptions().add(option); } } survey.getQuestions().add(question); } } Survey saved surveyRepository.save(survey); return saved.getId(); } }Transactional保证整个创建过程在一个事务里任何一步失败都回滚。这里手动组装实体而不是用 BeanUtils 拷贝是因为问卷的嵌套结构需要建立关联关系拷贝工具处理不了。注意question.setSurvey(survey)和option.setQuestion(question)这两行维护了双向关联JPA 保存时才能正确设置外键。4.2 答卷提交与答案存储答卷提交是另一个核心链路。用户填完问卷点提交前端把答案打包成 JSON 或表单数组发过来后端解析后存答卷和答案。PostMapping(/submit/{surveyId}) ResponseBody public Result submitAnswer(PathVariable Long surveyId, RequestBody AnswerSubmitDTO dto) { try { answerService.saveAnswer(surveyId, dto); return Result.success(提交成功); } catch (Exception e) { return Result.error(e.getMessage()); } }PathVariable从 URL 取问卷 IDRequestBody把 JSON 转成 DTO。返回Result是统一响应格式包含 code、msg、data 三个字段。Service 层public void saveAnswer(Long surveyId, AnswerSubmitDTO dto) { Survey survey surveyRepository.findById(surveyId) .orElseThrow(() - new RuntimeException(问卷不存在)); if (survey.getStatus() ! 1) { throw new RuntimeException(问卷未发布或已关闭); } AnswerSheet sheet new AnswerSheet(); sheet.setSurvey(survey); sheet.setSubmitTime(new Date()); sheet.setIp(dto.getIp()); answerSheetRepository.save(sheet); for (AnswerItemDTO item : dto.getItems()) { Answer answer new Answer(); answer.setSheet(sheet); answer.setQuestionId(item.getQuestionId()); answer.setContent(item.getContent()); answerRepository.save(answer); } }先校验问卷状态只有已发布的才能提交。答卷和答案分两次保存先存答卷拿到 ID再存答案关联答卷。这里没做批量插入答案多的时候性能一般优化的话可以用saveAll批量保存。4.3 统计结果查询聚合查询怎么写统计页面要展示每道题的选项分布。常见做法是用 SQL 聚合查询或者查出来在 Java 里算。数据量小的时候 Java 算更灵活public ListQuestionStatVO getStatistics(Long surveyId) { ListQuestion questions questionRepository.findBySurveyIdOrderBySortOrder(surveyId); ListQuestionStatVO result new ArrayList(); for (Question q : questions) { QuestionStatVO vo new QuestionStatVO(); vo.setQuestionId(q.getId()); vo.setContent(q.getContent()); ListAnswer answers answerRepository.findByQuestionId(q.getId()); MapString, Long countMap answers.stream() .collect(Collectors.groupingBy(Answer::getContent, Collectors.counting())); vo.setOptionCounts(countMap); vo.setTotal(answers.size()); result.add(vo); } return result; }groupingBy加counting是 Java 8 Stream 的经典组合按答案内容分组计数。这种写法在答案量几千条以内没问题上万条建议改成 SQL 的GROUP BY直接在数据库算。QuestionStatVO里 optionCounts 是 Map前端遍历渲染成柱状图或进度条。5. 避坑与常见问题跑不起来时先查这几处5.1 数据库连接失败时区和驱动是重灾区现象启动报Communications link failure或Unknown system variable。原因通常是 MySQL 8 的驱动类名变了老代码用com.mysql.jdbc.Driver新驱动是com.mysql.cj.jdbc.Driver。另外时区没配MySQL 8 默认 UTC连接会报时区错误。解决改 driver-class-nameURL 加serverTimezoneAsia/Shanghai。如果还不行检查 MySQL 是否允许远程连接bind-address是不是 127.0.0.1。5.2 表不存在或字段不匹配ddl-auto 的坑现象启动报Table survey_db.survey doesnt exist。原因ddl-auto 设成了 none 或 validate但数据库里没建表。解决第一次跑设成 update让 Hibernate 自动建表。如果表已存在但字段对不上update 会尝试加字段但不会删字段改实体后最好手动同步或重建库。注意生产环境别用 update容易出意外。5.3 中文乱码字符集要统一现象页面显示问号或乱码。原因数据库、连接、页面三处字符集不一致。解决建库时用 utf8mb4连接 URL 加characterEncodingutf8Thymeleaf 页面加meta charsetUTF-8。如果已经存了乱码数据改字符集后旧数据不会自动修复得重新录入。5.4 静态资源 404路径和拦截器问题现象页面样式丢失CSS/JS 加载 404。原因Spring Boot 默认静态资源在 static 目录下如果源码把资源放在 webapp 或自定义目录需要配spring.resources.static-locations。另外如果配了拦截器可能把静态资源也拦了。解决检查拦截器的 excludePathPatterns 有没有放行/static/**、/css/**、/js/**。5.5 提交答卷报错外键约束和事务回滚现象提交答卷时 500日志显示Cannot add or update a child row。原因答案表的外键指向答卷 ID但答卷还没保存成功就去存答案。解决确保先save(sheet)拿到 ID 再存答案或者用Transactional保证顺序。另一个可能是问卷 ID 传错答案关联了不存在的问卷。6. 二次开发改造从能跑到好用6.1 加一个问卷模板功能跑通之后想扩展我第一个想到的是问卷模板。现在建问卷得从零加题目如果常用问卷就那几种做个模板能省不少事。改造思路加一张 template 表存模板名称和题目结构 JSON建问卷时先选模板把 JSON 解析成题目列表预填进去。public Long createFromTemplate(Long templateId, String title) { Template template templateRepository.findById(templateId) .orElseThrow(() - new RuntimeException(模板不存在)); Survey survey new Survey(); survey.setTitle(title); survey.setStatus(0); // 解析模板 JSON ListQuestionDTO questions JSON.parseArray( template.getContent(), QuestionDTO.class); for (QuestionDTO qDto : questions) { Question question new Question(); question.setContent(qDto.getContent()); question.setType(qDto.getType()); question.setSurvey(survey); // 选项同理 survey.getQuestions().add(question); } return surveyRepository.save(survey).getId(); }这里用 FastJSON 或 Jackson 解析 JSON模板内容存成 JSON 字符串比存关联表灵活改模板结构不用动数据库。注意解析时要处理异常模板 JSON 格式错了要给提示。6.2 统计结果导出 Excel统计页面看完想导出用 EasyExcel 或 POI 都行。EasyExcel 更简单public void exportStatistics(Long surveyId, HttpServletResponse response) { ListQuestionStatVO stats getStatistics(surveyId); ListStatExportDTO exportData new ArrayList(); for (QuestionStatVO vo : stats) { for (Map.EntryString, Long entry : vo.getOptionCounts().entrySet()) { StatExportDTO dto new StatExportDTO(); dto.setQuestion(vo.getContent()); dto.setOption(entry.getKey()); dto.setCount(entry.getValue()); exportData.add(dto); } } response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setHeader(Content-Disposition, attachment;filenamestatistics.xlsx); EasyExcel.write(response.getOutputStream(), StatExportDTO.class) .sheet(统计) .doWrite(exportData); }StatExportDTO用ExcelProperty注解标列名。导出时注意 response 的 header 设置文件名用 URLEncoder 编码防止中文乱码。数据量大时用doWrite分批写别一次性全加载到内存。6.3 改造时的一个教训我改这个项目时踩过一个坑给问卷加了逻辑跳转功能题目 A 选某个选项才显示题目 B。前端用 JS 控制显示隐藏后端没做校验结果有人绕过前端直接提交了不该出现的答案。后来我在 Service 层加了校验根据已答题目动态判断哪些题该答、哪些不该答不匹配的直接拒绝。从那以后我每次加前端交互逻辑都强制走一遍后端校验前端控制体验后端控制数据两边都不能省。希望帮到你。本文还有配套的精品资源点击获取