|
|
@@ -0,0 +1,503 @@
|
|
|
+package com.xuekairui.quiz.service.impl;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
|
|
+import com.xuekairui.common.BusinessException;
|
|
|
+import com.xuekairui.common.ErrorCode;
|
|
|
+import com.xuekairui.quiz.entity.QuizChapter;
|
|
|
+import com.xuekairui.quiz.entity.QuizQuestion;
|
|
|
+import com.xuekairui.quiz.entity.QuizSubject;
|
|
|
+import com.xuekairui.quiz.entity.QuizTrackLog;
|
|
|
+import com.xuekairui.quiz.entity.QuizUserRecord;
|
|
|
+import com.xuekairui.quiz.mapper.QuizChapterMapper;
|
|
|
+import com.xuekairui.quiz.mapper.QuizQuestionMapper;
|
|
|
+import com.xuekairui.quiz.mapper.QuizSubjectMapper;
|
|
|
+import com.xuekairui.quiz.mapper.QuizTrackLogMapper;
|
|
|
+import com.xuekairui.quiz.mapper.QuizUserRecordMapper;
|
|
|
+import com.xuekairui.quiz.service.QuizTrackService;
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.scheduling.annotation.Async;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.util.StringUtils;
|
|
|
+
|
|
|
+import java.time.LocalDate;
|
|
|
+import java.time.LocalDateTime;
|
|
|
+import java.time.LocalTime;
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
+import java.util.*;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+
|
|
|
+@Slf4j
|
|
|
+@Service
|
|
|
+@RequiredArgsConstructor
|
|
|
+public class QuizTrackServiceImpl extends ServiceImpl<QuizTrackLogMapper, QuizTrackLog> implements QuizTrackService {
|
|
|
+
|
|
|
+ private final QuizUserRecordMapper userRecordMapper;
|
|
|
+ private final QuizQuestionMapper questionMapper;
|
|
|
+ private final QuizSubjectMapper subjectMapper;
|
|
|
+ private final QuizChapterMapper chapterMapper;
|
|
|
+
|
|
|
+ @Override
|
|
|
+ @Async
|
|
|
+ public void trackEvent(Long userId, String event, Map<String, Object> properties, String page, String deviceInfo, String ip) {
|
|
|
+ if (!StringUtils.hasText(event)) {
|
|
|
+ throw new BusinessException(ErrorCode.QUIZ_TRACK_EVENT_EMPTY);
|
|
|
+ }
|
|
|
+ QuizTrackLog trackLog = QuizTrackLog.builder()
|
|
|
+ .userId(userId)
|
|
|
+ .event(event)
|
|
|
+ .properties(properties)
|
|
|
+ .page(page)
|
|
|
+ .deviceInfo(deviceInfo)
|
|
|
+ .ip(ip)
|
|
|
+ .build();
|
|
|
+ save(trackLog);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Map<String, Object> getOverview(Long subjectId, Integer answerType, Integer completeStatus,
|
|
|
+ String startTime, String endTime, Integer days) {
|
|
|
+ LocalDateTime[] timeRange = parseTimeRange(startTime, endTime, days);
|
|
|
+
|
|
|
+ LambdaQueryWrapper<QuizUserRecord> wrapper = new LambdaQueryWrapper<QuizUserRecord>()
|
|
|
+ .eq(subjectId != null, QuizUserRecord::getSubjectId, subjectId)
|
|
|
+ .eq(answerType != null, QuizUserRecord::getAnswerType, answerType)
|
|
|
+ .ge(QuizUserRecord::getCreateTime, timeRange[0])
|
|
|
+ .le(QuizUserRecord::getCreateTime, timeRange[1]);
|
|
|
+
|
|
|
+ if (completeStatus != null) {
|
|
|
+ wrapper.eq(QuizUserRecord::getStatus, completeStatus);
|
|
|
+ }
|
|
|
+
|
|
|
+ List<QuizUserRecord> records = userRecordMapper.selectList(wrapper);
|
|
|
+
|
|
|
+ // 累计使用用户(去重userId)
|
|
|
+ long totalUser = records.stream().map(QuizUserRecord::getUserId).distinct().count();
|
|
|
+ // 累计做题场次
|
|
|
+ long totalAnswerSession = records.size();
|
|
|
+ // 平均正确率
|
|
|
+ double correctRate = records.stream()
|
|
|
+ .filter(r -> r.getCorrectRate() != null)
|
|
|
+ .mapToDouble(r -> r.getCorrectRate().doubleValue())
|
|
|
+ .average()
|
|
|
+ .orElse(0.0);
|
|
|
+
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
+ result.put("totalUser", totalUser);
|
|
|
+ result.put("totalAnswerSession", totalAnswerSession);
|
|
|
+ result.put("correctRate", Math.round(correctRate * 100.0) / 100.0);
|
|
|
+ // 高频错题数:调用getHotWrongList获取total
|
|
|
+ Map<String, Object> hotWrong = getHotWrongList(subjectId, answerType, startTime, endTime, days, 1, 1);
|
|
|
+ result.put("hotWrongCount", hotWrong.get("total"));
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Map<String, Object> getUserList(String userKeyword, Long subjectId, Integer answerType,
|
|
|
+ Integer completeStatus, String startTime, String endTime,
|
|
|
+ Integer days, int pageNo, int pageSize) {
|
|
|
+ LocalDateTime[] timeRange = parseTimeRange(startTime, endTime, days);
|
|
|
+
|
|
|
+ // 从做题记录中聚合用户维度数据
|
|
|
+ LambdaQueryWrapper<QuizUserRecord> wrapper = new LambdaQueryWrapper<QuizUserRecord>()
|
|
|
+ .eq(subjectId != null, QuizUserRecord::getSubjectId, subjectId)
|
|
|
+ .eq(answerType != null, QuizUserRecord::getAnswerType, answerType)
|
|
|
+ .eq(completeStatus != null, QuizUserRecord::getStatus, completeStatus)
|
|
|
+ .ge(QuizUserRecord::getCreateTime, timeRange[0])
|
|
|
+ .le(QuizUserRecord::getCreateTime, timeRange[1]);
|
|
|
+
|
|
|
+ List<QuizUserRecord> allRecords = userRecordMapper.selectList(wrapper);
|
|
|
+
|
|
|
+ // 按userId聚合
|
|
|
+ Map<Long, List<QuizUserRecord>> grouped = allRecords.stream()
|
|
|
+ .collect(Collectors.groupingBy(QuizUserRecord::getUserId));
|
|
|
+
|
|
|
+ List<Map<String, Object>> userList = new ArrayList<>();
|
|
|
+ for (Map.Entry<Long, List<QuizUserRecord>> entry : grouped.entrySet()) {
|
|
|
+ Long uid = entry.getKey();
|
|
|
+ List<QuizUserRecord> records = entry.getValue();
|
|
|
+ // 如果有关键词过滤(userId匹配)
|
|
|
+ if (StringUtils.hasText(userKeyword) && !String.valueOf(uid).contains(userKeyword)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ Map<String, Object> user = new LinkedHashMap<>();
|
|
|
+ user.put("userId", uid);
|
|
|
+ user.put("nickname", "用户" + uid); // TODO: 关联用户表获取昵称
|
|
|
+ user.put("phone", ""); // TODO: 关联用户表获取手机号
|
|
|
+ user.put("registerTime", ""); // TODO: 关联用户表
|
|
|
+ user.put("firstUseTime", records.stream().map(QuizUserRecord::getCreateTime).min(LocalDateTime::compareTo).orElse(null));
|
|
|
+ user.put("totalAnswerCount", records.stream().mapToInt(r -> r.getTotalCount() != null ? r.getTotalCount() : 0).sum());
|
|
|
+ user.put("lastAnswerTime", records.stream().map(QuizUserRecord::getCreateTime).max(LocalDateTime::compareTo).orElse(null));
|
|
|
+ userList.add(user);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 按最近做题时间倒序
|
|
|
+ userList.sort((a, b) -> {
|
|
|
+ LocalDateTime ta = (LocalDateTime) a.get("lastAnswerTime");
|
|
|
+ LocalDateTime tb = (LocalDateTime) b.get("lastAnswerTime");
|
|
|
+ if (ta == null && tb == null) return 0;
|
|
|
+ if (ta == null) return 1;
|
|
|
+ if (tb == null) return -1;
|
|
|
+ return tb.compareTo(ta);
|
|
|
+ });
|
|
|
+
|
|
|
+ int total = userList.size();
|
|
|
+ int from = Math.min((pageNo - 1) * pageSize, total);
|
|
|
+ int to = Math.min(from + pageSize, total);
|
|
|
+ List<Map<String, Object>> pageList = userList.subList(from, to);
|
|
|
+
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
+ result.put("total", total);
|
|
|
+ result.put("list", pageList);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Map<String, Object> getAnswerList(String userKeyword, Long subjectId, Integer answerType,
|
|
|
+ Integer completeStatus, String startTime, String endTime,
|
|
|
+ Integer days, int pageNo, int pageSize) {
|
|
|
+ LocalDateTime[] timeRange = parseTimeRange(startTime, endTime, days);
|
|
|
+
|
|
|
+ LambdaQueryWrapper<QuizUserRecord> wrapper = new LambdaQueryWrapper<QuizUserRecord>()
|
|
|
+ .eq(subjectId != null, QuizUserRecord::getSubjectId, subjectId)
|
|
|
+ .eq(answerType != null, QuizUserRecord::getAnswerType, answerType)
|
|
|
+ .eq(completeStatus != null, QuizUserRecord::getStatus, completeStatus)
|
|
|
+ .ge(QuizUserRecord::getCreateTime, timeRange[0])
|
|
|
+ .le(QuizUserRecord::getCreateTime, timeRange[1])
|
|
|
+ .orderByDesc(QuizUserRecord::getCreateTime);
|
|
|
+
|
|
|
+ // 简单分页
|
|
|
+ long total = userRecordMapper.selectCount(wrapper);
|
|
|
+ wrapper.last("LIMIT " + (pageNo - 1) * pageSize + "," + pageSize);
|
|
|
+ List<QuizUserRecord> records = userRecordMapper.selectList(wrapper);
|
|
|
+
|
|
|
+ List<Map<String, Object>> list = new ArrayList<>();
|
|
|
+ for (QuizUserRecord r : records) {
|
|
|
+ Map<String, Object> map = new LinkedHashMap<>();
|
|
|
+ map.put("userId", r.getUserId());
|
|
|
+ map.put("answerType", r.getAnswerType());
|
|
|
+ map.put("subjectId", r.getSubjectId());
|
|
|
+ map.put("totalCount", r.getTotalCount());
|
|
|
+ map.put("correctCount", r.getCorrectCount());
|
|
|
+ map.put("correctRate", r.getCorrectRate());
|
|
|
+ map.put("status", r.getStatus());
|
|
|
+ map.put("createTime", r.getCreateTime());
|
|
|
+ list.add(map);
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
+ result.put("total", total);
|
|
|
+ result.put("list", list);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Map<String, Object> getHotWrongList(Long subjectId, Integer answerType,
|
|
|
+ String startTime, String endTime,
|
|
|
+ Integer days, int pageNo, int pageSize) {
|
|
|
+ LocalDateTime[] timeRange = parseTimeRange(startTime, endTime, days);
|
|
|
+
|
|
|
+ LambdaQueryWrapper<QuizUserRecord> wrapper = new LambdaQueryWrapper<QuizUserRecord>()
|
|
|
+ .eq(subjectId != null, QuizUserRecord::getSubjectId, subjectId)
|
|
|
+ .eq(answerType != null, QuizUserRecord::getAnswerType, answerType)
|
|
|
+ .ge(QuizUserRecord::getCreateTime, timeRange[0])
|
|
|
+ .le(QuizUserRecord::getCreateTime, timeRange[1]);
|
|
|
+
|
|
|
+ List<QuizUserRecord> records = userRecordMapper.selectList(wrapper);
|
|
|
+
|
|
|
+ // 从answers JSON中聚合每道题的错误次数
|
|
|
+ // answers格式: [{"questionId":1,"userAnswer":"A","isCorrect":0},...]
|
|
|
+ Map<Long, int[]> questionStats = new HashMap<>(); // questionId -> [wrongCount, totalAnswerCount]
|
|
|
+ Map<Long, Set<Long>> questionUsers = new HashMap<>();
|
|
|
+
|
|
|
+ for (QuizUserRecord r : records) {
|
|
|
+ if (r.getAnswers() == null) continue;
|
|
|
+ for (Map<String, Object> ans : r.getAnswers()) {
|
|
|
+ Object qidObj = ans.get("questionId");
|
|
|
+ if (qidObj == null) continue;
|
|
|
+ Long qid = Long.valueOf(qidObj.toString());
|
|
|
+ int isCorrect = ans.get("isCorrect") != null ? Integer.parseInt(ans.get("isCorrect").toString()) : 0;
|
|
|
+
|
|
|
+ questionStats.computeIfAbsent(qid, k -> new int[]{0, 0});
|
|
|
+ questionStats.get(qid)[1]++;
|
|
|
+ if (isCorrect == 0) {
|
|
|
+ questionStats.get(qid)[0]++;
|
|
|
+ }
|
|
|
+ questionUsers.computeIfAbsent(qid, k -> new HashSet<>()).add(r.getUserId());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 过滤错误率>=30%的题目,按错误次数排序
|
|
|
+ List<Map<String, Object>> wrongList = new ArrayList<>();
|
|
|
+ for (Map.Entry<Long, int[]> entry : questionStats.entrySet()) {
|
|
|
+ Long qid = entry.getKey();
|
|
|
+ int[] stats = entry.getValue();
|
|
|
+ int wrongCount = stats[0];
|
|
|
+ int totalCount = stats[1];
|
|
|
+ if (totalCount == 0) continue;
|
|
|
+ double wrongRate = wrongCount * 100.0 / totalCount;
|
|
|
+ if (wrongRate < 30.0) continue;
|
|
|
+
|
|
|
+ QuizQuestion question = questionMapper.selectById(qid);
|
|
|
+ if (question == null) continue;
|
|
|
+
|
|
|
+ QuizSubject subject = subjectMapper.selectById(question.getSubjectId());
|
|
|
+ QuizChapter chapter = chapterMapper.selectById(question.getChapterId());
|
|
|
+
|
|
|
+ Map<String, Object> item = new LinkedHashMap<>();
|
|
|
+ item.put("questionId", qid);
|
|
|
+ item.put("stem", question.getStem() != null && question.getStem().length() > 50 ? question.getStem().substring(0, 50) + "..." : question.getStem());
|
|
|
+ item.put("subjectName", subject != null ? subject.getName() : "");
|
|
|
+ item.put("chapterName", chapter != null ? chapter.getName() : "");
|
|
|
+ item.put("wrongTimes", wrongCount);
|
|
|
+ item.put("wrongRate", Math.round(wrongRate * 100.0) / 100.0);
|
|
|
+ item.put("answerUserCount", questionUsers.getOrDefault(qid, Collections.emptySet()).size());
|
|
|
+ wrongList.add(item);
|
|
|
+ }
|
|
|
+
|
|
|
+ wrongList.sort((a, b) -> Integer.compare((int) b.get("wrongTimes"), (int) a.get("wrongTimes")));
|
|
|
+
|
|
|
+ // 添加排名
|
|
|
+ for (int i = 0; i < wrongList.size(); i++) {
|
|
|
+ wrongList.get(i).put("rank", i + 1);
|
|
|
+ }
|
|
|
+
|
|
|
+ int total = wrongList.size();
|
|
|
+ int from = Math.min((pageNo - 1) * pageSize, total);
|
|
|
+ int to = Math.min(from + pageSize, total);
|
|
|
+
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
+ result.put("total", total);
|
|
|
+ result.put("list", wrongList.subList(from, to));
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Map<String, Object> getAnswerTimeDistribution(Long subjectId, Integer answerType,
|
|
|
+ String startTime, String endTime, Integer days) {
|
|
|
+ LocalDateTime[] timeRange = parseTimeRange(startTime, endTime, days);
|
|
|
+
|
|
|
+ LambdaQueryWrapper<QuizUserRecord> wrapper = new LambdaQueryWrapper<QuizUserRecord>()
|
|
|
+ .eq(subjectId != null, QuizUserRecord::getSubjectId, subjectId)
|
|
|
+ .eq(answerType != null, QuizUserRecord::getAnswerType, answerType)
|
|
|
+ .ge(QuizUserRecord::getCreateTime, timeRange[0])
|
|
|
+ .le(QuizUserRecord::getCreateTime, timeRange[1]);
|
|
|
+
|
|
|
+ List<QuizUserRecord> records = userRecordMapper.selectList(wrapper);
|
|
|
+
|
|
|
+ int[] counts = new int[24];
|
|
|
+ for (QuizUserRecord r : records) {
|
|
|
+ if (r.getCreateTime() != null) {
|
|
|
+ counts[r.getCreateTime().getHour()]++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ List<Integer> hours = new ArrayList<>();
|
|
|
+ List<Integer> countList = new ArrayList<>();
|
|
|
+ for (int i = 0; i < 24; i++) {
|
|
|
+ hours.add(i);
|
|
|
+ countList.add(counts[i]);
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
+ result.put("hours", hours);
|
|
|
+ result.put("counts", countList);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Map<String, Object> getEventList(String event, Long userId, String startTime, String endTime,
|
|
|
+ int pageNo, int pageSize) {
|
|
|
+ LambdaQueryWrapper<QuizTrackLog> wrapper = new LambdaQueryWrapper<QuizTrackLog>()
|
|
|
+ .eq(StringUtils.hasText(event), QuizTrackLog::getEvent, event)
|
|
|
+ .eq(userId != null, QuizTrackLog::getUserId, userId);
|
|
|
+
|
|
|
+ if (StringUtils.hasText(startTime)) {
|
|
|
+ wrapper.ge(QuizTrackLog::getCreateTime, LocalDate.parse(startTime, DateTimeFormatter.ofPattern("yyyy-MM-dd")).atStartOfDay());
|
|
|
+ }
|
|
|
+ if (StringUtils.hasText(endTime)) {
|
|
|
+ wrapper.le(QuizTrackLog::getCreateTime, LocalDate.parse(endTime, DateTimeFormatter.ofPattern("yyyy-MM-dd")).atTime(LocalTime.MAX));
|
|
|
+ }
|
|
|
+ wrapper.orderByDesc(QuizTrackLog::getCreateTime);
|
|
|
+
|
|
|
+ long total = count(wrapper);
|
|
|
+ wrapper.last("LIMIT " + (pageNo - 1) * pageSize + "," + pageSize);
|
|
|
+ List<QuizTrackLog> logs = list(wrapper);
|
|
|
+
|
|
|
+ List<Map<String, Object>> list = new ArrayList<>();
|
|
|
+ for (QuizTrackLog log : logs) {
|
|
|
+ Map<String, Object> item = new LinkedHashMap<>();
|
|
|
+ item.put("id", log.getId());
|
|
|
+ item.put("userId", log.getUserId());
|
|
|
+ item.put("event", log.getEvent());
|
|
|
+ item.put("eventName", getEventName(log.getEvent()));
|
|
|
+ item.put("page", log.getPage());
|
|
|
+ item.put("pageName", getPageName(log.getPage()));
|
|
|
+ item.put("properties", log.getProperties());
|
|
|
+ item.put("ip", log.getIp());
|
|
|
+ item.put("createTime", log.getCreateTime());
|
|
|
+ list.add(item);
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
+ result.put("total", total);
|
|
|
+ result.put("list", list);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public byte[] exportReport(String type, Long subjectId, Integer answerType, Integer completeStatus,
|
|
|
+ String startTime, String endTime, Integer days) {
|
|
|
+ try (java.io.ByteArrayOutputStream bos = new java.io.ByteArrayOutputStream()) {
|
|
|
+ if ("user".equals(type)) {
|
|
|
+ Map<String, Object> data = getUserList(null, subjectId, answerType, completeStatus, startTime, endTime, days, 1, 100000);
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ List<Map<String, Object>> list = (List<Map<String, Object>>) data.get("list");
|
|
|
+ com.alibaba.excel.EasyExcel.write(bos)
|
|
|
+ .sheet("用户明细")
|
|
|
+ .head(buildUserHead())
|
|
|
+ .doWrite(buildUserData(list));
|
|
|
+ } else if ("answer".equals(type)) {
|
|
|
+ Map<String, Object> data = getAnswerList(null, subjectId, answerType, completeStatus, startTime, endTime, days, 1, 100000);
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ List<Map<String, Object>> list = (List<Map<String, Object>>) data.get("list");
|
|
|
+ com.alibaba.excel.EasyExcel.write(bos)
|
|
|
+ .sheet("做题记录")
|
|
|
+ .head(buildAnswerHead())
|
|
|
+ .doWrite(buildAnswerData(list));
|
|
|
+ } else if ("wrongQuestion".equals(type)) {
|
|
|
+ Map<String, Object> data = getHotWrongList(subjectId, answerType, startTime, endTime, days, 1, 100000);
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ List<Map<String, Object>> list = (List<Map<String, Object>>) data.get("list");
|
|
|
+ com.alibaba.excel.EasyExcel.write(bos)
|
|
|
+ .sheet("高频错题")
|
|
|
+ .head(buildWrongHead())
|
|
|
+ .doWrite(buildWrongData(list));
|
|
|
+ }
|
|
|
+ return bos.toByteArray();
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw new BusinessException(ErrorCode.INTERNAL_ERROR, "导出报表异常");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private String getEventName(String event) {
|
|
|
+ if (event == null) return "";
|
|
|
+ return switch (event) {
|
|
|
+ case "page_view" -> "页面浏览";
|
|
|
+ case "question_answer" -> "答题";
|
|
|
+ case "question_collect" -> "收藏";
|
|
|
+ case "exam_start" -> "开始模考";
|
|
|
+ case "exam_submit" -> "交卷";
|
|
|
+ default -> event;
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ private String getPageName(String page) {
|
|
|
+ if (page == null) return "";
|
|
|
+ if (page.contains("chapter")) return "章节练习";
|
|
|
+ if (page.contains("daily")) return "每日一练";
|
|
|
+ if (page.contains("exam")) return "真题模考";
|
|
|
+ if (page.contains("home")) return "首页";
|
|
|
+ return page;
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<List<String>> buildUserHead() {
|
|
|
+ List<List<String>> head = new ArrayList<>();
|
|
|
+ head.add(Collections.singletonList("用户ID"));
|
|
|
+ head.add(Collections.singletonList("昵称"));
|
|
|
+ head.add(Collections.singletonList("手机号"));
|
|
|
+ head.add(Collections.singletonList("累计做题"));
|
|
|
+ head.add(Collections.singletonList("首次使用"));
|
|
|
+ head.add(Collections.singletonList("最近做题"));
|
|
|
+ return head;
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<List<Object>> buildUserData(List<Map<String, Object>> list) {
|
|
|
+ List<List<Object>> data = new ArrayList<>();
|
|
|
+ for (Map<String, Object> item : list) {
|
|
|
+ List<Object> row = new ArrayList<>();
|
|
|
+ row.add(item.get("userId"));
|
|
|
+ row.add(item.get("nickname"));
|
|
|
+ row.add(item.get("phone"));
|
|
|
+ row.add(item.get("totalAnswerCount"));
|
|
|
+ row.add(item.get("firstUseTime"));
|
|
|
+ row.add(item.get("lastAnswerTime"));
|
|
|
+ data.add(row);
|
|
|
+ }
|
|
|
+ return data;
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<List<String>> buildAnswerHead() {
|
|
|
+ List<List<String>> head = new ArrayList<>();
|
|
|
+ head.add(Collections.singletonList("用户ID"));
|
|
|
+ head.add(Collections.singletonList("答题类型"));
|
|
|
+ head.add(Collections.singletonList("科目"));
|
|
|
+ head.add(Collections.singletonList("总题数"));
|
|
|
+ head.add(Collections.singletonList("正确数"));
|
|
|
+ head.add(Collections.singletonList("正确率"));
|
|
|
+ head.add(Collections.singletonList("状态"));
|
|
|
+ head.add(Collections.singletonList("答题时间"));
|
|
|
+ return head;
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<List<Object>> buildAnswerData(List<Map<String, Object>> list) {
|
|
|
+ List<List<Object>> data = new ArrayList<>();
|
|
|
+ for (Map<String, Object> item : list) {
|
|
|
+ List<Object> row = new ArrayList<>();
|
|
|
+ row.add(item.get("userId"));
|
|
|
+ row.add(item.get("answerType"));
|
|
|
+ row.add(item.get("subjectId"));
|
|
|
+ row.add(item.get("totalCount"));
|
|
|
+ row.add(item.get("correctCount"));
|
|
|
+ row.add(item.get("correctRate"));
|
|
|
+ row.add(item.get("status"));
|
|
|
+ row.add(item.get("createTime"));
|
|
|
+ data.add(row);
|
|
|
+ }
|
|
|
+ return data;
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<List<String>> buildWrongHead() {
|
|
|
+ List<List<String>> head = new ArrayList<>();
|
|
|
+ head.add(Collections.singletonList("排名"));
|
|
|
+ head.add(Collections.singletonList("题干"));
|
|
|
+ head.add(Collections.singletonList("科目"));
|
|
|
+ head.add(Collections.singletonList("考点"));
|
|
|
+ head.add(Collections.singletonList("错误次数"));
|
|
|
+ head.add(Collections.singletonList("错误率"));
|
|
|
+ head.add(Collections.singletonList("作答人数"));
|
|
|
+ return head;
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<List<Object>> buildWrongData(List<Map<String, Object>> list) {
|
|
|
+ List<List<Object>> data = new ArrayList<>();
|
|
|
+ for (Map<String, Object> item : list) {
|
|
|
+ List<Object> row = new ArrayList<>();
|
|
|
+ row.add(item.get("rank"));
|
|
|
+ row.add(item.get("stem"));
|
|
|
+ row.add(item.get("subjectName"));
|
|
|
+ row.add(item.get("chapterName"));
|
|
|
+ row.add(item.get("wrongTimes"));
|
|
|
+ row.add(item.get("wrongRate"));
|
|
|
+ row.add(item.get("answerUserCount"));
|
|
|
+ data.add(row);
|
|
|
+ }
|
|
|
+ return data;
|
|
|
+ }
|
|
|
+
|
|
|
+ private LocalDateTime[] parseTimeRange(String startTime, String endTime, Integer days) {
|
|
|
+ DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
|
|
+ if (StringUtils.hasText(startTime) && StringUtils.hasText(endTime)) {
|
|
|
+ return new LocalDateTime[]{
|
|
|
+ LocalDate.parse(startTime, fmt).atStartOfDay(),
|
|
|
+ LocalDate.parse(endTime, fmt).atTime(LocalTime.MAX)
|
|
|
+ };
|
|
|
+ }
|
|
|
+ int d = (days != null && days > 0) ? days : 30;
|
|
|
+ return new LocalDateTime[]{
|
|
|
+ LocalDate.now().minusDays(d).atStartOfDay(),
|
|
|
+ LocalDateTime.now()
|
|
|
+ };
|
|
|
+ }
|
|
|
+}
|