3c4db2fead
- /review/feed?mode=smart:时间衰减 × 回忆掌握度加权采样 - 越久未见、回忆覆盖率越低的内容出现概率越高 - 保持随机性,符合复习模块偶遇式设计理念 - 概率性单元测试验证低掌握内容显著优先
543 lines
24 KiB
Java
543 lines
24 KiB
Java
package com.guo.learningprogresstracker.service.impl;
|
|
|
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
import com.guo.learningprogresstracker.dto.ReviewFeedItem;
|
|
import com.guo.learningprogresstracker.dto.ReviewTaskStats;
|
|
import com.guo.learningprogresstracker.dto.request.UpsertReviewMindMapRequest;
|
|
import com.guo.learningprogresstracker.entity.ReviewMindMapEntity;
|
|
import com.guo.learningprogresstracker.entity.ReviewRecallRecordEntity;
|
|
import com.guo.learningprogresstracker.entity.StudyReportFragmentsEntity;
|
|
import com.guo.learningprogresstracker.entity.StudyReportsEntity;
|
|
import com.guo.learningprogresstracker.entity.StudySessionsEntity;
|
|
import com.guo.learningprogresstracker.entity.TaskEntity;
|
|
import com.guo.learningprogresstracker.enums.ReviewMindMapFileFormatEnum;
|
|
import com.guo.learningprogresstracker.enums.ReviewMindMapParseStatusEnum;
|
|
import com.guo.learningprogresstracker.exception.NotFindEntitiesException;
|
|
import com.guo.learningprogresstracker.mapper.ReviewMindMapMapper;
|
|
import com.guo.learningprogresstracker.mapper.ReviewRecallRecordMapper;
|
|
import com.guo.learningprogresstracker.mapper.StudyReportFragmentsMapper;
|
|
import com.guo.learningprogresstracker.mapper.StudyReportsMapper;
|
|
import com.guo.learningprogresstracker.mapper.StudySessionsMapper;
|
|
import com.guo.learningprogresstracker.mapper.TasksMapper;
|
|
import com.guo.learningprogresstracker.service.ReviewService;
|
|
import lombok.RequiredArgsConstructor;
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.util.StringUtils;
|
|
import org.springframework.web.multipart.MultipartFile;
|
|
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.nio.file.Files;
|
|
import java.nio.file.Path;
|
|
import java.nio.file.Paths;
|
|
import java.nio.file.StandardCopyOption;
|
|
import java.time.DayOfWeek;
|
|
import java.time.LocalDate;
|
|
import java.time.LocalDateTime;
|
|
import java.util.*;
|
|
import java.util.stream.Collectors;
|
|
import java.util.stream.Stream;
|
|
|
|
/**
|
|
* 复习模块 Service 实现
|
|
*/
|
|
@Service
|
|
@RequiredArgsConstructor
|
|
public class ReviewServiceImpl implements ReviewService {
|
|
|
|
private static final int DEFAULT_LIMIT = 30;
|
|
private static final int MAX_LIMIT = 100;
|
|
private static final String RANDOM_MODE = "random";
|
|
private static final String SMART_MODE = "smart";
|
|
private static final int SMART_CANDIDATE_MULTIPLIER = 5;
|
|
private static final String DEFAULT_MIND_MAP_FORMAT = "TEXT";
|
|
private static final String MANUAL_SOURCE_TYPE = "MANUAL";
|
|
private static final String FILE_SOURCE_TYPE = "FILE";
|
|
|
|
private final StudyReportsMapper studyReportsMapper;
|
|
private final StudyReportFragmentsMapper studyReportFragmentsMapper;
|
|
private final StudySessionsMapper studySessionsMapper;
|
|
private final TasksMapper tasksMapper;
|
|
private final ReviewMindMapMapper reviewMindMapMapper;
|
|
private final ReviewRecallRecordMapper reviewRecallRecordMapper;
|
|
private final MindMapFileParser mindMapFileParser = new MindMapFileParser(new ObjectMapper());
|
|
|
|
@Override
|
|
public List<ReviewFeedItem> getReviewFeed(int limit, String mode) {
|
|
int safeLimit = normalizeLimit(limit);
|
|
if (SMART_MODE.equalsIgnoreCase(mode)) {
|
|
return getSmartFeed(safeLimit);
|
|
}
|
|
boolean random = RANDOM_MODE.equalsIgnoreCase(mode);
|
|
|
|
List<StudyReportsEntity> reports = random
|
|
? studyReportsMapper.selectList(Wrappers.<StudyReportsEntity>lambdaQuery()
|
|
.last("ORDER BY RAND() LIMIT " + safeLimit))
|
|
: studyReportsMapper.selectList(Wrappers.<StudyReportsEntity>lambdaQuery()
|
|
.orderByDesc(StudyReportsEntity::getCreatedTime)
|
|
.last("LIMIT " + safeLimit));
|
|
|
|
List<StudyReportFragmentsEntity> fragments = random
|
|
? studyReportFragmentsMapper.selectList(Wrappers.<StudyReportFragmentsEntity>lambdaQuery()
|
|
.last("ORDER BY RAND() LIMIT " + safeLimit))
|
|
: studyReportFragmentsMapper.selectList(Wrappers.<StudyReportFragmentsEntity>lambdaQuery()
|
|
.orderByDesc(StudyReportFragmentsEntity::getCreatedTime)
|
|
.last("LIMIT " + safeLimit));
|
|
|
|
List<ReviewFeedItem> items = mergeAndConvert(reports, fragments);
|
|
if (random) {
|
|
Collections.shuffle(items);
|
|
}
|
|
return items.stream().limit(safeLimit).collect(Collectors.toList());
|
|
}
|
|
|
|
/**
|
|
* 智能模式:加权随机采样,让更需要复习的内容有更高概率出现。
|
|
* 权重 = 时间衰减(越久未见权重越高) × 任务回忆掌握度(覆盖率越低权重越高)。
|
|
* 仍保持随机性,只是"随机得不那么均匀"——符合复习模块无压力、偶遇式的设计理念。
|
|
*/
|
|
private List<ReviewFeedItem> getSmartFeed(int safeLimit) {
|
|
int candidateLimit = Math.min(safeLimit * SMART_CANDIDATE_MULTIPLIER, 500);
|
|
|
|
List<StudyReportsEntity> reports = studyReportsMapper.selectList(
|
|
Wrappers.<StudyReportsEntity>lambdaQuery()
|
|
.last("ORDER BY RAND() LIMIT " + candidateLimit));
|
|
List<StudyReportFragmentsEntity> fragments = studyReportFragmentsMapper.selectList(
|
|
Wrappers.<StudyReportFragmentsEntity>lambdaQuery()
|
|
.last("ORDER BY RAND() LIMIT " + candidateLimit));
|
|
|
|
List<ReviewFeedItem> candidates = mergeAndConvert(reports, fragments);
|
|
if (candidates.size() <= safeLimit) {
|
|
Collections.shuffle(candidates);
|
|
return candidates;
|
|
}
|
|
|
|
// 各任务最近一次回忆对比的覆盖率(没有记录视为 0 = 最需要复习)
|
|
Map<String, Double> taskRecallRatio = latestRecallRatioByTask(
|
|
candidates.stream().map(ReviewFeedItem::getTaskNum)
|
|
.filter(Objects::nonNull).collect(Collectors.toSet()));
|
|
|
|
LocalDateTime now = LocalDateTime.now();
|
|
List<Double> weights = new ArrayList<>(candidates.size());
|
|
for (ReviewFeedItem item : candidates) {
|
|
weights.add(reviewNeedWeight(item, taskRecallRatio, now));
|
|
}
|
|
return weightedSample(candidates, weights, safeLimit);
|
|
}
|
|
|
|
/** 单条内容的复习需求权重 */
|
|
private double reviewNeedWeight(ReviewFeedItem item, Map<String, Double> taskRecallRatio, LocalDateTime now) {
|
|
// 时间衰减:7 天内权重较低,之后随天数增长,90 天封顶
|
|
double ageDays = item.getCreatedTime() == null ? 30
|
|
: Math.max(0, java.time.Duration.between(item.getCreatedTime(), now).toDays());
|
|
double ageFactor = 0.3 + Math.min(ageDays, 90) / 90.0 * 0.7;
|
|
|
|
// 掌握度:最近回忆覆盖率越低,权重越高
|
|
double ratio = item.getTaskNum() == null ? 0
|
|
: taskRecallRatio.getOrDefault(item.getTaskNum(), 0D);
|
|
double masteryFactor = 1.0 - ratio * 0.7; // 覆盖率 100% 时权重降至 0.3
|
|
|
|
return ageFactor * masteryFactor;
|
|
}
|
|
|
|
/** 查询每个任务最近一次回忆对比的覆盖率 */
|
|
private Map<String, Double> latestRecallRatioByTask(Set<String> taskNums) {
|
|
if (taskNums.isEmpty()) {
|
|
return Collections.emptyMap();
|
|
}
|
|
List<ReviewRecallRecordEntity> records = reviewRecallRecordMapper.selectList(
|
|
Wrappers.<ReviewRecallRecordEntity>lambdaQuery()
|
|
.in(ReviewRecallRecordEntity::getTaskNum, taskNums)
|
|
.orderByDesc(ReviewRecallRecordEntity::getCreatedTime));
|
|
Map<String, Double> result = new HashMap<>();
|
|
for (ReviewRecallRecordEntity record : records) {
|
|
result.putIfAbsent(record.getTaskNum(),
|
|
record.getRecallRatio() == null ? 0D : record.getRecallRatio());
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/** 按权重不放回采样 */
|
|
private List<ReviewFeedItem> weightedSample(List<ReviewFeedItem> items, List<Double> weights, int count) {
|
|
List<ReviewFeedItem> pool = new ArrayList<>(items);
|
|
List<Double> poolWeights = new ArrayList<>(weights);
|
|
List<ReviewFeedItem> selected = new ArrayList<>(count);
|
|
Random random = new Random();
|
|
|
|
while (selected.size() < count && !pool.isEmpty()) {
|
|
double total = poolWeights.stream().mapToDouble(Double::doubleValue).sum();
|
|
double r = random.nextDouble() * total;
|
|
double cumulative = 0;
|
|
int chosen = pool.size() - 1;
|
|
for (int i = 0; i < pool.size(); i++) {
|
|
cumulative += poolWeights.get(i);
|
|
if (r <= cumulative) {
|
|
chosen = i;
|
|
break;
|
|
}
|
|
}
|
|
selected.add(pool.remove(chosen));
|
|
poolWeights.remove(chosen);
|
|
}
|
|
return selected;
|
|
}
|
|
|
|
@Override
|
|
public List<ReviewTaskStats> getReviewTaskStats() {
|
|
List<TaskEntity> tasks = tasksMapper.selectList(
|
|
Wrappers.<TaskEntity>lambdaQuery()
|
|
.orderByDesc(TaskEntity::getCalculatedPriority));
|
|
|
|
List<ReviewTaskStats> stats = tasks.stream()
|
|
.map(this::newTaskStats)
|
|
.collect(Collectors.toList());
|
|
|
|
fillReviewTaskStats(stats);
|
|
return stats;
|
|
}
|
|
|
|
@Override
|
|
public ReviewTaskStats getReviewTaskStats(String taskNum) {
|
|
TaskEntity task = tasksMapper.selectList(
|
|
Wrappers.<TaskEntity>lambdaQuery()
|
|
.eq(TaskEntity::getTaskNum, taskNum)
|
|
.last("LIMIT 1"))
|
|
.stream()
|
|
.findFirst()
|
|
.orElse(null);
|
|
|
|
ReviewTaskStats stats = newTaskStats(taskNum, task == null ? null : task.getTaskName());
|
|
fillReviewTaskStats(Collections.singletonList(stats));
|
|
return stats;
|
|
}
|
|
|
|
@Override
|
|
public List<ReviewFeedItem> getTaskReview(String taskNum) {
|
|
List<String> sessionNums = studySessionsMapper.selectList(
|
|
Wrappers.<StudySessionsEntity>lambdaQuery()
|
|
.eq(StudySessionsEntity::getTaskNum, taskNum)
|
|
.select(StudySessionsEntity::getSessionNum))
|
|
.stream()
|
|
.map(StudySessionsEntity::getSessionNum)
|
|
.collect(Collectors.toList());
|
|
|
|
if (sessionNums.isEmpty()) {
|
|
return Collections.emptyList();
|
|
}
|
|
|
|
List<StudyReportsEntity> reports = studyReportsMapper.selectList(
|
|
Wrappers.<StudyReportsEntity>lambdaQuery()
|
|
.in(StudyReportsEntity::getSessionNum, sessionNums)
|
|
.orderByDesc(StudyReportsEntity::getCreatedTime));
|
|
|
|
List<StudyReportFragmentsEntity> fragments = studyReportFragmentsMapper.selectList(
|
|
Wrappers.<StudyReportFragmentsEntity>lambdaQuery()
|
|
.in(StudyReportFragmentsEntity::getSessionNum, sessionNums)
|
|
.orderByDesc(StudyReportFragmentsEntity::getCreatedTime));
|
|
|
|
return mergeAndConvert(reports, fragments);
|
|
}
|
|
|
|
@Override
|
|
public StudyReportsEntity getReportDetail(int id) throws NotFindEntitiesException {
|
|
return Optional.ofNullable(studyReportsMapper.selectById(id))
|
|
.orElseThrow(() -> new NotFindEntitiesException("学习报告[" + id + "]不存在"));
|
|
}
|
|
|
|
@Override
|
|
public StudyReportFragmentsEntity getFragmentDetail(int id) throws NotFindEntitiesException {
|
|
return Optional.ofNullable(studyReportFragmentsMapper.selectById(id))
|
|
.orElseThrow(() -> new NotFindEntitiesException("学习残片[" + id + "]不存在"));
|
|
}
|
|
|
|
@Override
|
|
public ReviewMindMapEntity getMindMap(String taskNum) throws NotFindEntitiesException {
|
|
ensureTaskExists(taskNum);
|
|
return reviewMindMapMapper.selectOne(
|
|
Wrappers.<ReviewMindMapEntity>lambdaQuery()
|
|
.eq(ReviewMindMapEntity::getTaskNum, taskNum)
|
|
.last("LIMIT 1"));
|
|
}
|
|
|
|
@Override
|
|
public ReviewMindMapEntity upsertMindMap(String taskNum, UpsertReviewMindMapRequest request) throws NotFindEntitiesException {
|
|
ensureTaskExists(taskNum);
|
|
ReviewMindMapEntity entity = getMindMap(taskNum);
|
|
boolean create = entity == null;
|
|
if (create) {
|
|
entity = new ReviewMindMapEntity();
|
|
entity.setTaskNum(taskNum);
|
|
}
|
|
entity.setTitle(request.getTitle());
|
|
entity.setContent(request.getContent());
|
|
entity.setContentFormat(normalizeMindMapFormat(request.getContentFormat()));
|
|
entity.setSourceType(MANUAL_SOURCE_TYPE);
|
|
entity.setFileName(null);
|
|
entity.setFilePath(null);
|
|
entity.setFileFormat(null);
|
|
entity.setParsedContent(null);
|
|
entity.setSummary(null);
|
|
entity.setLastParsedTime(null);
|
|
entity.setParseStatus(ReviewMindMapParseStatusEnum.SUCCESS.getCode());
|
|
entity.setParseError(null);
|
|
if (create) {
|
|
reviewMindMapMapper.insert(entity);
|
|
} else {
|
|
reviewMindMapMapper.updateById(entity);
|
|
}
|
|
return entity;
|
|
}
|
|
|
|
@Override
|
|
public ReviewMindMapEntity uploadMindMap(String taskNum, MultipartFile file) throws NotFindEntitiesException, IOException {
|
|
ensureTaskExists(taskNum);
|
|
if (file == null || file.isEmpty()) {
|
|
throw new IOException("思维导图文件不可为空");
|
|
}
|
|
|
|
String originalFileName = StringUtils.cleanPath(
|
|
Optional.ofNullable(file.getOriginalFilename()).orElse("mind-map.txt"));
|
|
ReviewMindMapFileFormatEnum format = ReviewMindMapFileFormatEnum.fromFileName(originalFileName);
|
|
Path storedPath = storeMindMapFile(taskNum, originalFileName, file);
|
|
|
|
ReviewMindMapEntity entity = getMindMap(taskNum);
|
|
boolean create = entity == null;
|
|
if (create) {
|
|
entity = new ReviewMindMapEntity();
|
|
entity.setTaskNum(taskNum);
|
|
}
|
|
|
|
entity.setSourceType(FILE_SOURCE_TYPE);
|
|
entity.setFileName(originalFileName);
|
|
entity.setFilePath(storedPath.toString());
|
|
entity.setFileFormat(format.getCode());
|
|
entity.setContentFormat("JSON");
|
|
entity.setLastParsedTime(LocalDateTime.now());
|
|
|
|
try {
|
|
MindMapFileParser.ParseResult parseResult = mindMapFileParser.parse(file, format);
|
|
entity.setTitle(StringUtils.hasText(parseResult.title()) ? parseResult.title() : originalFileName);
|
|
entity.setContent(parseResult.outline());
|
|
entity.setParsedContent(parseResult.parsedContent());
|
|
entity.setSummary(parseResult.summary());
|
|
entity.setParseStatus(ReviewMindMapParseStatusEnum.SUCCESS.getCode());
|
|
entity.setParseError(null);
|
|
} catch (IOException e) {
|
|
entity.setTitle(originalFileName);
|
|
entity.setContent("");
|
|
entity.setParsedContent(null);
|
|
entity.setSummary(null);
|
|
entity.setParseStatus(ReviewMindMapParseStatusEnum.FAILED.getCode());
|
|
entity.setParseError(e.getMessage());
|
|
}
|
|
|
|
if (create) {
|
|
reviewMindMapMapper.insert(entity);
|
|
} else {
|
|
reviewMindMapMapper.updateById(entity);
|
|
}
|
|
return entity;
|
|
}
|
|
|
|
/**
|
|
* 将报告和残片合并转换为 ReviewFeedItem 列表,按创建时间倒序排列
|
|
*/
|
|
private List<ReviewFeedItem> mergeAndConvert(List<StudyReportsEntity> reports,
|
|
List<StudyReportFragmentsEntity> fragments) {
|
|
Set<String> allSessionNums = Stream.concat(
|
|
reports.stream().map(StudyReportsEntity::getSessionNum),
|
|
fragments.stream().map(StudyReportFragmentsEntity::getSessionNum)
|
|
).collect(Collectors.toSet());
|
|
|
|
if (allSessionNums.isEmpty()) {
|
|
return Collections.emptyList();
|
|
}
|
|
|
|
Map<String, String> sessionToTaskMap = studySessionsMapper.selectList(
|
|
Wrappers.<StudySessionsEntity>lambdaQuery()
|
|
.in(StudySessionsEntity::getSessionNum, allSessionNums)
|
|
.select(StudySessionsEntity::getSessionNum, StudySessionsEntity::getTaskNum))
|
|
.stream()
|
|
.collect(Collectors.toMap(
|
|
StudySessionsEntity::getSessionNum,
|
|
StudySessionsEntity::getTaskNum,
|
|
(a, b) -> a));
|
|
|
|
Set<String> taskNums = new HashSet<>(sessionToTaskMap.values());
|
|
Map<String, String> taskNameMap = tasksMapper.selectList(
|
|
Wrappers.<TaskEntity>lambdaQuery()
|
|
.in(TaskEntity::getTaskNum, taskNums)
|
|
.select(TaskEntity::getTaskNum, TaskEntity::getTaskName))
|
|
.stream()
|
|
.collect(Collectors.toMap(
|
|
TaskEntity::getTaskNum,
|
|
TaskEntity::getTaskName,
|
|
(a, b) -> a));
|
|
|
|
List<ReviewFeedItem> reportItems = reports.stream().map(r -> {
|
|
ReviewFeedItem item = new ReviewFeedItem();
|
|
item.setId(r.getId());
|
|
item.setSessionNum(r.getSessionNum());
|
|
item.setSourceType("REPORT");
|
|
item.setContent(r.getContent());
|
|
item.setCreatedTime(r.getCreatedTime());
|
|
String taskNum = sessionToTaskMap.get(r.getSessionNum());
|
|
item.setTaskNum(taskNum);
|
|
item.setTaskName(taskNum != null ? taskNameMap.get(taskNum) : null);
|
|
return item;
|
|
}).collect(Collectors.toList());
|
|
|
|
List<ReviewFeedItem> fragmentItems = fragments.stream().map(f -> {
|
|
ReviewFeedItem item = new ReviewFeedItem();
|
|
item.setId(f.getId());
|
|
item.setSessionNum(f.getSessionNum());
|
|
item.setSourceType("FRAGMENT");
|
|
item.setContent(f.getContent());
|
|
item.setCreatedTime(f.getCreatedTime());
|
|
String taskNum = sessionToTaskMap.get(f.getSessionNum());
|
|
item.setTaskNum(taskNum);
|
|
item.setTaskName(taskNum != null ? taskNameMap.get(taskNum) : null);
|
|
return item;
|
|
}).collect(Collectors.toList());
|
|
|
|
return Stream.concat(reportItems.stream(), fragmentItems.stream())
|
|
.sorted(Comparator.comparing(ReviewFeedItem::getCreatedTime,
|
|
Comparator.nullsLast(Comparator.reverseOrder())))
|
|
.collect(Collectors.toList());
|
|
}
|
|
|
|
private ReviewTaskStats newTaskStats(TaskEntity task) {
|
|
return newTaskStats(task.getTaskNum(), task.getTaskName());
|
|
}
|
|
|
|
private ReviewTaskStats newTaskStats(String taskNum, String taskName) {
|
|
ReviewTaskStats stats = new ReviewTaskStats();
|
|
stats.setTaskNum(taskNum);
|
|
stats.setTaskName(taskName);
|
|
return stats;
|
|
}
|
|
|
|
private int normalizeLimit(int limit) {
|
|
if (limit <= 0) {
|
|
return DEFAULT_LIMIT;
|
|
}
|
|
return Math.min(limit, MAX_LIMIT);
|
|
}
|
|
|
|
private void ensureTaskExists(String taskNum) throws NotFindEntitiesException {
|
|
if (!StringUtils.hasText(taskNum) || !tasksMapper.exists(
|
|
Wrappers.<TaskEntity>lambdaQuery().eq(TaskEntity::getTaskNum, taskNum))) {
|
|
throw new NotFindEntitiesException("任务[" + taskNum + "]不存在");
|
|
}
|
|
}
|
|
|
|
private String normalizeMindMapFormat(String contentFormat) {
|
|
if (!StringUtils.hasText(contentFormat)) {
|
|
return DEFAULT_MIND_MAP_FORMAT;
|
|
}
|
|
String normalized = contentFormat.trim().toUpperCase(Locale.ROOT);
|
|
if (!Set.of("TEXT", "MERMAID", "JSON").contains(normalized)) {
|
|
return DEFAULT_MIND_MAP_FORMAT;
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
private Path storeMindMapFile(String taskNum, String originalFileName, MultipartFile file) throws IOException {
|
|
Path directory = Paths.get("uploads", "review-mind-maps", taskNum);
|
|
Files.createDirectories(directory);
|
|
String storedFileName = UUID.randomUUID() + "-" + originalFileName.replaceAll("[\\\\/:*?\"<>|]", "_");
|
|
Path target = directory.resolve(storedFileName);
|
|
try (InputStream inputStream = file.getInputStream()) {
|
|
Files.copy(inputStream, target, StandardCopyOption.REPLACE_EXISTING);
|
|
}
|
|
return target;
|
|
}
|
|
|
|
private void fillReviewTaskStats(List<ReviewTaskStats> statsList) {
|
|
Map<String, ReviewTaskStats> statsByTaskNum = statsList.stream()
|
|
.filter(item -> StringUtils.hasText(item.getTaskNum()))
|
|
.collect(Collectors.toMap(
|
|
ReviewTaskStats::getTaskNum,
|
|
item -> item,
|
|
(a, b) -> a,
|
|
LinkedHashMap::new));
|
|
|
|
if (statsByTaskNum.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
List<StudySessionsEntity> sessions = studySessionsMapper.selectList(
|
|
Wrappers.<StudySessionsEntity>lambdaQuery()
|
|
.in(StudySessionsEntity::getTaskNum, statsByTaskNum.keySet()));
|
|
|
|
LocalDateTime todayStart = LocalDate.now().atStartOfDay();
|
|
LocalDateTime weekStart = LocalDate.now().with(DayOfWeek.MONDAY).atStartOfDay();
|
|
Map<String, String> sessionToTaskNum = new HashMap<>();
|
|
Map<String, Double> effectivenessRatioSum = new HashMap<>();
|
|
|
|
for (StudySessionsEntity session : sessions) {
|
|
ReviewTaskStats stats = statsByTaskNum.get(session.getTaskNum());
|
|
if (stats == null) {
|
|
continue;
|
|
}
|
|
|
|
if (StringUtils.hasText(session.getSessionNum())) {
|
|
sessionToTaskNum.put(session.getSessionNum(), session.getTaskNum());
|
|
}
|
|
|
|
double effectiveTime = session.getEffectiveTime();
|
|
stats.setSessionCount(stats.getSessionCount() + 1);
|
|
stats.setEffectiveTime(stats.getEffectiveTime() + effectiveTime);
|
|
effectivenessRatioSum.merge(session.getTaskNum(), session.getEffectivenessRatio(), Double::sum);
|
|
|
|
LocalDateTime startTime = session.getStartTime();
|
|
if (startTime != null && !startTime.isBefore(todayStart)) {
|
|
stats.setTodayEffectiveTime(stats.getTodayEffectiveTime() + effectiveTime);
|
|
}
|
|
if (startTime != null && !startTime.isBefore(weekStart)) {
|
|
stats.setWeekEffectiveTime(stats.getWeekEffectiveTime() + effectiveTime);
|
|
}
|
|
}
|
|
|
|
statsByTaskNum.forEach((taskNum, stats) -> {
|
|
if (stats.getSessionCount() > 0) {
|
|
stats.setAvgEffectiveTime(stats.getEffectiveTime() / stats.getSessionCount());
|
|
stats.setAvgEffectivenessRatio(effectivenessRatioSum.getOrDefault(taskNum, 0D) / stats.getSessionCount());
|
|
}
|
|
});
|
|
|
|
fillReportAndFragmentCount(sessionToTaskNum, statsByTaskNum);
|
|
}
|
|
|
|
private void fillReportAndFragmentCount(Map<String, String> sessionToTaskNum,
|
|
Map<String, ReviewTaskStats> statsByTaskNum) {
|
|
if (sessionToTaskNum.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
Set<String> sessionNums = sessionToTaskNum.keySet();
|
|
List<StudyReportsEntity> reports = studyReportsMapper.selectList(
|
|
Wrappers.<StudyReportsEntity>lambdaQuery()
|
|
.in(StudyReportsEntity::getSessionNum, sessionNums)
|
|
.select(StudyReportsEntity::getSessionNum));
|
|
for (StudyReportsEntity report : reports) {
|
|
ReviewTaskStats stats = statsByTaskNum.get(sessionToTaskNum.get(report.getSessionNum()));
|
|
if (stats != null) {
|
|
stats.setReportCount(stats.getReportCount() + 1);
|
|
}
|
|
}
|
|
|
|
List<StudyReportFragmentsEntity> fragments = studyReportFragmentsMapper.selectList(
|
|
Wrappers.<StudyReportFragmentsEntity>lambdaQuery()
|
|
.in(StudyReportFragmentsEntity::getSessionNum, sessionNums)
|
|
.select(StudyReportFragmentsEntity::getSessionNum));
|
|
for (StudyReportFragmentsEntity fragment : fragments) {
|
|
ReviewTaskStats stats = statsByTaskNum.get(sessionToTaskNum.get(fragment.getSessionNum()));
|
|
if (stats != null) {
|
|
stats.setFragmentCount(stats.getFragmentCount() + 1);
|
|
}
|
|
}
|
|
}
|
|
}
|