在当今数据驱动的决策环境中,高效、精准地收集和分析用户反馈变得至关重要。传统纸质问卷或功能单一的在线工具往往存在数据采集效率低下、统计功能薄弱、难以集中管理等痛点。为此,我们设计并实现了一套专业级的问卷调研与数据分析平台,该系统基于成熟的SSM(Spring + Spring MVC + MyBatis)技术栈构建,为企业市场部门、学术研究人员及教育培训机构提供了从问卷设计、发布到数据回收与深度分析的全流程解决方案。
系统架构与技术栈
该平台采用经典的三层架构设计,确保了系统的高内聚、低耦合特性。表现层由Spring MVC框架负责,通过注解驱动的控制器(Controller)处理前端请求,并利用JSP结合JSTL标签库动态渲染视图。业务逻辑层依托Spring框架的IoC容器,统一管理各类服务组件(Service),处理复杂的问卷业务逻辑,如题目顺序校验、答卷有效性判断及统计计算。数据持久层则采用MyBatis框架,通过灵活的XML映射文件实现Java对象与数据库表字段的关联,高效执行增删改查操作。
技术选型上,系统使用Maven进行项目构建和依赖管理,前端采用HTML、CSS和JavaScript实现交互界面,并集成Jackson库为前端提供RESTful风格的JSON接口。数据库选用MySQL,确保了数据存储的稳定性和可靠性。
数据库设计亮点
数据库设计是系统稳定性的基石。该平台的数据库 schema 体现了高度的规范性和可扩展性,核心表之间通过外键关联,确保了数据的完整性和一致性。
问卷主表(tb_survey)设计分析
CREATE TABLE `tb_survey` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`title` varchar(100) DEFAULT NULL COMMENT '问卷标题',
`remark` varchar(200) DEFAULT NULL COMMENT '问卷备注',
`bounds` int(1) DEFAULT NULL COMMENT '0:不限制;1:限制',
`start_time` datetime DEFAULT NULL COMMENT '开始时间',
`end_time` datetime DEFAULT NULL COMMENT '结束时间',
`rules` int(1) DEFAULT NULL COMMENT '0公开;1密码',
`password` varchar(50) DEFAULT NULL COMMENT '密码',
`url` varchar(200) DEFAULT NULL COMMENT '问卷链接',
`state` varchar(50) DEFAULT NULL COMMENT '创建、执行中、结束',
`logo` varchar(200) DEFAULT NULL COMMENT 'logo图片',
`bgimg` varchar(200) DEFAULT NULL COMMENT '背景图片',
`anon` int(1) DEFAULT NULL COMMENT '0匿名;1不匿名',
`creator` int(11) DEFAULT NULL COMMENT '创建人',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci ROW_FORMAT=DYNAMIC COMMENT='问卷表'
该表设计具有以下技术亮点:
- 字段类型精准匹配业务需求:
bounds、rules、anon等字段使用int(1)类型,有效存储布尔型状态值,节省存储空间 - 时间范围控制:
start_time和end_time字段精确到datetime粒度,支持问卷的精确时间控制 - 多状态管理:
state字段采用varchar(50)类型,灵活支持"创建、执行中、结束"等多种状态 - 权限控制完善:通过
rules和password字段实现问卷的公开/密码访问控制 - 索引优化:主键采用自增ID,确保数据插入性能和数据有序性
问题表(tb_question)与答案表设计
CREATE TABLE `tb_question` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`title` varchar(200) DEFAULT NULL COMMENT '问题标题',
`remark` varchar(200) DEFAULT NULL COMMENT '问题备注',
`type` int(1) DEFAULT NULL COMMENT '1radio|2checkbox|3text|4textarea',
`required` int(1) DEFAULT NULL COMMENT '0非必填1必填',
`check_style` varchar(50) DEFAULT NULL COMMENT 'text;number;date',
`order_style` int(1) DEFAULT NULL COMMENT '0顺序1随机',
`show_style` int(1) DEFAULT NULL COMMENT '1;2;3;4',
`test` int(1) DEFAULT NULL COMMENT '0不测评1测评',
`score` int(3) DEFAULT NULL COMMENT '分数',
`orderby` int(11) DEFAULT NULL COMMENT '排序',
`creator` int(11) DEFAULT NULL COMMENT '创建人',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`survey_id` int(11) DEFAULT NULL COMMENT '问卷ID',
PRIMARY KEY (`id`) USING BTREE,
KEY `FK_Reference_1` (`survey_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=43 DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci ROW_FORMAT=DYNAMIC COMMENT='问题表'
问题表的设计体现了高度的灵活性:
- 多题型支持:
type字段支持单选、多选、文本、文本域四种题型,满足多样化数据采集需求 - 验证机制:
check_style字段支持文本、数字、日期等格式验证,提升数据质量 - 显示控制:
order_style和show_style字段实现题目顺序和显示样式的灵活配置 - 外键优化:
survey_id字段建立索引,优化问卷与问题之间的关联查询性能
答案表采用分表设计策略,tb_answer_opt存储选项类答案,tb_answer_txt存储文本类答案,这种设计有效避免了数据冗余,提升了查询效率。
核心功能实现
1. 问卷设计与管理系统
系统提供直观的问卷设计界面,支持拖拽式题目添加和实时预览。管理员可以灵活配置问卷属性,包括访问权限、时间限制、匿名设置等。

控制器实现代码:
@Controller
@RequestMapping("/survey")
public class SurveyController {
@Autowired
private SurveyService surveyService;
@PostMapping("/create")
@ResponseBody
public Map<String,Object> create(@RequestBody Survey survey){
survey.setCreateTime(new Date());
survey.setState("创建");
survey.setUrl(generateUniqueUrl());
int result = surveyService.create(survey);
if(result <= 0){
return MapControl.getInstance().error().getMap();
}
return MapControl.getInstance().success().add("id", survey.getId()).getMap();
}
@PostMapping("/update")
@ResponseBody
public Map<String,Object> update(@RequestBody Survey survey){
int result = surveyService.update(survey);
if(result <= 0){
return MapControl.getInstance().error().getMap();
}
return MapControl.getInstance().success().getMap();
}
private String generateUniqueUrl() {
return "survey_" + System.currentTimeMillis() + "_" + UUID.randomUUID().toString().substring(0, 8);
}
}
服务层业务逻辑:
@Service
public class SurveyService {
@Autowired
private SurveyMapper surveyMapper;
public int create(Survey survey) {
// 验证问卷时间有效性
if(survey.getBounds() == 1) {
if(survey.getStartTime().after(survey.getEndTime())) {
throw new BusinessException("开始时间不能晚于结束时间");
}
}
// 设置默认值
if(survey.getAnon() == null) {
survey.setAnon(0); // 默认匿名
}
return surveyMapper.insert(survey);
}
public List<Survey> queryByCreator(Integer creatorId) {
return surveyMapper.selectByCreator(creatorId);
}
}
2. 题目管理与动态渲染
系统支持多种题型配置,包括单选题、多选题、文本题等,并提供了丰富的题目属性设置。

题目实体类设计:
public class Question extends Entity {
private Integer id;
private String title;
private String remark;
private Integer type; // 1:单选 2:多选 3:文本 4:文本域
private Integer required; // 0:非必填 1:必填
private String checkStyle; // 验证格式
private Integer orderStyle; // 排序方式
private Integer showStyle; // 显示样式
private Integer test; // 是否测评
private Integer score; // 分数
private Integer orderby; // 排序
private Integer creator;
private Date createTime;
private Integer surveyId;
// 关联的选项列表
private List<Option> options;
// getter和setter方法
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
// ... 其他getter/setter方法
}
题目查询与渲染逻辑:
@Controller
@RequestMapping("/question")
public class QuestionController {
@Autowired
private QuestionService questionService;
@GetMapping("/list")
public String list(Integer surveyId, ModelMap modelMap) {
List<Question> questions = questionService.queryBySurvey(surveyId);
modelMap.addAttribute("questions", questions);
modelMap.addAttribute("surveyId", surveyId);
return "question/list";
}
@PostMapping("/queryBySurvey")
@ResponseBody
public Map<String,Object> queryBySurvey(Integer surveyId) {
List<Question> questions = questionService.queryBySurvey(surveyId);
return MapControl.getInstance().success().add("data", questions).getMap();
}
}
3. 答卷提交与数据验证
系统提供友好的答卷界面,支持前端实时验证和服务器端数据校验,确保数据的完整性和准确性。

答卷提交控制器:
@Controller
@RequestMapping("/answer")
public class AnswerController {
@Autowired
private AnswerService answerService;
@PostMapping("/submit")
@ResponseBody
public Map<String,Object> submit(@RequestBody AnswerForm answerForm) {
try {
// 验证问卷状态
Survey survey = surveyService.detail(answerForm.getSurveyId());
if(!"执行中".equals(survey.getState())) {
return MapControl.getInstance().error("问卷已结束").getMap();
}
// 验证时间限制
if(survey.getBounds() == 1) {
Date now = new Date();
if(now.before(survey.getStartTime()) || now.after(survey.getEndTime())) {
return MapControl.getInstance().error("不在问卷开放时间内").getMap();
}
}
// 保存答案
answerService.saveAnswer(answerForm);
return MapControl.getInstance().success().getMap();
} catch (Exception e) {
return MapControl.getInstance().error("提交失败").getMap();
}
}
}
答案服务层实现:
@Service
@Transactional
public class AnswerService {
@Autowired
private AnswerOptMapper answerOptMapper;
@Autowired
private AnswerTxtMapper answerTxtMapper;
@Autowired
private QuestionMapper questionMapper;
public void saveAnswer(AnswerForm answerForm) {
String voter = generateVoterId(answerForm);
for (AnswerDetail detail : answerForm.getDetails()) {
Question question = questionMapper.selectById(detail.getQuestionId());
// 必填验证
if(question.getRequired() == 1 &&
(detail.getOptIds() == null || detail.getOptIds().isEmpty()) &&
(detail.getTextResult() == null || detail.getTextResult().trim().isEmpty())) {
throw new BusinessException("问题" + question.getTitle() + "为必填项");
}
// 根据题型保存答案
if(question.getType() == 1 || question.getType() == 2) {
saveOptionAnswer(detail, voter, answerForm.getSurveyId());
} else {
saveTextAnswer(detail, voter, answerForm.getSurveyId());
}
}
}
private void saveOptionAnswer(AnswerDetail detail, String voter, Integer surveyId) {
for (Integer optId : detail.getOptIds()) {
AnswerOpt answerOpt = new AnswerOpt();
answerOpt.setSurveyId(surveyId);
answerOpt.setQuestionId(detail.getQuestionId());
answerOpt.setOptId(optId);
answerOpt.setType(detail.getQuestionType() == 1 ? "radio" : "checkbox");
answerOpt.setVoter(voter);
answerOpt.setCreateTime(new Date());
answerOptMapper.insert(answerOpt);
}
}
private void saveTextAnswer(AnswerDetail detail, String voter, Integer surveyId) {
AnswerTxt answerTxt = new AnswerTxt();
answerTxt.setSurveyId(surveyId);
answerTxt.setQuestionId(detail.getQuestionId());
answerTxt.setResult(detail.getTextResult());
answerTxt.setVoter(voter);
answerTxt.setCreateTime(new Date());
answerTxtMapper.insert(answerTxt);
}
private String generateVoterId(AnswerForm answerForm) {
// 生成唯一投票者标识,支持匿名和非匿名模式
if(answerForm.isAnonymous()) {
return "anon_" + System.currentTimeMillis() + "_" +
UUID.randomUUID().toString().substring(0, 6);
} else {
return "user_" + answerForm.getUserId();
}
}
}
4. 数据统计与可视化分析
系统提供强大的数据统计功能,支持多种图表展示和交叉分析,帮助用户深入理解数据背后的洞察。

统计服务层实现:
@Service
public class StatisticsService {
@Autowired
private AnswerOptMapper answerOptMapper;
@Autowired
private AnswerTxtMapper answerTxtMapper;
public SurveyStatistics getSurveyStatistics(Integer surveyId) {
SurveyStatistics statistics = new SurveyStatistics();
// 获取问卷基本信息
Survey survey = surveyMapper.selectById(surveyId);
statistics.setSurvey(survey);
// 统计答卷数量
Integer answerCount = getAnswerCount(surveyId);
statistics.setTotalAnswers(answerCount);
// 统计各题目答案分布
List<QuestionStatistics> questionStats = getQuestionStatistics(surveyId);
statistics.setQuestionStatistics(questionStats);
return statistics;
}
private List<QuestionStatistics> getQuestionStatistics(Integer surveyId) {
List<Question> questions = questionMapper.selectBySurvey(surveyId);
List<QuestionStatistics> statsList = new ArrayList<>();
for (Question question : questions) {
QuestionStatistics stats = new QuestionStatistics();
stats.setQuestion(question);
if(question.getType() == 1 || question.getType() == 2) {
// 选项类题目的统计
List<OptionStatistics> optionStats = getOptionStatistics(question.getId());
stats.setOptionStatistics(optionStats);
} else {
// 文本类题目的统计(词频分析等)
List<TextAnswer> textAnswers = answerTxtMapper.selectByQuestion(question.getId());
stats.setTextAnswers(textAnswers);
}
statsList.add(stats);
}
return statsList;
}
private List<OptionStatistics> getOptionStatistics(Integer questionId) {
return answerOptMapper.countByQuestionAndOption(questionId);
}
}
实体模型设计
系统采用面向对象的实体设计,每个数据库表对应一个Java实体类,通过MyBatis实现ORM映射。
管理员实体类优化:
package com.yanzhen.entity;
import com.yanzhen.utils.Entity;
import java.util.Date;
/**
* 管理员实体类
* @author 596183363@qq.com
* @time 2020-06-09 10:18:04
*/
public class Admin extends Entity {
private Integer id;
private String account;
private String name;
private String password;
private String phone;
private String remark;
private Date createTime;
private Date updateTime;
// 业务逻辑字段(不持久化到数据库)
private transient String confirmPassword;
private transient String newPassword;
public Admin() {}
public Admin(String account, String password) {
this.account = account;
this.password = password;
}
// Getter和Setter方法
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
public String getAccount() { return account; }
public void setAccount(String account) {
if(account != null) {
this.account = account.trim();
}
}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getPassword() { return password; }
public void setPassword(String password) {
if(password != null && !password.isEmpty()) {
this.password = MD5Utils.getMD5(password);
}
}
public String getPhone() { return phone; }
public void setPhone(String phone) {
// 手机号格式验证
if(phone != null && !phone.matches("^1[3-9]\\d{9}$")) {
throw new IllegalArgumentException("手机号格式不正确");
}
this.phone = phone;
}
public String getRemark() { return remark; }
public void setRemark(String remark) { this.remark = remark; }
public Date getCreateTime() { return createTime; }
public void setCreateTime(Date create