feat: 新增会话误操作结束接口

This commit is contained in:
2026-08-15 21:24:59 +08:00
parent 0413013f24
commit a6f684c8b7
7 changed files with 175 additions and 0 deletions
@@ -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<Void> abortStudySession(@PathVariable("sessionNum") String sessionNum,
@Valid @RequestBody AbortStudySessionRequest request) throws ErrorParameterException {
studySessionsServiceImpl.abortStudySession(sessionNum, request.getConfirmation());
return CommonResult.success();
}
/**
* 获取【学习会话】所有的学习残片数据,以列表返回
*/
@@ -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;
}
@@ -17,4 +17,9 @@ public interface StudyExpectationsService {
* 查询学习会话的学习预期,不存在时返回 null
*/
StudyExpectationsEntity getBySessionNum(String sessionNum);
/**
* 删除学习会话的学习预期(误操作结束时会话不留任何数据)
*/
void deleteBySessionNum(String sessionNum);
}
@@ -26,6 +26,11 @@ public interface StudySessionsService extends IService<StudySessionsEntity> {
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;
@@ -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.<StudyExpectationsEntity>lambdaQuery()
.eq(StudyExpectationsEntity::getSessionNum, sessionNum));
}
}
@@ -40,6 +40,11 @@ public class StudySessionsServiceImpl extends ServiceImpl<StudySessionsMapper, S
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;
@@ -120,6 +125,31 @@ public class StudySessionsServiceImpl extends ServiceImpl<StudySessionsMapper, S
}
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 {
@@ -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));
}
}