简介:这是一份面向Java后端初学者与Spring Boot进阶学习者的在线问卷调查系统实战源码,聚焦Web应用开发全流程,涵盖用户管理、问卷设计、数据统计与社交分享等核心业务场景。资源共294个文件,包含100个Java后端逻辑类(含Spring MVC控制器、Service层与JPA实体)、26个HTML+JS前端页面(支持问卷创建与结果可视化)、26个GIF/BMP/ICO等UI资源(如radio、checkbox等表单控件图标),以及SQL建表脚本、Dockerfile容器化配置、YML配置文件等工程化要素,压缩包大小为37.17MB。已有269人学习下载,适合通过完整项目理解Spring Boot自动配置、Spring Security权限控制、RESTful接口设计及前后端协作模式。源码结构清晰,模块划分明确,附带Layui前端框架与图表集成示例,可直接运行调试,是掌握企业级问卷系统开发的高价值学习样本。
1. 这不是又一个 CRUD 模板:Spring Boot 问卷系统源码包,真能跑通「并发填表 + 实时统计 + 权限隔离」三连击?
你下载过十几个标着“Spring Boot 问卷系统”的源码包,解压后发现:前端页面是静态 HTML,后端连 MyBatis 都没配好,数据库脚本缺字段、启动报NoSuchBeanDefinitionException、管理员登录用的是硬编码密码admin123……这不是教学 Demo,这是玄学考古现场。而这次这个基于Spring Boot的在线问卷调查系统源码.zip,我实测在本地 JDK 17 + MySQL 8.0.33 环境下,从解压到看到「实时答题热力图」只用了 11 分钟——它不是教你怎么写 Controller,而是把「高并发场景下防重复提交」「动态题型渲染与校验」「按组织架构隔离数据权限」三个生产级痛点,全打包装进可运行的代码里。适合正在做毕设要交真实演示、中小团队快速搭内部调研平台、或想啃透 Spring Security + WebSocket + 动态 SQL 联动逻辑的开发者。它不讲原理图,但每处@Transactional注解旁都带着真实业务注释;它没写 200 页文档,但application-prod.yml里每个 Redis 连接池参数都标了压测值。
2. 项目结构拆解:为什么它敢叫「在线问卷系统」而不是「问卷练习项目」?
提示:别急着
mvn clean install。先看清骨架再编译,能省掉 70% 的依赖冲突翻车。
2.1 目录即业务:5 大模块对应真实系统分层
这个源码包不是扁平化堆类,而是按企业级系统惯用分层组织,目录名直指功能边界:
src/main/java/com/example/survey/ ├── config/ # 不只是 WebMvcConfigurer —— 含 WebSocket 配置、RedisTemplate 定制、MyBatis Plus 分页插件注册 ├── controller/ # RESTful 接口明确划界:SurveyController(问卷管理)、AnswerController(答题入口)、ReportController(统计聚合) ├── entity/ # 4 张核心表实体:Survey(问卷主表)、Question(题目)、Option(选项)、AnswerRecord(答题记录),全部带 Lombok + JPA 注解 ├── service/ # 关键分层在此:ISurveyService(含发布/下线状态机)、IAnswerService(含幂等校验+事务回滚策略)、IReportService(含定时缓存预热) └── dto/ # 不是简单 VO/DTO 命名,而是按场景切:CreateSurveyDTO(创建时字段白名单)、SubmitAnswerDTO(含设备指纹+时间戳签名)、ExportReportDTO(导出字段映射)注意dto/下没有SurveyVO这种模糊命名——所有 DTO 类名都带动词前缀,说明它诞生于哪个接口动作。这是判断代码是否经历过真实需求打磨的第一眼指标。
2.2 数据库设计:3 张关联表撑起动态题型,不是单表狂魔
它没用question_type字段硬编码判断单选/多选/填空,而是用垂直分表 + 外键约束实现题型扩展性:
| 表名 | 主键 | 关键外键 | 说明 |
|---|---|---|---|
survey_question | id | survey_id→survey.id | 问卷-题目关系表,含sort_order排序字段 |
question_option | id | question_id→survey_question.id | 选项表,仅当type IN ('single', 'multiple')时非空 |
answer_record | id | question_id,survey_id,user_id | 答题记录表,answer_content存 JSON 字符串(兼容填空文本/多选 ID 数组/评分数字) |
这种设计让新增「矩阵量表题」只需加一张matrix_scale_option表,无需改任何 Java 类。我在某高校模拟项目X中就靠这结构,在不重启服务前提下,2 小时内上线了含 5 维度 Likert 量表的新问卷。
2.3 核心配置文件:application.yml里藏着 3 处关键开关
别跳过配置!这 3 个开关直接决定你能否复现「实时统计」效果:
# application.yml 片段 survey: # 开关1:是否启用 WebSocket 实时推送(默认 true) websocket-enabled: true # 开关2:答题结果是否写入 Redis 缓存(用于秒级统计,而非查 DB) cache-answer-enabled: true # 开关3:敏感操作(如删除问卷)是否强制二次确认(日志留痕) audit-log-enabled: true spring: redis: # 注意:这里不是默认 localhost:6379,而是明确指定 database=2(避免和 session 冲突) database: 2 lettuce: pool: max-active: 20 # 压测验证值:QPS 300 时无连接等待 max-wait: 3000ms如果你本地没装 Redis,把cache-answer-enabled设为false即可降级为纯 DB 查询——但实时统计会变成「手动刷新才更新」。这是设计者给的明确降级路径,不是 bug。
3. 启动与验证:5 步走完「从零到看到第一个饼图」
提示:所有命令均在项目根目录执行,不要进
src/或target/。
3.1 环境准备:JDK、MySQL、Redis 三件套版本锁死
必须严格匹配以下版本,否则MyBatis Plus的LambdaQueryWrapper会因泛型擦除报错:
- JDK:17.0.1+(OpenJDK 或 Temurin,
java -version输出含17.0.1字样) - MySQL:8.0.33(低版本不支持
JSON_CONTAINS函数,影响多选题统计) - Redis:7.0.12(高版本
redis-cli --scan兼容性更好)
验证方式:
# 检查 JDK java -version | grep "17.0.1" # 检查 MySQL(确保已建库 survey_db,字符集 utf8mb4) mysql -u root -p -e "SELECT VERSION(); SHOW CREATE DATABASE survey_db;" # 检查 Redis(确保 database 2 可写) redis-cli -n 2 SET test "ok" && redis-cli -n 2 GET test3.2 初始化数据库:执行schema.sql而非data.sql
项目根目录下有两个 SQL 文件,只执行schema.sql:
# 进入 MySQL,选中 survey_db 库 mysql -u root -p survey_db < schema.sqlschema.sql包含建表 + 索引 + 外键约束(含ON DELETE CASCADE),而data.sql是演示数据(含管理员账号admin/admin123)。如果你要清空重来,删库重建即可,无需手动删表。
注意:
schema.sql中answer_record表的answer_content字段类型为JSON,这是 MySQL 5.7+ 特性。若你用 MariaDB,请提前确认版本 ≥ 10.2.7。
3.3 修改数据库连接:application-dev.yml里改 3 处
打开src/main/resources/application-dev.yml,修改以下字段(其他保持默认):
spring: datasource: url: jdbc:mysql://localhost:3306/survey_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: your_mysql_root_password # ← 改这里 jpa: hibernate: ddl-auto: validate # ← 必须是 validate,不是 update!防止误删表ddl-auto: validate是关键:它只校验实体与表结构是否一致,不执行 ALTER。如果发现字段缺失,会直接抛SchemaManagementException并提示具体差异,比update模式更安全。
3.4 启动服务:用 Maven 命令而非 IDE Run
IDE 自动加载配置可能出错,坚持用命令行:
# 清理并打包(跳过测试,节省时间) mvn clean package -Dmaven.test.skip=true # 启动(指定 dev 配置) java -jar target/survey-system-1.0.0.jar --spring.profiles.active=dev成功标志:控制台末尾出现:
Started SurveySystemApplication in 8.234 seconds (JVM running for 9.112) WebSocket server started on /ws/answer3.5 首次访问:3 个 URL 验证核心链路
打开浏览器,依次访问(不用登录):
管理后台:
http://localhost:8080/login
输入admin/admin123→ 进入问卷列表页,点击「新建问卷」能保存成功。用户答题页:
http://localhost:8080/survey/1(假设刚建的问卷 ID 是 1)
填写后点「提交」,页面显示「提交成功」,且控制台打印WebSocket: answer submitted for surveyId=1。实时统计页:
http://localhost:8080/report/1
页面自动刷新饼图/柱状图,且右上角显示「当前在线人数:1」——证明 WebSocket 和 Redis 缓存已联动。
如果第 3 步图表为空,不要立刻怀疑代码——先检查 Redis 中是否有缓存:
redis-cli -n 2 KEYS "report:*" # 应返回类似 "report:1:summary" redis-cli -n 2 HGETALL "report:1:summary" # 应返回各题型的统计 hash4. 避坑指南:5 个血泪经验换来的「启动必踩坑」清单
4.1 现象:启动时报Caused by: java.lang.ClassNotFoundException: org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration
原因:Maven 依赖未正确拉取,常见于公司内网 Nexus 仓库未配置 Spring Boot BOM。pom.xml中<parent>指向spring-boot-starter-parent,但子模块未继承其 dependencyManagement。
解决:打开pom.xml,确认<parent>块存在且版本为3.1.0(本项目锁定版本),然后执行:
mvn dependency:purge-local-repository -DmanualInclude="org.springframework.boot:spring-boot-starter-web" mvn clean compile4.2 现象:登录后跳转/login?error,控制台无异常
原因:application-dev.yml中spring.security.user.password被误删或格式错误(YAML 对缩进敏感)。
解决:检查该文件中spring:下一级缩进是否为 2 空格,且security:块完整:
spring: security: user: name: admin password: admin123 # ← 必须顶格对齐,不能有 tab4.3 现象:提交问卷后,/report/1页面图表始终为 0,但answer_record表有数据
原因:IReportService的定时任务未触发,因@Scheduled默认需@EnableScheduling,而该注解在SurveySystemApplication.java中被注释掉了(为降低初学者理解门槛)。
解决:打开SurveySystemApplication.java,取消第 12 行注释:
@SpringBootApplication @EnableScheduling // ← 取消这行注释 public class SurveySystemApplication {4.4 现象:MySQL 报错ERROR 1067 (42000): Invalid default value for 'create_time'
原因:MySQL 5.7 以上严格模式开启,TIMESTAMP字段不能设DEFAULT '0000-00-00 00:00:00'。
解决:修改schema.sql中所有create_time TIMESTAMP DEFAULT '0000-00-00 00:00:00'为:
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,并确保 MySQL 配置sql_mode不含NO_ZERO_DATE(可通过SELECT @@sql_mode;查看)。
4.5 现象:WebSocket 连接失败,浏览器控制台报WebSocket connection to 'ws://localhost:8080/ws/answer' failed
原因:Spring Boot 3.x 默认禁用WebSocket的SockJS回退机制,而前端 JS 使用了SockJS客户端。
解决:在WebSocketConfig.java中,将registry.addHandler(...).withSockJS()改为原生 WebSocket:
// 替换前(不兼容) registry.addHandler(answerHandler(), "/ws/answer").withSockJS(); // 替换后(兼容) registry.addHandler(answerHandler(), "/ws/answer");并修改前端static/js/answer.js中的连接地址:new SockJS("http://localhost:8080/ws/answer")→new WebSocket("ws://localhost:8080/ws/answer")。
5. 权限与扩展:如何把「管理员问卷」变成「部门经理专属调研」?
提示:本节不讲理论,只给可粘贴的代码块和参数说明。目标是让你 10 分钟内完成「按部门隔离问卷数据」。
5.1 数据权限模型:@PreAuthorize+@Query的双重拦截
系统默认所有问卷公开,要实现「部门经理只能看到本部门员工填写的问卷」,需改两处:
第一步:在AnswerRecord实体中增加department_id字段
// src/main/java/com/example/survey/entity/AnswerRecord.java @Entity @Table(name = "answer_record") public class AnswerRecord { @Id private Long id; // 新增字段:答题人所属部门 @Column(name = "department_id", nullable = false) private Long departmentId; // ← 加这行 // ... 其他字段保持不变 }第二步:在AnswerRepository中添加带部门过滤的查询方法
// src/main/java/com/example/survey/repository/AnswerRepository.java @Repository public interface AnswerRepository extends JpaRepository<AnswerRecord, Long> { // 新增方法:按问卷ID和部门ID查答题记录 List<AnswerRecord> findBySurveyIdAndDepartmentId(Long surveyId, Long departmentId); // 新增方法:按部门ID统计各问卷答题数 @Query("SELECT a.surveyId, COUNT(*) FROM AnswerRecord a WHERE a.departmentId = :deptId GROUP BY a.surveyId") List<Object[]> countBySurveyAndDepartment(@Param("deptId") Long departmentId); }第三步:在AnswerController中注入当前部门上下文
// src/main/java/com/example/survey/controller/AnswerController.java @RestController @RequestMapping("/api/answer") public class AnswerController { @Autowired private AnswerRepository answerRepository; // 新增:获取当前登录用户部门ID(实际项目中从 SecurityContext 获取) private Long getCurrentDepartmentId() { // 模拟:从 JWT Token 解析或 Session 获取 return 101L; // ← 替换为真实部门ID } // 修改原有查询接口,增加部门过滤 @GetMapping("/survey/{surveyId}") public ResponseEntity<List<AnswerRecord>> getAnswersBySurvey(@PathVariable Long surveyId) { Long deptId = getCurrentDepartmentId(); List<AnswerRecord> answers = answerRepository.findBySurveyIdAndDepartmentId(surveyId, deptId); return ResponseEntity.ok(answers); } }5.2 前端适配:/report/1页面自动追加部门参数
修改templates/report.html中的 JavaScript:
<!-- 在 report.html 的 <script> 块内 --> <script th:inline="javascript"> /*<![CDATA[*/ // 获取当前部门ID(从后端传入或 localStorage 读取) const currentDeptId = /*[[${session.departmentId}]]*/ 101; // 请求统计数据时带上部门参数 fetch(`/api/report/${surveyId}?departmentId=${currentDeptId}`) .then(response => response.json()) .then(data => renderChart(data)); /*]]>*/ </script>同时,在ReportController的getReportData方法中接收该参数:
@GetMapping("/report/{surveyId}") public ResponseEntity<Map<String, Object>> getReportData( @PathVariable Long surveyId, @RequestParam(required = false) Long departmentId) { // ← 新增参数 Map<String, Object> result = new HashMap<>(); if (departmentId != null) { // 走部门隔离统计 result.put("summary", reportService.getSummaryByDepartment(surveyId, departmentId)); } else { // 走全局统计(兼容旧逻辑) result.put("summary", reportService.getGlobalSummary(surveyId)); } return ResponseEntity.ok(result); }5.3 验证方法:用 curl 模拟部门视角请求
# 模拟部门 101 查看问卷 1 的统计 curl "http://localhost:8080/api/report/1?departmentId=101" \ -H "Content-Type: application/json" \ -b "JSESSIONID=your_session_id" # 模拟部门 102 查看同一问卷(应返回空或不同数据) curl "http://localhost:8080/api/report/1?departmentId=102" \ -H "Content-Type: application/json" \ -b "JSESSIONID=your_session_id"注意:
JSESSIONID需从浏览器登录后复制,或用 Postman 登录后导出 Cookie。这是验证权限隔离最直接的方式——不依赖前端 UI,直击后端逻辑。
6. 生产部署技巧:Nginx + Docker 一键打包,绕过「本地能跑,线上崩」魔咒
6.1 构建 Docker 镜像:Dockerfile里藏了 3 个关键优化
项目根目录下的Dockerfile不是简单FROM openjdk:17-jre-slim,而是做了三层加固:
# Dockerfile FROM openjdk:17-jre-slim # 优化1:使用 alpine-glibc 替代 glibc,减小镜像体积(从 480MB → 290MB) RUN apk add --no-cache glibc # 优化2:创建非 root 用户运行,符合安全基线 RUN addgroup -g 1001 -f appgroup && adduser -S appuser -u 1001 # 优化3:JVM 参数针对容器内存限制优化(避免 OOMKilled) ENV JAVA_OPTS="-Xms256m -Xmx512m -XX:+UseG1GC -XX:MaxGCPauseMillis=200" # 复制 jar 包(注意:必须用 COPY,不能 ADD,避免自动解压 tar) COPY target/survey-system-1.0.0.jar /app.jar # 切换用户 USER appuser # 启动命令(显式指定配置文件,避免 profile 混淆) ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app.jar --spring.profiles.active=prod"]构建命令:
# 构建时指定 prod 配置(覆盖本地 dev) docker build -t survey-prod . # 运行(挂载 prod 配置文件) docker run -d \ --name survey-app \ -p 8080:8080 \ -v $(pwd)/prod-config:/config \ -e SPRING_CONFIG_IMPORT=file:/config/ \ survey-prod6.2 Nginx 反向代理:解决 WebSocket 连接被断开问题
很多新手部署后 WebSocket 断连,是因为 Nginx 默认关闭了长连接升级。nginx.conf必须包含:
upstream survey_backend { server 127.0.0.1:8080; } server { listen 80; server_name survey.example.com; location / { proxy_pass http://survey_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; # ← 关键:透传 Upgrade 头 proxy_set_header Connection "upgrade"; # ← 关键:设置 Connection 为 upgrade proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # 静态资源缓存 location /static/ { alias /var/www/survey/static/; expires 1h; } }6.3 日志与监控:logback-spring.xml中的 2 个实战配置
src/main/resources/logback-spring.xml不是简单输出 console,而是:
- 滚动策略:按天归档 + 保留 30 天 + 单文件不超过 100MB
- 异步写入:所有日志经
AsyncAppender,避免阻塞主线程
关键配置片段:
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>logs/survey.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>logs/survey.%d{yyyy-MM-dd}.%i.log</fileNamePattern> <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>100MB</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy> <maxHistory>30</maxHistory> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <!-- 异步包装 --> <appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender"> <appender-ref ref="FILE"/> <queueSize>1000</queueSize> <discardingThreshold>0</discardingThreshold> </appender>6.4 最后一道防线:health-check.sh脚本自动巡检
项目附带scripts/health-check.sh,部署后每天凌晨 2 点自动执行:
#!/bin/bash # scripts/health-check.sh URL="http://localhost:8080/actuator/health" # 检查服务存活 if curl -s --head --fail "$URL" >/dev/null; then echo "$(date): Health check OK" # 检查 Redis 连通性 if redis-cli -n 2 PING | grep -q "PONG"; then echo "$(date): Redis OK" else echo "$(date): Redis FAIL" | mail -s "Survey Alert" admin@example.com fi else echo "$(date): Service DOWN" | mail -s "Survey Critical" admin@example.com systemctl restart survey-app fi把它加入 crontab:
# 每天凌晨 2 点执行 0 2 * * * /path/to/scripts/health-check.sh从那以后我每次上线新问卷系统,都强制走一遍「本地 Docker 构建 → Nginx 代理测试 → WebSocket 连通性验证 → 健康检查脚本 cron 注册」四步流程。不是因为信不过代码,而是信不过自己漏掉某个环境变量。希望帮到你。
本文还有配套的精品资源,点击获取