feat: 新增会话误操作结束接口
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package com.guo.learningprogresstracker.controller;
|
package com.guo.learningprogresstracker.controller;
|
||||||
|
|
||||||
import com.google.protobuf.ServiceException;
|
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.EndedStudySessionRequest;
|
||||||
import com.guo.learningprogresstracker.dto.request.UpsertExpectationRequest;
|
import com.guo.learningprogresstracker.dto.request.UpsertExpectationRequest;
|
||||||
import com.guo.learningprogresstracker.dto.response.StudySessionResponse;
|
import com.guo.learningprogresstracker.dto.response.StudySessionResponse;
|
||||||
@@ -93,6 +94,16 @@ public class StudySessionController {
|
|||||||
return message != null ? CommonResult.success(message) : CommonResult.success();
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取【学习会话】所有的学习残片数据,以列表返回
|
* 获取【学习会话】所有的学习残片数据,以列表返回
|
||||||
*/
|
*/
|
||||||
|
|||||||
+14
@@ -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
|
* 查询学习会话的学习预期,不存在时返回 null
|
||||||
*/
|
*/
|
||||||
StudyExpectationsEntity getBySessionNum(String sessionNum);
|
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;
|
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 pauseStudySession(String sessionNum, LocalDateTime endTime) throws ErrorParameterException, ServiceException;
|
||||||
|
|
||||||
void continueStudySession(String sessionNum) throws ErrorParameterException, ServiceException;
|
void continueStudySession(String sessionNum) throws ErrorParameterException, ServiceException;
|
||||||
|
|||||||
+7
@@ -52,4 +52,11 @@ public class StudyExpectationsServiceImpl implements StudyExpectationsService {
|
|||||||
.eq(StudyExpectationsEntity::getSessionNum, sessionNum)
|
.eq(StudyExpectationsEntity::getSessionNum, sessionNum)
|
||||||
.last("LIMIT 1"));
|
.last("LIMIT 1"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteBySessionNum(String sessionNum) {
|
||||||
|
studyExpectationsMapper.delete(
|
||||||
|
Wrappers.<StudyExpectationsEntity>lambdaQuery()
|
||||||
|
.eq(StudyExpectationsEntity::getSessionNum, sessionNum));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+30
@@ -40,6 +40,11 @@ public class StudySessionsServiceImpl extends ServiceImpl<StudySessionsMapper, S
|
|||||||
|
|
||||||
public static final int WORK_DURATION = 25;
|
public static final int WORK_DURATION = 25;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 误操作结束的确认语:用户必须输入完全一致的内容才能零数据关闭会话
|
||||||
|
*/
|
||||||
|
public static final String MISOPERATION_CONFIRMATION = "我误操作导致开启了本次学习";
|
||||||
|
|
||||||
private final TasksServiceImpl tasksServiceImpl;
|
private final TasksServiceImpl tasksServiceImpl;
|
||||||
private final StudyReportFragmentsServiceImpl studyReportFragmentsServiceImpl;
|
private final StudyReportFragmentsServiceImpl studyReportFragmentsServiceImpl;
|
||||||
private final StudyReportFragmentsMapper studyReportFragmentsMapper;
|
private final StudyReportFragmentsMapper studyReportFragmentsMapper;
|
||||||
@@ -120,6 +125,31 @@ public class StudySessionsServiceImpl extends ServiceImpl<StudySessionsMapper, S
|
|||||||
}
|
}
|
||||||
return null;
|
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
|
@Override
|
||||||
public StudySessionResponse getNotEndedStudySessionByTaskNum(String taskNum) throws ErrorParameterException {
|
public StudySessionResponse getNotEndedStudySessionByTaskNum(String taskNum) throws ErrorParameterException {
|
||||||
|
|||||||
+103
@@ -4,9 +4,11 @@ import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
|||||||
import com.guo.learningprogresstracker.entity.StudyReportsEntity;
|
import com.guo.learningprogresstracker.entity.StudyReportsEntity;
|
||||||
import com.guo.learningprogresstracker.entity.StudySessionsEntity;
|
import com.guo.learningprogresstracker.entity.StudySessionsEntity;
|
||||||
import com.guo.learningprogresstracker.enums.StudySessionStateEnum;
|
import com.guo.learningprogresstracker.enums.StudySessionStateEnum;
|
||||||
|
import com.guo.learningprogresstracker.exception.ErrorParameterException;
|
||||||
import com.guo.learningprogresstracker.mapper.StudyReportFragmentsMapper;
|
import com.guo.learningprogresstracker.mapper.StudyReportFragmentsMapper;
|
||||||
import com.guo.learningprogresstracker.mapper.StudyReportsMapper;
|
import com.guo.learningprogresstracker.mapper.StudyReportsMapper;
|
||||||
import com.guo.learningprogresstracker.mapper.StudySessionsMapper;
|
import com.guo.learningprogresstracker.mapper.StudySessionsMapper;
|
||||||
|
import com.guo.learningprogresstracker.service.StudyExpectationsService;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
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.quality.Strictness;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@@ -41,10 +44,15 @@ class StudySessionsServiceImplTest {
|
|||||||
@Mock
|
@Mock
|
||||||
private StudySessionsMapper studySessionsMapper;
|
private StudySessionsMapper studySessionsMapper;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private StudyExpectationsService studyExpectationsService;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
// stub updateById,避免调用真实的 getBaseMapper()
|
// stub updateById,避免调用真实的 getBaseMapper()
|
||||||
doReturn(true).when(studySessionsService).updateById(any());
|
doReturn(true).when(studySessionsService).updateById(any());
|
||||||
|
// stub removeById,避免调用真实的 getBaseMapper()
|
||||||
|
doReturn(true).when(studySessionsService).removeById(any(Serializable.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
private StudySessionsEntity createSession(String state, int minutesAgo) {
|
private StudySessionsEntity createSession(String state, int minutesAgo) {
|
||||||
@@ -210,4 +218,99 @@ class StudySessionsServiceImplTest {
|
|||||||
|
|
||||||
verify(studySessionsService).updateById(session);
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user