简介:这份资源是基于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 List<Question> 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?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/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.sql,Spring 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 层保存。先看 Controller:
@Controller @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 取问卷 ID,@RequestBody把 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 List<QuestionStatVO> getStatistics(Long surveyId) { List<Question> questions = questionRepository.findBySurveyIdOrderBySortOrder(surveyId); List<QuestionStatVO> result = new ArrayList<>(); for (Question q : questions) { QuestionStatVO vo = new QuestionStatVO(); vo.setQuestionId(q.getId()); vo.setContent(q.getContent()); List<Answer> answers = answerRepository.findByQuestionId(q.getId()); Map<String, 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-name,URL 加serverTimezone=Asia/Shanghai。如果还不行,检查 MySQL 是否允许远程连接,bind-address是不是 127.0.0.1。
5.2 表不存在或字段不匹配:ddl-auto 的坑
现象:启动报Table 'survey_db.survey' doesn't exist。原因:ddl-auto 设成了 none 或 validate,但数据库里没建表。解决:第一次跑设成 update,让 Hibernate 自动建表。如果表已存在但字段对不上,update 会尝试加字段但不会删字段,改实体后最好手动同步或重建库。注意生产环境别用 update,容易出意外。
5.3 中文乱码:字符集要统一
现象:页面显示问号或乱码。原因:数据库、连接、页面三处字符集不一致。解决:建库时用 utf8mb4,连接 URL 加characterEncoding=utf8,Thymeleaf 页面加<meta charset="UTF-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 List<QuestionDTO> 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) { List<QuestionStatVO> stats = getStatistics(surveyId); List<StatExportDTO> exportData = new ArrayList<>(); for (QuestionStatVO vo : stats) { for (Map.Entry<String, 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;filename=statistics.xlsx"); EasyExcel.write(response.getOutputStream(), StatExportDTO.class) .sheet("统计") .doWrite(exportData); }StatExportDTO用@ExcelProperty注解标列名。导出时注意 response 的 header 设置,文件名用 URLEncoder 编码防止中文乱码。数据量大时用doWrite分批写,别一次性全加载到内存。
6.3 改造时的一个教训
我改这个项目时踩过一个坑:给问卷加了逻辑跳转功能,题目 A 选某个选项才显示题目 B。前端用 JS 控制显示隐藏,后端没做校验,结果有人绕过前端直接提交了不该出现的答案。后来我在 Service 层加了校验,根据已答题目动态判断哪些题该答、哪些不该答,不匹配的直接拒绝。从那以后我每次加前端交互逻辑,都强制走一遍后端校验,前端控制体验,后端控制数据,两边都不能省。希望帮到你。
本文还有配套的精品资源,点击获取