From a6f684c8b77a10b2adb63a1bdfe9857f1b9f737c Mon Sep 17 00:00:00 2001 From: cat-shark <1716967236@qq.com> Date: Sat, 15 Aug 2026 21:24:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E4=BC=9A=E8=AF=9D?= =?UTF-8?q?=E8=AF=AF=E6=93=8D=E4=BD=9C=E7=BB=93=E6=9D=9F=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/StudySessionController.java | 11 ++ .../dto/request/AbortStudySessionRequest.java | 14 +++ .../service/StudyExpectationsService.java | 5 + .../service/StudySessionsService.java | 5 + .../impl/StudyExpectationsServiceImpl.java | 7 ++ .../impl/StudySessionsServiceImpl.java | 30 +++++ .../impl/StudySessionsServiceImplTest.java | 103 ++++++++++++++++++ 7 files changed, 175 insertions(+) create mode 100644 src/main/java/com/guo/learningprogresstracker/dto/request/AbortStudySessionRequest.java diff --git a/src/main/java/com/guo/learningprogresstracker/controller/StudySessionController.java b/src/main/java/com/guo/learningprogresstracker/controller/StudySessionController.java index 1a228d2..08a465a 100644 --- a/src/main/java/com/guo/learningprogresstracker/controller/StudySessionController.java +++ b/src/main/java/com/guo/learningprogresstracker/controller/StudySessionController.java @@ -1,6 +1,7 @@ package com.guo.learningprogresstracker.controller; import com.google.protobuf.ServiceException; +import com.guo.learningprogresstracker.dto.request.AbortStudySessionRequest; import com.guo.learningprogresstracker.dto.request.EndedStudySessionRequest; import com.guo.learningprogresstracker.dto.request.UpsertExpectationRequest; import com.guo.learningprogresstracker.dto.response.StudySessionResponse; @@ -93,6 +94,16 @@ public class StudySessionController { return message != null ? CommonResult.success(message) : CommonResult.success(); } + /** + * 误操作结束一个【学习会话】:需输入确认语,严格零数据(删除会话与预期),仅未产生学习残片时可用 + */ + @PostMapping("/{sessionNum}/study-sessions/abort") + public CommonResult abortStudySession(@PathVariable("sessionNum") String sessionNum, + @Valid @RequestBody AbortStudySessionRequest request) throws ErrorParameterException { + studySessionsServiceImpl.abortStudySession(sessionNum, request.getConfirmation()); + return CommonResult.success(); + } + /** * 获取【学习会话】所有的学习残片数据,以列表返回 */ diff --git a/src/main/java/com/guo/learningprogresstracker/dto/request/AbortStudySessionRequest.java b/src/main/java/com/guo/learningprogresstracker/dto/request/AbortStudySessionRequest.java new file mode 100644 index 0000000..5a2b3f6 --- /dev/null +++ b/src/main/java/com/guo/learningprogresstracker/dto/request/AbortStudySessionRequest.java @@ -0,0 +1,14 @@ +package com.guo.learningprogresstracker.dto.request; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +/** + * 误操作结束学习会话的请求体 + */ +@Data +public class AbortStudySessionRequest { + + @NotBlank(message = "请填写确认语") + private String confirmation; +} diff --git a/src/main/java/com/guo/learningprogresstracker/service/StudyExpectationsService.java b/src/main/java/com/guo/learningprogresstracker/service/StudyExpectationsService.java index 04a8de8..efb3852 100644 --- a/src/main/java/com/guo/learningprogresstracker/service/StudyExpectationsService.java +++ b/src/main/java/com/guo/learningprogresstracker/service/StudyExpectationsService.java @@ -17,4 +17,9 @@ public interface StudyExpectationsService { * 查询学习会话的学习预期,不存在时返回 null */ StudyExpectationsEntity getBySessionNum(String sessionNum); + + /** + * 删除学习会话的学习预期(误操作结束时会话不留任何数据) + */ + void deleteBySessionNum(String sessionNum); } diff --git a/src/main/java/com/guo/learningprogresstracker/service/StudySessionsService.java b/src/main/java/com/guo/learningprogresstracker/service/StudySessionsService.java index 9932da1..74c367e 100644 --- a/src/main/java/com/guo/learningprogresstracker/service/StudySessionsService.java +++ b/src/main/java/com/guo/learningprogresstracker/service/StudySessionsService.java @@ -26,6 +26,11 @@ public interface StudySessionsService extends IService { String endedStudySession(String sessionNum, String content) throws ErrorParameterException; + /** + * 误操作结束学习会话:需输入确认语,严格零数据(删除会话与预期),仅未产生学习残片时可用 + */ + void abortStudySession(String sessionNum, String confirmation) throws ErrorParameterException; + void pauseStudySession(String sessionNum, LocalDateTime endTime) throws ErrorParameterException, ServiceException; void continueStudySession(String sessionNum) throws ErrorParameterException, ServiceException; diff --git a/src/main/java/com/guo/learningprogresstracker/service/impl/StudyExpectationsServiceImpl.java b/src/main/java/com/guo/learningprogresstracker/service/impl/StudyExpectationsServiceImpl.java index 692a79e..52ce8d2 100644 --- a/src/main/java/com/guo/learningprogresstracker/service/impl/StudyExpectationsServiceImpl.java +++ b/src/main/java/com/guo/learningprogresstracker/service/impl/StudyExpectationsServiceImpl.java @@ -52,4 +52,11 @@ public class StudyExpectationsServiceImpl implements StudyExpectationsService { .eq(StudyExpectationsEntity::getSessionNum, sessionNum) .last("LIMIT 1")); } + + @Override + public void deleteBySessionNum(String sessionNum) { + studyExpectationsMapper.delete( + Wrappers.lambdaQuery() + .eq(StudyExpectationsEntity::getSessionNum, sessionNum)); + } } diff --git a/src/main/java/com/guo/learningprogresstracker/service/impl/StudySessionsServiceImpl.java b/src/main/java/com/guo/learningprogresstracker/service/impl/StudySessionsServiceImpl.java index 55780e1..8b4244d 100644 --- a/src/main/java/com/guo/learningprogresstracker/service/impl/StudySessionsServiceImpl.java +++ b/src/main/java/com/guo/learningprogresstracker/service/impl/StudySessionsServiceImpl.java @@ -40,6 +40,11 @@ public class StudySessionsServiceImpl extends ServiceImpl 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 { diff --git a/src/test/java/com/guo/learningprogresstracker/service/impl/StudySessionsServiceImplTest.java b/src/test/java/com/guo/learningprogresstracker/service/impl/StudySessionsServiceImplTest.java index 552b958..77635bb 100644 --- a/src/test/java/com/guo/learningprogresstracker/service/impl/StudySessionsServiceImplTest.java +++ b/src/test/java/com/guo/learningprogresstracker/service/impl/StudySessionsServiceImplTest.java @@ -4,9 +4,11 @@ import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.guo.learningprogresstracker.entity.StudyReportsEntity; import com.guo.learningprogresstracker.entity.StudySessionsEntity; import com.guo.learningprogresstracker.enums.StudySessionStateEnum; +import com.guo.learningprogresstracker.exception.ErrorParameterException; 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 org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -17,6 +19,7 @@ import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; import org.mockito.junit.jupiter.MockitoExtension; +import java.io.Serializable; import java.time.LocalDateTime; import java.util.Optional; @@ -41,10 +44,15 @@ class StudySessionsServiceImplTest { @Mock private StudySessionsMapper studySessionsMapper; + @Mock + private StudyExpectationsService studyExpectationsService; + @BeforeEach void setUp() { // stub updateById,避免调用真实的 getBaseMapper() doReturn(true).when(studySessionsService).updateById(any()); + // stub removeById,避免调用真实的 getBaseMapper() + doReturn(true).when(studySessionsService).removeById(any(Serializable.class)); } private StudySessionsEntity createSession(String state, int minutesAgo) { @@ -210,4 +218,99 @@ class StudySessionsServiceImplTest { verify(studySessionsService).updateById(session); } + + // ============ 误操作结束(零数据关闭) ============ + + /** + * 场景11:确认语正确且未产生残片 → 删除预期与会话,零数据关闭 + */ + @Test + void abortStudySession_success_shouldDeleteEverything() throws Exception { + StudySessionsEntity session = createSession(StudySessionStateEnum.ONGOING.name(), 5); + session.setId(1); + mockGetOneOpt(session); + doReturn(0L).when(studyReportFragmentsMapper).selectCount(any()); + + studySessionsService.abortStudySession("SESSION_TEST", StudySessionsServiceImpl.MISOPERATION_CONFIRMATION); + + verify(studyExpectationsService).deleteBySessionNum("SESSION_TEST"); + verify(studySessionsService).removeById(1); + } + + /** + * 场景12:确认语不正确 → 抛异常,不删除任何数据 + */ + @Test + void abortStudySession_wrongConfirmation_shouldThrow() { + StudySessionsEntity session = createSession(StudySessionStateEnum.ONGOING.name(), 5); + mockGetOneOpt(session); + + ErrorParameterException ex = assertThrows(ErrorParameterException.class, () -> + studySessionsService.abortStudySession("SESSION_TEST", "我不是确认语")); + + assertEquals("确认语输入不正确,请核对后重新输入", ex.getMessage()); + verify(studyExpectationsService, never()).deleteBySessionNum(any()); + verify(studySessionsService, never()).removeById(any()); + } + + /** + * 场景13:确认语首尾空白应被忽略,仍视为正确 + */ + @Test + void abortStudySession_confirmationWithSpaces_shouldSucceed() throws Exception { + StudySessionsEntity session = createSession(StudySessionStateEnum.PAUSED.name(), 5); + session.setId(2); + mockGetOneOpt(session); + doReturn(0L).when(studyReportFragmentsMapper).selectCount(any()); + + studySessionsService.abortStudySession("SESSION_TEST", + " " + StudySessionsServiceImpl.MISOPERATION_CONFIRMATION + " "); + + verify(studyExpectationsService).deleteBySessionNum("SESSION_TEST"); + verify(studySessionsService).removeById(2); + } + + /** + * 场景14:已产生学习残片 → 抛异常,不能误操作结束 + */ + @Test + void abortStudySession_hasFragments_shouldThrow() { + StudySessionsEntity session = createSession(StudySessionStateEnum.ONGOING.name(), 5); + mockGetOneOpt(session); + doReturn(1L).when(studyReportFragmentsMapper).selectCount(any()); + + ErrorParameterException ex = assertThrows(ErrorParameterException.class, () -> + studySessionsService.abortStudySession("SESSION_TEST", StudySessionsServiceImpl.MISOPERATION_CONFIRMATION)); + + assertEquals("这次学习已经产生了学习残片,不能按误操作结束了哦", ex.getMessage()); + verify(studyExpectationsService, never()).deleteBySessionNum(any()); + verify(studySessionsService, never()).removeById(any()); + } + + /** + * 场景15:会话已结束 → 抛异常 + */ + @Test + void abortStudySession_endedSession_shouldThrow() { + StudySessionsEntity session = createSession(StudySessionStateEnum.ENDED.name(), 5); + mockGetOneOpt(session); + + ErrorParameterException ex = assertThrows(ErrorParameterException.class, () -> + studySessionsService.abortStudySession("SESSION_TEST", StudySessionsServiceImpl.MISOPERATION_CONFIRMATION)); + + assertEquals("这次学习会话已经结束,不能再误操作结束了", ex.getMessage()); + verify(studySessionsService, never()).removeById(any()); + } + + /** + * 场景16:会话不存在 → 抛异常 + */ + @Test + void abortStudySession_sessionNotFound_shouldThrow() { + doReturn(Optional.empty()).when(studySessionsService).getOneOpt(any(Wrapper.class)); + + assertThrows(ErrorParameterException.class, () -> + studySessionsService.abortStudySession("NOT_EXIST", StudySessionsServiceImpl.MISOPERATION_CONFIRMATION)); + } + }