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; /** * 误操作结束的确认语:用户必须输入完全一致的内容才能零数据关闭会话 */ public static final String MISOPERATION_CONFIRMATION = "我误操作导致开启了本次学习"; 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(() -> { log.warn("[{}]不存在", taskNum); return new NotFindEntitiesException("这个任务不存在或已被删除"); }); 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(() -> sessionNotFound(sessionNum)); if (StudySessionStateEnum.PAUSED.name().equals(studySessionsEntity.getSessionState())) { log.error("会话[" + studySessionsEntity.getSessionNum() + "]重复暂停"); throw new ErrorParameterException("这次学习会话已经暂停了,不用重复暂停哦"); } 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(() -> sessionNotFound(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 @Transactional public void abortStudySession(String sessionNum, String confirmation) throws ErrorParameterException { StudySessionsEntity studySessionsEntity = this.getOneOpt(Wrappers.lambdaQuery(StudySessionsEntity.class) .eq(StudySessionsEntity::getSessionNum, sessionNum)) .orElseThrow(() -> sessionNotFound(sessionNum)); if (StudySessionStateEnum.ENDED.name().equals(studySessionsEntity.getSessionState())) { log.warn("会话[{}]已结束,不能误操作结束", sessionNum); throw new ErrorParameterException("这次学习会话已经结束,不能再误操作结束了"); } if (!MISOPERATION_CONFIRMATION.equals(confirmation == null ? "" : confirmation.trim())) { log.warn("会话[{}]误操作结束的确认语不正确", sessionNum); throw new ErrorParameterException("确认语输入不正确,请核对后重新输入"); } long fragmentCount = studyReportFragmentsMapper.selectCount(Wrappers.lambdaQuery(StudyReportFragmentsEntity.class) .eq(StudyReportFragmentsEntity::getSessionNum, sessionNum)); if (fragmentCount > 0) { log.warn("会话[{}]已产生{}条学习残片,不能误操作结束", sessionNum, fragmentCount); throw new ErrorParameterException("这次学习已经产生了学习残片,不能按误操作结束了哦"); } // 严格零数据:学习预期与会话本身一并删除,系统视角下会话从未发生 studyExpectationsService.deleteBySessionNum(sessionNum); this.removeById(studySessionsEntity.getId()); log.info("会话[{}]误操作结束,未产生任何学习数据", sessionNum); } @Override public StudySessionResponse getNotEndedStudySessionByTaskNum(String taskNum) throws ErrorParameterException { TaskEntity task = tasksServiceImpl.getOneOpt(Wrappers.lambdaQuery(TaskEntity.class) .eq(TaskEntity::getTaskNum, taskNum)) .orElseThrow(() -> taskNotFound(taskNum)); StudySessionsDto dto = Optional.ofNullable(studyReportsMapper.getNotEndedStudySessionDtoByTaskNum(taskNum)) .orElseThrow(() -> { log.warn("任务[{}]不存在进行中或暂停中的会话", taskNum); return new ErrorParameterException("这个任务当前没有进行中或已暂停的学习会话"); }); 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 sessionNotFound(sessionNum); } } @Override @Transactional public void continueStudySession(String sessionNum) throws ErrorParameterException, ServiceException { StudySessionsEntity studySessionsEntity = this.getOneOpt(Wrappers.lambdaQuery(StudySessionsEntity.class) .eq(StudySessionsEntity::getSessionNum, sessionNum)) .orElseThrow(() -> sessionNotFound(sessionNum)); if (StudySessionStateEnum.ONGOING.name().equals(studySessionsEntity.getSessionState())) { log.error("会话[" + studySessionsEntity.getSessionNum() + "]重复开始"); throw new ErrorParameterException("这次学习会话已经开始了,不用重复开始哦"); } else if (StudySessionStateEnum.ENDED.name().equals(studySessionsEntity.getSessionState())) { log.error("会话[" + studySessionsEntity.getSessionNum() + "]处于结束状态,不可开始该会话!"); throw new ErrorParameterException("这次学习会话已经结束,无法再次开始"); } 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(() -> sessionNotFound(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(() -> sessionNotFound(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; } private ErrorParameterException sessionNotFound(String sessionNum) { log.warn("会话[{}]不存在", sessionNum); return new ErrorParameterException("这次学习会话不存在或已结束"); } private ErrorParameterException taskNotFound(String taskNum) { log.warn("任务[{}]不存在", taskNum); return new ErrorParameterException("这个任务不存在或已被删除"); } }