☰
SSM+JSP+Layui问卷系统实战:中小团队轻量级开发指南
2026/10/14 11:44:17 网站建设 项目流程

简介:这是一套完整的Java Web问卷调查系统实战项目,面向Java初学者与SSM框架学习者,聚焦Web应用开发全流程实践,涵盖需求分析、前后端开发、数据库设计与部署上线等核心环节。资源包共847个文件,含138个JS脚本(实现前端交互与Layui组件调用)、36个JSP页面(构成用户端与管理端视图层)、31个Java源码(含SurveyController、QuestionService等关键业务类)、35个JAR依赖及1个SQL建库脚本,辅以CSS、XML配置与图片资源,结构完整、模块清晰。压缩包大小为42.05MB,采用RAR格式封装。已有1227人学习下载,可直接导入IDEA等主流开发工具,基于JDK 1.8+Tomcat+MySQL 5.7环境快速运行;配套完整权限体系(管理员CRUD、问卷发布与统计报表)与前后端分离式代码组织,便于理解SSM整合原理与企业级问卷系统设计逻辑。

1. 为什么一个“老派”技术栈的问卷系统,反而在中小团队里活得很稳?

你可能刚刷到过这类招聘JD:“熟悉SSM+JSP+Layui开发经验者优先”,或者在某高校课程设计仓库里看到标着“Java问卷调查系统”的毕业项目——界面朴素、没用Spring Boot自动装配、连前端都还是JSP混写JS。第一反应是:这玩意儿2024年还值得碰?但现实是:某高校信息中心用它支撑全校每学期30+场教学评教;某区级政务服务平台的满意度采集模块,三年零宕机,后台日志里最常出现的报错是“学生填太快导致重复提交”,而不是“Tomcat OOM”或“Redis连接超时”。

这不是怀旧,是权衡。SSM(Spring+SpringMVC+MyBatis)提供清晰的分层控制力,JSP在服务端完成模板渲染,规避了前后端分离带来的跨域调试、Token管理、接口联调等隐性成本;Layui作为轻量级UI框架,不搞虚拟DOM、不学Vue语法,一个<script src>就能让表单带校验、表格可分页;MySQL则把事务一致性、索引优化、备份恢复这些事交给了成熟生态。它不炫技,但能让你把80%精力放在“题型逻辑怎么嵌套”“导出Excel字段顺序怎么对齐”“匿名提交如何防刷”这些真实业务问题上。适合需要快速交付、运维人力有限、对高并发无硬性要求,且团队里有1~2位能手写SQL和调试Filter链的老Java人。

提示:这不是推荐你放弃Spring Boot或Vue,而是说——当需求明确、周期紧张、服务器资源只有1核2G时,“够用、可控、易排查”比“先进、解耦、可扩展”更接近交付本质。


2. 搭建最小可运行骨架:从零初始化SSM+Layui+JSP环境

2.1 创建Maven工程并声明核心依赖(含版本锁定逻辑)

新建Maven Webapp项目后,在pom.xml中声明以下依赖。注意三点:MyBatis版本与Spring 4.x兼容性、JSP API必须显式引入(Tomcat 9+默认不带)、Layui不走Maven,但需预留CDN或本地路径:

<properties> <spring.version>4.3.30.RELEASE</spring.version> <mybatis.version>3.4.6</mybatis.version> <mysql.version>5.1.47</mysql.version> </properties> <dependencies> <!-- Spring Core --> <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> <!-- 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>1.3.2</version> </dependency> <!-- MySQL驱动(注意:5.1.x适配JDK7/8,若用JDK11请升至8.0.28+) --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> <!-- JSP支持(关键!否则404) --> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.5</version> <scope>provided</scope> </dependency> <!-- 日志(Log4j2更现代,但Logback+SLF4J组合更贴合SSM惯用法) --> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.11</version> </dependency> </dependencies>

逻辑说明:

  • servlet-api设为provided,因为Tomcat容器已提供,打包时排除避免冲突;
  • jstl是JSP标签库基础,没有它<c:if>等标签无法解析;
  • MyBatis-Spring 1.3.2是适配Spring 4.3.x的最终稳定版,更高版本会报NoSuchBeanDefinitionException;
  • MySQL驱动选5.1.47而非8.x,因后者默认启用serverTimezone校验,而老项目常忽略时区配置,直接报错“Could not retrieve transation read-only status server”。

2.2 配置Web.xml:注册DispatcherServlet与字符过滤器

src/main/webapp/WEB-INF/web.xml是SSM的入口契约。必须按顺序声明:字符编码过滤器 → Spring上下文监听器 → DispatcherServlet → JSP Servlet映射。漏掉任一环,JSP乱码、Controller找不到、静态资源404全来:

<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <!-- 1. 字符编码过滤器(必须在最前) --> <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> <!-- 2. Spring根上下文(加载Service/DAO) --> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring/applicationContext.xml</param-value> </context-param> <!-- 3. SpringMVC前端控制器 --> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring/spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <!-- 4. JSP Servlet显式声明(Tomcat 9+必需) --> <servlet> <servlet-name>jsp</servlet-name> <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class> <init-param> <param-name>fork</param-name> <param-value>false</param-value> </init-param> <init-param> <param-name>xpoweredBy</param-name> <param-value>false</param-value> </init-param> <load-on-startup>3</load-on-startup> </servlet> <servlet-mapping> <servlet-name>jsp</servlet-name> <url-pattern>*.jsp</url-pattern> </servlet-mapping> </web-app>

参数说明:

  • load-on-startup=1确保DispatcherServlet早于其他Servlet初始化;
  • xpoweredBy=false关闭JSP响应头泄露容器信息,属基础安全加固;
  • <url-pattern>/</url-pattern>表示拦截所有请求,但JSP文件因被*.jsp映射捕获,不会进入SpringMVC流程——这是JSP能直出的关键;
  • 若删掉JSP Servlet映射,访问/index.jsp会返回404,因为SpringMVC默认不处理.jsp后缀。

2.3 初始化Layui静态资源与基础页面结构

Layui不依赖构建工具,直接下载 官网v2.9.1 压缩包,解压后将layui文件夹放入src/main/webapp/下(即与WEB-INF同级)。创建src/main/webapp/index.jsp作为首页:

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>问卷调查系统 - 首页</title> <link rel="stylesheet" href="layui/css/layui.css"> </head> <body class="layui-layout-body"> <div class="layui-layout layui-layout-admin"> <div class="layui-header"> <div class="layui-logo">问卷调查系统</div> <ul class="layui-nav layui-layout-left"> <li class="layui-nav-item"><a href="survey/list">问卷列表</a></li> <li class="layui-nav-item"><a href="user/login">用户登录</a></li> </ul> </div> <div class="layui-body"> <div style="padding: 15px;"> <blockquote class="layui-elem-quote"> 欢迎使用基于SSM+Layui的问卷系统。本系统支持单选、多选、填空、矩阵题型。 </blockquote> </div> </div> </div> <script src="layui/layui.js"></script> <script> layui.use(['element'], function(){ var element = layui.element; // 导航高亮逻辑(根据URL路径) var path = window.location.pathname; if(path.indexOf('survey') > -1) { element.tabChange('nav', 'survey'); } }); </script> </body> </html>

关键点说明:

  • layui.css和layui.js路径必须与实际存放位置一致,若放错层级(如放进WEB-INF),浏览器控制台会报404;
  • Layui的element模块负责导航栏、选项卡等交互,此处用tabChange实现菜单高亮,避免用户迷失当前页面;
  • 所有JSP页面顶部必须声明<%@ page contentType="text/html;charset=UTF-8" %>,否则中文标题、提示文字会显示为方块;
  • 不要试图用<script type="module">或ES6语法,Layui 2.x基于原生JS,import会直接报错。

3. 实现问卷核心流程:从创建、填写到数据落库的闭环

3.1 数据库设计:三张表撑起问卷生命周期

问卷系统本质是“元数据+实例数据”双层结构。我们只用三张表实现最小闭环:

  • survey_info:问卷主表(标题、描述、状态、创建时间)
  • survey_question:题目表(关联问卷ID、题干、题型、是否必答)
  • survey_answer:回答表(关联题目ID、用户标识、答案内容、提交时间)
-- 问卷主表 CREATE TABLE survey_info ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL COMMENT '问卷标题', description TEXT COMMENT '问卷描述', status TINYINT DEFAULT 1 COMMENT '状态:1-启用,0-停用', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='问卷主表'; -- 题目表 CREATE TABLE survey_question ( id BIGINT PRIMARY KEY AUTO_INCREMENT, survey_id BIGINT NOT NULL COMMENT '所属问卷ID', content TEXT NOT NULL COMMENT '题干内容', type TINYINT NOT NULL COMMENT '题型:1-单选,2-多选,3-填空,4-矩阵', required TINYINT DEFAULT 1 COMMENT '是否必答:1-是,0-否', sort_order INT DEFAULT 0 COMMENT '排序序号', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (survey_id) REFERENCES survey_info(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='题目表'; -- 回答表(简化版:不存用户ID,用IP+时间戳防重) CREATE TABLE survey_answer ( id BIGINT PRIMARY KEY AUTO_INCREMENT, question_id BIGINT NOT NULL COMMENT '题目ID', ip_address VARCHAR(45) COMMENT '提交IP(IPv4/v6)', answer_text TEXT COMMENT '文本答案(单选/多选存逗号分隔值,填空存原文)', submit_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (question_id) REFERENCES survey_question(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='回答表';

设计理由:

  • ON DELETE CASCADE确保删除问卷时自动清理题目和答案,避免外键残留;
  • ip_address代替用户ID,满足匿名问卷需求,且IPv6地址最长45字符,VARCHAR(45)足够;
  • 矩阵题型暂不拆分为子题,答案以JSON字符串存入answer_text(如{"row1":"A","row2":"C"}),降低表结构复杂度;
  • sort_order允许拖拽调整题目顺序,前端只需传新序号数组,后端批量更新。

3.2 MyBatis映射:手写SQL应对动态查询场景

SSM中MyBatis负责SQL执行。针对问卷列表页的“按状态筛选+分页”需求,不能只靠@Select注解,需用XML实现动态SQL。创建SurveyInfoMapper.xml:

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.mapper.SurveyInfoMapper"> <!-- 查询问卷列表(带条件分页) --> <select id="selectList" resultType="com.example.entity.SurveyInfo"> SELECT * FROM survey_info <where> <if test="status != null and status != ''"> AND status = #{status} </if> </where> ORDER BY create_time DESC LIMIT #{offset}, #{limit} </select> <!-- 统计总数(用于分页计算) --> <select id="count" resultType="java.lang.Integer"> SELECT COUNT(*) FROM survey_info <where> <if test="status != null and status != ''"> AND status = #{status} </if> </where> </select> <!-- 根据ID查问卷及题目(一对多) --> <resultMap id="SurveyWithQuestions" type="com.example.entity.SurveyInfo"> <id property="id" column="id"/> <result property="title" column="title"/> <result property="description" column="description"/> <result property="status" column="status"/> <collection property="questions" ofType="com.example.entity.SurveyQuestion"> <id property="id" column="q_id"/> <result property="content" column="q_content"/> <result property="type" column="q_type"/> <result property="required" column="q_required"/> </collection> </resultMap> <select id="selectByIdWithQuestions" resultMap="SurveyWithQuestions"> SELECT s.id, s.title, s.description, s.status, q.id as q_id, q.content as q_content, q.type as q_type, q.required as q_required FROM survey_info s LEFT JOIN survey_question q ON s.id = q.survey_id WHERE s.id = #{id} ORDER BY q.sort_order </select> </mapper>

参数说明:

  • #{offset}和#{limit}由PageHelper插件注入,无需手动计算(见3.3节);
  • <collection>实现一对多查询,property="questions"对应实体类中的List<SurveyQuestion>字段;
  • LEFT JOIN确保问卷无题目时仍能查出主记录,避免空结果;
  • ORDER BY q.sort_order保证题目按设定顺序返回,前端渲染不乱序。

3.3 Controller层:用ModelAndView承载JSP视图逻辑

SpringMVC的Controller不返回JSON,而是ModelAndView对象,将数据推给JSP渲染。以问卷填写页为例:

@Controller @RequestMapping("/survey") public class SurveyController { @Autowired private SurveyInfoService surveyInfoService; @Autowired private SurveyQuestionService questionService; /** * 展示问卷填写页(GET) */ @RequestMapping(value = "/fill/{id}", method = RequestMethod.GET) public ModelAndView fillPage(@PathVariable Long id) { ModelAndView mav = new ModelAndView("survey/fill"); SurveyInfo survey = surveyInfoService.selectByIdWithQuestions(id); if (survey == null || survey.getStatus() != 1) { mav.addObject("error", "问卷不存在或已关闭"); mav.setViewName("error/404"); return mav; } mav.addObject("survey", survey); return mav; } /** * 处理问卷提交(POST) */ @RequestMapping(value = "/submit", method = RequestMethod.POST) public String handleSubmission( @RequestParam Long questionId, @RequestParam(required = false) String answerText, @RequestParam(required = false) String[] answerOptions, HttpServletRequest request) { // 1. 获取客户端IP(兼容Nginx反向代理) String ip = getClientIp(request); // 2. 构建答案实体 SurveyAnswer answer = new SurveyAnswer(); answer.setQuestionId(questionId); answer.setIpAddress(ip); // 3. 根据题型组装答案 if (answerOptions != null && answerOptions.length > 0) { // 多选/单选:转为逗号分隔字符串 answer.setAnswerText(String.join(",", answerOptions)); } else if (answerText != null && !answerText.trim().isEmpty()) { // 填空:直接存原文 answer.setAnswerText(answerText.trim()); } else { // 必答题未填,跳回填写页 return "redirect:/survey/fill/" + questionId; } // 4. 保存答案 answerService.insert(answer); return "redirect:/survey/thanks"; } private String getClientIp(HttpServletRequest request) { String xForwardedFor = request.getHeader("X-Forwarded-For"); if (xForwardedFor != null && !xForwardedFor.isEmpty() && !"unknown".equalsIgnoreCase(xForwardedFor)) { return xForwardedFor.split(",")[0].trim(); } return request.getRemoteAddr(); } }

逻辑说明:

  • @RequestMapping(value = "/fill/{id}", method = RequestMethod.GET)绑定URL路径,{id}自动注入为Long类型;
  • ModelAndView("survey/fill")指向/WEB-INF/jsp/survey/fill.jsp,符合SpringMVC默认视图解析规则;
  • redirect:/survey/thanks返回302重定向,避免用户刷新导致重复提交;
  • getClientIp()方法处理Nginx反向代理场景,若直接部署Tomcat可简化为request.getRemoteAddr();
  • 答案存储策略:单选/多选存选项值(如"A,B"),填空存文本,矩阵题后续扩展为JSON字符串。

3.4 JSP页面:用Layui组件实现题型渲染与校验

/WEB-INF/jsp/survey/fill.jsp是核心交互页面。利用Layui的form模块实现表单校验,radio/checkbox组件渲染题型:

<%@ 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>${survey.title}</title> <link rel="stylesheet" href="${pageContext.request.contextPath}/layui/css/layui.css"> </head> <body> <div class="layui-container" style="padding: 20px 0;"> <div class="layui-row"> <div class="layui-col-md12"> <h2 class="layui-font-black">${survey.title}</h2> <p class="layui-text">${survey.description}</p> <form class="layui-form" action="${pageContext.request.contextPath}/survey/submit" method="post" id="surveyForm"> <c:forEach items="${survey.questions}" var="q" varStatus="status"> <div class="layui-form-item" style="margin-top: 30px;"> <label class="layui-form-label">${status.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="answerOptions" value="${opt.value}" title="${opt.label}" <c:if test="${q.required == 1}">lay-req-text="请选择一项"</c:if>> </c:forEach> </c:when> <c:when test="${q.type == 2}"> <!-- 多选 --> <c:forEach items="${q.options}" var="opt"> <input type="checkbox" name="answerOptions" value="${opt.value}" title="${opt.label}"> </c:forEach> </c:when> <c:when test="${q.type == 3}"> <!-- 填空 --> <input type="text" name="answerText" required="${q.required == 1 ? 'required' : ''}" placeholder="请输入您的答案" class="layui-input" maxlength="500"> </c:when> </c:choose> </div> </div> </c:forEach> <div class="layui-form-item"> <div class="layui-input-block"> <button class="layui-btn" lay-submit lay-filter="submit">提交问卷</button> <button type="reset" class="layui-btn layui-btn-primary">重填</button> </div> </div> </form> </div> </div> </div> <script src="${pageContext.request.contextPath}/layui/layui.js"></script> <script> layui.use(['form', 'layer'], function(){ var form = layui.form; var layer = layui.layer; // 自定义验证规则(单选必答) form.verify({ 'radio-required': function(value, item){ var name = $(item).attr('name'); if(name === 'answerOptions' && $('input[name="answerOptions"]:checked').length === 0){ return '请选择一项'; } } }); // 监听提交 form.on('submit(submit)', function(data){ // 简单防重复提交(实际项目应加按钮禁用+Token) $('#surveyForm .layui-btn').prop('disabled', true).text('提交中...'); return true; // 允许表单提交 }); }); </script> </body> </html>

关键细节:

  • ${pageContext.request.contextPath}确保静态资源路径正确,避免部署到子路径(如/survey-system)时404;
  • lay-req-text是Layui自定义必填提示,比HTML5原生required更友好;
  • form.verify注册自定义规则,解决单选框组无法用原生required校验的问题;
  • 提交按钮禁用逻辑防止网络延迟导致用户狂点,但这不是防刷方案,仅提升体验(防刷需后端IP限流+时间窗口校验);
  • maxlength="500"限制填空长度,避免恶意长文本撑爆数据库字段。

4. 避坑指南:SSM+JSP项目上线前必须检查的5个血泪现场

4.1 现象:JSP页面中文显示为“???”,但数据库存取正常

原因:JSP文件本身编码、IDE编辑器编码、Tomcat启动参数、JDBC URL四者未统一为UTF-8。常见组合错误是:

  • 文件用UTF-8保存,但IDE(如IntelliJ)默认用GBK打开;
  • web.xml中<jsp-config>未声明<page-encoding>UTF-8</page-encoding>;
  • Tomcat的conf/server.xml中Connector缺少URIEncoding="UTF-8";
  • JDBC URL未加useUnicode=true&characterEncoding=UTF-8。

解决:

  1. 在IDE中右键JSP文件 → File Encoding → Convert to UTF-8;
  2. 在web.xml的<jsp-config>节点内添加:
<jsp-property-group> <url-pattern>*.jsp</url-pattern> <page-encoding>UTF-8</page-encoding> </jsp-property-group>
  1. 修改conf/server.xml中Connector:
<Connector port="8080" protocol="HTTP/1.1" URIEncoding="UTF-8" ... />
  1. JDBC URL补全:jdbc:mysql://localhost:3306/db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai。

4.2 现象:Layui图标不显示,控制台报404

原因:Layui的图标字体文件(iconfont.ttf)路径错误。Layui 2.x默认从./fonts/加载,但若layui.css被复制到其他目录,相对路径失效。

解决:

  • 方法1(推荐):保持layui文件夹原始结构,CSS和JS均从/layui/路径引用;
  • 方法2:修改layui.css中@font-face的src路径,例如:
@font-face { font-family: layui-icon; src: url("../fonts/iconfont.ttf") format("truetype"); }

→ 改为绝对路径:url("/layui/fonts/iconfont.ttf")。

4.3 现象:MyBatis执行INSERT后SELECT LAST_INSERT_ID()返回0

原因:MySQL驱动版本与useAffectedRows参数冲突。MySQL 5.7+默认useAffectedRows=false,导致insert语句影响行数为0,MyBatis误判主键未生成。

解决:
在JDBC URL中强制开启:
jdbc:mysql://localhost:3306/db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useAffectedRows=true
同时确保MyBatis的<insert>标签设置useGeneratedKeys="true"且keyProperty指向实体主键字段。

4.4 现象:SpringMVC拦截/static/**资源,导致Layui CSS/JS 404

原因:spring-mvc.xml中<mvc:default-servlet-handler/>缺失,或<mvc:resources>配置路径与实际不符。

解决:
在spring-mvc.xml中添加:

<!-- 启用默认Servlet处理静态资源 --> <mvc:default-servlet-handler/> <!-- 或显式声明资源映射(二选一) --> <mvc:resources mapping="/layui/**" location="/layui/" /> <mvc:resources mapping="/static/**" location="/static/" />

注意:location值以/结尾,且必须与webapp下文件夹名完全一致(大小写敏感)。

4.5 现象:Tomcat启动报java.lang.NoSuchMethodError: org.springframework.core.convert.support.DefaultConversionService.getSharedInstance()

原因:Spring版本混用。spring-core、spring-beans、spring-context等jar包版本不一致,常见于手动拷贝jar包时遗漏。

解决:

  1. 清理WEB-INF/lib下所有Spring相关jar;
  2. 严格使用Maven管理,通过mvn dependency:tree | grep spring检查冲突;
  3. 若必须手动部署,确保所有Spring jar版本均为4.3.30.RELEASE(与pom.xml声明一致)。

注意:以上5条均来自某高校信息中心部署时的真实日志,第3条曾导致问卷提交后无法跳转感谢页,排查耗时3小时——建议上线前逐条验证。


5. 进阶技巧:用JSP Fragment+Layui Table实现答案实时统计

问卷系统价值不仅在于收集,更在于快速反馈。我们不用ECharts等重型图表库,而是用Layui Table结合JSP片段(Fragment)实现“填完即看”的轻量统计。核心思路:答案表按题目ID分组聚合,JSP循环渲染表格行,Layui Table自动分页。

5.1 后端:编写按题目统计的答案聚合SQL

在SurveyAnswerMapper.xml中新增统计查询:

<!-- 按题目ID统计各选项选择次数 --> <select id="countByQuestionId" resultType="java.util.Map"> SELECT answer_text as optionValue, COUNT(*) as count FROM survey_answer WHERE question_id = #{questionId} GROUP BY answer_text ORDER BY count DESC </select> <!-- 查询题目选项(用于匹配选项标签) --> <select id="selectOptionsByQuestionId" resultType="com.example.entity.SurveyOption"> SELECT * FROM survey_option WHERE question_id = #{questionId} ORDER BY sort_order </select>

注:survey_option表需提前创建,存储单选/多选题的选项值(value)与标签(label),如{"value":"A","label":"非常满意"}。

5.2 JSP Fragment:封装可复用的统计表格组件

创建/WEB-INF/jsp/fragments/stat-table.jspf(注意.jspf后缀):

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> <c:if test="${not empty stats}"> <table class="layui-table" lay-size="sm"> <colgroup> <col width="150"> <col width="150"> <col> </colgroup> <thead> <tr> <th>选项</th> <th>次数</th> <th>占比</th> </tr> </thead> <tbody> <c:forEach items="${stats}" var="stat"> <tr> <td> <c:choose> <c:when test="${fn:contains(stat.optionValue, ',')}"> <!-- 多选答案:拆分显示 --> <c:forTokens items="${stat.optionValue}" delims="," var="opt"> <span class="layui-badge-rim">${opt}</span>&nbsp; </c:forTokens> </c:when> <c:otherwise> ${stat.optionValue} </c:otherwise> </c:choose> </td> <td>${stat.count}</td> <td> <div class="layui-progress" lay-showPercent="true" style="margin: 5px 0;"> <div class="layui-progress-bar" lay-percent="${stat.percent}%"></div> </div> </td> </tr> </c:forEach> </tbody> </table> </c:if>

技巧说明:

  • .jspf后缀告诉JSP引擎这是片段,不会被单独访问;
  • <c:forTokens>处理多选答案(如"A,B,C"),拆成多个<span>标签;
  • lay-percent属性由后端计算好传入(stat.percent = (count / totalCount) * 100),Layui自动渲染进度条;
  • lay-size="sm"缩小表格尺寸,适配问卷详情页紧凑布局。

5.3 在问卷详情页中嵌入统计片段

修改/WEB-INF/jsp/survey/detail.jsp,在题目下方插入:

<%@ include file="/fragments/stat-table.jspf" %>

并在Controller中补充统计数据:

@RequestMapping("/detail/{id}") public ModelAndView detailPage(@PathVariable Long id) { ModelAndView mav = new ModelAndView("survey/detail"); SurveyInfo survey = surveyInfoService.selectByIdWithQuestions(id); // 为每个题目加载统计 for (SurveyQuestion q : survey.getQuestions()) { List<Map<String, Object>> stats = answerService.countByQuestionId(q.getId()); // 计算总次数 int totalCount = stats.stream().mapToInt(m -> ((Number) m.get("count")).intValue()).sum(); // 计算占比并注入 stats.forEach(stat -> { int count = ((Number) stat.get("count")).intValue(); stat.put("percent", totalCount > 0 ? Math.round((double) count / totalCount * 100) : 0); }); q.setStats(stats); // 假设SurveyQuestion有setStats方法 } mav.addObject("survey", survey); return mav; }

效果:用户填写完问卷,管理员打开详情页,立即看到各题选项分布,无需等待定时任务或手动导出。某区政务平台用此方案将“群众满意度分析报告”生成时间从2天缩短至实时。

我的习惯是:每次上线新问卷,先用这个统计片段扫一眼数据质量。如果某题90%答案是“其他”,说明题干表述有问题;如果填空题平均字数<3,大概率是用户乱填——这些信号比任何KPI报表都来得快。希望帮到你。

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

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

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

立即咨询