JavaWeb在线问卷调查系统:从建表到统计的完整实践
简介这是一份基于JavaWeb的在线问卷调查系统课程设计源码包涵盖前后端完整工程与数据库脚本面向需要完成Java课设或学习Spring Boot、ServletJSP项目的开发者。系统实现用户注册登录、问卷创建与填写、单选题多选题文本题、发布暂停结束、数据统计及管理员后台管理等核心功能业务流程完整适合直接用于项目答辩或二次扩展。压缩包共1366个文件大小约93.77MB以java后端源码、html/css/js前端页面、sql建表脚本、xml配置文件为主体并包含png/svg等界面资源与md说明文档结构清晰便于定位。数据库按用户表、问卷表、问题表、选项表、答卷表等设计代码注释与目录组织可帮助理解问卷业务的数据流导入IDE并配置数据库即可运行节省从零搭建的时间。已有50人浏览学习可作为JavaWeb课程设计、毕业设计或就业项目练手的实用参考。1. 在线问卷调查系统JavaWeb 课程设计里最值得复现的一类全栈项目做 JavaWeb 课程设计时在线问卷调查系统是出现频率最高的选题之一但很多同学拿到的源码要么缺数据库脚本要么前后端对不上跑起来全是坑。这份资源是完整的 JavaWeb 前后端项目附带数据库脚本覆盖用户注册登录、问卷创建与填写、管理员管理、发布/暂停/结束状态控制以及基于单选题、多选题、文本题三种题型的数据统计。适合正在做 JavaWeb 课程设计的学生也适合想快速搭一套问卷原型、需要参考增删改查与业务状态流转写法的初级开发者。核心价值在于它不是单点功能的 Demo而是一条从建表到统计分析的完整链路。2. 架构选型与数据库设计六张表如何撑起问卷的创建、答题与统计2.1 技术选型Spring Boot JSP MySQL 为什么适合课程设计这套系统的后端主体是 Java Web 技术栈常见做法是 Spring Boot JSP配合 MyBatis 操作 MySQL。很多课程设计用 Servlet JSP 做也不是不行但 Spring Boot 内置 Tomcat省去单独配置服务器这一步导入 IDEA 直接跑起来对答辩演示更友好。资源里能看到 Controller、Service、Mapper 分层这是标准的 Spring Boot 三层结构。选型理由要落在两点上。第一JSP 做服务端渲染问卷页面这种表单密集型页面不需要额外写 Vue/React 的前后端对接代码JSTL 标签遍历问题列表即可代码量少一半第二MyBatis 的 XML 里写动态 SQL联表查询和统计聚合比 JPA 更直观也方便后期加条件。构建工具用 Maven依赖管理统一走 pom.xml换机器导入时不容易丢包。一个容易被忽略的点是 Java 版本。建议 JDK 8 或 11配 Spring Boot 2.x。IDEA 运行 JavaWeb 项目时记得确认 Maven 仓库路径和 JDK 编译级别否则会出现 java: 无效的目标发行版 这类启动即崩的问题。2.2 六张核心表的字段设计与建表 SQL根据需求分析数据库至少需要六张表users用户、surveys问卷、questions问题、options选项、responses答卷、answers具体答案。下面是我按这套系统整理出的建表脚本字段注释直接写在 SQL 里CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(100) NOT NULL COMMENT MD5加密后的密码, role TINYINT DEFAULT 0 COMMENT 0-普通用户,1-管理员, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE surveys ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(200) NOT NULL COMMENT 问卷标题, description VARCHAR(500) COMMENT 问卷说明, creator_id INT NOT NULL COMMENT 创建人ID, status TINYINT DEFAULT 0 COMMENT 0-草稿,1-发布中,2-已暂停,3-已结束, start_time DATETIME COMMENT 发布时间, end_time DATETIME COMMENT 结束时间, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_creator (creator_id) ); CREATE TABLE questions ( id INT AUTO_INCREMENT PRIMARY KEY, survey_id INT NOT NULL COMMENT 所属问卷, question_type TINYINT NOT NULL COMMENT 1-单选,2-多选,3-文本, title VARCHAR(500) NOT NULL COMMENT 题目内容, sort_no INT DEFAULT 0 COMMENT 排序号, required TINYINT DEFAULT 1 COMMENT 是否必答, KEY idx_survey (survey_id) ); CREATE TABLE options ( id INT AUTO_INCREMENT PRIMARY KEY, question_id INT NOT NULL COMMENT 所属题目, option_text VARCHAR(200) NOT NULL COMMENT 选项文字, sort_no INT DEFAULT 0, KEY idx_question (question_id) ); CREATE TABLE responses ( id INT AUTO_INCREMENT PRIMARY KEY, survey_id INT NOT NULL COMMENT 被答问卷, user_id INT COMMENT 答题人,未登录可为NULL, submit_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_survey_user (survey_id, user_id) ); CREATE TABLE answers ( id INT AUTO_INCREMENT PRIMARY KEY, response_id INT NOT NULL COMMENT 所属答卷, question_id INT NOT NULL COMMENT 对应题目, answer_text TEXT COMMENT 文本题答案, option_id INT COMMENT 选择题选中的选项ID, KEY idx_response (response_id) );这段脚本里有几个设计决策值得说明。answers 表同时保留 answer_text 和 option_id 两个字段是为了让选择题和文本题共用一张答案表后端查统计时按 question_type 分流处理不必为每种题型单独建表。surveys 表的 status 字段是状态机入口发布、暂停、结束都靠这个字段的数值切换后面管理端的所有逻辑都围绕它展开。另外我没有在 SQL 里强制声明外键而是通过索引和业务层保证关联。课程设计场景下这样写的好处是导入数据、删除数据不会被外键约束卡住避免演示时删个问卷直接报外键错误。2.3 表关系与状态字段发布、暂停、结束是怎么控制的六张表的关系是users 和 surveys 是 1 对 N一个用户可创建多份问卷管理员其实是 role1 的 users 记录surveys 和 questions 是 1 对 Nquestions 和 options 是 1 对 Nresponses 以 survey_id 关联问卷、user_id 关联用户answers 通过 response_id 挂到一张答卷下再通过 question_id 回查题目。状态流转是这套系统的业务核心。status 字段的四个值对应的行为如下status含义前端行为管理端行为0草稿不展示可编辑题目、可发布1发布中展示并可答题可暂停、可结束2已暂停不展示可恢复发布、可结束3已结束永久隐藏仅查看统计草稿状态的问卷用户可以继续编辑题目发布后前端答题列表只展示 status1 的数据暂停时保留已提交的答卷但答题入口关闭结束状态前端隐藏问卷管理端仍能看到统计结果。这个状态字面量在前后端要一致最怕数据库里写了 0-3 的注释前端 JS 里判断却写成了字符串 0后面排查半天才发现是类型不一致。3. 问卷创建到答案落库一条完整链路的实现与参数说明3.1 创建问卷动态生成问题与选项创建问卷是整套系统里最复杂的写操作因为一次请求要同时插入 surveys、questions、options 三张表。前端表单通常用动态添加行的方式让用户录入题目提交时把问题列表拼成一个 JSON 数组传给后端。后端需要做的是在事务里先插入问卷主记录拿到自增主键后再循环插入题目每个题目再循环插入选项。核心 Controller 代码如下PostMapping(/survey/save) ResponseBody public Result save(RequestBody SurveySaveRequest req, HttpSession session) { User user (User) session.getAttribute(loginUser); if (user null) { return Result.error(请先登录); } Survey survey new Survey(); survey.setTitle(req.getTitle()); survey.setDescription(req.getDescription()); survey.setCreatorId(user.getId()); survey.setStatus(0); // 新建问卷默认为草稿 ListQuestion questions req.getQuestions(); if (questions null || questions.isEmpty()) { return Result.error(问卷至少需要一道题目); } surveyService.saveWithQuestions(survey, questions); return Result.success(); }saveWithQuestions 的实现要点在 Service 层先调用 surveyMapper.insert 回填 survey.getId()再遍历 questions 调用 questionMapper.insert接着遍历每个 question 的 options 列表调用 optionMapper.insert。因为是多表写入方法上必须标注 Transactional任何一道题插入失败都要回滚整个问卷。我一般还会在题目循环里做一道校验——如果 questionType 是 1 或 2单选、多选options 必须至少有两个如果是 3文本题options 应该为空否则统计时会出现文本题带选项的脏数据。3.2 前端渲染JSP JSTL 怎么把问卷变成可答题表单问卷页面渲染有两个方案JSP 服务端渲染和纯 JS 动态生成。这套资源用的是 JSP 方案页面通过 JSTL 的 c:forEach 遍历 questions 列表每个问题根据 questionType 分支渲染成 radio、checkbox 或 textarea。这里有一个非常关键的细节——input 的 name 属性。单选组的 name 用 question_{id}多选的 name 用 question_{id}[]文本题用 question_{id}这样后端按固定前缀解析参数时才能把答案对应到具体题目。c:forEach items${questionList} varq varStatusst div classquestion-item p${st.index 1}. ${q.title} ${q.required 1 ? * : }/p c:choose c:when test${q.questionType 1} c:forEach items${q.options} varopt labelinput typeradio namequestion_${q.id} value${opt.id} / ${opt.optionText}/label /c:forEach /c:when c:when test${q.questionType 2} c:forEach items${q.options} varopt labelinput typecheckbox namequestion_${q.id}[] value${opt.id} / ${opt.optionText}/label /c:forEach /c:when c:otherwise textarea namequestion_${q.id} rows3 cols40/textarea /c:otherwise /c:choose /div /c:forEach这段 JSP 的核心是 name 命名规范。单选和文本题用 question_${q.id}多选用 question_${q.id}[]后端用 RequestParam MapString, String[] 接收所有参数然后遍历 Map 的 key凡是前缀是 question_ 的都按规则拆分出题目 ID再根据题目类型读取一个值还是多个值。多选用数组接收还有一个好处用户一个多选都不勾时请求里根本没有这个名字的参数后端要单独处理这种未作答情况把它记为空集合而不是直接抛参数缺失。3.3 答案提交事务与校验答题提交接口的输入是一张问卷的完整答案集合后端要同时写 responses 和 answers 两张表。先插入 responses 拿到自增主键再循环题目参数逐个插入 answers。由于是多表写入同样要加 Transactional任何一道题的答案写失败整个答卷要回滚避免出现有答卷没答案的半截数据。PostMapping(/survey/submit) Transactional public Result submit(RequestBody SubmitRequest req, HttpSession session) { // 校验问卷真实存在且状态为发布中 Survey survey surveyMapper.findById(req.getSurveyId()); if (survey null || survey.getStatus() ! 1) { return Result.error(问卷不存在或已停止); } Response resp new Response(); resp.setSurveyId(req.getSurveyId()); User user (User) session.getAttribute(loginUser); resp.setUserId(user null ? null : user.getId()); responseMapper.insert(resp); // 插入后 MyBatis 回填 resp.getId() MapInteger, String[] answers req.getAnswers(); for (Map.EntryInteger, String[] entry : answers.entrySet()) { Answer ans new Answer(); ans.setResponseId(resp.getId()); ans.setQuestionId(entry.getKey()); if (entry.getValue() ! null entry.getValue().length 1) { // 多选:每个选中选项生成一条答案记录 for (String optId : entry.getValue()) { ans.setOptionId(Integer.parseInt(optId)); answerMapper.insert(ans); } } else { // 单选或文本:数组长度一定为 1 String raw entry.getValue()[0]; Question q questionMapper.findById(entry.getKey()); if (q.getQuestionType() 3) { ans.setAnswerText(raw); } else { ans.setOptionId(Integer.parseInt(raw)); } answerMapper.insert(ans); } } return Result.success(); }这里要提醒一个细节同一张答题表单里文本题提交的值在 Map 里是 String[]但数组长度永远是 1可以用数组长度是否大于 1 来区分多选和单选/文本。还有一种更稳的做法是提交前先查一次题目类型按题型的预期数据规模处理但那样要多一次查询。课程设计规模下用数组长度判断足够但代码注释里要写清楚这个约定否则后来人看不懂为什么多选要循环插入。4. 管理后台与统计分析发布/暂停/结束状态和结果聚合4.1 管理端功能边界管理端要回答管理员能做什么这个问题。梳理下来的功能边界是查看所有问卷列表、查看每份问卷的答卷明细、切换问卷状态、查看统计结果。注意管理端不需要也不能编辑普通用户创建的问卷内容改题目会影响已提交答案的对应关系这是业务上要避免的。权限控制这里有一个常见做法登录接口校验通过后把用户对象放进 session管理端接口统一判断 session 里的 role 是否为 1。用拦截器做会更干净——写一个 HandlerInterceptor在 preHandle 里查 session不是管理员直接重定向到登录页同时放行 /login、/register 和静态资源路径。还有一个容易被答辩老师追问的点普通用户创建的问卷管理员能不能改这套资源的定位是管理员负责全局管理但问卷内容归创建人所有。所以我的建议是权限校验分两层状态切换和管理操作允许管理员或创建人问卷内容编辑只允许创建人。这样回答管理员能随便改用户问卷吗时就有明确答复。4.2 状态流转与权限控制状态切换的接口很简单参数只有 surveyId 和目标状态但要注意两点约束只有创建人或管理员可以调用状态只能按草稿 → 发布中 → 暂停/结束的方向走不能从已结束跳回草稿否则统计结果和状态会自相矛盾。下面是一个状态切换的 Service 示例Transactional public int changeStatus(int surveyId, int targetStatus, User operator) { Survey survey surveyMapper.findById(surveyId); if (survey null) { throw new BizException(问卷不存在); } if (operator.getId() ! survey.getCreatorId() operator.getRole() ! 1) { throw new BizException(无权操作该问卷); } if (survey.getStatus() 3) { throw new BizException(已结束的问卷不能改状态); } Survey update new Survey(); update.setId(surveyId); update.setStatus(targetStatus); if (targetStatus 1) update.setStartTime(new Date()); if (targetStatus 3) update.setEndTime(new Date()); return surveyMapper.updateStatus(update); }这套逻辑里最容易翻车的是校验顺序先查存在性再查权限最后查状态合法性。如果把状态判断放在权限之前普通用户可以通过改状态接口探出问卷是否处于草稿阶段属于越权信息泄露。状态切换成功后列表页和答题页都是实时查数据库所以不存在缓存一致性问题——前提是别在查询方法上画蛇添足加缓存。4.3 统计聚合单选、多选、文本题的三种计算方式统计分析是这份资源里技术含量最高的一块。三种题型分别处理单选统计是 group by option_id 计数多选要把每条答案里的每个选项拆开再聚合因为一套多选答案会生成多条 answers 记录文本题不参与图表统计直接列出原文列表分页展示。单选和多选的统计可以共用一条 SQL区别只在于多选答案天然是逐条记录的不需要额外拆分select idcountByOption resultTypemap SELECT a.option_id AS optionId, o.option_text AS optionText, COUNT(*) AS cnt FROM answers a INNER JOIN options o ON a.option_id o.id WHERE a.question_id #{questionId} GROUP BY a.option_id, o.option_text ORDER BY cnt DESC /select这条 SQL 的结果集能直接喂给前端 ECharts 的饼图不需要额外拼接数据。统计口径按题型区分如下题型统计方式占比口径单选GROUP BY option_id 计数占比分母 答卷数多选按 option_id 逐条计数占比分母 答卷数各选项占比之和可能超过 100%文本查 answer_text 原文列表不分页展示即可无占比概念多选统计的百分比分母不是答案条数而是答卷数这是多选和单选在统计口径上最大的区别。如果按答案条数当分母每个选项的占比都会被低估算出的总和也毫无意义。另一个细节是文本题的展示要加分页否则一份回收量大的问卷会让管理页面卡死。5. 避坑指南把项目跑起来时最常见的五个问题5.1 数据库连接报错Communications link failure现象项目启动后访问首页页面直接报 Communications link failureIDEA 控制台里 Spring Boot 的启动日志停在数据源初始化Tomcat 端口倒是起来了但所有请求都进不去。原因八成是 MySQL 8 的驱动与连接串不匹配。MySQL 8 使用的驱动类是 com.mysql.cj.jdbc.Driver连接串必须带 serverTimezoneAsia/Shanghai否则时区校验直接拒绝连接。另一类高频原因是本地 MySQL 的 root 密码和配置文件里不一致。解决核对 application.properties按下面这份配置逐项比对spring.datasource.urljdbc:mysql://localhost:3306/survey_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 spring.datasource.usernameroot spring.datasource.password你自己的密码 spring.datasource.driver-class-namecom.mysql.cj.jdbc.Driver如果用 MySQL 5.7driver-class-name 要改回 com.mysql.jdbc.Driver并且不需要 serverTimezone 参数。这个驱动类差异是课程设计项目里最典型的启动拦路虎版本和配置对着抄就能解决。5.2 中文乱码插入数据库后全是问号现象问卷标题在页面上显示正常但提交后到 MySQL 里查出来的全是 ??更诡异的是部分表中文正常只有后创建的几张表乱码。原因连接串没加 characterEncodingutf8或者建表时用的字符集不是 utf8mb4。IDEA 里新建数据库时不指定字符集建表脚本里又写了 DEFAULT CHARSETutf8就容易出现这种半正常半乱码。解决连接串补上 characterEncodingutf8已经建好的表执行 ALTER TABLE 转字符集新脚本直接在建表语句里带上 DEFAULT CHARSETutf8mb4。注意 CONVERT 会重建表数据量小没问题数据多的话先备份。ALTER TABLE surveys CONVERT TO CHARACTER SET utf8mb4; ALTER TABLE questions CONVERT TO CHARACTER SET utf8mb4;5.3 题目 ID 对不上提交答案时 name 属性丢失现象答题页渲染正常提交后后端 Map 里只有部分 question_ 参数另外一部分题目永远统计不到数据。原因如果问卷在编辑时删过题目又重新添加数据库里自增 ID 不连续前端页面若还缓存着旧 DOM提交的 name 还是旧的 question_id后端按新题目集合查不到这道题数据就被静默丢弃。解决编辑问卷保存时先对比前后题目 ID 集合把被删除题目的 answers 和 options 一并清掉答题页面提交前做一次 DOM 重载别用浏览器缓存。更稳的办法是在提交接口里校验解析出的 question_id 必须存在于当前问卷题目集合中不存在的直接报错让问题在测试阶段暴露而不是让统计悄悄缺数据。5.4 问卷状态更新不生效页面还是显示发布中现象管理员点击暂停数据库里 surveys.status 确实变成了 2但前端首页仍然能看到问卷入口点进去还能答题。原因管理端调用 updateStatus 更新的是数据库而首页列表查询走了另一段代码——很可能加了一层本地缓存或者是查询方法用了 Cacheable 之类的注解。总之是写一个通道、读一个通道两边没打通。解决统一让首页列表和管理端状态切换走同一个 Service 方法去掉课程设计阶段不必要的缓存注解。这个项目阶段不需要 Redis数据库实时查询完全扛得住还能避免演示时状态切换不生效的尴尬。从那以后我凡是遇到库里改了页面没变第一反应就是查读路径上有没有缓存。5.5 统计结果出现 nullLEFT JOIN 的坑现象单选统计结果里 optionText 出现 nullECharts 饼图多出一个未命名扇区占比还不小。原因countByOption 最初用了 LEFT JOIN选项文字理论上不为空但问卷编辑时删除过选项answers 表里的 option_id 指向了已经不存在的记录LEFT JOIN 匹配不上就返回 NULL。解决统计 SQL 用 INNER JOIN 替代 LEFT JOIN自动过滤悬空选项删除题目时连带清理该题目下的 answers 记录。前端渲染时对 optionText 做空值兜底显示为已删除选项至少让答辩老师看到的是可解释的数据而不是裸 null。6. 从课设答辩到可演示项目导出功能与自测清单一份能上台演示的课程设计光跑通还不够我通常会再加一个导出问卷结果的功能成本很低但答辩效果提升明显。导出本质上是把统计 SQL 的结果集转成 CSV 文件输出通过 HttpServletResponse 设置 Content-Type 为 text/csv文件名带时间戳前端一个按钮就能触发下载。这种方式比接 POI 导出 Excel 轻量得多不需要引入额外依赖CSV 用 Excel 或 WPS 打开一样能做图表。response.setContentType(text/csv;charsetUTF-8); response.setHeader(Content-Disposition, attachment;filenamesurvey_ surveyId .csv); Writer writer response.getWriter(); writer.write(\ufeff); // 写入BOM,解决Excel打开中文乱码 writer.write(选项,票数\n); // 遍历统计结果逐行写入:optionText , cnt代码核心就这几行唯一要提醒的是 BOM 头 \ufeff——CSV 文件用 Excel 直接打开中文会乱码加这一行就解决不加就翻车。我给课设加上导出功能后答辩老师最常问的数据怎么给甲方看就有了现成答案。最后分享一个我的习惯每次改完状态流转或统计逻辑我会强制走一遍完整的自测清单——新建问卷、添加单选/多选/文本三种题、发布、用两个账号分别填写、暂停、看统计、结束、导出结果。这个流程模拟了系统从生到死的完整生命周期任何一环出问题都能在演示前暴露。尤其是状态切换后立刻去查前端是否还有答题入口以及统计占比是否随答卷数变化这两处是这套系统最薄弱的环节也是答辩时最容易被追问的地方。希望帮到你。本文还有配套的精品资源点击获取