核心表设计
1、问卷表
create table t_questionnaire_form
(
id bigint auto_increment comment 'id'
primary key,
uuid varchar(36) not null comment 'uuid',
biz_type smallint null comment '对应业务类型',
title varchar(255) null comment '标题',
remark varchar(1024) null comment '说明',
form_status tinyint default 1 null comment '状态 1:未发布 2:已发布 3:暂停发布',
update_time datetime default CURRENT_TIMESTAMP null comment '修改时间',
create_user varchar(36) null comment '创建人',
create_time datetime default CURRENT_TIMESTAMP not null comment '创建时间',
update_user varchar(36) null comment '修改人',
constraint questionnaire_form_uuid_uindex
unique (uuid)
)
comment '问卷表';
2、题目表
create table t_questionnaire_question
(
id bigint auto_increment comment 'id'
primary key,
uuid varchar(36) not null comment 'uuid',
form_uuid varchar(36) not null comment '对应问卷表单uuid',
type tinyint not null comment '题目类型 1:单选 2:多选 3:填空',
question_title varchar(255) null comment '题目标题',
sort int default 0 not null comment '排序',
update_time datetime default CURRENT_TIMESTAMP not null comment '修改时间',
create_time datetime default CURRENT_TIMESTAMP null comment '创建时间',
create_user varchar(36) null comment '创建人',
update_user varchar(36) null comment '修改人',
constraint questionnaire_question_uuid_uindex
unique (uuid)
)
comment '问卷题目表';
3、选项表
create table t_questionnaire_option
(
id bigint auto_increment comment 'id'
primary key,
uuid varchar(36) null comment 'uuid',
form_uuid varchar(36) not null comment '问卷uuid',
question_uuid varchar(36) null comment '题目uuid',
option_name varchar(255) null comment '选项名称',
sort int default 0 not null comment '排序',
create_time datetime default CURRENT_TIMESTAMP not null comment '创建时间',
update_time datetime default CURRENT_TIMESTAMP null comment '修改时间',
create_user varchar(36) null comment '创建人',
update_user varchar(36) null comment '修改人'
)
comment '问卷选项表';
4、用户答题表
create table t_questionnaire_user_option_relation
(
id bigint auto_increment comment 'id'
primary key,
uuid varchar(36) null comment 'uuid',
biz_type smallint null comment '业务类型',
answer_user varchar(36) not null comment '回答用户',
form_uuid varchar(36) not null comment '问卷uuid',
question_uuid varchar(36) not null comment '问题uuid',
option_uuid varchar(36) null comment '选项uuid',
answer varchar(1024) not null comment '回答(多选用,拼接)',
create_time datetime default CURRENT_TIMESTAMP null comment '创建时间',
constraint questionnaire_user_option_relation_uuid_uindex
unique (uuid)
)
comment '用户与选项关系表';
5、用户答题记录表
create table t_questionnaire_user_form_relation
(
id bigint auto_increment comment 'id'
primary key,
uuid varchar(36) not null comment 'uuid',
biz_type smallint null comment '业务类型',
form_uuid varchar(36) null comment '问卷uuid',
answer_user varchar(36) not null comment '回答用户',
use_times int null comment '用时 单位秒',
end_time datetime null comment '答题开始时间',
start_time datetime null comment '答题结束时间',
create_time timestamp default CURRENT_TIMESTAMP null comment '创建时间',
constraint questionnaire_user_form_relation_uuid_uindex
unique (uuid)
)
comment '用户与问卷关系表';
数据流向说明
题目的3张表就是简单的一对多对多 多的一方记录一的一方的uuid
页面接口设计
运营侧 :
新增/修改/停启用调查问卷 (一下操作3个表)
统计相关答题人次 提交时间区间
问卷选择题的选项占比分布 问卷简答题的 高频词Top10
用户侧:
详情 答题
Service接口设计
暂无
选项分布统计核心代码
统计
public OptionStatisticsVO calculateOptionStats(Long questionId) {
// 1. 获取问题基本信息
Question question = questionRepository.findById(questionId)
.orElseThrow(() -> new ResourceNotFoundException("问题不存在"));
if (!question.getType().isChoiceType()) {
throw new BusinessException("非选择题类型");
}
// 2. 获取所有选项
List<Option> options = optionRepository.findByQuestionId(questionId);
// 3. 获取原始答案数据
List<AnswerDetail> answers = answerDetailRepository.findByQuestionId(questionId);
// 4. 统计计算
Map<Long, AtomicInteger> optionCountMap = new HashMap<>();
Set<Long> respondentSet = new HashSet<>();
// 数据初始化
options.forEach(option -> optionCountMap.put(option.getId(), new AtomicInteger(0)));
for (AnswerDetail answer : answers) {
if (StringUtils.isBlank(answer.getOptionIds())) continue;
//处理多选情况
Set<Long> selectedOptions = Arrays.stream(answer.getOptionIds().split(","))
.map(Long::parseLong)
.collect(Collectors.toSet());
// 统计选项
selectedOptions.forEach(optionId -> {
if (optionCountMap.containsKey(optionId)) {
//数量自增
optionCountMap.get(optionId).incrementAndGet();
}
});
// 统计总人数
respondentSet.add(answer.getAnswerSheetId());
}
// 5. 计算百分比
int totalRespondents = respondentSet.size();
List<OptionStatDTO> stats = new ArrayList<>();
for (Option option : options) {
int count = optionCountMap.get(option.getId()).get();
double percentage = totalRespondents > 0 ?
(count * 100.0) / totalRespondents : 0.0;
stats.add(new OptionStatDTO(
option.getId(),
option.getContent(),
count,
Math.round(percentage * 100.0) / 100.0
));
}
// 6. 按选择次数排序
stats.sort((a, b) -> b.getCount() - a.getCount());
return new OptionStatisticsVO(
question.getId(),
question.getContent(),
totalRespondents,
stats
);
}
选项DTO
@Data
@AllArgsConstructor
public class OptionStatDTO {
@ApiModelProperty(value = "选项id")
private Long optionId;
@ApiModelProperty(value = "选项内容")
private String optionContent;
@ApiModelProperty(value = "被选择次数")
private Integer count;
@ApiModelProperty(value = "百分比(0-100)")
private Double percentage;
}
统计结果VO
@Data
@AllArgsConstructor
public class OptionStatisticsVO {
@ApiModelProperty(value = "问题id")
private Long questionId;
@ApiModelProperty(value = "问题内容")
private String questionContent;
@ApiModelProperty(value = "总回答数")
private Integer totalRespondents;
@ApiModelProperty(value = "选项DTO集合")
private List<OptionStatDTO> optionsStats;
}
注意事项&拓展
每走一次统计 上边算一遍 不如放缓存里面 后面直接读缓存
搞个定时任务 扫表写缓存 key 问卷uuid value对应 statisticsVOList
public void preCalculateStatistics() {
//获取所有选择题目
List<Question> questions = questionRepository.findAllChoiceQuestions();
//根据问卷id分组
Map<String, List<Question>> questionMap =
questions.stream().collect(Collectors.groupingBy(Question::getFormId));
//遍历questionMap
questionMap.forEach((formId, questionList) -> {
List<OptionStatisticsVO> statisticsVOList = new ArrayList<>();
questionList.forEach(question -> {
OptionStatisticsVO stats = calculateOptionStats(question.getId());
statisticsVOList.add(stats);
});
RedisUtil.addIntoList("question:stats:" + formId, statisticsVOList);
});
}
如果问题多 可以考虑多线程并发执行