Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11a7c8aaea | ||
|
|
e691dfaa95 | ||
|
|
a6f684c8b7 |
@@ -0,0 +1,4 @@
|
||||
# 行尾规范:文本文件统一 LF,Windows 批处理保留 CRLF
|
||||
* text=auto eol=lf
|
||||
*.bat text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
@@ -44,16 +44,17 @@ lpt-be/src/main/java/com/guo/learningprogresstracker/
|
||||
- entity 使用 `@TableName`、`@TableField` 映射数据库字段。
|
||||
- mapStruct 承载 DTO 转换。
|
||||
|
||||
## API 端点总览(32 个)
|
||||
## API 端点总览(44 个)
|
||||
|
||||
| 模块 | 端点数 | 端点 |
|
||||
|------|--------|------|
|
||||
| 学习会话 | 9 | `POST /study-sessions/{taskNum}/start`, `PUT .../pause`, `PUT .../resume`, `PUT .../end`, `POST .../fragments`, `GET .../history`, `PUT .../expectation`, `GET .../expectation`, `GET .../report-draft` |
|
||||
| 标准思维导图 | 6 | `GET /review/standard-mind-map/{taskNum}`, `POST .../regenerate`, `PUT ...`, `POST .../recall`, `POST .../find-node`, `GET .../recall-records` |
|
||||
| 复习模块 | 6 | `GET /review/feed`, `GET /review/task/{taskNum}`, `GET /review/report/{id}`, `GET /review/fragment/{id}`, `GET /review/standard-mind-map/recall-records/{recordId}`, `GET /review/tasks` |
|
||||
| 任务管理 | 6 | `GET /tasks`, `POST /tasks`, `PUT /tasks/{taskNum}`, `DELETE /tasks/{taskNum}`, `GET /tasks/priority-weights`, `PUT /tasks/priority-weights` |
|
||||
| 应用场景 | 4 | `GET/POST/PUT/DELETE /tasks/{taskNum}/applications[/{id}]` |
|
||||
| 登录认证 | 2 | `POST /login`, `POST /logout` |
|
||||
| 学习会话 | 11 | `PUT /study-sessions/{sessionNum}/expectation`, `GET /study-sessions/{sessionNum}/expectation`, `GET /study-sessions/{sessionNum}`, `POST /study-sessions/{sessionNum}/study-sessions/continue`, `POST /study-sessions/{sessionNum}/study-sessions/pause`, `POST /study-sessions/{sessionNum}/study-sessions/ended`, `GET /study-sessions/{sessionNum}/all-fragments`, `GET /study-sessions/{sessionNum}/report-draft`, `GET /study-sessions/tasks/{taskNum}/fragments`, `GET /study-sessions/tasks/{taskNum}/reports`, `GET /study-sessions/active` |
|
||||
| 任务管理 | 13 | `GET/POST /tasks`, `GET/PUT/DELETE /tasks/{taskId}`, `GET/PUT /tasks/priority-weights`, `GET/POST /tasks/{taskNum}/applications`, `PUT/DELETE /tasks/applications/{id}`, `GET /tasks/{taskNum}/study-sessions/start-or-continue`, `GET /tasks/{taskNum}/not-ended-study-session` |
|
||||
| 复习与导图 | 13 | `GET /review/feed`, `GET /review/tasks`, `GET /review/tasks/{taskNum}`, `GET /review/task/{taskNum}`, `GET /review/report/{id}`, `GET /review/fragment/{id}`, `GET /review/standard-mind-map/{taskNum}`, `POST /review/standard-mind-map/{taskNum}/regenerate`, `PUT /review/standard-mind-map/{taskNum}`, `POST /review/standard-mind-map/{taskNum}/recall`, `POST /review/standard-mind-map/{taskNum}/find-node`, `GET /review/standard-mind-map/{taskNum}/recall-records`, `GET /review/standard-mind-map/recall-records/{recordId}` |
|
||||
| 学习残片 | 3 | `POST /report-fragments`, `PUT /report-fragments/{id}`, `GET /report-fragments/session/{sessionNum}` |
|
||||
| 工具 | 1 | `GET /utils/fetch-title?url=...` |
|
||||
| 测试 | 1 | `GET /test/{id}` |
|
||||
|
||||
## 数据库(10 个核心表)
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取【学习会话】所有的学习残片数据,以列表返回
|
||||
*/
|
||||
|
||||
+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
|
||||
*/
|
||||
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;
|
||||
|
||||
+7
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
+30
@@ -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 {
|
||||
|
||||
+103
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user