package com.guo.learningprogresstracker.service.impl; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.google.protobuf.ServiceException; import com.guo.learningprogresstracker.dto.StudySessionsDto; import com.guo.learningprogresstracker.dto.response.StudySessionResponse; 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.StudySessionStateEnum; import com.guo.learningprogresstracker.exception.ErrorParameterException; import com.guo.learningprogresstracker.exception.NotFindEntitiesException; import com.guo.learningprogresstracker.mapStruct.StudySessionConvert; import com.guo.learningprogresstracker.mapper.StudyReportFragmentsMapper; import com.guo.learningprogresstracker.mapper.StudyReportsMapper; import com.guo.learningprogresstracker.mapper.StudySessionsMapper; import com.guo.learningprogresstracker.service.StudyExpectationsService; import com.guo.learningprogresstracker.service.StudySessionsService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Optional; import java.util.stream.Collectors; /** * 针对表【study_sessions(记录每次学习会话的具体数据)】的数据库操作Service实现 */ @Slf4j @Service @RequiredArgsConstructor public class StudySessionsServiceImpl extends ServiceImpl implements StudySessionsService { public static final int WORK_DURATION = 25; private final TasksServiceImpl tasksServiceImpl; private final StudyReportFragmentsServiceImpl studyReportFragmentsServiceImpl; private final StudyReportFragmentsMapper studyReportFragmentsMapper; private final StudyReportsMapper studyReportsMapper; private final AiServiceClient aiServiceClient; private final StudyExpectationsService studyExpectationsService; public StudySessionResponse startOrContinueStudySession(String taskNum) throws NotFindEntitiesException, ServiceException { TaskEntity taskEntity = tasksServiceImpl.getOneOpt( Wrappers.lambdaQuery(TaskEntity.class).eq(TaskEntity::getTaskNum, taskNum)) .orElseThrow(() -> new NotFindEntitiesException(String.format("[%s]不存在", taskNum))); StudySessionsEntity session = this.getOneOpt(Wrappers.lambdaQuery() .eq(StudySessionsEntity::getTaskNum, taskNum) .ne(StudySessionsEntity::getSessionState, StudySessionStateEnum.ENDED.name())) .orElseGet(() -> { // 如果没有则创建新会话 StudySessionsEntity newSession = StudySessionsEntity.initNewSession(); newSession.setTaskNum(taskNum); newSession.setLastStartTime(LocalDateTime.now()); this.save(newSession); return newSession; }); session.calculatePointerPosition(); if (session.isOverTime()) { session.pausedStudySession(session.getLastStartTime().plusMinutes(WORK_DURATION)); this.updateById(session); } StudySessionResponse response = StudySessionConvert.MAPPER.toStudySessionResponse(session); response.setTaskName(taskEntity.getTaskName()); response.setMaterialUrl(taskEntity.getMaterialUrl()); response.setTaskId(taskEntity.getId()); if (session.isOverTime()) { response.setSystemMessage("上段学习任务已经超时25分钟,将仅计算为25分钟的有效学习时间,请注意休息!"); } return response; } @Override public void pauseStudySession(String sessionNum, LocalDateTime endTime) throws ErrorParameterException, ServiceException { StudySessionsEntity studySessionsEntity = this.getOneOpt(Wrappers.lambdaQuery(StudySessionsEntity.class) .eq(StudySessionsEntity::getSessionNum, sessionNum)) .orElseThrow(() -> new ErrorParameterException("会话[" + sessionNum + "]不存在")); if (StudySessionStateEnum.PAUSED.name().equals(studySessionsEntity.getSessionState())) { log.error("会话[" + studySessionsEntity.getSessionNum() + "]重复暂停"); throw new ErrorParameterException("会话[" + sessionNum + "]已经暂停,请勿重复暂停!"); } studySessionsEntity.calculatePointerPosition(); studySessionsEntity.pausedStudySession(endTime); this.updateById(studySessionsEntity); } @Override @Transactional public String endedStudySession(String sessionNum, String content) throws ErrorParameterException { StudySessionsEntity studySessionsEntity = this.getOneOpt(Wrappers.lambdaQuery(StudySessionsEntity.class) .eq(StudySessionsEntity::getSessionNum, sessionNum)) .orElseThrow(() -> new ErrorParameterException("会话[" + sessionNum + "]不存在")); boolean wasOngoing = StudySessionStateEnum.ONGOING.name().equals(studySessionsEntity.getSessionState()); studySessionsEntity.endedStudySession(); this.updateById(studySessionsEntity); StudyReportsEntity studyReportsEntity = new StudyReportsEntity(); studyReportsEntity.setSessionNum(sessionNum); studyReportsEntity.setContent(content); studyReportsMapper.insert(studyReportsEntity); if (wasOngoing && studySessionsEntity.getEffectiveTime() == 0) { return "本次有效学习时间不足10分钟,不计入总学习时间"; } return null; } @Override public StudySessionResponse getNotEndedStudySessionByTaskNum(String taskNum) throws ErrorParameterException { TaskEntity task = tasksServiceImpl.getOneOpt(Wrappers.lambdaQuery(TaskEntity.class) .eq(TaskEntity::getTaskNum, taskNum)) .orElseThrow(() -> new ErrorParameterException("任务[" + taskNum + "]不存在")); StudySessionsDto dto = Optional.ofNullable(studyReportsMapper.getNotEndedStudySessionDtoByTaskNum(taskNum)) .orElseThrow(() -> new ErrorParameterException("任务[" + taskNum + "]不存在进行中或暂停中的会话")); return StudySessionConvert.MAPPER.toStudySessionResponse(dto); } @Override public ArrayList getAllFragments(String sessionNum) throws ErrorParameterException { if (this.exists(Wrappers.lambdaQuery(StudySessionsEntity.class) .eq(StudySessionsEntity::getSessionNum, sessionNum))) { return studyReportFragmentsMapper.selectList(Wrappers.lambdaQuery(StudyReportFragmentsEntity.class) .eq(StudyReportFragmentsEntity::getSessionNum, sessionNum)) .stream().map(StudyReportFragmentsEntity::getContent).collect(Collectors.toCollection(ArrayList::new)); } else { throw new ErrorParameterException("会话[" + sessionNum + "]不存在"); } } @Override @Transactional public void continueStudySession(String sessionNum) throws ErrorParameterException, ServiceException { StudySessionsEntity studySessionsEntity = this.getOneOpt(Wrappers.lambdaQuery(StudySessionsEntity.class) .eq(StudySessionsEntity::getSessionNum, sessionNum)) .orElseThrow(() -> new ErrorParameterException("会话[" + sessionNum + "]不存在")); if (StudySessionStateEnum.ONGOING.name().equals(studySessionsEntity.getSessionState())) { log.error("会话[" + studySessionsEntity.getSessionNum() + "]重复开始"); throw new ErrorParameterException("会话[" + sessionNum + "]已经开始,请勿重复开始!"); } else if (StudySessionStateEnum.ENDED.name().equals(studySessionsEntity.getSessionState())) { log.error("会话[" + studySessionsEntity.getSessionNum() + "]处于结束状态,不可开始该会话!"); throw new ErrorParameterException("会话[" + sessionNum + "]处于结束状态,不可开始该会话!"); } studySessionsEntity.continueStudySession(); this.updateById(studySessionsEntity); } /** * 通过 sessionNum 获取学习会话 */ public StudySessionResponse getStudySessionBySessionNum(String sessionNum) throws ErrorParameterException { StudySessionsEntity session = this.getOneOpt(Wrappers.lambdaQuery(StudySessionsEntity.class) .eq(StudySessionsEntity::getSessionNum, sessionNum)) .orElseThrow(() -> new ErrorParameterException("会话[" + sessionNum + "]不存在")); return StudySessionConvert.MAPPER.toStudySessionResponse(session); } /** * 生成学习报告草稿:优先调用 lpt-ai 聚合残片,服务不可用时降级为按序拼接。 * 草稿仅作为编辑起点返回,不落库——最终报告由用户确认后经 endedStudySession 保存。 */ public String generateReportDraft(String sessionNum) throws ErrorParameterException { StudySessionsEntity session = this.getOneOpt(Wrappers.lambdaQuery(StudySessionsEntity.class) .eq(StudySessionsEntity::getSessionNum, sessionNum)) .orElseThrow(() -> new ErrorParameterException("会话[" + sessionNum + "]不存在")); ArrayList fragments = getAllFragments(sessionNum); if (fragments.isEmpty()) { return ""; } String taskName = tasksServiceImpl.getOneOpt( Wrappers.lambdaQuery(TaskEntity.class).eq(TaskEntity::getTaskNum, session.getTaskNum())) .map(TaskEntity::getTaskName) .orElse("学习任务"); String expectation = Optional.ofNullable(studyExpectationsService.getBySessionNum(sessionNum)) .map(e -> e.getDescription()) .orElse(null); return aiServiceClient.aggregateReport(taskName, fragments, expectation) .orElseGet(() -> String.join("\n", fragments)); } // ============ 分页查询任务的历史残片/报告 ============ @Override public Page getTaskFragments(String taskNum, int page, int size, String keyword) { Page pg = new Page<>(page, size); var wrapper = Wrappers.lambdaQuery(StudyReportFragmentsEntity.class) .apply("session_num in (select session_num from study_sessions where task_num = {0})", taskNum) .orderByDesc(StudyReportFragmentsEntity::getCreatedTime); if (keyword != null && !keyword.isBlank()) { wrapper.like(StudyReportFragmentsEntity::getContent, keyword); } return studyReportFragmentsMapper.selectPage(pg, wrapper); } @Override public Page getTaskReports(String taskNum, int page, int size, String keyword) { Page pg = new Page<>(page, size); var wrapper = Wrappers.lambdaQuery(StudyReportsEntity.class) .apply("session_num in (select session_num from study_sessions where task_num = {0})", taskNum) .orderByDesc(StudyReportsEntity::getCreatedTime); if (keyword != null && !keyword.isBlank()) { wrapper.like(StudyReportsEntity::getContent, keyword); } Page result = studyReportsMapper.selectPage(pg, wrapper); // 填充每个报告对应会话的预期目标 for (StudyReportsEntity report : result.getRecords()) { if (report.getSessionNum() != null) { Optional.ofNullable(studyExpectationsService.getBySessionNum(report.getSessionNum())) .ifPresent(e -> report.setSessionExpectation(e.getDescription())); } } return result; } // ============ 活跃会话查询 ============ /** * 查询当前用户是否有活跃会话(ONGOING 或 PAUSED)。 * * @param excludeTaskNum 可选,排除指定任务(同一任务继续学习时不会视为冲突) * @return 活跃会话响应;无活跃会话时返回 null */ @Override public StudySessionResponse getActiveSession(String excludeTaskNum) { var wrapper = Wrappers.lambdaQuery(StudySessionsEntity.class) .in(StudySessionsEntity::getSessionState, StudySessionStateEnum.ONGOING.name(), StudySessionStateEnum.PAUSED.name()) .orderByDesc(StudySessionsEntity::getCreatedTime) .last("LIMIT 1"); StudySessionsEntity session = this.getOne(wrapper); if (session == null) return null; // 如果活跃会话属于被排除的任务,视为无冲突 if (excludeTaskNum != null && excludeTaskNum.equals(session.getTaskNum())) { return null; } String taskName = tasksServiceImpl.getOneOpt( Wrappers.lambdaQuery(TaskEntity.class).eq(TaskEntity::getTaskNum, session.getTaskNum())) .map(TaskEntity::getTaskName) .orElse(null); StudySessionResponse response = StudySessionConvert.MAPPER.toStudySessionResponse(session); response.setTaskName(taskName); return response; } }