☰
SSM+Layui+JSP问卷调查系统实战:从建表到部署的完整链路
2026/10/2 8:32:43 网站建设 项目流程

简介:这是一套基于SpringMVC+Spring+MyBatis的SSM问卷调查系统完整项目源码,采用JSP、jQuery与layui构建前端界面,适合Java Web初学者、课程设计或毕业设计开发者参考学习。系统分为管理端与用户端:管理员可完成信息增删改查、制作与发布问卷、统计调查结果并生成报表展示;普通用户则可在线填写问卷,覆盖问卷全流程业务场景。资源包共847个文件,包含300个gif、138个js、44个css、36个jsp、31个java源码及35个jar依赖等,涵盖页面素材、样式脚本、业务代码与运行库,压缩包约42.05MB,并附有sql建库脚本与war包,便于快速部署。项目基于Maven构建,推荐JDK1.8、Tomcat7至9及MySQL5.7环境运行,导入数据库后即可启动体验。目前已有1225人学习下载,可作为SSM框架整合练习与问卷类系统二次开发的实用参考。

1. 问卷调查系统:SSM + Layui + JSP 这套老架构,为什么还值得动手做一遍

如果你正在搜「Java项目 问卷调查系统 SSM Layui JSP Mysql」,大概率不是单纯想找个源码交差,而是想搞明白:一套看起来“过时”的 JSP 技术栈,怎么把问卷的创建、发布、填写、统计这条链路真正跑通。我见过太多人卡在环境上——IDEA 新建 JSP 项目目录不对、Tomcat 不认、MySQL 连不上报 error 2002、Layui 表格数据渲染不出来。这套系统的价值恰恰在于:它把 JavaWeb 的核心链路压缩在一个你能完全掌控的项目里,没有 Spring Boot 自动配置的黑匣子,每一层都看得见摸得着。适合课程设计、毕设选题,也适合想补 JavaWeb 基础再过渡到 SSM 或 Spring Boot 的开发者。下面我按实际搭建顺序,把选型理由、建表、后端接口、前端渲染和排错一次讲透。

2. 选型与架构拆解:为什么是 SSM + Layui + JSP 而不是 Spring Boot

2.1 这套组合各自解决什么问题

先把四个技术点摆清楚,不然后面写代码容易混。

SSM是 Spring + SpringMVC + MyBatis 的缩写。Spring 管 Bean 和事务,SpringMVC 管请求路由和参数绑定,MyBatis 管 SQL 映射。三者合起来,就是一个不依赖 Spring Boot 自动配置的手动装配 Web 层。问卷调查系统的业务逻辑不复杂——问卷增删改查、题目管理、答卷提交、结果统计——但正好覆盖了 Controller → Service → Mapper → MySQL 这条完整链路,拿来练手非常合适。

Layui是一个经典的前端 UI 库,表格、表单、弹层、分页都有现成组件。问卷系统的后台管理页面(问卷列表、题目编辑、答卷查看)用 Layui 的 table 和 form 模块能省掉大量手写 HTML 和 CSS 的时间。注意 Layui 的数据表格默认要求后端返回特定格式的 JSON,这个后面会讲。

JSP负责页面渲染。有人会问 nginx 支持 jsp 吗——不支持,JSP 必须由 Servlet 容器(Tomcat、Jetty)编译执行。所以这套项目的部署方式是打 war 包丢进 Tomcat,不是丢 nginx。传统 JSP 项目打包 war 这个操作后面会给命令。

MySQL存问卷、题目、选项、答卷、答案五类核心数据。版本用 5.7 或 8.0 都行,8.0 注意驱动包和时区配置。

2.2 项目分层与目录结构

我一般按这个结构组织,清晰且好排查:

survey-system/ ├── src/main/java/com/survey/ │ ├── controller/ # SpringMVC 控制器 │ ├── service/ # 业务接口 │ ├── service/impl/ # 业务实现 │ ├── mapper/ # MyBatis Mapper 接口 │ ├── entity/ # 实体类 │ └── util/ # 工具类(统一返回、分页等) ├── src/main/resources/ │ ├── mapper/ # MyBatis XML 映射文件 │ ├── spring-core.xml # Spring 核心配置 │ ├── spring-mvc.xml # SpringMVC 配置 │ └── jdbc.properties # 数据库连接 ├── src/main/webapp/ │ ├── WEB-INF/ │ │ ├── web.xml # 部署描述符 │ │ └── views/ # JSP 页面 │ ├── static/ │ │ ├── layui/ # Layui 静态资源 │ │ └── js/ # 自定义 JS │ └── index.jsp └── pom.xml

这个结构的关键点:JSP 放在 WEB-INF/views 下,不能直接被浏览器访问,必须走 Controller 转发,这样权限控制才有意义。Layui 放 static 下,SpringMVC 要配置静态资源放行,否则 404。

2.3 依赖配置:pom.xml 里必须锁死的几个版本

<properties> <spring.version>5.3.30</spring.version> <mybatis.version>3.5.13</mybatis.version> <mysql.driver.version>8.0.33</mysql.driver.version> <servlet.api.version>4.0.1</servlet.api.version> <jstl.version>1.2</jstl.version> </properties> <dependencies> <!-- Spring 核心 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <!-- MyBatis 与 Spring 整合 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>${mybatis.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.1.2</version> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.driver.version}</version> </dependency> <!-- Servlet 与 JSTL --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>${servlet.api.version}</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>${jstl.version}</version> </dependency> <!-- 连接池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.20</version> </dependency> </dependencies>

参数说明:servlet-api的 scope 必须是provided,因为 Tomcat 自带,打 war 时不能重复打包,否则启动报类冲突。MySQL 8 驱动类名是com.mysql.cj.jdbc.Driver,URL 要带serverTimezone=Asia/Shanghai,不然时间字段会差 8 小时。Druid 连接池的初始连接数、最大连接数在 jdbc.properties 里配,后面给。

2.4 数据库连接与 Spring 整合配置

jdbc.properties:

jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/survey_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false jdbc.username=root jdbc.password=你的密码 druid.initialSize=5 druid.maxActive=20 druid.minIdle=5 druid.maxWait=60000

spring-core.xml 关键片段:

<!-- 加载数据库配置 --> <context:property-placeholder location="classpath:jdbc.properties"/> <!-- Druid 数据源 --> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> <property name="initialSize" value="${druid.initialSize}"/> <property name="maxActive" value="${druid.maxActive}"/> <property name="minIdle" value="${druid.minIdle}"/> <property name="maxWait" value="${druid.maxWait}"/> </bean> <!-- MyBatis SqlSessionFactory --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> <property name="typeAliasesPackage" value="com.survey.entity"/> </bean> <!-- Mapper 扫描 --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.survey.mapper"/> </bean> <!-- 事务管理 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <tx:annotation-driven transaction-manager="transactionManager"/>

这里有个容易翻车的点:context:property-placeholder和MapperScannerConfigurer同时存在时,如果 placeholder 加载顺序不对,数据源里的${jdbc.url}会解析失败。解决办法是把 placeholder 放在最前面,或者用ignore-unresolvable="true"。我一般直接放最前面,省事。

spring-mvc.xml 关键片段:

<!-- 开启注解驱动 --> <mvc:annotation-driven/> <!-- 扫描 Controller --> <context:component-scan base-package="com.survey.controller"/> <!-- 静态资源放行,Layui 必须走这里 --> <mvc:resources mapping="/static/**" location="/static/"/> <!-- JSP 视图解析器 --> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/views/"/> <property name="suffix" value=".jsp"/> </bean> <!-- 处理 JSON 响应乱码 --> <mvc:message-converters> <bean class="org.springframework.http.converter.StringHttpMessageConverter"> <property name="supportedMediaTypes"> <list> <value>text/plain;charset=UTF-8</value> <value>application/json;charset=UTF-8</value> </list> </property> </bean> </mvc:message-converters>

静态资源放行这条如果漏了,Layui 的 css 和 js 全部 404,页面会裸奔。这是新手最常见的翻车点之一。

3. 数据库表设计与问卷核心链路实现

3.1 五张核心表怎么建

问卷系统的数据模型不复杂,但设计时要想清楚“问卷—题目—选项—答卷—答案”的层级关系。我一般用五张表:

CREATE DATABASE survey_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE survey_db; -- 问卷表 CREATE TABLE t_survey ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL COMMENT '问卷标题', description VARCHAR(500) DEFAULT '' COMMENT '问卷说明', status TINYINT DEFAULT 0 COMMENT '0草稿 1发布 2关闭', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 题目表 CREATE TABLE t_question ( id BIGINT PRIMARY KEY AUTO_INCREMENT, survey_id BIGINT NOT NULL COMMENT '所属问卷', content VARCHAR(500) NOT NULL COMMENT '题目内容', type TINYINT NOT NULL COMMENT '1单选 2多选 3填空', sort_order INT DEFAULT 0 COMMENT '排序', required TINYINT DEFAULT 1 COMMENT '是否必填 1是 0否', INDEX idx_survey (survey_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 选项表 CREATE TABLE t_option ( id BIGINT PRIMARY KEY AUTO_INCREMENT, question_id BIGINT NOT NULL, content VARCHAR(200) NOT NULL, sort_order INT DEFAULT 0, INDEX idx_question (question_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 答卷表(一次提交对应一条) CREATE TABLE t_answer_sheet ( id BIGINT PRIMARY KEY AUTO_INCREMENT, survey_id BIGINT NOT NULL, submit_time DATETIME DEFAULT CURRENT_TIMESTAMP, ip VARCHAR(50) DEFAULT '', INDEX idx_survey (survey_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 答案明细表 CREATE TABLE t_answer_detail ( id BIGINT PRIMARY KEY AUTO_INCREMENT, sheet_id BIGINT NOT NULL, question_id BIGINT NOT NULL, option_id BIGINT DEFAULT NULL COMMENT '选择题选中的选项,填空题为NULL', answer_text VARCHAR(1000) DEFAULT NULL COMMENT '填空题答案', INDEX idx_sheet (sheet_id), INDEX idx_question (question_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

参数说明:字符集统一用utf8mb4,不然问卷标题里出现 emoji 或生僻字会插入失败。t_answer_detail里option_id和answer_text二选一,选择题存 option_id,填空题存 answer_text,这样统计时不用做类型判断。多选题一次提交会有多条 detail 记录,同一个 question_id 对应多个 option_id。

3.2 问卷创建与题目保存的 Service 实现

问卷创建不是单表插入,而是问卷 + 题目 + 选项三张表一起写,必须加事务。这是 Java 怎么保证数据一致性的典型场景。

@Service public class SurveyServiceImpl implements SurveyService { @Autowired private SurveyMapper surveyMapper; @Autowired private QuestionMapper questionMapper; @Autowired private OptionMapper optionMapper; @Override @Transactional(rollbackFor = Exception.class) public Long createSurvey(SurveyDTO dto) { // 1. 插入问卷主表 Survey survey = new Survey(); survey.setTitle(dto.getTitle()); survey.setDescription(dto.getDescription()); survey.setStatus(0); surveyMapper.insert(survey); Long surveyId = survey.getId(); // 2. 遍历题目,逐条插入 List<QuestionDTO> questions = dto.getQuestions(); if (questions != null) { for (int i = 0; i < questions.size(); i++) { QuestionDTO qDto = questions.get(i); Question question = new Question(); question.setSurveyId(surveyId); question.setContent(qDto.getContent()); question.setType(qDto.getType()); question.setSortOrder(i); question.setRequired(qDto.getRequired()); questionMapper.insert(question); Long questionId = question.getId(); // 3. 选择题插入选项 if (qDto.getType() != 3 && qDto.getOptions() != null) { for (int j = 0; j < qDto.getOptions().size(); j++) { Option option = new Option(); option.setQuestionId(questionId); option.setContent(qDto.getOptions().get(j)); option.setSortOrder(j); optionMapper.insert(option); } } } } return surveyId; } }

逻辑说明:@Transactional的rollbackFor = Exception.class必须加,因为默认只回滚 RuntimeException,如果中间抛了受检异常,前面的插入不会回滚,数据就脏了。surveyMapper.insert后要能拿到自增主键,MyBatis 的 XML 里要配useGeneratedKeys="true" keyProperty="id",否则getId()返回 null,后面题目关联就断了。

3.3 答卷提交与统计查询

答卷提交同样是一个事务:先插t_answer_sheet拿 sheetId,再批量插t_answer_detail。

@Override @Transactional(rollbackFor = Exception.class) public void submitAnswer(AnswerSubmitDTO dto) { AnswerSheet sheet = new AnswerSheet(); sheet.setSurveyId(dto.getSurveyId()); sheet.setIp(dto.getIp()); answerSheetMapper.insert(sheet); Long sheetId = sheet.getId(); for (AnswerItemDTO item : dto.getItems()) { if (item.getOptionIds() != null && !item.getOptionIds().isEmpty()) { // 选择题:每个选中选项一条记录 for (Long optionId : item.getOptionIds()) { AnswerDetail detail = new AnswerDetail(); detail.setSheetId(sheetId); detail.setQuestionId(item.getQuestionId()); detail.setOptionId(optionId); answerDetailMapper.insert(detail); } } else { // 填空题 AnswerDetail detail = new AnswerDetail(); detail.setSheetId(sheetId); detail.setQuestionId(item.getQuestionId()); detail.setAnswerText(item.getAnswerText()); answerDetailMapper.insert(detail); } } }

统计查询用一条 GROUP BY 就能出结果:

SELECT o.id AS optionId, o.content AS optionContent, COUNT(d.id) AS count FROM t_option o LEFT JOIN t_answer_detail d ON o.id = d.option_id WHERE o.question_id = #{questionId} GROUP BY o.id, o.content ORDER BY o.sort_order;

用 LEFT JOIN 保证没人选的选项也显示,count 为 0。如果只统计已选中的,用 INNER JOIN 就行,但问卷统计通常要展示全部选项的占比,所以 LEFT JOIN 更合适。

4. Layui 前端渲染与 JSP 页面集成

4.1 Layui 数据表格对接后端接口

Layui 的 table 模块对后端返回格式有硬性要求,必须是:

{ "code": 0, "msg": "", "count": 100, "data": [...] }

code 为 0 表示成功,count 是总记录数用于分页,data 是当前页数据。后端 Controller 要按这个格式封装。我一般写一个统一返回类:

public class LayuiResult<T> { private int code; private String msg; private long count; private List<T> data; public static <T> LayuiResult<T> success(long count, List<T> data) { LayuiResult<T> r = new LayuiResult<>(); r.code = 0; r.msg = ""; r.count = count; r.data = data; return r; } // getter/setter 省略 }

Controller 示例:

@RestController @RequestMapping("/api/survey") public class SurveyApiController { @Autowired private SurveyService surveyService; @GetMapping("/list") public LayuiResult<Survey> list( @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int limit) { int offset = (page - 1) * limit; List<Survey> list = surveyService.listByPage(offset, limit); long total = surveyService.countAll(); return LayuiResult.success(total, list); } }

参数说明:page和limit是 Layui table 默认传的分页参数,不用自己定义。offset是 SQL 的 LIMIT 偏移量,MyBatis 里用LIMIT #{offset}, #{limit}接收。注意@RestController返回的是 JSON,不要用@Controller,否则会去找 JSP 视图。

4.2 JSP 页面里引入 Layui 和渲染表格

JSP 页面头部:

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>问卷管理</title> <link rel="stylesheet" href="${pageContext.request.contextPath}/static/layui/css/layui.css"> </head> <body> <table id="surveyTable" lay-filter="surveyTable"></table> <script src="${pageContext.request.contextPath}/static/layui/layui.js"></script> <script> layui.use(['table', 'layer'], function () { var table = layui.table; var layer = layui.layer; table.render({ elem: '#surveyTable', url: '${pageContext.request.contextPath}/api/survey/list', method: 'get', page: true, cols: [[ {field: 'id', title: 'ID', width: 80}, {field: 'title', title: '问卷标题', minWidth: 200}, {field: 'status', title: '状态', width: 100, templet: function (d) { return d.status === 1 ? '<span style="color:green">已发布</span>' : '<span style="color:orange">草稿</span>'; }}, {field: 'createTime', title: '创建时间', width: 180}, {title: '操作', width: 200, templet: function (d) { return '<a class="layui-btn layui-btn-xs" lay-event="edit">编辑</a>' + '<a class="layui-btn layui-btn-xs layui-btn-danger" lay-event="del">删除</a>'; }} ]] }); // 监听工具条事件 table.on('tool(surveyTable)', function (obj) { if (obj.event === 'del') { layer.confirm('确定删除?', function (index) { $.get('${pageContext.request.contextPath}/api/survey/delete', {id: obj.data.id}, function (res) { if (res.code === 0) { table.reload('surveyTable'); } layer.close(index); }); }); } }); }); </script> </body> </html>

逻辑说明:${pageContext.request.contextPath}是 JSP 获取项目上下文路径的标准写法,避免部署到 Tomcat 后路径不对导致 404。templet是 Layui 的列模板函数,用来做状态转换和操作按钮渲染。table.on('tool(...)')监听行内按钮点击,obj.data是当前行数据。

4.3 问卷填写页的动态渲染

填写页需要根据题目类型动态渲染单选、多选、填空。JSP 里用 JSTL 遍历题目列表:

<form class="layui-form" id="answerForm"> <c:forEach items="${questions}" var="q" varStatus="st"> <div class="layui-form-item"> <label class="layui-form-label">${st.index + 1}. ${q.content}</label> <div class="layui-input-block"> <c:choose> <c:when test="${q.type == 1}"> <c:forEach items="${q.options}" var="opt"> <input type="radio" name="q_${q.id}" value="${opt.id}" title="${opt.content}"> </c:forEach> </c:when> <c:when test="${q.type == 2}"> <c:forEach items="${q.options}" var="opt"> <input type="checkbox" name="q_${q.id}" value="${opt.id}" title="${opt.content}"> </c:forEach> </c:when> <c:otherwise> <textarea name="q_${q.id}" class="layui-textarea"></textarea> </c:otherwise> </c:choose> </div> </div> </c:forEach> <button class="layui-btn" lay-submit lay-filter="submitAnswer">提交</button> </form>

注意 Layui 的 radio 和 checkbox 需要form.render()才能显示样式,在layui.use(['form'])里调用。提交时用 JS 收集表单数据组装成 JSON 发给后端,不能直接 form submit,因为后端接口是@RequestBody接收 JSON 的。

5. 避坑与排查:从环境到运行时的 5 个血泪教训

5.1 Tomcat 启动报 ClassNotFoundException: com.mysql.cj.jdbc.Driver

现象:项目编译通过,Tomcat 启动时控制台报驱动类找不到。

原因:MySQL 驱动的 jar 没有放到WEB-INF/lib下。IDEA 里 Maven 依赖默认不会自动复制到 war 的 lib 目录,除非在 Project Structure 的 Artifacts 里手动添加,或者用mvn package打 war 时由 Maven 处理。

解决:在 IDEA 的 Project Structure → Artifacts → 你的 war exploded → WEB-INF/lib 里,把 mysql-connector-java 加进去。或者直接用mvn clean package生成 war 包,Maven 会自动把 compile scope 的依赖打进 lib。

5.2 Layui 表格请求成功但数据不显示

现象:浏览器 Network 里看到接口返回了 JSON,格式也对,但表格是空的。

原因:Layui table 默认要求返回的code字段为 0,count和data字段名不能改。如果后端返回的是{"total": 100, "rows": [...]},Layui 不认。

解决:要么改后端返回格式,要么在 table.render 里配response: {statusCode: 200}和parseData做字段映射。我一般直接改后端,用统一返回类,省得前端到处配。

5.3 MySQL 连接报 error 2002 (HY000): Can't connect through socket

现象:命令行或 Navicat 连 MySQL 报 socket 错误。

原因:MySQL 服务没启动,或者 socket 文件路径不对。Linux 下常见于/tmp/mysql.sock不存在,Windows 下一般是服务没起。

解决:Linux 用systemctl start mysqld或service mysql start;Windows 在服务管理器里启动 MySQL 服务。如果 socket 路径不对,在连接配置里显式指定socket=/var/lib/mysql/mysql.sock。Navicat 连接时选 TCP/IP 而不是 Socket。

5.4 JSP 页面中文乱码

现象:页面上的中文显示成问号或乱码。

原因:JSP 文件本身的编码、pageEncoding、response 的 contentType 三者不一致。

解决:JSP 第一行写<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-8" %>,文件保存为 UTF-8。web.xml 里加字符编码过滤器:

<filter> <filter-name>encodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>

5.5 事务不生效,问卷插入了但题目没插入

现象:创建问卷时,问卷主表有数据,但题目表是空的,或者部分题目插入失败后前面的没回滚。

原因:@Transactional没生效。常见原因有三个:一是 Service 类没被 Spring 扫描到(不在 component-scan 范围内);二是方法不是 public;三是异常被 catch 了没抛出去,Spring 感知不到。

解决:确认 spring-core.xml 里<context:component-scan base-package="com.survey.service"/>包含 Service 包;@Transactional加在 public 方法上;catch 块里要么不 catch,要么 catch 后throw new RuntimeException(e)。另外rollbackFor = Exception.class要加上,覆盖受检异常。

6. 进阶技巧:用一条 SQL 出统计报表和 war 包部署

6.1 问卷结果统计的聚合查询

问卷统计最常用的两个视图:每个选项的选择人数和占比,以及填空题的答案列表。选项统计前面给过,这里补一个带占比的:

SELECT o.content AS optionContent, COUNT(d.id) AS voteCount, ROUND(COUNT(d.id) * 100.0 / ( SELECT COUNT(DISTINCT d2.sheet_id) FROM t_answer_detail d2 WHERE d2.question_id = #{questionId} ), 2) AS percentage FROM t_option o LEFT JOIN t_answer_detail d ON o.id = d.option_id WHERE o.question_id = #{questionId} GROUP BY o.id, o.content ORDER BY o.sort_order;

这个查询的分母是“答了这道题的人数”,不是“总答卷数”,因为填空题和选择题的答题人数可能不同。COUNT(DISTINCT d2.sheet_id)保证同一份答卷只算一次。如果问卷允许跳过非必填题,这个分母才是准确的。

6.2 打 war 包部署到 Tomcat

# 在项目根目录执行 mvn clean package -DskipTests # 生成的 war 包在 target/ 下,复制到 Tomcat cp target/survey-system.war $TOMCAT_HOME/webapps/ # 启动 Tomcat $TOMCAT_HOME/bin/startup.sh # Linux # 或 %TOMCAT_HOME%\bin\startup.bat # Windows

访问路径是http://localhost:8080/survey-system/,注意 war 包名就是上下文路径。如果想改成根路径,把 war 包重命名为ROOT.war。Tomcat 会自动解压 war 包,修改 JSP 后如果不想重启,可以配置reloadable="true",但生产环境不建议开,性能损耗明显。

6.3 一个我踩过的坑

有次部署到服务器后,问卷列表能打开但填写页 500。查了半天发现是 JSP 里用了${q.options},但后端返回的 Question 实体里 options 字段是 null——因为查询题目时没有关联查选项。后来在 MyBatis 的 resultMap 里加了<collection>嵌套查询,或者 Service 层手动组装。这个问题的教训是:JSP 页面报错信息很模糊,一定要看 Tomcat 的 catalina.out 日志,里面会有完整的异常栈。

我现在做这类项目,习惯先把数据库和 MyBatis 映射跑通,用单元测试验证每个 Mapper 方法,再写 Service 和 Controller,最后才动 JSP。这样出问题时能快速定位是哪一层。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询